add
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../services/payment_service.dart';
|
||||
|
||||
/// 充值页状态:套餐 + 选中档位/自定义金额 + 选中支付方式 + 下单中
|
||||
class PaymentState {
|
||||
const PaymentState({
|
||||
this.result,
|
||||
this.selectedPackageId,
|
||||
this.customYuan,
|
||||
this.selectedMethod,
|
||||
this.isLoading = false,
|
||||
this.loadFailed = false,
|
||||
this.creating = false,
|
||||
});
|
||||
|
||||
final PackagesResult? result;
|
||||
|
||||
/// null = 使用自定义金额
|
||||
final int? selectedPackageId;
|
||||
|
||||
/// 自定义金额(元);null 表示未输入
|
||||
final int? customYuan;
|
||||
final int? selectedMethod;
|
||||
final bool isLoading;
|
||||
final bool loadFailed;
|
||||
final bool creating;
|
||||
|
||||
/// 实际应付金额(元):优先选中档位,否则自定义金额
|
||||
int? get amountYuan {
|
||||
final r = result;
|
||||
if (r == null) return null;
|
||||
final pkgId = selectedPackageId;
|
||||
if (pkgId != null) {
|
||||
for (final p in r.packages) {
|
||||
if (p.id == pkgId) return p.amountYuan;
|
||||
}
|
||||
}
|
||||
return customYuan;
|
||||
}
|
||||
|
||||
PaymentState copyWith({
|
||||
PackagesResult? result,
|
||||
int? Function()? selectedPackageId,
|
||||
int? Function()? customYuan,
|
||||
int? Function()? selectedMethod,
|
||||
bool? isLoading,
|
||||
bool? loadFailed,
|
||||
bool? creating,
|
||||
}) =>
|
||||
PaymentState(
|
||||
result: result ?? this.result,
|
||||
selectedPackageId: selectedPackageId != null
|
||||
? selectedPackageId()
|
||||
: this.selectedPackageId,
|
||||
customYuan: customYuan != null ? customYuan() : this.customYuan,
|
||||
selectedMethod: selectedMethod != null
|
||||
? selectedMethod()
|
||||
: this.selectedMethod,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
loadFailed: loadFailed ?? this.loadFailed,
|
||||
creating: creating ?? this.creating,
|
||||
);
|
||||
}
|
||||
|
||||
class PaymentNotifier extends StateNotifier<PaymentState> {
|
||||
PaymentNotifier() : super(const PaymentState()) {
|
||||
load();
|
||||
}
|
||||
|
||||
Future<void> load() async {
|
||||
state = state.copyWith(isLoading: true, loadFailed: false);
|
||||
try {
|
||||
final result = await PaymentService.fetchPackages();
|
||||
state = state.copyWith(
|
||||
result: result,
|
||||
// 默认选中第一档 + 第一种支付方式
|
||||
selectedPackageId: () =>
|
||||
result.packages.isNotEmpty ? result.packages.first.id : null,
|
||||
customYuan: () => null,
|
||||
selectedMethod: () =>
|
||||
result.payMethods.isNotEmpty ? result.payMethods.first.method : null,
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (_) {
|
||||
state = state.copyWith(isLoading: false, loadFailed: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// 选择档位(清空自定义金额)
|
||||
void selectPackage(int id) {
|
||||
state = state.copyWith(selectedPackageId: () => id, customYuan: () => null);
|
||||
}
|
||||
|
||||
/// 输入自定义金额(取消档位选中)
|
||||
void setCustomYuan(int? yuan) {
|
||||
state = state.copyWith(
|
||||
customYuan: () => yuan,
|
||||
selectedPackageId: () => yuan == null ? state.selectedPackageId : null,
|
||||
);
|
||||
}
|
||||
|
||||
void selectMethod(int method) {
|
||||
state = state.copyWith(selectedMethod: () => method);
|
||||
}
|
||||
|
||||
/// 创建充值订单。成功返回订单;失败抛异常由 UI 提示(1001 金额不在档位)。
|
||||
Future<RechargeOrderInfo> createOrder() async {
|
||||
final amount = state.amountYuan;
|
||||
final method = state.selectedMethod;
|
||||
if (amount == null || method == null) {
|
||||
throw ArgumentError('未选择金额或支付方式');
|
||||
}
|
||||
state = state.copyWith(creating: true);
|
||||
try {
|
||||
final order = await PaymentService.createRecharge(
|
||||
amountYuan: amount,
|
||||
payMethod: method,
|
||||
);
|
||||
return order;
|
||||
} finally {
|
||||
if (mounted) state = state.copyWith(creating: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final paymentProvider = StateNotifierProvider<PaymentNotifier, PaymentState>(
|
||||
(_) => PaymentNotifier(),
|
||||
);
|
||||
|
||||
/// 订单轮询状态
|
||||
enum OrderPollStatus { polling, paid, closed, timeout, failed }
|
||||
|
||||
class OrderPollState {
|
||||
const OrderPollState({
|
||||
this.status = OrderPollStatus.polling,
|
||||
required this.order,
|
||||
this.walletPoints,
|
||||
});
|
||||
|
||||
final OrderPollStatus status;
|
||||
final RechargeOrderInfo order;
|
||||
final int? walletPoints;
|
||||
|
||||
OrderPollState copyWith({
|
||||
OrderPollStatus? status,
|
||||
RechargeOrderInfo? order,
|
||||
int? walletPoints,
|
||||
}) =>
|
||||
OrderPollState(
|
||||
status: status ?? this.status,
|
||||
order: order ?? this.order,
|
||||
walletPoints: walletPoints ?? this.walletPoints,
|
||||
);
|
||||
}
|
||||
|
||||
/// 订单轮询:每 2 秒查一次,直至 status != 0 或超过 30 次(《03-接口文档》8.3)
|
||||
class OrderPollNotifier extends StateNotifier<OrderPollState> {
|
||||
OrderPollNotifier(RechargeOrderInfo order)
|
||||
: super(OrderPollState(order: order)) {
|
||||
_start();
|
||||
}
|
||||
|
||||
Timer? _timer;
|
||||
int _count = 0;
|
||||
static const _maxCount = 30;
|
||||
|
||||
void _start() {
|
||||
_timer = Timer.periodic(const Duration(seconds: 2), (_) => _poll());
|
||||
}
|
||||
|
||||
Future<void> _poll() async {
|
||||
if (!mounted) return;
|
||||
_count++;
|
||||
try {
|
||||
final info = await PaymentService.fetchOrder(state.order.orderNo);
|
||||
if (!mounted) return;
|
||||
if (info.isPaid) {
|
||||
_finish(OrderPollStatus.paid, walletPoints: info.walletPoints);
|
||||
} else if (!info.isPending) {
|
||||
_finish(OrderPollStatus.closed);
|
||||
} else if (_count >= _maxCount) {
|
||||
_finish(OrderPollStatus.timeout);
|
||||
}
|
||||
} catch (_) {
|
||||
// 网络抖动不立即失败,继续下一轮;累计超限由 _count 兜底
|
||||
if (_count >= _maxCount && mounted) _finish(OrderPollStatus.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
void _finish(OrderPollStatus status, {int? walletPoints}) {
|
||||
_timer?.cancel();
|
||||
state = state.copyWith(status: status, walletPoints: walletPoints);
|
||||
}
|
||||
|
||||
/// 取消订单(仅待支付):成功后结束轮询
|
||||
Future<void> cancel() async {
|
||||
_timer?.cancel();
|
||||
try {
|
||||
await PaymentService.cancelOrder(state.order.orderNo);
|
||||
if (mounted) state = state.copyWith(status: OrderPollStatus.closed);
|
||||
} catch (_) {
|
||||
// 取消失败恢复轮询
|
||||
if (mounted) _start();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 主动停止轮询(离开页面时)
|
||||
void stop() => _timer?.cancel();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final orderPollProvider = StateNotifierProvider.autoDispose
|
||||
.family<OrderPollNotifier, OrderPollState, RechargeOrderInfo>(
|
||||
(ref, order) => OrderPollNotifier(order),
|
||||
);
|
||||
Reference in New Issue
Block a user