Files
video/app/lib/providers/payment_provider.dart
T
2026-10-15 20:36:59 +08:00

225 lines
6.3 KiB
Dart

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),
);