225 lines
6.3 KiB
Dart
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),
|
|
);
|