diff --git a/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill b/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill index 9851cd8..f2219b4 100644 Binary files a/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill and b/flutter_monisuo/build/99111e0c5b6228829e100ef67db14ea2.cache.dill.track.dill differ diff --git a/flutter_monisuo/lib/core/constants/api_endpoints.dart b/flutter_monisuo/lib/core/constants/api_endpoints.dart index e705727..bac0c4e 100644 --- a/flutter_monisuo/lib/core/constants/api_endpoints.dart +++ b/flutter_monisuo/lib/core/constants/api_endpoints.dart @@ -89,6 +89,9 @@ class ApiEndpoints { /// 取消订单 static const String cancelOrder = '/api/fund/cancel'; + /// 获取可用提现网络列表 + static const String walletNetworks = '/api/wallet/networks'; + /// 获取充提记录 static const String fundOrders = '/api/fund/orders'; diff --git a/flutter_monisuo/lib/data/models/order_models.dart b/flutter_monisuo/lib/data/models/order_models.dart index 8ae736a..b31f73a 100644 --- a/flutter_monisuo/lib/data/models/order_models.dart +++ b/flutter_monisuo/lib/data/models/order_models.dart @@ -82,6 +82,7 @@ class OrderFund { // 提现状态: 1=待审批, 2=已完成, 3=已驳回, 4=已取消, 5=待财务审核 final int? walletId; // 冷钱包ID(充值) final String? walletAddress; // 钱包地址(充值/提现) + final String? network; // 提现网络类型 final String? withdrawContact; // 提现联系方式 final String remark; final String? rejectReason; @@ -102,6 +103,7 @@ class OrderFund { required this.status, this.walletId, this.walletAddress, + this.network, this.withdrawContact, required this.remark, this.rejectReason, @@ -124,6 +126,7 @@ class OrderFund { status: json['status'] as int? ?? 1, walletId: json['walletId'] as int?, walletAddress: json['walletAddress'] as String?, + network: json['network'] as String?, withdrawContact: json['withdrawContact'] as String?, remark: json['remark']?.toString() ?? '', rejectReason: json['rejectReason'] as String?, @@ -167,7 +170,7 @@ class OrderFund { case 1: return '待审批'; case 2: - return '已完成'; + return '已出款'; case 3: return '已驳回'; case 4: diff --git a/flutter_monisuo/lib/data/services/fund_service.dart b/flutter_monisuo/lib/data/services/fund_service.dart index 155fd00..c44124f 100644 --- a/flutter_monisuo/lib/data/services/fund_service.dart +++ b/flutter_monisuo/lib/data/services/fund_service.dart @@ -51,6 +51,7 @@ class FundService { Future>> withdraw({ required String amount, required String withdrawAddress, + String? network, String? withdrawContact, String? remark, }) async { @@ -59,12 +60,27 @@ class FundService { data: { 'amount': amount, 'withdrawAddress': withdrawAddress, + if (network != null) 'network': network, if (withdrawContact != null) 'withdrawContact': withdrawContact, if (remark != null) 'remark': remark, }, ); } + /// 获取可用的提现网络列表 + Future>> getWalletNetworks() async { + final response = await _client.get>( + ApiEndpoints.walletNetworks, + ); + if (response.success && response.data != null) { + return ApiResponse.success( + response.data!.cast(), + response.message, + ); + } + return ApiResponse.fail(response.message ?? '获取网络列表失败'); + } + /// 取消订单 Future> cancelOrder(String orderNo) async { return _client.post( diff --git a/flutter_monisuo/lib/providers/asset_provider.dart b/flutter_monisuo/lib/providers/asset_provider.dart index dc731b9..65c6e9c 100644 --- a/flutter_monisuo/lib/providers/asset_provider.dart +++ b/flutter_monisuo/lib/providers/asset_provider.dart @@ -185,6 +185,7 @@ class AssetProvider extends ChangeNotifier { Future>> withdraw({ required String amount, required String withdrawAddress, + String? network, String? withdrawContact, String? remark, }) async { @@ -192,6 +193,7 @@ class AssetProvider extends ChangeNotifier { final response = await _fundService.withdraw( amount: amount, withdrawAddress: withdrawAddress, + network: network, withdrawContact: withdrawContact, remark: remark, ); @@ -244,6 +246,19 @@ class AssetProvider extends ChangeNotifier { } } + /// 获取可用提现网络列表 + Future> getWalletNetworks() async { + try { + final response = await _fundService.getWalletNetworks(); + if (response.success) { + return response.data ?? []; + } + return []; + } catch (_) { + return []; + } + } + /// 刷新所有资产数据 Future refreshAll({bool force = false}) async { await Future.wait([ diff --git a/flutter_monisuo/lib/ui/pages/asset/asset_page.dart b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart index 3f1a6fa..2f4c88f 100644 --- a/flutter_monisuo/lib/ui/pages/asset/asset_page.dart +++ b/flutter_monisuo/lib/ui/pages/asset/asset_page.dart @@ -926,6 +926,8 @@ void _showWithdrawDialog(BuildContext context, String? balance) { final formKey = GlobalKey(); final feeNotifier = ValueNotifier('提现将扣除10%手续费'); final colorScheme = Theme.of(context).colorScheme; + final networksNotifier = ValueNotifier>([]); + final selectedNetworkNotifier = ValueNotifier(null); amountController.addListener(() { final amount = double.tryParse(amountController.text) ?? 0; @@ -938,211 +940,261 @@ void _showWithdrawDialog(BuildContext context, String? balance) { } }); + // 获取网络列表 + context.read().getWalletNetworks().then((list) { + networksNotifier.value = list; + if (list.isNotEmpty) { + selectedNetworkNotifier.value = list.first; + } + }); + showShadDialog( context: context, - builder: (ctx) => Dialog( - backgroundColor: Colors.transparent, - child: GlassPanel( - borderRadius: BorderRadius.circular(AppRadius.lg), - padding: EdgeInsets.all(AppSpacing.lg), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: EdgeInsets.all(AppSpacing.sm), - decoration: BoxDecoration( - color: colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(AppRadius.md), + builder: (ctx) => StatefulBuilder( + builder: (ctx, setState) => Dialog( + backgroundColor: Colors.transparent, + child: GlassPanel( + borderRadius: BorderRadius.circular(AppRadius.lg), + padding: EdgeInsets.all(AppSpacing.lg), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(AppRadius.md), + ), + child: Icon( + LucideIcons.wallet, + color: colorScheme.primary, + ), ), - child: Icon( - LucideIcons.wallet, - color: colorScheme.primary, + SizedBox(width: AppSpacing.sm), + Text( + '提现 (Withdraw)', + style: GoogleFonts.spaceGrotesk( + fontSize: 16, + fontWeight: FontWeight.bold, + color: colorScheme.onSurface, + ), ), + ], + ), + SizedBox(height: AppSpacing.xs), + Text( + 'Securely transfer your assets to an external wallet address.', + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, ), - SizedBox(width: AppSpacing.sm), - Text( - '提现 (Withdraw)', - style: GoogleFonts.spaceGrotesk( - fontSize: 16, - fontWeight: FontWeight.bold, - color: colorScheme.onSurface, + ), + if (balance != null) ...[ + SizedBox(height: AppSpacing.md), + Container( + padding: EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + decoration: BoxDecoration( + color: AppColorScheme.up.withOpacity(0.1), + borderRadius: BorderRadius.circular(AppRadius.full), + border: Border.all( + color: AppColorScheme.up.withOpacity(0.2), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '可用余额: ', + style: TextStyle( + fontSize: 10, + letterSpacing: 0.1, + color: colorScheme.onSurfaceVariant, + ), + ), + Text( + '$balance USDT', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: AppColorScheme.up, + ), + ), + ], ), ), ], - ), - SizedBox(height: AppSpacing.xs), - Text( - 'Securely transfer your assets to an external wallet address.', - style: TextStyle( - fontSize: 12, - color: colorScheme.onSurfaceVariant, - ), - ), - if (balance != null) ...[ - SizedBox(height: AppSpacing.md), - Container( - padding: EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), - decoration: BoxDecoration( - color: AppColorScheme.up.withOpacity(0.1), - borderRadius: BorderRadius.circular(AppRadius.full), - border: Border.all( - color: AppColorScheme.up.withOpacity(0.2), - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, + SizedBox(height: AppSpacing.lg), + ShadForm( + key: formKey, + child: Column( children: [ - Text( - '可用余额: ', - style: TextStyle( - fontSize: 10, - letterSpacing: 0.1, - color: colorScheme.onSurfaceVariant, + ShadInputFormField( + id: 'amount', + controller: amountController, + label: const Text('提现金额'), + placeholder: const Text('请输入提现金额(USDT)'), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: Validators.amount, + ), + SizedBox(height: AppSpacing.xs), + Container( + padding: EdgeInsets.symmetric( + horizontal: AppSpacing.md, + vertical: AppSpacing.sm, + ), + decoration: BoxDecoration( + color: Colors.orange.withOpacity(0.1), + borderRadius: BorderRadius.circular(AppRadius.md), + border: Border.all(color: Colors.orange.withOpacity(0.3)), + ), + child: Row( + children: [ + Icon(Icons.info_outline, size: 14, color: Colors.orange), + SizedBox(width: AppSpacing.xs), + Expanded( + child: ValueListenableBuilder( + valueListenable: feeNotifier, + builder: (_, text, __) => Text( + text, + style: TextStyle( + fontSize: 11, + color: Colors.orange.shade800, + ), + ), + ), + ), + ], ), ), - Text( - '$balance USDT', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: AppColorScheme.up, - ), + SizedBox(height: AppSpacing.md), + // 提现网络选择 + ValueListenableBuilder>( + valueListenable: networksNotifier, + builder: (_, networks, __) { + if (networks.isEmpty) return const SizedBox.shrink(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '提现网络', + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + SizedBox( + width: double.infinity, + child: ValueListenableBuilder( + valueListenable: selectedNetworkNotifier, + builder: (_, selected, __) => ShadSelect( + initialValue: selected ?? networks.first, + placeholder: const Text('选择提现网络'), + onChanged: (value) { + selectedNetworkNotifier.value = value; + }, + selectedOptionBuilder: (context, value) => Text(value), + options: networks.map((network) => ShadOption( + value: network, + child: Text(network), + )), + ), + ), + ), + ], + ); + }, + ), + SizedBox(height: AppSpacing.md), + ShadInputFormField( + id: 'address', + controller: addressController, + label: const Text('目标地址'), + placeholder: const Text('请输入提现地址'), + validator: (v) => Validators.required(v, '提现地址'), + ), + SizedBox(height: AppSpacing.md), + ShadInputFormField( + id: 'contact', + controller: contactController, + label: const Text('联系方式(可选)'), + placeholder: const Text('联系方式'), ), ], ), ), - ], - SizedBox(height: AppSpacing.lg), - ShadForm( - key: formKey, - child: Column( + SizedBox(height: AppSpacing.lg), + Row( children: [ - ShadInputFormField( - id: 'amount', - controller: amountController, - label: const Text('提现金额'), - placeholder: const Text('请输入提现金额(USDT)'), - keyboardType: const TextInputType.numberWithOptions(decimal: true), - validator: Validators.amount, - ), - SizedBox(height: AppSpacing.xs), - Container( - padding: EdgeInsets.symmetric( - horizontal: AppSpacing.md, - vertical: AppSpacing.sm, - ), - decoration: BoxDecoration( - color: Colors.orange.withOpacity(0.1), - borderRadius: BorderRadius.circular(AppRadius.md), - border: Border.all(color: Colors.orange.withOpacity(0.3)), - ), - child: Row( - children: [ - Icon(Icons.info_outline, size: 14, color: Colors.orange), - SizedBox(width: AppSpacing.xs), - Expanded( - child: ValueListenableBuilder( - valueListenable: feeNotifier, - builder: (_, text, __) => Text( - text, - style: TextStyle( - fontSize: 11, - color: Colors.orange.shade800, - ), - ), - ), - ), - ], + Expanded( + child: NeonButton( + text: '取消', + type: NeonButtonType.outline, + onPressed: () => Navigator.of(ctx).pop(), + height: 44, + showGlow: false, ), ), - SizedBox(height: AppSpacing.md), - ShadInputFormField( - id: 'address', - controller: addressController, - label: const Text('目标地址'), - placeholder: const Text('请输入提现地址'), - validator: (v) => Validators.required(v, '提现地址'), - ), - SizedBox(height: AppSpacing.md), - ShadInputFormField( - id: 'contact', - controller: contactController, - label: const Text('联系方式(可选)'), - placeholder: const Text('联系方式'), + SizedBox(width: AppSpacing.sm), + Expanded( + child: NeonButton( + text: '提交', + type: NeonButtonType.primary, + onPressed: () async { + if (formKey.currentState!.saveAndValidate()) { + Navigator.of(ctx).pop(); + final response = await context.read().withdraw( + amount: amountController.text, + withdrawAddress: addressController.text, + network: selectedNetworkNotifier.value, + withdrawContact: contactController.text.isNotEmpty + ? contactController.text + : null, + ); + if (context.mounted) { + _showResultDialog( + context, + response.success ? '申请成功' : '申请失败', + response.success ? '请等待管理员审批' : response.message, + ); + } + } + }, + height: 44, + showGlow: true, + ), ), ], ), - ), - SizedBox(height: AppSpacing.lg), - Row( - children: [ - Expanded( - child: NeonButton( - text: '取消', - type: NeonButtonType.outline, - onPressed: () => Navigator.of(ctx).pop(), - height: 44, - showGlow: false, - ), - ), - SizedBox(width: AppSpacing.sm), - Expanded( - child: NeonButton( - text: '提交', - type: NeonButtonType.primary, - onPressed: () async { - if (formKey.currentState!.saveAndValidate()) { - Navigator.of(ctx).pop(); - final response = await context.read().withdraw( - amount: amountController.text, - withdrawAddress: addressController.text, - withdrawContact: contactController.text.isNotEmpty - ? contactController.text - : null, - ); - if (context.mounted) { - _showResultDialog( - context, - response.success ? '申请成功' : '申请失败', - response.success ? '请等待管理员审批' : response.message, - ); - } - } - }, - height: 44, - showGlow: true, - ), - ), - ], - ), - SizedBox(height: AppSpacing.md), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.verified_user, - size: 12, - color: colorScheme.onSurfaceVariant.withOpacity(0.5), - ), - SizedBox(width: AppSpacing.xs), - Text( - 'End-to-End Encrypted Transaction', - style: TextStyle( - fontSize: 10, - letterSpacing: 0.1, + SizedBox(height: AppSpacing.md), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.verified_user, + size: 12, color: colorScheme.onSurfaceVariant.withOpacity(0.5), ), - ), - ], - ), - ], + SizedBox(width: AppSpacing.xs), + Text( + 'End-to-End Encrypted Transaction', + style: TextStyle( + fontSize: 10, + letterSpacing: 0.1, + color: colorScheme.onSurfaceVariant.withOpacity(0.5), + ), + ), + ], + ), + ], + ), ), ), ), diff --git a/flutter_monisuo/lib/ui/pages/orders/fund_order_card.dart b/flutter_monisuo/lib/ui/pages/orders/fund_order_card.dart index 723a718..f3ffaeb 100644 --- a/flutter_monisuo/lib/ui/pages/orders/fund_order_card.dart +++ b/flutter_monisuo/lib/ui/pages/orders/fund_order_card.dart @@ -68,7 +68,7 @@ class _FundOrderCard extends StatelessWidget { case 1: return '待审批'; case 2: - return '已完成'; + return '已出款'; case 3: return '已驳回'; case 4: diff --git a/flutter_monisuo/lib/ui/pages/orders/fund_orders_page.dart b/flutter_monisuo/lib/ui/pages/orders/fund_orders_page.dart index 656f608..79e1201 100644 --- a/flutter_monisuo/lib/ui/pages/orders/fund_orders_page.dart +++ b/flutter_monisuo/lib/ui/pages/orders/fund_orders_page.dart @@ -278,6 +278,34 @@ class _FundOrdersPageState extends State { style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], + // 提现详情 + if (!isDeposit) ...[ + if (order.fee != null) ...[ + const SizedBox(height: 4), + Row( + children: [ + const Text('手续费(10%): ', style: TextStyle(fontSize: 12, color: Colors.grey)), + Text('-${order.fee} USDT', style: const TextStyle(fontSize: 12)), + ], + ), + ], + if (order.receivableAmount != null) ...[ + const SizedBox(height: 4), + Row( + children: [ + const Text('到账金额: ', style: TextStyle(fontSize: 12, color: Colors.grey)), + Text('${order.receivableAmount} USDT', style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold)), + ], + ), + ], + if (order.network != null) ...[ + const SizedBox(height: 4), + Text( + '提现网络: ${order.network}', + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ], const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/monisuo-admin/src/pages/monisuo/dashboard.vue b/monisuo-admin/src/pages/monisuo/dashboard.vue index 5992811..1289dee 100644 --- a/monisuo-admin/src/pages/monisuo/dashboard.vue +++ b/monisuo-admin/src/pages/monisuo/dashboard.vue @@ -16,151 +16,191 @@ const isLoading = computed(() => overviewLoading.value || cashFlowLoading.value) const overview = computed(() => overviewData.value?.data) const cashFlow = computed(() => cashFlowData.value?.data || []) -// ========== 模块1: 资金概览 ========== -const fundMetrics = computed(() => [ - { - label: '在管资金', - value: overview.value?.fundBalance || 0, - icon: 'lucide:wallet', - color: 'text-blue-600', - bgColor: 'bg-blue-50 dark:bg-blue-950', - }, - { - label: '交易账户', - value: overview.value?.tradeValue || 0, - icon: 'lucide:bar-chart-3', - color: 'text-purple-600', - bgColor: 'bg-purple-50 dark:bg-purple-950', - }, - { - label: '总资产', - value: (overview.value?.fundBalance || 0) + (overview.value?.tradeValue || 0), - icon: 'lucide:landmark', - color: 'text-orange-600', - bgColor: 'bg-orange-50 dark:bg-orange-950', - }, -]) +// ========== 工具函数 ========== -// ========== 模块2: 资金流动 ========== -function calcGrowthRate(current: number, previous: number): string { - if (previous === 0) - return current > 0 ? '+100%' : '0%' - const rate = new Decimal(current).minus(previous).div(previous).mul(100).toDecimalPlaces(1) - const sign = rate.gte(0) ? '+' : '' - return `{sign}{rate}%` +function formatCurrency(value: number | undefined): string { + const v = value || 0 + if (v >= 100_000_000) + return `${(v / 100_000_000).toFixed(2)}亿` + if (v >= 10_000) + return `${(v / 10_000).toFixed(1)}万` + return v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) } -const flowMetrics = computed(() => { - const flow = cashFlow.value - const len = flow.length - const thisMonth = len >= 1 ? flow[len - 1] : null - const lastMonth = len >= 2 ? flow[len - 2] : null +function calcGrowthRate(current: number, previous: number): { text: string, up: boolean } { + if (previous === 0) { + return current > 0 ? { text: '+100%', up: true } : { text: '0%', up: true } + } + const rate = new Decimal(current).minus(previous).div(previous).mul(100).toDecimalPlaces(1) + const sign = rate.gte(0) ? '+' : '' + return { text: `${sign}${rate}%`, up: rate.gte(0) } +} - const depositTrend = thisMonth && lastMonth - ? calcGrowthRate(thisMonth.deposit as number, lastMonth.deposit as number) - : '+0%' - const withdrawTrend = thisMonth && lastMonth - ? calcGrowthRate(thisMonth.withdraw as number, lastMonth.withdraw as number) - : '+0%' - const netInflowTrend = thisMonth && lastMonth - ? calcGrowthRate(thisMonth.netInflow as number, lastMonth.netInflow as number) - : '+0%' +// ========== 第一层:核心 KPI ========== + +const kpiItems = computed(() => { + const o = overview.value + if (!o) return [] + + const netInflow = (o.totalDeposit || 0) - (o.totalWithdraw || 0) + const depositGrowth = calcGrowthRate(o.monthlyDeposit, o.lastMonthDeposit) + const withdrawGrowth = calcGrowthRate(o.monthlyWithdraw, o.lastMonthWithdraw) + + // 净流入环比用充值环比近似(实际出款与提现趋势一致) + const netMonthGrowth = calcGrowthRate( + o.monthlyDeposit - o.monthlyWithdraw, + o.lastMonthDeposit - o.lastMonthWithdraw, + ) return [ { label: '累计充值', - value: overview.value?.totalDeposit || 0, - icon: 'lucide:arrow-down-circle', - color: 'text-green-600', - bgColor: 'bg-green-50 dark:bg-green-950', - trend: depositTrend, + value: o.totalDeposit, + icon: 'lucide:arrow-down-to-line', + color: 'text-emerald-600', + bgColor: 'bg-emerald-50 dark:bg-emerald-950/40', + growth: depositGrowth, }, { label: '累计提现', - value: overview.value?.totalWithdraw || 0, - icon: 'lucide:arrow-up-circle', - color: 'text-red-600', - bgColor: 'bg-red-50 dark:bg-red-950', - trend: withdrawTrend, + value: o.totalWithdraw, + icon: 'lucide:arrow-up-from-line', + color: 'text-red-500', + bgColor: 'bg-red-50 dark:bg-red-950/40', + growth: withdrawGrowth, + }, + { + label: '实际出款', + value: o.totalActualPayout, + icon: 'lucide:banknote', + color: 'text-amber-600', + bgColor: 'bg-amber-50 dark:bg-amber-950/40', + growth: withdrawGrowth, }, { label: '净流入', - value: (overview.value?.totalDeposit || 0) - (overview.value?.totalWithdraw || 0), + value: netInflow, icon: 'lucide:trending-up', - color: 'text-emerald-600', - bgColor: 'bg-emerald-50 dark:bg-emerald-950', - trend: netInflowTrend, + color: 'text-sky-600', + bgColor: 'bg-sky-50 dark:bg-sky-950/40', + growth: netMonthGrowth, }, ] }) -// ========== 模块3: 资金趋势图 ========== +// ========== 第二层:资金趋势图 ========== + const trendChartOption = computed(() => ({ - tooltip: { trigger: 'axis' }, - legend: { data: ['充值', '提现'], bottom: 0, top: 'auto' }, - grid: { left: '3%', right: '4%', bottom: '15%', top: '5%', containLabel: true }, + tooltip: { + trigger: 'axis', + formatter(params: any) { + const items = Array.isArray(params) ? params : [params] + const lines = items.map((p: any) => + `${p.marker} ${p.seriesName}: USDT ${Number(p.value).toLocaleString()}`, + ) + return `
${items[0].axisValue}
${lines.join('
')}
` + }, + }, + legend: { data: ['充值', '提现'], bottom: 0 }, + grid: { left: '3%', right: '4%', bottom: '14%', top: '8%', containLabel: true }, xAxis: { type: 'category', data: cashFlow.value.map((t: any) => t.month), + axisTick: { show: false }, + axisLine: { lineStyle: { color: '#e5e7eb' } }, }, yAxis: { type: 'value', - axisLabel: { formatter: 'USDT{value}K' }, + axisLabel: { + formatter(value: number) { + if (value >= 10_000) return `${(value / 10_000).toFixed(0)}万` + return `${value}` + }, + }, + splitLine: { lineStyle: { color: '#f3f4f6', type: 'dashed' } }, }, series: [ { name: '充值', type: 'line', smooth: true, + symbol: 'circle', + symbolSize: 6, data: cashFlow.value.map((t: any) => t.deposit), itemStyle: { color: '#10b981' }, - areaStyle: { color: 'rgba(16, 185, 129, 0.1)' }, + areaStyle: { + color: { + type: 'linear', x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: 'rgba(16, 185, 129, 0.25)' }, + { offset: 1, color: 'rgba(16, 185, 129, 0.02)' }, + ], + }, + }, }, { name: '提现', type: 'line', smooth: true, + symbol: 'circle', + symbolSize: 6, data: cashFlow.value.map((t: any) => t.withdraw), itemStyle: { color: '#ef4444' }, - areaStyle: { color: 'rgba(239, 68, 68, 0.1)' }, + areaStyle: { + color: { + type: 'linear', x: 0, y: 0, x2: 0, y2: 1, + colorStops: [ + { offset: 0, color: 'rgba(239, 68, 68, 0.25)' }, + { offset: 1, color: 'rgba(239, 68, 68, 0.02)' }, + ], + }, + }, }, ], })) -// ========== 模块4: 资金分布 ========== -const distributionOption = computed(() => { - const fundBalance = overview.value?.fundBalance || 0 - const tradeValue = overview.value?.tradeValue || 0 +// ========== 第二层:资产状态 ========== - return { - tooltip: { trigger: 'item', formatter: '{b}: {d}%' }, - legend: { orient: 'vertical', right: '5%', top: 'center' }, - series: [{ - type: 'pie', - radius: ['50%', '75%'], - center: ['35%', '50%'], - avoidLabelOverlap: false, - itemStyle: { borderRadius: 8, borderColor: '#fff', borderWidth: 2 }, - label: { show: true, position: 'inside', formatter: '{d}%', fontSize: 12 }, - data: [ - { value: fundBalance, name: '在管资金', itemStyle: { color: '#3b82f6' } }, - { value: tradeValue, name: '交易账户', itemStyle: { color: '#8b5cf6' } }, - ], - }], - } +const assetMetrics = computed(() => { + const o = overview.value + if (!o) return [] + const totalAssets = (o.fundBalance || 0) + (o.tradeValue || 0) + + return [ + { + label: '平台总资产', + value: totalAssets, + icon: 'lucide:landmark', + color: 'text-violet-600', + }, + { + label: '在管资金', + value: o.fundBalance, + icon: 'lucide:wallet', + color: 'text-blue-600', + }, + { + label: '冻结中', + value: o.totalFrozen, + icon: 'lucide:lock', + color: 'text-slate-500', + }, + ] }) -// ========== 模块5: 运营指标 ========== -const operationMetrics = computed(() => [ - { label: '用户总数', value: overview.value?.userCount || 0, icon: 'lucide:users' }, - { label: '待审批', value: overview.value?.pendingCount || 0, icon: 'lucide:clock' }, -]) +// ========== 第三层:运营快报 ========== -function formatCurrency(value: number): string { - if (value >= 10000) - return `USDT{(value / 10000).toFixed(1)}万` - return `USDT{value.toLocaleString()}` -} +const operationMetrics = computed(() => { + const o = overview.value + if (!o) return [] + + return [ + { label: '用户总数', value: o.userCount, icon: 'lucide:users', color: 'text-blue-600', bgColor: 'bg-blue-50 dark:bg-blue-950/40' }, + { label: '今日活跃', value: o.todayActiveUsers, icon: 'lucide:activity', color: 'text-green-600', bgColor: 'bg-green-50 dark:bg-green-950/40' }, + { label: '本月新增', value: o.monthNewUsers, icon: 'lucide:user-plus', color: 'text-purple-600', bgColor: 'bg-purple-50 dark:bg-purple-950/40' }, + { label: '待审批', value: o.pendingCount, icon: 'lucide:clock', color: o.pendingCount > 0 ? 'text-amber-600' : 'text-slate-500', bgColor: o.pendingCount > 0 ? 'bg-amber-50 dark:bg-amber-950/40' : 'bg-slate-50 dark:bg-slate-950/40' }, + ] +}) function navigateTo(path: string) { router.push(path) @@ -168,126 +208,114 @@ function navigateTo(path: string) { + + +
创建时间
diff --git a/monisuo-admin/src/services/api/monisuo-admin.api.ts b/monisuo-admin/src/services/api/monisuo-admin.api.ts index 16bbbad..091fb05 100644 --- a/monisuo-admin/src/services/api/monisuo-admin.api.ts +++ b/monisuo-admin/src/services/api/monisuo-admin.api.ts @@ -60,9 +60,10 @@ export interface OrderFund { amount: number fee?: number // 手续费 receivableAmount?: number // 应收款项 - status: number // 充值: 1待付款 2待确认 3已完成 4已驳回 5已取消; 提现: 1待审批 2已完成 3已驳回 4已取消 5待财务审核 + status: number // 充值: 1待付款 2待确认 3已完成 4已驳回 5已取消; 提现: 1待审批 2已出款 3已驳回 4已取消 5待财务审核 walletId?: number walletAddress?: string + network?: string // 提现网络类型 withdrawContact?: string payTime?: string confirmTime?: string @@ -88,12 +89,24 @@ export interface ColdWallet { } export interface FinanceOverview { + // 核心 KPI totalDeposit: number totalWithdraw: number + totalActualPayout: number // 实际出款金额 + // 资金状态 fundBalance: number + totalFrozen: number // 冻结中金额 tradeValue: number + // 运营数据 pendingCount: number userCount: number + monthNewUsers: number // 本月新增用户 + todayActiveUsers: number // 今日活跃用户 + // 环比数据 + monthlyDeposit: number + monthlyWithdraw: number + lastMonthDeposit: number + lastMonthWithdraw: number } // Auth API diff --git a/src/main/java/com/it/rattan/monisuo/controller/AdminController.java b/src/main/java/com/it/rattan/monisuo/controller/AdminController.java index 33fb144..720dce3 100644 --- a/src/main/java/com/it/rattan/monisuo/controller/AdminController.java +++ b/src/main/java/com/it/rattan/monisuo/controller/AdminController.java @@ -8,11 +8,13 @@ import com.it.rattan.monisuo.context.UserContext; import com.it.rattan.monisuo.entity.*; import com.it.rattan.monisuo.mapper.AccountFundMapper; import com.it.rattan.monisuo.mapper.OrderFundMapper; +import com.it.rattan.monisuo.mapper.OrderTradeMapper; import com.it.rattan.monisuo.service.*; import com.it.rattan.monisuo.util.JwtUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.math.BigDecimal; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; @@ -46,6 +48,9 @@ public class AdminController { @Autowired private OrderFundMapper orderFundMapper; + @Autowired + private OrderTradeMapper orderTradeMapper; + // ==================== 公开接口 ==================== /** @@ -451,7 +456,7 @@ public class AdminController { } /** - * 资金总览 + * 资金总览(看板专用,一次 API 返回所有数据) */ @GetMapping("/finance/overview") public Result> getFinanceOverview() { @@ -461,21 +466,47 @@ public class AdminController { Map data = new HashMap<>(); - // 合并为一次查询获取充值总额、提现总额、待审批数 - Map fundStats = orderFundMapper.sumFinanceOverview(); + // 本月和上月时间范围(用于环比) + LocalDate today = LocalDate.now(); + LocalDateTime monthStart = today.withDayOfMonth(1).atStartOfDay(); + LocalDateTime monthEnd = today.plusMonths(1).withDayOfMonth(1).atStartOfDay(); + LocalDateTime lastMonthStart = today.minusMonths(1).withDayOfMonth(1).atStartOfDay(); + LocalDateTime lastMonthEnd = monthStart; + + // 查询1:order_fund 一次聚合(充值总额、提现总额、实际出款、待审批、环比) + Map fundStats = orderFundMapper.sumDashboardStats( + monthStart, monthEnd, lastMonthStart, lastMonthEnd); data.put("totalDeposit", fundStats.get("totalDeposit")); data.put("totalWithdraw", fundStats.get("totalWithdraw")); + data.put("totalActualPayout", fundStats.get("totalActualPayout")); data.put("pendingCount", ((Number) fundStats.get("pendingCount")).intValue()); + data.put("monthlyDeposit", fundStats.get("monthlyDeposit")); + data.put("monthlyWithdraw", fundStats.get("monthlyWithdraw")); + data.put("lastMonthDeposit", fundStats.get("lastMonthDeposit")); + data.put("lastMonthWithdraw", fundStats.get("lastMonthWithdraw")); - BigDecimal fundBalance = accountFundMapper.sumAllBalance(); - data.put("fundBalance", fundBalance); + // 查询2:account_fund 余额和冻结 + Map balanceStats = accountFundMapper.sumBalanceAndFrozen(); + data.put("fundBalance", balanceStats.get("totalBalance")); + data.put("totalFrozen", balanceStats.get("totalFrozen")); + // 查询3:交易账户市值 BigDecimal tradeValue = accountFundMapper.sumAllTradeValue(); data.put("tradeValue", tradeValue != null ? tradeValue : BigDecimal.ZERO); + // 查询4:用户统计 long userCount = userService.count(); data.put("userCount", userCount); + int monthNewUsers = userService.count(new LambdaQueryWrapper() + .ge(User::getCreateTime, monthStart)); + data.put("monthNewUsers", monthNewUsers); + + // 查询5:今日活跃用户(今日有过交易的独立用户数) + int todayActiveUsers = orderTradeMapper.countDistinctUserByTime( + today.atStartOfDay(), LocalDateTime.now()); + data.put("todayActiveUsers", todayActiveUsers); + return Result.success(data); } diff --git a/src/main/java/com/it/rattan/monisuo/controller/ColdWalletController.java b/src/main/java/com/it/rattan/monisuo/controller/ColdWalletController.java index 5ba2851..51a5c54 100644 --- a/src/main/java/com/it/rattan/monisuo/controller/ColdWalletController.java +++ b/src/main/java/com/it/rattan/monisuo/controller/ColdWalletController.java @@ -135,4 +135,13 @@ public class ColdWalletController { result.put("network", wallet.getNetwork()); return Result.success(result); } + + /** + * 用户端 - 获取可用的提现网络列表 + */ + @GetMapping("/api/wallet/networks") + public Result> getNetworks() { + List networks = coldWalletService.getEnabledNetworks(); + return Result.success(networks); + } } diff --git a/src/main/java/com/it/rattan/monisuo/controller/FundController.java b/src/main/java/com/it/rattan/monisuo/controller/FundController.java index 96cd450..edc9930 100644 --- a/src/main/java/com/it/rattan/monisuo/controller/FundController.java +++ b/src/main/java/com/it/rattan/monisuo/controller/FundController.java @@ -87,6 +87,7 @@ public class FundController { BigDecimal amount = request.getAmount(); String withdrawAddress = request.getWithdrawAddress(); + String network = request.getNetwork(); String withdrawContact = request.getWithdrawContact(); String remark = request.getRemark(); @@ -99,7 +100,7 @@ public class FundController { } try { - Map result = fundService.withdraw(userId, amount, withdrawAddress, withdrawContact, remark); + Map result = fundService.withdraw(userId, amount, withdrawAddress, network, withdrawContact, remark); return Result.success("申请成功,等待审批", result); } catch (Exception e) { return Result.fail(e.getMessage()); diff --git a/src/main/java/com/it/rattan/monisuo/dto/WithdrawRequest.java b/src/main/java/com/it/rattan/monisuo/dto/WithdrawRequest.java index cf5e52d..99976a6 100644 --- a/src/main/java/com/it/rattan/monisuo/dto/WithdrawRequest.java +++ b/src/main/java/com/it/rattan/monisuo/dto/WithdrawRequest.java @@ -12,6 +12,7 @@ import java.math.BigDecimal; public class WithdrawRequest { private BigDecimal amount; private String withdrawAddress; + private String network; private String withdrawContact; private String remark; } diff --git a/src/main/java/com/it/rattan/monisuo/entity/OrderFund.java b/src/main/java/com/it/rattan/monisuo/entity/OrderFund.java index eb8f2f3..159c7e2 100644 --- a/src/main/java/com/it/rattan/monisuo/entity/OrderFund.java +++ b/src/main/java/com/it/rattan/monisuo/entity/OrderFund.java @@ -47,6 +47,9 @@ public class OrderFund implements Serializable { /** 冷钱包地址(充值)/提现地址 */ private String walletAddress; + /** 提现网络类型(TRC20/ERC20等) */ + private String network; + /** 提现联系方式 */ private String withdrawContact; diff --git a/src/main/java/com/it/rattan/monisuo/mapper/AccountFundMapper.java b/src/main/java/com/it/rattan/monisuo/mapper/AccountFundMapper.java index 89898b2..0c9a09c 100644 --- a/src/main/java/com/it/rattan/monisuo/mapper/AccountFundMapper.java +++ b/src/main/java/com/it/rattan/monisuo/mapper/AccountFundMapper.java @@ -5,6 +5,7 @@ import com.it.rattan.monisuo.entity.AccountFund; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.math.BigDecimal; +import java.util.Map; /** * 资金账户Mapper @@ -15,6 +16,12 @@ public interface AccountFundMapper extends BaseMapper { @Select("SELECT IFNULL(SUM(balance), 0) FROM account_fund") BigDecimal sumAllBalance(); + /** + * 一次查询获取总余额和总冻结金额 + */ + @Select("SELECT IFNULL(SUM(balance), 0) as totalBalance, IFNULL(SUM(frozen), 0) as totalFrozen FROM account_fund") + Map sumBalanceAndFrozen(); + @Select("SELECT IFNULL(SUM(total_deposit), 0) FROM account_fund") BigDecimal sumTotalDeposit(); diff --git a/src/main/java/com/it/rattan/monisuo/mapper/OrderFundMapper.java b/src/main/java/com/it/rattan/monisuo/mapper/OrderFundMapper.java index fe53504..e8b49ba 100644 --- a/src/main/java/com/it/rattan/monisuo/mapper/OrderFundMapper.java +++ b/src/main/java/com/it/rattan/monisuo/mapper/OrderFundMapper.java @@ -16,31 +16,34 @@ import java.util.Map; @Mapper public interface OrderFundMapper extends BaseMapper { - @Select("SELECT IFNULL(SUM(amount), 0) FROM order_fund WHERE type = 1 AND status = 2") - BigDecimal sumCompletedDeposit(); - - @Select("SELECT IFNULL(SUM(amount), 0) FROM order_fund WHERE type = 2 AND status = 2") - BigDecimal sumCompletedWithdraw(); - - @Select("SELECT COUNT(*) FROM order_fund WHERE status = 1") - int countPending(); - /** - * 一次性聚合查询:充值总额、提现总额、待审批数 + * 一次性聚合查询:充值总额、提现总额、实际出款、待审批数、本月/上月环比数据 + * 充值已完成 status=3,提现已完成 status=2 */ @Select("SELECT " + - "IFNULL(SUM(CASE WHEN type = 1 AND status = 2 THEN amount ELSE 0 END), 0) as totalDeposit, " + + "IFNULL(SUM(CASE WHEN type = 1 AND status = 3 THEN amount ELSE 0 END), 0) as totalDeposit, " + "IFNULL(SUM(CASE WHEN type = 2 AND status = 2 THEN amount ELSE 0 END), 0) as totalWithdraw, " + - "SUM(CASE WHEN status IN (1, 5) THEN 1 ELSE 0 END) as pendingCount " + + "IFNULL(SUM(CASE WHEN type = 2 AND status = 2 THEN IFNULL(receivable_amount, amount * 0.9) ELSE 0 END), 0) as totalActualPayout, " + + "SUM(CASE WHEN (type = 1 AND status = 2) OR (type = 2 AND status IN (1, 5)) THEN 1 ELSE 0 END) as pendingCount, " + + "IFNULL(SUM(CASE WHEN type = 1 AND status = 3 AND create_time >= #{monthStart} AND create_time < #{monthEnd} THEN amount ELSE 0 END), 0) as monthlyDeposit, " + + "IFNULL(SUM(CASE WHEN type = 2 AND status = 2 AND create_time >= #{monthStart} AND create_time < #{monthEnd} THEN amount ELSE 0 END), 0) as monthlyWithdraw, " + + "IFNULL(SUM(CASE WHEN type = 1 AND status = 3 AND create_time >= #{lastMonthStart} AND create_time < #{lastMonthEnd} THEN amount ELSE 0 END), 0) as lastMonthDeposit, " + + "IFNULL(SUM(CASE WHEN type = 2 AND status = 2 AND create_time >= #{lastMonthStart} AND create_time < #{lastMonthEnd} THEN amount ELSE 0 END), 0) as lastMonthWithdraw " + "FROM order_fund") - Map sumFinanceOverview(); + Map sumDashboardStats( + @Param("monthStart") LocalDateTime monthStart, + @Param("monthEnd") LocalDateTime monthEnd, + @Param("lastMonthStart") LocalDateTime lastMonthStart, + @Param("lastMonthEnd") LocalDateTime lastMonthEnd); // ========== 分析相关查询 ========== /** * 指定时间段内的手续费总额(0.5%) + * 充值已完成 status=3,提现已完成 status=2 */ - @Select("SELECT IFNULL(SUM(amount * 0.005), 0) FROM order_fund WHERE status = 2 AND create_time >= #{startTime}") + @Select("SELECT IFNULL(SUM(amount * 0.005), 0) FROM order_fund WHERE " + + "((type = 1 AND status = 3) OR (type = 2 AND status = 2)) AND create_time >= #{startTime}") BigDecimal sumFeeByTime(@Param("startTime") LocalDateTime startTime); /** @@ -58,9 +61,10 @@ public interface OrderFundMapper extends BaseMapper { /** * 按月分组统计充值/提现金额(替代循环 N 次查询) + * 充值已完成 status=3,提现已完成 status=2 */ @Select("SELECT DATE_FORMAT(create_time, '%Y-%m') as month, " + - "IFNULL(SUM(CASE WHEN type = 1 AND status = 2 THEN amount ELSE 0 END), 0) as deposit, " + + "IFNULL(SUM(CASE WHEN type = 1 AND status = 3 THEN amount ELSE 0 END), 0) as deposit, " + "IFNULL(SUM(CASE WHEN type = 2 AND status = 2 THEN amount ELSE 0 END), 0) as withdraw " + "FROM order_fund WHERE create_time >= #{startTime} AND create_time < #{endTime} " + "GROUP BY DATE_FORMAT(create_time, '%Y-%m') ORDER BY month") diff --git a/src/main/java/com/it/rattan/monisuo/service/ColdWalletService.java b/src/main/java/com/it/rattan/monisuo/service/ColdWalletService.java index 556dfa1..8e26d51 100644 --- a/src/main/java/com/it/rattan/monisuo/service/ColdWalletService.java +++ b/src/main/java/com/it/rattan/monisuo/service/ColdWalletService.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.List; +import java.util.stream.Collectors; /** * 冷钱包地址服务 @@ -28,6 +29,18 @@ public class ColdWalletService { return coldWalletMapper.selectList(wrapper); } + /** + * 获取所有启用的网络类型(去重) + */ + public List getEnabledNetworks() { + List wallets = getEnabledList(); + return wallets.stream() + .map(ColdWallet::getNetwork) + .filter(n -> n != null && !n.isEmpty()) + .distinct() + .collect(Collectors.toList()); + } + /** * 获取启用的钱包列表 */ diff --git a/src/main/java/com/it/rattan/monisuo/service/FundService.java b/src/main/java/com/it/rattan/monisuo/service/FundService.java index 70fbf06..6bccf84 100644 --- a/src/main/java/com/it/rattan/monisuo/service/FundService.java +++ b/src/main/java/com/it/rattan/monisuo/service/FundService.java @@ -25,7 +25,7 @@ import java.util.*; * * 状态定义: * 充值: 1=待付款, 2=待确认, 3=已完成, 4=已驳回, 5=已取消 - * 提现: 1=待审批, 2=已完成, 3=已驳回, 4=已取消 + * 提现: 1=待审批, 2=已出款, 3=已驳回, 4=已取消 * * 审批参数说明: * status=2 表示审批通过(充值订单最终状态为3,提现订单最终状态为2) @@ -128,7 +128,7 @@ public class FundService { */ @Transactional public Map withdraw(Long userId, BigDecimal amount, String withdrawAddress, - String withdrawContact, String remark) { + String network, String withdrawContact, String remark) { if (amount.compareTo(BigDecimal.ZERO) <= 0) { throw new RuntimeException("提现金额必须大于0"); } @@ -200,6 +200,7 @@ public class FundService { order.setReceivableAmount(receivableAmount); order.setStatus(1); // 待审批 order.setWalletAddress(withdrawAddress); + order.setNetwork(network); order.setWithdrawContact(withdrawContact); order.setRemark(remark); order.setCreateTime(LocalDateTime.now());