优化
This commit is contained in:
126
.claude/plan.md
Normal file
126
.claude/plan.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# 用户详情数据面板 - 实现方案
|
||||||
|
|
||||||
|
## 需求概述
|
||||||
|
在管理后台用户管理页面,点击用户详情时展示全面的数据分析视图,包括:充值记录、累计充值/提现、推广人数信息、领取福利信息等。
|
||||||
|
|
||||||
|
## 方案设计
|
||||||
|
|
||||||
|
### 一、后端:新增用户详情聚合接口
|
||||||
|
|
||||||
|
**文件**: `AdminController.java`
|
||||||
|
|
||||||
|
新增 `GET /admin/user/stats?userId=xxx` 接口,一次性返回所有统计数据:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "0000",
|
||||||
|
"data": {
|
||||||
|
"user": { ... }, // 用户基本信息
|
||||||
|
"fundAccount": { ... }, // 资金账户(余额、冻结、累计充值、累计提现)
|
||||||
|
"tradeAccounts": [...], // 交易账户持仓列表
|
||||||
|
"depositStats": { // 充值统计
|
||||||
|
"totalCount": 5,
|
||||||
|
"totalAmount": 10000.00,
|
||||||
|
"successCount": 4,
|
||||||
|
"successAmount": 8000.00
|
||||||
|
},
|
||||||
|
"withdrawStats": { // 提现统计
|
||||||
|
"totalCount": 3,
|
||||||
|
"totalAmount": 3000.00,
|
||||||
|
"successCount": 2,
|
||||||
|
"successAmount": 2000.00,
|
||||||
|
"totalFee": 200.00
|
||||||
|
},
|
||||||
|
"referralStats": { // 推广统计
|
||||||
|
"directCount": 5, // 直接推广人数
|
||||||
|
"indirectCount": 8, // 间接推广人数
|
||||||
|
"referrals": [ // 直接推广人列表
|
||||||
|
{ "userId": 10, "username": "user1", "nickname": "用户1", "createTime": "...", "deposited": true }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"bonusStats": { // 福利统计
|
||||||
|
"newUserBonusClaimed": true,
|
||||||
|
"totalBonusClaimed": 500.00,
|
||||||
|
"records": [ // 领取记录
|
||||||
|
{ "type": "新人首充福利", "amount": 100, "time": "..." },
|
||||||
|
{ "type": "邀请奖励-10-1", "amount": 100, "time": "..." }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"recentOrders": [...], // 最近10笔充提订单
|
||||||
|
"recentTrades": [...] // 最近10笔交易订单
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**实现要点**:
|
||||||
|
- 在 `AssetService` 或新建 `AdminDataService` 中封装聚合查询逻辑
|
||||||
|
- 使用已有的 `AccountFundMapper`、`OrderFundMapper`、`AccountFlowMapper`、`OrderTradeMapper` 进行数据查询
|
||||||
|
- 推广人列表通过 `UserMapper` 查询 `referredBy = userId` 获取
|
||||||
|
- 福利记录通过 `AccountFlow` 查询 `flowType=7` (福利类型) 或通过 `remark` LIKE 匹配福利关键词
|
||||||
|
|
||||||
|
### 二、前端:用户详情弹窗升级为多标签页
|
||||||
|
|
||||||
|
**文件**: `monisuo-admin/src/pages/monisuo/users.vue`
|
||||||
|
|
||||||
|
将现有的简单详情弹窗升级为多标签页数据面板:
|
||||||
|
|
||||||
|
#### 弹窗布局
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ 用户详情 - username │
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
|
│ [概览] [充提记录] [推广信息] [福利记录] [交易记录] │
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ (各标签页内容区) │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 标签页内容设计
|
||||||
|
|
||||||
|
**1. 概览 Tab**
|
||||||
|
- 用户基本信息卡片(用户名、昵称、手机、邮箱、注册时间、最后登录)
|
||||||
|
- KPI 统计卡片行(4列网格):
|
||||||
|
- 资金账户余额 / 冻结金额
|
||||||
|
- 累计充值 / 累计提现
|
||||||
|
- 直接推广人数 / 间接推广人数
|
||||||
|
- 累计领取福利金额
|
||||||
|
- 交易持仓列表(当前持有的币种)
|
||||||
|
|
||||||
|
**2. 充提记录 Tab**
|
||||||
|
- 充提统计摘要(总充值笔数/金额、总提现笔数/金额、手续费总计)
|
||||||
|
- 充提订单列表表格(时间、类型、金额、手续费、到账金额、状态)
|
||||||
|
|
||||||
|
**3. 推广信息 Tab**
|
||||||
|
- 推广概览卡片(推广码、直接推广人数、间接推广人数)
|
||||||
|
- 直接推广人列表表格(用户名、昵称、注册时间、是否已充值)
|
||||||
|
|
||||||
|
**4. 福利记录 Tab**
|
||||||
|
- 福利统计摘要(已领取总额、各类型数量)
|
||||||
|
- 福利领取记录表格(类型、金额、时间)
|
||||||
|
|
||||||
|
**5. 交易记录 Tab**
|
||||||
|
- 最近交易订单列表(时间、币种、方向、价格、数量、金额)
|
||||||
|
|
||||||
|
### 三、前端 API 层
|
||||||
|
|
||||||
|
**文件**: `monisuo-admin/src/services/api/monisuo-admin.api.ts`
|
||||||
|
|
||||||
|
新增 `useGetUserStatsQuery(userId)` composable,调用 `/admin/user/stats` 接口。
|
||||||
|
|
||||||
|
## 实现步骤
|
||||||
|
|
||||||
|
1. **后端** - 在 `AssetService` 中新增 `getUserStats(userId)` 方法
|
||||||
|
2. **后端** - 在 `AdminController` 中新增 `/admin/user/stats` 接口
|
||||||
|
3. **前端** - 在 `monisuo-admin.api.ts` 中新增 API composable
|
||||||
|
4. **前端** - 重构 `users.vue` 详情弹窗为多标签页数据面板
|
||||||
|
|
||||||
|
## 文件变更清单
|
||||||
|
|
||||||
|
| 文件 | 操作 |
|
||||||
|
|------|------|
|
||||||
|
| `src/.../service/AssetService.java` | 新增 `getUserStats()` 方法 |
|
||||||
|
| `src/.../controller/AdminController.java` | 新增 `/admin/user/stats` 接口 |
|
||||||
|
| `monisuo-admin/src/services/api/monisuo-admin.api.ts` | 新增 `useGetUserStatsQuery` |
|
||||||
|
| `monisuo-admin/src/pages/monisuo/users.vue` | 重构详情弹窗 |
|
||||||
Binary file not shown.
@@ -1 +1 @@
|
|||||||
{"version":2,"files":[{"path":"D:\\flutter\\bin\\cache\\dart-sdk\\version","hash":"800169ad7335b889bf428af171476466"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","hash":"d6b4a7aa67aeb750be9e5aec884f1f73"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\pubspec.yaml","hash":"03c567345af5a72ca098cfa0a67b3423"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\build\\9a5d09bec60a9bd952a3f584c1b9bd3b\\dart_build_result.json","hash":"92b3cbdc74276047205a9d2c62d9222f"},{"path":"D:\\flutter\\packages\\flutter_tools\\lib\\src\\build_system\\targets\\native_assets.dart","hash":"f78c405bcece3968277b212042da9ed6"},{"path":"d:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","hash":"d6b4a7aa67aeb750be9e5aec884f1f73"}]}
|
{"version":2,"files":[{"path":"D:\\flutter\\bin\\cache\\dart-sdk\\version","hash":"800169ad7335b889bf428af171476466"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","hash":"046ef7e2da888c543069af4aac7edaaf"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\pubspec.yaml","hash":"48b58d9876a092c0f3058304ec7c663a"},{"path":"D:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\build\\9a5d09bec60a9bd952a3f584c1b9bd3b\\dart_build_result.json","hash":"b04615af5d7000fefcedb23fde3ace71"},{"path":"D:\\flutter\\packages\\flutter_tools\\lib\\src\\build_system\\targets\\native_assets.dart","hash":"f78c405bcece3968277b212042da9ed6"},{"path":"d:\\workspace\\project\\com-rattan-spccloud\\flutter_monisuo\\.dart_tool\\package_config.json","hash":"046ef7e2da888c543069af4aac7edaaf"}]}
|
||||||
@@ -1 +1 @@
|
|||||||
{"build_start":"2026-04-06T18:39:07.915511","build_end":"2026-04-06T18:39:11.314962","dependencies":["file:///D:/flutter/bin/cache/dart-sdk/version","file:///D:/workspace/project/com-rattan-spccloud/flutter_monisuo/.dart_tool/package_config.json","file:///D:/workspace/project/com-rattan-spccloud/flutter_monisuo/pubspec.yaml","file:///d:/workspace/project/com-rattan-spccloud/flutter_monisuo/.dart_tool/package_config.json"],"code_assets":[],"data_assets":[]}
|
{"build_start":"2026-04-07T23:07:26.594437","build_end":"2026-04-07T23:07:33.162061","dependencies":["file:///D:/flutter/bin/cache/dart-sdk/version","file:///D:/workspace/project/com-rattan-spccloud/flutter_monisuo/.dart_tool/package_config.json","file:///D:/workspace/project/com-rattan-spccloud/flutter_monisuo/pubspec.yaml","file:///d:/workspace/project/com-rattan-spccloud/flutter_monisuo/.dart_tool/package_config.json"],"code_assets":[],"data_assets":[]}
|
||||||
@@ -140,9 +140,11 @@ class _TransferPageState extends State<TransferPage> {
|
|||||||
void _setQuickAmount(double percent) {
|
void _setQuickAmount(double percent) {
|
||||||
final available = double.tryParse(_availableBalance) ?? 0;
|
final available = double.tryParse(_availableBalance) ?? 0;
|
||||||
final amount = available * percent;
|
final amount = available * percent;
|
||||||
_amountController.text = amount
|
// 向下截斷到2位小數,避免四捨五入超出餘額
|
||||||
.toStringAsFixed(8)
|
_amountController.text =
|
||||||
.replaceAll(RegExp(r'\.?0+$'), '');
|
((amount * 100).truncateToDouble() / 100).toStringAsFixed(2);
|
||||||
|
|
||||||
|
// Trigger haptic feedback
|
||||||
HapticFeedback.selectionClick();
|
HapticFeedback.selectionClick();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ class _FundOrderCard extends StatelessWidget {
|
|||||||
final isDeposit = order.type == 1;
|
final isDeposit = order.type == 1;
|
||||||
final statusColor = _getStatusColor(order.status, isDeposit);
|
final statusColor = _getStatusColor(order.status, isDeposit);
|
||||||
|
|
||||||
|
// 已出款的提現訂單顯示到賬金額,其餘顯示應付金額
|
||||||
|
final displayAmount = (!isDeposit && order.status == 2 && order.receivableAmount != null)
|
||||||
|
? order.receivableAmount
|
||||||
|
: order.amount;
|
||||||
|
|
||||||
return ShadCard(
|
return ShadCard(
|
||||||
padding: AppSpacing.cardPadding,
|
padding: AppSpacing.cardPadding,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -97,7 +102,7 @@ class _FundOrderCard extends StatelessWidget {
|
|||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${isDeposit ? '+' : '-'}${order.amount} USDT',
|
'${isDeposit ? '+' : '-'}$displayAmount USDT',
|
||||||
style: AppTextStyles.headlineMedium(context).copyWith(
|
style: AppTextStyles.headlineMedium(context).copyWith(
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
|
|||||||
const SizedBox(height: AppSpacing.sm - AppSpacing.xs),
|
const SizedBox(height: AppSpacing.sm - AppSpacing.xs),
|
||||||
],
|
],
|
||||||
if (order.fee != null && !order.isDeposit) ...[
|
if (order.fee != null && !order.isDeposit) ...[
|
||||||
_buildDetailRow('手續費', '${order.fee}%'),
|
_buildDetailRow('手續費', '${order.fee} USDT'),
|
||||||
const SizedBox(height: AppSpacing.sm - AppSpacing.xs),
|
const SizedBox(height: AppSpacing.sm - AppSpacing.xs),
|
||||||
],
|
],
|
||||||
_buildDetailRow(
|
_buildDetailRow(
|
||||||
@@ -400,7 +400,7 @@ class _FundOrdersPageState extends State<FundOrdersPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text('應付金額', style: AppTextStyles.headlineSmall(context).copyWith(color: context.colors.onSurfaceVariant)),
|
Text(order.status == 2 ? '到賬金額' : '應付金額', style: AppTextStyles.headlineSmall(context).copyWith(color: context.colors.onSurfaceVariant)),
|
||||||
Text('${order.receivableAmount} USDT', style: AppTextStyles.headlineMedium(context).copyWith(color: context.colors.onSurface)),
|
Text('${order.receivableAmount} USDT', style: AppTextStyles.headlineMedium(context).copyWith(color: context.colors.onSurface)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -97,19 +97,27 @@ class _TradePageState extends State<TradePage>
|
|||||||
if (_tradeType == 0) {
|
if (_tradeType == 0) {
|
||||||
return _availableUsdt;
|
return _availableUsdt;
|
||||||
} else {
|
} else {
|
||||||
|
// 賣出:qty * price 截斷到2位
|
||||||
final qty = double.tryParse(_availableCoinQty) ?? 0;
|
final qty = double.tryParse(_availableCoinQty) ?? 0;
|
||||||
return (qty * price).toStringAsFixed(2);
|
return ((qty * price * 100).truncateToDouble() / 100).toStringAsFixed(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 計算數量
|
/// 計算數量(向下截斷到4位小數,確保 price * quantity <= amount)
|
||||||
String get _calculatedQuantity {
|
String get _calculatedQuantity {
|
||||||
final amount = double.tryParse(_amountController.text) ?? 0;
|
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||||
final price = _selectedCoin?.price ?? 0;
|
final price = _selectedCoin?.price ?? 0;
|
||||||
if (price <= 0 || amount <= 0) return '0';
|
if (price <= 0 || amount <= 0) return '0';
|
||||||
// 向下截斷到4位小數,避免回算超出金額
|
// 使用與後端一致的截斷邏輯:先算原始數量,截斷到4位,再回算金額確保不超
|
||||||
final qty = amount / price;
|
final rawQty = amount / price;
|
||||||
return ((qty * 10000).truncateToDouble() / 10000).toStringAsFixed(4);
|
final truncatedQty = (rawQty * 10000).truncateToDouble() / 10000;
|
||||||
|
// 回算:roundedPrice * truncatedQty,確保不超過 amount
|
||||||
|
final roundedPrice = (price * 100).truncateToDouble() / 100;
|
||||||
|
if (roundedPrice * truncatedQty > amount) {
|
||||||
|
// 回退一個最小單位(0.0001)
|
||||||
|
return (truncatedQty - 0.0001).toStringAsFixed(4);
|
||||||
|
}
|
||||||
|
return truncatedQty.toStringAsFixed(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -208,11 +216,16 @@ class _TradePageState extends State<TradePage>
|
|||||||
void _fillPercent(double pct) {
|
void _fillPercent(double pct) {
|
||||||
final max = double.tryParse(_maxAmount) ?? 0;
|
final max = double.tryParse(_maxAmount) ?? 0;
|
||||||
final value = max * pct;
|
final value = max * pct;
|
||||||
// 向下截斷到2位小數,避免四捨五入超出可用餘額
|
|
||||||
// 向下截斷到2位小數,再減0.01作為安全緩衝,避免精度問題導致餘額不足
|
if (_tradeType == 0) {
|
||||||
final truncated = ((value * 100).truncateToDouble() / 100);
|
// 買入:向下截斷到2位小數
|
||||||
final safe = truncated > 0.01 ? truncated - 0.01 : truncated;
|
_amountController.text =
|
||||||
_amountController.text = safe.toStringAsFixed(2);
|
((value * 100).truncateToDouble() / 100).toStringAsFixed(2);
|
||||||
|
} else {
|
||||||
|
// 賣出:_maxAmount 已是 qty*price 的四捨五入值,直接截斷
|
||||||
|
_amountController.text =
|
||||||
|
((value * 100).truncateToDouble() / 100).toStringAsFixed(2);
|
||||||
|
}
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
|
|
||||||
import type { User } from '@/services/api/monisuo-admin.api'
|
import type { User, UserStats } from '@/services/api/monisuo-admin.api'
|
||||||
|
|
||||||
import { BasicPage } from '@/components/global-layout'
|
import { BasicPage } from '@/components/global-layout'
|
||||||
import { useGetUserListQuery, useUpdateUserStatusMutation } from '@/services/api/monisuo-admin.api'
|
import { useGetUserListQuery, useGetUserStatsQuery, useUpdateUserStatusMutation } from '@/services/api/monisuo-admin.api'
|
||||||
|
|
||||||
const pageNum = ref(1)
|
const pageNum = ref(1)
|
||||||
const pageSize = ref(10)
|
const pageSize = ref(10)
|
||||||
@@ -29,12 +29,47 @@ const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
|
|||||||
// 用户详情弹窗
|
// 用户详情弹窗
|
||||||
const showDetailDialog = ref(false)
|
const showDetailDialog = ref(false)
|
||||||
const selectedUser = ref<User | null>(null)
|
const selectedUser = ref<User | null>(null)
|
||||||
|
const activeTab = ref('overview')
|
||||||
|
|
||||||
|
const { data: statsData, isLoading: statsLoading } = useGetUserStatsQuery(
|
||||||
|
computed(() => selectedUser.value?.id ?? 0),
|
||||||
|
)
|
||||||
|
const stats = computed(() => statsData.value?.data as UserStats | undefined)
|
||||||
|
|
||||||
function viewUserDetail(user: User) {
|
function viewUserDetail(user: User) {
|
||||||
selectedUser.value = user
|
selectedUser.value = user
|
||||||
|
activeTab.value = 'overview'
|
||||||
showDetailDialog.value = true
|
showDetailDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatAmount(val: number | string | undefined | null): string {
|
||||||
|
if (val == null)
|
||||||
|
return '0.00'
|
||||||
|
const n = typeof val === 'string' ? Number.parseFloat(val) : val
|
||||||
|
if (Number.isNaN(n))
|
||||||
|
return '0.00'
|
||||||
|
return n.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(val: string | undefined | null): string {
|
||||||
|
if (!val)
|
||||||
|
return '-'
|
||||||
|
return val.replace('T', ' ').substring(0, 19)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFundOrderStatusText(type: number, status: number): string {
|
||||||
|
if (type === 1) {
|
||||||
|
const map: Record<number, string> = { 1: '待付款', 2: '待确认', 3: '已完成', 4: '已驳回', 5: '已取消' }
|
||||||
|
return map[status] || '未知'
|
||||||
|
}
|
||||||
|
const map: Record<number, string> = { 1: '待审批', 2: '已出款', 3: '已驳回', 4: '已取消', 5: '待财务审核' }
|
||||||
|
return map[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTradeDirectionText(dir: number): string {
|
||||||
|
return dir === 1 ? '买入' : '卖出'
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleStatus(user: User) {
|
async function toggleStatus(user: User) {
|
||||||
const newStatus = user.status === 1 ? 0 : 1
|
const newStatus = user.status === 1 ? 0 : 1
|
||||||
const action = newStatus === 0 ? '禁用' : '启用'
|
const action = newStatus === 0 ? '禁用' : '启用'
|
||||||
@@ -292,73 +327,469 @@ function handlePageSizeChange(size: unknown) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 用户详情弹窗 -->
|
<!-- 用户详情弹窗 - 多标签页数据面板 -->
|
||||||
<UiDialog v-model:open="showDetailDialog">
|
<UiDialog v-model:open="showDetailDialog">
|
||||||
<UiDialogContent class="max-w-md">
|
<UiDialogContent class="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
<UiDialogHeader>
|
<UiDialogHeader>
|
||||||
<UiDialogTitle>用户详情</UiDialogTitle>
|
<UiDialogTitle>
|
||||||
</UiDialogHeader>
|
用户详情
|
||||||
<div v-if="selectedUser" class="space-y-4">
|
<span v-if="selectedUser" class="text-muted-foreground font-normal ml-2">
|
||||||
<div class="grid grid-cols-3 gap-2 text-sm">
|
|
||||||
<div class="text-muted-foreground">
|
|
||||||
用户ID
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2 font-medium">
|
|
||||||
{{ selectedUser.id }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-muted-foreground">
|
|
||||||
用户名
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2 font-medium">
|
|
||||||
{{ selectedUser.username }}
|
{{ selectedUser.username }}
|
||||||
</div>
|
</span>
|
||||||
|
</UiDialogTitle>
|
||||||
|
</UiDialogHeader>
|
||||||
|
|
||||||
<div class="text-muted-foreground">
|
<!-- Loading -->
|
||||||
昵称
|
<div v-if="statsLoading" class="py-12 text-center">
|
||||||
</div>
|
<UiSpinner class="mx-auto" />
|
||||||
<div class="col-span-2">
|
|
||||||
{{ selectedUser.nickname || '-' }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-muted-foreground">
|
|
||||||
手机
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
{{ selectedUser.phone || '-' }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-muted-foreground">
|
|
||||||
邮箱
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
{{ selectedUser.email || '-' }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-muted-foreground">
|
|
||||||
状态
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
<UiBadge :variant="selectedUser.status === 1 ? 'default' : 'destructive'">
|
|
||||||
{{ selectedUser.status === 1 ? '正常' : '禁用' }}
|
|
||||||
</UiBadge>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-muted-foreground">
|
|
||||||
注册时间
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
{{ selectedUser.createTime }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-muted-foreground">
|
|
||||||
更新时间
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
{{ selectedUser.updateTime }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="stats" class="space-y-4">
|
||||||
|
<!-- 标签页 -->
|
||||||
|
<UiTabs v-model="activeTab">
|
||||||
|
<UiTabsList class="w-full">
|
||||||
|
<UiTabsTrigger value="overview" class="flex-1">
|
||||||
|
概览
|
||||||
|
</UiTabsTrigger>
|
||||||
|
<UiTabsTrigger value="fund" class="flex-1">
|
||||||
|
充提记录
|
||||||
|
</UiTabsTrigger>
|
||||||
|
<UiTabsTrigger value="referral" class="flex-1">
|
||||||
|
推广信息
|
||||||
|
</UiTabsTrigger>
|
||||||
|
<UiTabsTrigger value="bonus" class="flex-1">
|
||||||
|
福利记录
|
||||||
|
</UiTabsTrigger>
|
||||||
|
<UiTabsTrigger value="trade" class="flex-1">
|
||||||
|
交易记录
|
||||||
|
</UiTabsTrigger>
|
||||||
|
</UiTabsList>
|
||||||
|
|
||||||
|
<!-- ====== 概览 ====== -->
|
||||||
|
<UiTabsContent value="overview" class="space-y-4 mt-4">
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="grid grid-cols-3 gap-x-6 gap-y-2 text-sm">
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
用户ID
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 font-medium">
|
||||||
|
{{ stats.user.id }}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
用户名
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 font-medium">
|
||||||
|
{{ stats.user.username }}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
昵称
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
{{ stats.user.nickname || '-' }}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
手机
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
{{ stats.user.phone || '-' }}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
推广码
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 font-mono font-medium">
|
||||||
|
{{ stats.user.referralCode || '-' }}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
状态
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<UiBadge :variant="stats.user.status === 1 ? 'default' : 'destructive'">
|
||||||
|
{{ stats.user.status === 1 ? '正常' : '禁用' }}
|
||||||
|
</UiBadge>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted-foreground">
|
||||||
|
注册时间
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
{{ formatTime(stats.user.createTime) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- KPI 卡片 -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
<UiCard class="p-4">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
资金账户余额
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold font-mono">
|
||||||
|
{{ formatAmount(stats.fundAccount?.balance) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted-foreground mt-1">
|
||||||
|
冻结: {{ formatAmount(stats.fundAccount?.frozen) }}
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
<UiCard class="p-4">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
累计充值
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold font-mono text-green-600 dark:text-green-400">
|
||||||
|
{{ formatAmount(stats.fundAccount?.totalDeposit) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted-foreground mt-1">
|
||||||
|
{{ stats.depositStats?.successCount || 0 }} 笔成功
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
<UiCard class="p-4">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
累计提现
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold font-mono text-red-600 dark:text-red-400">
|
||||||
|
{{ formatAmount(stats.fundAccount?.totalWithdraw) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted-foreground mt-1">
|
||||||
|
手续费: {{ formatAmount(stats.withdrawStats?.totalFee) }}
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
<UiCard class="p-4">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
累计领取福利
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold font-mono text-amber-600 dark:text-amber-400">
|
||||||
|
{{ formatAmount(stats.bonusStats?.totalBonusClaimed) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-muted-foreground mt-1">
|
||||||
|
{{ stats.bonusStats?.totalBonusCount || 0 }} 次领取
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 持仓列表 -->
|
||||||
|
<div v-if="stats.tradeAccounts?.length">
|
||||||
|
<div class="text-sm font-medium mb-2">
|
||||||
|
交易持仓
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md border">
|
||||||
|
<UiTable>
|
||||||
|
<UiTableHeader>
|
||||||
|
<UiTableRow>
|
||||||
|
<UiTableHead>币种</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
数量
|
||||||
|
</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
现价
|
||||||
|
</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
市值
|
||||||
|
</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
成本价
|
||||||
|
</UiTableHead>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableHeader>
|
||||||
|
<UiTableBody>
|
||||||
|
<UiTableRow v-for="t in stats.tradeAccounts" :key="t.coinCode">
|
||||||
|
<UiTableCell class="font-medium">
|
||||||
|
{{ t.coinCode }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right font-mono">
|
||||||
|
{{ Number(t.quantity).toFixed(4) }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right font-mono">
|
||||||
|
{{ formatAmount(t.price) }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right font-mono">
|
||||||
|
{{ formatAmount(t.value) }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right font-mono">
|
||||||
|
{{ formatAmount(t.avgPrice) }}
|
||||||
|
</UiTableCell>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableBody>
|
||||||
|
</UiTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-muted-foreground text-center py-4">
|
||||||
|
暂无持仓
|
||||||
|
</div>
|
||||||
|
</UiTabsContent>
|
||||||
|
|
||||||
|
<!-- ====== 充提记录 ====== -->
|
||||||
|
<UiTabsContent value="fund" class="space-y-4 mt-4">
|
||||||
|
<!-- 充提统计 -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
|
<UiCard class="p-4">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
充值笔数
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold font-mono">
|
||||||
|
{{ stats.depositStats?.totalCount || 0 }}
|
||||||
|
<span class="text-xs text-muted-foreground font-normal">({{ stats.depositStats?.successCount || 0 }} 笔成功)</span>
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
<UiCard class="p-4">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
充值总额
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold font-mono text-green-600 dark:text-green-400">
|
||||||
|
{{ formatAmount(stats.depositStats?.totalAmount) }}
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
<UiCard class="p-4">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
提现笔数
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold font-mono">
|
||||||
|
{{ stats.withdrawStats?.totalCount || 0 }}
|
||||||
|
<span class="text-xs text-muted-foreground font-normal">({{ stats.withdrawStats?.successCount || 0 }} 笔成功)</span>
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
<UiCard class="p-4">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
提现总额
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold font-mono text-red-600 dark:text-red-400">
|
||||||
|
{{ formatAmount(stats.withdrawStats?.totalAmount) }}
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 订单列表 -->
|
||||||
|
<div v-if="stats.recentFundOrders?.length">
|
||||||
|
<div class="rounded-md border">
|
||||||
|
<UiTable>
|
||||||
|
<UiTableHeader>
|
||||||
|
<UiTableRow>
|
||||||
|
<UiTableHead>时间</UiTableHead>
|
||||||
|
<UiTableHead>类型</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
金额
|
||||||
|
</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
手续费
|
||||||
|
</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
到账
|
||||||
|
</UiTableHead>
|
||||||
|
<UiTableHead>状态</UiTableHead>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableHeader>
|
||||||
|
<UiTableBody>
|
||||||
|
<UiTableRow v-for="o in stats.recentFundOrders" :key="o.orderNo">
|
||||||
|
<UiTableCell class="text-xs">
|
||||||
|
{{ formatTime(o.createTime) }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>
|
||||||
|
<UiBadge :variant="o.type === 1 ? 'default' : 'secondary'">
|
||||||
|
{{ o.type === 1 ? '充值' : '提现' }}
|
||||||
|
</UiBadge>
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right font-mono">
|
||||||
|
{{ formatAmount(o.amount) }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right font-mono">
|
||||||
|
{{ o.fee ? formatAmount(o.fee) : '-' }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right font-mono">
|
||||||
|
{{ o.receivableAmount ? formatAmount(o.receivableAmount) : '-' }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>
|
||||||
|
<UiBadge variant="outline">
|
||||||
|
{{ getFundOrderStatusText(o.type, o.status) }}
|
||||||
|
</UiBadge>
|
||||||
|
</UiTableCell>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableBody>
|
||||||
|
</UiTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-muted-foreground text-center py-4">
|
||||||
|
暂无充提记录
|
||||||
|
</div>
|
||||||
|
</UiTabsContent>
|
||||||
|
|
||||||
|
<!-- ====== 推广信息 ====== -->
|
||||||
|
<UiTabsContent value="referral" class="space-y-4 mt-4">
|
||||||
|
<!-- 推广概览 -->
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<UiCard class="p-4 text-center">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
推广码
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold font-mono">
|
||||||
|
{{ stats.user.referralCode || '-' }}
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
<UiCard class="p-4 text-center">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
直接推广
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold font-mono">
|
||||||
|
{{ stats.referralStats?.directCount || 0 }} 人
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
<UiCard class="p-4 text-center">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
间接推广
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold font-mono">
|
||||||
|
{{ stats.referralStats?.indirectCount || 0 }} 人
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 推广人列表 -->
|
||||||
|
<div v-if="stats.referralStats?.referrals?.length">
|
||||||
|
<div class="text-sm font-medium mb-2">
|
||||||
|
直接推广人列表
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md border">
|
||||||
|
<UiTable>
|
||||||
|
<UiTableHeader>
|
||||||
|
<UiTableRow>
|
||||||
|
<UiTableHead>用户名</UiTableHead>
|
||||||
|
<UiTableHead>昵称</UiTableHead>
|
||||||
|
<UiTableHead>注册时间</UiTableHead>
|
||||||
|
<UiTableHead>已充值</UiTableHead>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableHeader>
|
||||||
|
<UiTableBody>
|
||||||
|
<UiTableRow v-for="r in stats.referralStats.referrals" :key="r.userId">
|
||||||
|
<UiTableCell class="font-medium">
|
||||||
|
{{ r.username }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>{{ r.nickname || '-' }}</UiTableCell>
|
||||||
|
<UiTableCell class="text-xs">
|
||||||
|
{{ formatTime(r.createTime) }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>
|
||||||
|
<UiBadge :variant="r.deposited ? 'default' : 'secondary'">
|
||||||
|
{{ r.deposited ? '是' : '否' }}
|
||||||
|
</UiBadge>
|
||||||
|
</UiTableCell>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableBody>
|
||||||
|
</UiTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-muted-foreground text-center py-4">
|
||||||
|
暂无推广记录
|
||||||
|
</div>
|
||||||
|
</UiTabsContent>
|
||||||
|
|
||||||
|
<!-- ====== 福利记录 ====== -->
|
||||||
|
<UiTabsContent value="bonus" class="space-y-4 mt-4">
|
||||||
|
<!-- 福利统计 -->
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<UiCard class="p-4">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
累计领取金额
|
||||||
|
</div>
|
||||||
|
<div class="text-xl font-bold font-mono text-amber-600 dark:text-amber-400">
|
||||||
|
{{ formatAmount(stats.bonusStats?.totalBonusClaimed) }} USDT
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
<UiCard class="p-4">
|
||||||
|
<div class="text-xs text-muted-foreground mb-1">
|
||||||
|
累计领取次数
|
||||||
|
</div>
|
||||||
|
<div class="text-xl font-bold font-mono">
|
||||||
|
{{ stats.bonusStats?.totalBonusCount || 0 }} 次
|
||||||
|
</div>
|
||||||
|
</UiCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 福利记录列表 -->
|
||||||
|
<div v-if="stats.bonusStats?.records?.length">
|
||||||
|
<div class="rounded-md border">
|
||||||
|
<UiTable>
|
||||||
|
<UiTableHeader>
|
||||||
|
<UiTableRow>
|
||||||
|
<UiTableHead>类型</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
金额
|
||||||
|
</UiTableHead>
|
||||||
|
<UiTableHead>时间</UiTableHead>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableHeader>
|
||||||
|
<UiTableBody>
|
||||||
|
<UiTableRow v-for="(b, i) in stats.bonusStats.records" :key="i">
|
||||||
|
<UiTableCell>
|
||||||
|
<UiBadge variant="outline">
|
||||||
|
{{ b.type }}
|
||||||
|
</UiBadge>
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right font-mono text-amber-600 dark:text-amber-400">
|
||||||
|
+{{ formatAmount(b.amount) }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-xs">
|
||||||
|
{{ formatTime(b.time) }}
|
||||||
|
</UiTableCell>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableBody>
|
||||||
|
</UiTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-muted-foreground text-center py-4">
|
||||||
|
暂无福利领取记录
|
||||||
|
</div>
|
||||||
|
</UiTabsContent>
|
||||||
|
|
||||||
|
<!-- ====== 交易记录 ====== -->
|
||||||
|
<UiTabsContent value="trade" class="space-y-4 mt-4">
|
||||||
|
<div v-if="stats.recentTradeOrders?.length">
|
||||||
|
<div class="rounded-md border">
|
||||||
|
<UiTable>
|
||||||
|
<UiTableHeader>
|
||||||
|
<UiTableRow>
|
||||||
|
<UiTableHead>时间</UiTableHead>
|
||||||
|
<UiTableHead>币种</UiTableHead>
|
||||||
|
<UiTableHead>方向</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
价格
|
||||||
|
</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
数量
|
||||||
|
</UiTableHead>
|
||||||
|
<UiTableHead class="text-right">
|
||||||
|
金额
|
||||||
|
</UiTableHead>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableHeader>
|
||||||
|
<UiTableBody>
|
||||||
|
<UiTableRow v-for="t in stats.recentTradeOrders" :key="t.orderNo">
|
||||||
|
<UiTableCell class="text-xs">
|
||||||
|
{{ formatTime(t.createTime) }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="font-medium">
|
||||||
|
{{ t.coinCode }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell>
|
||||||
|
<UiBadge :variant="t.direction === 1 ? 'default' : 'destructive'">
|
||||||
|
{{ getTradeDirectionText(t.direction) }}
|
||||||
|
</UiBadge>
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right font-mono">
|
||||||
|
{{ formatAmount(t.price) }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right font-mono">
|
||||||
|
{{ Number(t.quantity).toFixed(4) }}
|
||||||
|
</UiTableCell>
|
||||||
|
<UiTableCell class="text-right font-mono">
|
||||||
|
{{ formatAmount(t.amount) }}
|
||||||
|
</UiTableCell>
|
||||||
|
</UiTableRow>
|
||||||
|
</UiTableBody>
|
||||||
|
</UiTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-muted-foreground text-center py-4">
|
||||||
|
暂无交易记录
|
||||||
|
</div>
|
||||||
|
</UiTabsContent>
|
||||||
|
</UiTabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
<UiDialogFooter>
|
<UiDialogFooter>
|
||||||
<UiButton variant="outline" @click="showDetailDialog = false">
|
<UiButton variant="outline" @click="showDetailDialog = false">
|
||||||
关闭
|
关闭
|
||||||
|
|||||||
@@ -148,6 +148,83 @@ export function useGetUserDetailQuery(userId: number) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserStats {
|
||||||
|
user: User
|
||||||
|
fundAccount: {
|
||||||
|
balance: number
|
||||||
|
frozen: number
|
||||||
|
totalDeposit: number
|
||||||
|
totalWithdraw: number
|
||||||
|
}
|
||||||
|
tradeAccounts: Array<{
|
||||||
|
coinCode: string
|
||||||
|
coinName: string
|
||||||
|
quantity: number
|
||||||
|
price: number
|
||||||
|
value: number
|
||||||
|
avgPrice: number
|
||||||
|
change24h: number
|
||||||
|
}>
|
||||||
|
depositStats: {
|
||||||
|
totalCount: number
|
||||||
|
totalAmount: number
|
||||||
|
successCount: number
|
||||||
|
successAmount: number
|
||||||
|
}
|
||||||
|
withdrawStats: {
|
||||||
|
totalCount: number
|
||||||
|
totalAmount: number
|
||||||
|
successCount: number
|
||||||
|
successAmount: number
|
||||||
|
totalFee: number
|
||||||
|
}
|
||||||
|
referralStats: {
|
||||||
|
directCount: number
|
||||||
|
indirectCount: number
|
||||||
|
referrals: Array<{
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickname: string
|
||||||
|
createTime: string
|
||||||
|
deposited: boolean
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
bonusStats: {
|
||||||
|
totalBonusClaimed: number
|
||||||
|
totalBonusCount: number
|
||||||
|
records: Array<{
|
||||||
|
type: string
|
||||||
|
amount: number
|
||||||
|
time: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
recentFundOrders: OrderFund[]
|
||||||
|
recentTradeOrders: Array<{
|
||||||
|
id: number
|
||||||
|
orderNo: string
|
||||||
|
coinCode: string
|
||||||
|
direction: number
|
||||||
|
price: number
|
||||||
|
quantity: number
|
||||||
|
amount: number
|
||||||
|
createTime: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetUserStatsQuery(userId: Ref<number> | ComputedRef<number> | number) {
|
||||||
|
const { axiosInstance } = useAxios()
|
||||||
|
|
||||||
|
return useQuery<ApiResult<UserStats>, AxiosError>({
|
||||||
|
queryKey: computed(() => ['useGetUserStatsQuery', unref(userId)]),
|
||||||
|
queryFn: async () => {
|
||||||
|
const id = unref(userId)
|
||||||
|
const response = await axiosInstance.get('/admin/user/stats', { params: { userId: id } })
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
enabled: computed(() => !!unref(userId)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function useUpdateUserStatusMutation() {
|
export function useUpdateUserStatusMutation() {
|
||||||
const { axiosInstance } = useAxios()
|
const { axiosInstance } = useAxios()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|||||||
@@ -254,6 +254,25 @@ public class AdminController {
|
|||||||
return Result.success(data);
|
return Result.success(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户统计数据(管理后台详情面板)
|
||||||
|
*/
|
||||||
|
@GetMapping("/user/stats")
|
||||||
|
public Result<Map<String, Object>> getUserStats(@RequestParam Long userId) {
|
||||||
|
if (!UserContext.isSuperAdmin()) {
|
||||||
|
return Result.fail("无权限访问");
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = userService.getById(userId);
|
||||||
|
if (user == null) {
|
||||||
|
return Result.fail("用户不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Object> stats = assetService.getUserStats(userId);
|
||||||
|
stats.put("user", user);
|
||||||
|
return Result.success(stats);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 禁用/启用用户
|
* 禁用/启用用户
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import com.it.rattan.monisuo.entity.AccountFlow;
|
|||||||
import com.it.rattan.monisuo.entity.AccountFund;
|
import com.it.rattan.monisuo.entity.AccountFund;
|
||||||
import com.it.rattan.monisuo.entity.AccountTrade;
|
import com.it.rattan.monisuo.entity.AccountTrade;
|
||||||
import com.it.rattan.monisuo.entity.Coin;
|
import com.it.rattan.monisuo.entity.Coin;
|
||||||
|
import com.it.rattan.monisuo.entity.OrderFund;
|
||||||
import com.it.rattan.monisuo.entity.OrderTrade;
|
import com.it.rattan.monisuo.entity.OrderTrade;
|
||||||
|
import com.it.rattan.monisuo.entity.User;
|
||||||
import com.it.rattan.monisuo.mapper.AccountFlowMapper;
|
import com.it.rattan.monisuo.mapper.AccountFlowMapper;
|
||||||
import com.it.rattan.monisuo.mapper.AccountFundMapper;
|
import com.it.rattan.monisuo.mapper.AccountFundMapper;
|
||||||
import com.it.rattan.monisuo.mapper.AccountTradeMapper;
|
import com.it.rattan.monisuo.mapper.AccountTradeMapper;
|
||||||
|
import com.it.rattan.monisuo.mapper.OrderFundMapper;
|
||||||
import com.it.rattan.monisuo.mapper.OrderTradeMapper;
|
import com.it.rattan.monisuo.mapper.OrderTradeMapper;
|
||||||
|
import com.it.rattan.monisuo.mapper.UserMapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
import com.it.rattan.monisuo.util.OrderNoUtil;
|
import com.it.rattan.monisuo.util.OrderNoUtil;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -44,6 +48,12 @@ public class AssetService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private AccountFlowMapper accountFlowMapper;
|
private AccountFlowMapper accountFlowMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private OrderFundMapper orderFundMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserMapper userMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private CoinService coinService;
|
private CoinService coinService;
|
||||||
|
|
||||||
@@ -379,4 +389,142 @@ public class AssetService {
|
|||||||
result.put("totalProfit", totalProfit);
|
result.put("totalProfit", totalProfit);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户统计数据(管理后台用)
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getUserStats(Long userId) {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
|
||||||
|
// 1. 资金账户
|
||||||
|
AccountFund fund = getOrCreateFundAccount(userId);
|
||||||
|
Map<String, Object> fundAccount = new HashMap<>();
|
||||||
|
fundAccount.put("balance", fund.getBalance());
|
||||||
|
fundAccount.put("frozen", fund.getFrozen());
|
||||||
|
fundAccount.put("totalDeposit", fund.getTotalDeposit());
|
||||||
|
fundAccount.put("totalWithdraw", fund.getTotalWithdraw());
|
||||||
|
result.put("fundAccount", fundAccount);
|
||||||
|
|
||||||
|
// 2. 交易账户持仓
|
||||||
|
result.put("tradeAccounts", getTradeAccount(userId));
|
||||||
|
|
||||||
|
// 3. 充提统计(一次查询按类型分组,避免两次查询)
|
||||||
|
LambdaQueryWrapper<OrderFund> fundOrderWrapper = new LambdaQueryWrapper<>();
|
||||||
|
fundOrderWrapper.eq(OrderFund::getUserId, userId);
|
||||||
|
List<OrderFund> allFundOrders = orderFundMapper.selectList(fundOrderWrapper);
|
||||||
|
|
||||||
|
List<OrderFund> allDeposits = allFundOrders.stream()
|
||||||
|
.filter(o -> o.getType() == 1).collect(Collectors.toList());
|
||||||
|
List<OrderFund> allWithdraws = allFundOrders.stream()
|
||||||
|
.filter(o -> o.getType() == 2).collect(Collectors.toList());
|
||||||
|
|
||||||
|
Map<String, Object> depositStats = new HashMap<>();
|
||||||
|
depositStats.put("totalCount", allDeposits.size());
|
||||||
|
depositStats.put("totalAmount", allDeposits.stream().map(OrderFund::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||||
|
depositStats.put("successCount", allDeposits.stream().filter(o -> o.getStatus() == 3).count());
|
||||||
|
depositStats.put("successAmount", allDeposits.stream().filter(o -> o.getStatus() == 3).map(OrderFund::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||||
|
result.put("depositStats", depositStats);
|
||||||
|
|
||||||
|
Map<String, Object> withdrawStats = new HashMap<>();
|
||||||
|
withdrawStats.put("totalCount", allWithdraws.size());
|
||||||
|
withdrawStats.put("totalAmount", allWithdraws.stream().map(OrderFund::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||||
|
withdrawStats.put("successCount", allWithdraws.stream().filter(o -> o.getStatus() == 2).count());
|
||||||
|
withdrawStats.put("successAmount", allWithdraws.stream().filter(o -> o.getStatus() == 2).map(OrderFund::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||||
|
withdrawStats.put("totalFee", allWithdraws.stream().filter(o -> o.getStatus() == 2).map(o -> o.getFee() != null ? o.getFee() : BigDecimal.ZERO).reduce(BigDecimal.ZERO, BigDecimal::add));
|
||||||
|
result.put("withdrawStats", withdrawStats);
|
||||||
|
|
||||||
|
// 5. 推广统计(优化:批量查询消除 N+1)
|
||||||
|
// 直接推广人(referredBy = userId)
|
||||||
|
LambdaQueryWrapper<User> referralWrapper = new LambdaQueryWrapper<>();
|
||||||
|
referralWrapper.eq(User::getReferredBy, userId)
|
||||||
|
.orderByDesc(User::getCreateTime);
|
||||||
|
List<User> directReferrals = userMapper.selectList(referralWrapper);
|
||||||
|
List<Long> directIds = directReferrals.stream().map(User::getId).collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 间接推广人:一次 IN 查询
|
||||||
|
long indirectCount = 0;
|
||||||
|
if (!directIds.isEmpty()) {
|
||||||
|
LambdaQueryWrapper<User> indirectWrapper = new LambdaQueryWrapper<>();
|
||||||
|
indirectWrapper.in(User::getReferredBy, directIds);
|
||||||
|
indirectCount = userMapper.selectCount(indirectWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量查询直接推广人是否已充值(一次 IN 查询)
|
||||||
|
Set<Long> depositedUserIds = new HashSet<>();
|
||||||
|
if (!directIds.isEmpty()) {
|
||||||
|
LambdaQueryWrapper<OrderFund> batchDepositWrapper = new LambdaQueryWrapper<>();
|
||||||
|
batchDepositWrapper.in(OrderFund::getUserId, directIds)
|
||||||
|
.eq(OrderFund::getType, 1).eq(OrderFund::getStatus, 3)
|
||||||
|
.select(OrderFund::getUserId)
|
||||||
|
.groupBy(OrderFund::getUserId);
|
||||||
|
orderFundMapper.selectList(batchDepositWrapper)
|
||||||
|
.forEach(o -> depositedUserIds.add(o.getUserId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Map<String, Object>> referralList = new ArrayList<>();
|
||||||
|
for (User ref : directReferrals) {
|
||||||
|
Map<String, Object> refInfo = new HashMap<>();
|
||||||
|
refInfo.put("userId", ref.getId());
|
||||||
|
refInfo.put("username", ref.getUsername());
|
||||||
|
refInfo.put("nickname", ref.getNickname());
|
||||||
|
refInfo.put("createTime", ref.getCreateTime());
|
||||||
|
refInfo.put("deposited", depositedUserIds.contains(ref.getId()));
|
||||||
|
referralList.add(refInfo);
|
||||||
|
}
|
||||||
|
Map<String, Object> referralStats = new HashMap<>();
|
||||||
|
referralStats.put("directCount", directReferrals.size());
|
||||||
|
referralStats.put("indirectCount", indirectCount);
|
||||||
|
referralStats.put("referrals", referralList);
|
||||||
|
result.put("referralStats", referralStats);
|
||||||
|
|
||||||
|
// 6. 福利统计(从 account_flow 查询福利类型记录)
|
||||||
|
LambdaQueryWrapper<AccountFlow> bonusWrapper = new LambdaQueryWrapper<>();
|
||||||
|
bonusWrapper.eq(AccountFlow::getUserId, userId)
|
||||||
|
.eq(AccountFlow::getFlowType, 7)
|
||||||
|
.orderByDesc(AccountFlow::getCreateTime);
|
||||||
|
List<AccountFlow> bonusFlows = accountFlowMapper.selectList(bonusWrapper);
|
||||||
|
BigDecimal totalBonusClaimed = bonusFlows.stream()
|
||||||
|
.map(AccountFlow::getAmount)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
List<Map<String, Object>> bonusRecords = new ArrayList<>();
|
||||||
|
for (AccountFlow flow : bonusFlows) {
|
||||||
|
Map<String, Object> record = new HashMap<>();
|
||||||
|
String remark = flow.getRemark() != null ? flow.getRemark() : "福利";
|
||||||
|
String type;
|
||||||
|
if (remark.startsWith("新人首充")) {
|
||||||
|
type = "新人首充福利";
|
||||||
|
} else if (remark.startsWith("间接推广")) {
|
||||||
|
type = "间接推广奖励";
|
||||||
|
} else if (remark.startsWith("邀请奖励")) {
|
||||||
|
type = "直接推广奖励";
|
||||||
|
} else {
|
||||||
|
type = remark;
|
||||||
|
}
|
||||||
|
record.put("type", type);
|
||||||
|
record.put("amount", flow.getAmount());
|
||||||
|
record.put("time", flow.getCreateTime());
|
||||||
|
bonusRecords.add(record);
|
||||||
|
}
|
||||||
|
Map<String, Object> bonusStats = new HashMap<>();
|
||||||
|
bonusStats.put("totalBonusClaimed", totalBonusClaimed);
|
||||||
|
bonusStats.put("totalBonusCount", bonusFlows.size());
|
||||||
|
bonusStats.put("records", bonusRecords);
|
||||||
|
result.put("bonusStats", bonusStats);
|
||||||
|
|
||||||
|
// 7. 最近充提订单(最近20笔)
|
||||||
|
LambdaQueryWrapper<OrderFund> recentOrderWrapper = new LambdaQueryWrapper<>();
|
||||||
|
recentOrderWrapper.eq(OrderFund::getUserId, userId)
|
||||||
|
.orderByDesc(OrderFund::getCreateTime)
|
||||||
|
.last("LIMIT 20");
|
||||||
|
result.put("recentFundOrders", orderFundMapper.selectList(recentOrderWrapper));
|
||||||
|
|
||||||
|
// 8. 最近交易订单(最近20笔)
|
||||||
|
LambdaQueryWrapper<OrderTrade> recentTradeWrapper = new LambdaQueryWrapper<>();
|
||||||
|
recentTradeWrapper.eq(OrderTrade::getUserId, userId)
|
||||||
|
.orderByDesc(OrderTrade::getCreateTime)
|
||||||
|
.last("LIMIT 20");
|
||||||
|
result.put("recentTradeOrders", orderTradeMapper.selectList(recentTradeWrapper));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user