feat(app): 接入金币充值与会员商品下单
This commit is contained in:
1 parent
ec2db6842e
commit
730e38622f
21 files changed
+1325
-1
No files matched your search
@@ -0,0 +1,49 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'json_numbers.dart';
|
||||
|
||||
/// 充值订单:`POST /app/orders` 创建的 `CREATED` 订单,等待支付。
|
||||
class AppOrder {
|
||||
const AppOrder({
|
||||
required this.id,
|
||||
required this.orderNo,
|
||||
required this.productName,
|
||||
required this.coinAmount,
|
||||
required this.status,
|
||||
this.productId = '',
|
||||
this.type = '',
|
||||
this.amount = '',
|
||||
this.currency = '',
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
static const String created = 'CREATED';
|
||||
|
||||
final String id;
|
||||
final String orderNo;
|
||||
final String productId;
|
||||
final String productName;
|
||||
final String type;
|
||||
final int coinAmount;
|
||||
final String amount;
|
||||
final String currency;
|
||||
final String status;
|
||||
final DateTime? createdAt;
|
||||
|
||||
bool get isCreated => status == created;
|
||||
|
||||
factory AppOrder.fromJson(Map<String, dynamic> json) {
|
||||
return AppOrder(
|
||||
id: (json['id'] ?? '').toString(),
|
||||
orderNo: json['orderNo'] as String? ?? '',
|
||||
productId: (json['productId'] ?? '').toString(),
|
||||
productName: json['productName'] as String? ?? '',
|
||||
type: json['type'] as String? ?? '',
|
||||
coinAmount: jsonToInt(json['coinAmount']),
|
||||
amount: (json['amount'] ?? '').toString(),
|
||||
currency: json['currency'] as String? ?? '',
|
||||
status: json['status'] as String? ?? '',
|
||||
createdAt: DateTime.tryParse(json['createdAt'] as String? ?? ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'json_numbers.dart';
|
||||
|
||||
/// 充值商品:`GET /app/products` 的 `COIN_PACK` / `VIP` 条目。
|
||||
class AppProduct {
|
||||
const AppProduct({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.name,
|
||||
required this.type,
|
||||
this.coinAmount = 0,
|
||||
this.bonusCoin = 0,
|
||||
this.totalCoin = 0,
|
||||
this.displayPrice = '',
|
||||
this.currency = '',
|
||||
this.vipDays = 0,
|
||||
});
|
||||
|
||||
static const String coinPackType = 'COIN_PACK';
|
||||
static const String vipType = 'VIP';
|
||||
|
||||
final String id;
|
||||
final String code;
|
||||
final String name;
|
||||
final String type;
|
||||
final int coinAmount;
|
||||
final int bonusCoin;
|
||||
final int totalCoin;
|
||||
final String displayPrice;
|
||||
final String currency;
|
||||
final int vipDays;
|
||||
|
||||
bool get isVip => type == vipType;
|
||||
|
||||
factory AppProduct.fromJson(Map<String, dynamic> json) {
|
||||
final coinAmount = jsonToInt(json['coinAmount']);
|
||||
final bonusCoin = jsonToInt(json['bonusCoin']);
|
||||
return AppProduct(
|
||||
id: (json['id'] ?? '').toString(),
|
||||
code: json['code'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
type: json['type'] as String? ?? '',
|
||||
coinAmount: coinAmount,
|
||||
bonusCoin: bonusCoin,
|
||||
totalCoin: json['totalCoin'] == null
|
||||
? coinAmount + bonusCoin
|
||||
: jsonToInt(json['totalCoin']),
|
||||
displayPrice: json['displayPrice'] as String? ?? '',
|
||||
currency: json['currency'] as String? ?? '',
|
||||
vipDays: jsonToInt(json['vipDays']),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
// Author: fruit.com.cn
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import '../models/order.dart';
|
||||
import '../models/product.dart';
|
||||
import '../models/wallet.dart';
|
||||
import 'api_client.dart';
|
||||
|
||||
@@ -15,6 +17,31 @@ class CommerceApi {
|
||||
throw const ApiException(-1, '返回数据格式异常');
|
||||
}
|
||||
|
||||
/// 上架商品列表;`type` 传 `COIN_PACK` 或 `VIP`。
|
||||
Future<List<AppProduct>> products({String? type}) async {
|
||||
final data = await _client.get(
|
||||
'/app/products',
|
||||
query: type == null ? null : {'type': type},
|
||||
);
|
||||
if (data is List) {
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(AppProduct.fromJson)
|
||||
.toList();
|
||||
}
|
||||
throw const ApiException(-1, '返回数据格式异常');
|
||||
}
|
||||
|
||||
/// 创建充值订单,返回 `CREATED` 状态订单。
|
||||
Future<AppOrder> createOrder(String productId) async {
|
||||
final data = await _client.post(
|
||||
'/app/orders',
|
||||
body: {'productId': productId},
|
||||
);
|
||||
if (data is Map<String, dynamic>) return AppOrder.fromJson(data);
|
||||
throw const ApiException(-1, '返回数据格式异常');
|
||||
}
|
||||
|
||||
Future<UnlockResult> unlock(String episodeId) async {
|
||||
final data = await _client.post('/app/episodes/$episodeId/unlock');
|
||||
if (data is Map<String, dynamic>) return UnlockResult.fromJson(data);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import '../models/order.dart';
|
||||
import '../models/product.dart';
|
||||
import '../remote/commerce_api.dart';
|
||||
|
||||
/// 商品与订单来源:充值页、会员页共用。
|
||||
abstract interface class StoreRepository {
|
||||
Future<List<AppProduct>> fetchProducts({String? type});
|
||||
|
||||
Future<AppOrder> createOrder(String productId);
|
||||
}
|
||||
|
||||
class HttpStoreRepository implements StoreRepository {
|
||||
const HttpStoreRepository(this._api);
|
||||
|
||||
final CommerceApi _api;
|
||||
|
||||
@override
|
||||
Future<List<AppProduct>> fetchProducts({String? type}) =>
|
||||
_api.products(type: type);
|
||||
|
||||
@override
|
||||
Future<AppOrder> createOrder(String productId) => _api.createOrder(productId);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../models/order.dart';
|
||||
import '../models/product.dart';
|
||||
import '../repositories/store_repository.dart';
|
||||
import '../remote/api_exception.dart';
|
||||
|
||||
/// 商品货架状态:按类型拉取商品并创建订单。
|
||||
class StoreController extends ChangeNotifier {
|
||||
StoreController(this._repository, {required this.type});
|
||||
|
||||
static const String coinPackType = AppProduct.coinPackType;
|
||||
static const String vipType = AppProduct.vipType;
|
||||
|
||||
final StoreRepository _repository;
|
||||
final String type;
|
||||
|
||||
List<AppProduct> products = const [];
|
||||
bool loading = false;
|
||||
String? error;
|
||||
String? purchasingProductId;
|
||||
String? purchaseError;
|
||||
AppOrder? order;
|
||||
bool _disposed = false;
|
||||
|
||||
Future<void> load() async {
|
||||
loading = true;
|
||||
error = null;
|
||||
_notify();
|
||||
try {
|
||||
products = await _repository.fetchProducts(type: type);
|
||||
} on ApiException catch (exception) {
|
||||
error = exception.message;
|
||||
} catch (_) {
|
||||
error = 'productsLoadFailed';
|
||||
} finally {
|
||||
loading = false;
|
||||
_notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建订单;成功后保留 [order] 供页面展示订单号。
|
||||
Future<AppOrder?> buy(AppProduct product) async {
|
||||
if (purchasingProductId != null) return null;
|
||||
purchasingProductId = product.id;
|
||||
purchaseError = null;
|
||||
_notify();
|
||||
try {
|
||||
final created = await _repository.createOrder(product.id);
|
||||
order = created;
|
||||
return created;
|
||||
} on ApiException catch (exception) {
|
||||
purchaseError = exception.message;
|
||||
return null;
|
||||
} catch (_) {
|
||||
purchaseError = 'orderCreateFailed';
|
||||
return null;
|
||||
} finally {
|
||||
purchasingProductId = null;
|
||||
_notify();
|
||||
}
|
||||
}
|
||||
|
||||
void _notify() {
|
||||
if (!_disposed) notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposed = true;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user