| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- import 'dart:async';
- import 'package:decimal/decimal.dart';
- import 'package:flutter_riverpod/flutter_riverpod.dart';
- import '../core/network/dio_client.dart';
- import '../core/utils/dialog_utils.dart' show extractErrorMessage;
- import '../data/models/asset/account_auth.dart';
- import '../data/models/asset/deposit_wallet.dart';
- import '../data/models/asset/withdraw_balance.dart';
- import '../data/models/asset/withdraw_record.dart';
- import '../data/services/asset_service.dart';
- import '../data/services/withdraw_service.dart';
- // ══════════════════════════════════════════════════════════════
- // Withdraw State
- // ══════════════════════════════════════════════════════════════
- class WithdrawState {
- /// 0=链上提币, 1=内部转账
- final int tabIndex;
- /// 钱包列表(含网络信息,复用充币的 DepositWallet)
- final List<DepositWallet> usdtWallets;
- /// 当前选中的网络索引
- final int selectedNetworkIndex;
- /// 可用余额
- final WithdrawBalance? balance;
- /// 认证信息
- final AccountAuth? auth;
- /// 验证码倒计时秒数(0=可发送)
- final int codeCountdown;
- /// 提交中
- final bool isSubmitting;
- final bool isLoading;
- final String? errorMessage;
- /// 内部转账最小金额(USDT 基础配置,null 表示未加载)
- final Decimal? _transferMinAmount;
- const WithdrawState({
- this.tabIndex = 0,
- this.usdtWallets = const [],
- this.selectedNetworkIndex = -1,
- this.balance,
- this.auth,
- Decimal? transferMinAmount,
- this.codeCountdown = 0,
- this.isSubmitting = false,
- this.isLoading = false,
- this.errorMessage,
- }) : _transferMinAmount = transferMinAmount;
- /// 当前选中的钱包(链上提币模式下使用)
- DepositWallet? get selectedWallet =>
- selectedNetworkIndex >= 0 && usdtWallets.isNotEmpty && selectedNetworkIndex < usdtWallets.length
- ? usdtWallets[selectedNetworkIndex]
- : null;
- /// 当前币种信息
- WalletCoin? get currentCoin => selectedWallet?.coin;
- /// 网络名称列表
- List<String> get networkNames =>
- usdtWallets.map((w) => w.coin?.networkName ?? '').toList();
- /// 链上提币的 unit(币种代号)
- String get onChainUnit => currentCoin?.code ?? 'TUSDT';
- /// 手续费(withdrawFeeValue)
- Decimal get fee => currentCoin?.fee ?? Decimal.zero;
- /// 链上提币最小额
- Decimal get minWithdrawAmount => currentCoin?.minWithdrawAmount ?? Decimal.zero;
- /// 内部转账最小额(USDT 基础配置)
- Decimal get transferMinAmount => _transferMinAmount ?? Decimal.zero;
- /// 当前模式下的最小额(链上 or 内部转账)
- Decimal get currentMinAmount => tabIndex == 0 ? minWithdrawAmount : transferMinAmount;
- /// 可提现余额(链上提币)
- Decimal get withdrawableBalance => balance?.withdrawableBalance ?? Decimal.zero;
- /// 可转账余额(内部转账)
- Decimal get transferableBalance => balance?.transferableBalance ?? Decimal.zero;
- /// 当前可用余额(根据 Tab 决定)
- Decimal get availableBalance =>
- tabIndex == 0 ? withdrawableBalance : transferableBalance;
- /// 是否已绑定 Google 验证
- bool get isGoogleBound => auth?.isGoogleVerified ?? false;
- WithdrawState copyWith({
- int? tabIndex,
- List<DepositWallet>? usdtWallets,
- int? selectedNetworkIndex,
- WithdrawBalance? balance,
- AccountAuth? auth,
- Decimal? transferMinAmount,
- int? codeCountdown,
- bool? isSubmitting,
- bool? isLoading,
- String? errorMessage,
- }) =>
- WithdrawState(
- tabIndex: tabIndex ?? this.tabIndex,
- usdtWallets: usdtWallets ?? this.usdtWallets,
- selectedNetworkIndex: selectedNetworkIndex ?? this.selectedNetworkIndex,
- balance: balance ?? this.balance,
- auth: auth ?? this.auth,
- transferMinAmount: transferMinAmount ?? _transferMinAmount,
- codeCountdown: codeCountdown ?? this.codeCountdown,
- isSubmitting: isSubmitting ?? this.isSubmitting,
- isLoading: isLoading ?? this.isLoading,
- errorMessage: errorMessage,
- );
- }
- // ══════════════════════════════════════════════════════════════
- // Withdraw Notifier
- // ══════════════════════════════════════════════════════════════
- class WithdrawNotifier extends Notifier<WithdrawState> {
- Timer? _countdownTimer;
- @override
- WithdrawState build() {
- ref.onDispose(() => _countdownTimer?.cancel());
- Future.microtask(_load);
- return const WithdrawState(isLoading: true);
- }
- Future<void> _load() async {
- state = state.copyWith(isLoading: true, errorMessage: null, selectedNetworkIndex: -1);
- try {
- final dio = ref.read(dioClientProvider);
- final assetService = AssetService(dio);
- final withdrawService = WithdrawService(dio);
- // 并发请求:钱包地址 + 可用余额 + 认证信息 + 内部转账最小额
- final results = await Future.wait([
- assetService.getWalletAddresses(),
- withdrawService.getBalance('usdt'),
- withdrawService.getSecuritySetting(),
- withdrawService.getTransferMinAmount(),
- ]);
- final wallets = results[0] as List<DepositWallet>;
- final balance = results[1] as WithdrawBalance;
- final auth = results[2] as AccountAuth;
- final transferMinAmount = results[3] as Decimal;
- // 过滤 USDT 钱包,TRC20 排最前
- const networkOrder = {'TRC20': 0, 'ERC20': 1, 'BEP20': 2};
- final usdtWallets = wallets
- .where((w) => w.coin != null && w.coin!.code.contains('USDT'))
- .toList()
- ..sort((a, b) {
- final oa = networkOrder[a.coin!.networkName] ?? 99;
- final ob = networkOrder[b.coin!.networkName] ?? 99;
- return oa.compareTo(ob);
- });
- state = state.copyWith(
- usdtWallets: usdtWallets,
- balance: balance,
- auth: auth,
- transferMinAmount: transferMinAmount,
- selectedNetworkIndex: -1,
- isLoading: false,
- );
- } catch (e) {
- state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e));
- }
- }
- Future<void> refresh() => _load();
- void setTab(int index) {
- state = state.copyWith(tabIndex: index);
- }
- void selectNetwork(int index) {
- state = state.copyWith(selectedNetworkIndex: index);
- }
- /// 发送邮箱验证码,返回错误信息(null 表示成功)
- Future<String?> sendEmailCode({
- required String address,
- required String amount,
- }) async {
- if (state.tabIndex == 0 && state.selectedNetworkIndex < 0) return 'errSelectNetwork';
- if (address.isEmpty) return 'errEnterAddress';
- if (amount.isEmpty) return 'errEnterAmount';
- try {
- final dio = ref.read(dioClientProvider);
- await WithdrawService(dio).sendWithdrawEmailCode(
- unit: state.tabIndex == 1 ? 'USDT' : state.onChainUnit,
- address: address,
- amount: amount,
- );
- // 启动 60 秒倒计时
- state = state.copyWith(codeCountdown: 60);
- _countdownTimer?.cancel();
- _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
- final remaining = state.codeCountdown - 1;
- if (remaining <= 0) {
- timer.cancel();
- state = state.copyWith(codeCountdown: 0);
- } else {
- state = state.copyWith(codeCountdown: remaining);
- }
- });
- return null;
- } catch (e) {
- return extractErrorMessage(e);
- }
- }
- /// 链上提币提交
- Future<bool> submitOnChainWithdraw({
- required String address,
- required String amount,
- required String jyPassword,
- required String vcode,
- required String vcode2,
- }) async {
- state = state.copyWith(isSubmitting: true, errorMessage: null);
- try {
- final dio = ref.read(dioClientProvider);
- await WithdrawService(dio).withdrawApply(
- unit: state.onChainUnit,
- amount: amount,
- address: address,
- fee: state.fee.toString(),
- vcode: vcode,
- jyPassword: jyPassword,
- vcode2: vcode2,
- );
- state = state.copyWith(isSubmitting: false);
- // 刷新余额
- _load();
- return true;
- } catch (e) {
- state = state.copyWith(isSubmitting: false, errorMessage: extractErrorMessage(e));
- return false;
- }
- }
- /// 内部转账提交
- Future<bool> submitInternalTransfer({
- required String address,
- required String amount,
- required String jyPassword,
- required String vcode,
- required String vcode2,
- }) async {
- state = state.copyWith(isSubmitting: true, errorMessage: null);
- try {
- final dio = ref.read(dioClientProvider);
- await WithdrawService(dio).internalTransfer(
- unit: 'USDT', // 内部转账用币种名
- amount: amount,
- address: address,
- vcode: vcode,
- jyPassword: jyPassword,
- vcode2: vcode2,
- );
- state = state.copyWith(isSubmitting: false);
- _load();
- return true;
- } catch (e) {
- state = state.copyWith(isSubmitting: false, errorMessage: extractErrorMessage(e));
- return false;
- }
- }
- /// 表单校验
- String? validate({
- required String address,
- required String amount,
- required String jyPassword,
- required String vcode,
- required String vcode2,
- }) {
- if (state.tabIndex == 0 && state.selectedNetworkIndex < 0) return 'errSelectNetwork';
- if (address.isEmpty) return 'errEnterAddress';
- if (amount.isEmpty) return 'errEnterAmount';
- if (jyPassword.isEmpty) return 'errEnterFundPassword';
- if (vcode.isEmpty) return 'errEnterVerifyCode';
- if (!state.isGoogleBound) return 'errBindGoogleFirst';
- if (vcode2.isEmpty) return 'errEnterGoogleCode';
- final amountDecimal = Decimal.tryParse(amount);
- if (amountDecimal == null) return 'errAmountFormat';
- if (amountDecimal < state.currentMinAmount) {
- return state.tabIndex == 0
- ? 'errMinWithdraw:${state.currentMinAmount}'
- : 'errMinTransfer:${state.currentMinAmount}';
- }
- if (amountDecimal > state.availableBalance) {
- return 'errExceedBalance';
- }
- return null;
- }
- }
- final withdrawProvider = NotifierProvider<WithdrawNotifier, WithdrawState>(
- WithdrawNotifier.new,
- );
- // ══════════════════════════════════════════════════════════════
- // Withdraw History
- // ══════════════════════════════════════════════════════════════
- class WithdrawHistoryState {
- final List<WithdrawRecord> records;
- final bool isLoading;
- final bool hasMore;
- /// 链上提币当前页(后端 page 从 0 开始)
- final int withdrawPage;
- /// 内部转账当前页(后端 pageNo 从 0 开始)
- final int transferPage;
- final String? errorMessage;
- const WithdrawHistoryState({
- this.records = const [],
- this.isLoading = false,
- this.hasMore = true,
- this.withdrawPage = 0,
- this.transferPage = 0,
- this.errorMessage,
- });
- WithdrawHistoryState copyWith({
- List<WithdrawRecord>? records,
- bool? isLoading,
- bool? hasMore,
- int? withdrawPage,
- int? transferPage,
- String? errorMessage,
- }) =>
- WithdrawHistoryState(
- records: records ?? this.records,
- isLoading: isLoading ?? this.isLoading,
- hasMore: hasMore ?? this.hasMore,
- withdrawPage: withdrawPage ?? this.withdrawPage,
- transferPage: transferPage ?? this.transferPage,
- errorMessage: errorMessage,
- );
- }
- class WithdrawHistoryNotifier extends Notifier<WithdrawHistoryState> {
- static const _pageSize = 10;
- @override
- WithdrawHistoryState build() {
- Future.microtask(_loadFirst);
- return const WithdrawHistoryState(isLoading: true);
- }
- Future<void> _loadFirst() async {
- state = state.copyWith(isLoading: true, errorMessage: null);
- try {
- final service = WithdrawService(ref.read(dioClientProvider));
- final results = await Future.wait([
- service.getWithdrawRecords(page: 0, pageSize: _pageSize),
- service.getTransferRecords(pageNo: 0, pageSize: _pageSize),
- ]);
- final withdrawRecords = results[0] as List<WithdrawRecord>;
- final transferRecords = (results[1] as List<WithdrawRecord>)
- .map((r) => r.copyWith(isTransfer: true))
- .toList();
- final all = _merge(withdrawRecords, transferRecords);
- state = state.copyWith(
- records: all,
- isLoading: false,
- withdrawPage: 0,
- transferPage: 0,
- hasMore: withdrawRecords.length >= _pageSize ||
- transferRecords.length >= _pageSize,
- );
- } catch (e) {
- state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e));
- }
- }
- Future<void> refresh() => _loadFirst();
- Future<void> loadMore() async {
- if (state.isLoading || !state.hasMore) return;
- state = state.copyWith(isLoading: true);
- try {
- final service = WithdrawService(ref.read(dioClientProvider));
- final nextW = state.withdrawPage + 1;
- final nextT = state.transferPage + 1;
- final results = await Future.wait([
- service.getWithdrawRecords(page: nextW, pageSize: _pageSize),
- service.getTransferRecords(pageNo: nextT, pageSize: _pageSize),
- ]);
- final withdrawRecords = results[0] as List<WithdrawRecord>;
- final transferRecords = (results[1] as List<WithdrawRecord>)
- .map((r) => r.copyWith(isTransfer: true))
- .toList();
- final appended = _merge(withdrawRecords, transferRecords);
- state = state.copyWith(
- records: [...state.records, ...appended],
- isLoading: false,
- withdrawPage: nextW,
- transferPage: nextT,
- hasMore: withdrawRecords.length >= _pageSize ||
- transferRecords.length >= _pageSize,
- );
- } catch (e) {
- state = state.copyWith(isLoading: false, errorMessage: extractErrorMessage(e));
- }
- }
- /// 合并两类记录,按 createTime 倒序
- List<WithdrawRecord> _merge(
- List<WithdrawRecord> withdrawRecords,
- List<WithdrawRecord> transferRecords,
- ) {
- final all = [...withdrawRecords, ...transferRecords];
- all.sort((a, b) => b.createTime.compareTo(a.createTime));
- return all;
- }
- /// 取消链上提现
- Future<bool> cancelWithdraw(String id) async {
- try {
- final dio = ref.read(dioClientProvider);
- await WithdrawService(dio).cancelWithdraw(id);
- await refresh();
- return true;
- } catch (e) {
- state = state.copyWith(errorMessage: extractErrorMessage(e));
- return false;
- }
- }
- }
- final withdrawHistoryProvider =
- NotifierProvider<WithdrawHistoryNotifier, WithdrawHistoryState>(
- WithdrawHistoryNotifier.new,
- );
|