feat(mining-admin): display adoption order count in user management

Backend:
- Add personalOrders and teamOrders to adoption stats
- Return order count alongside tree count in user list API

Frontend:
- Add personalAdoptionOrders and teamAdoptionOrders to UserOverview type
- Display format: "树数量(订单数)" e.g. "6(3单)"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-13 01:03:59 -08:00
parent 56ff8290c1
commit 2f3a0f3652
5 changed files with 28 additions and 6 deletions

View File

@ -767,7 +767,8 @@
"Bash(git -C \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\" commit -m \"$\\(cat <<''EOF''\nfix\\(mining-app\\): update splash page theme and fix token refresh\n\n- Update splash_page.dart to orange theme \\(#FF6B00\\) matching other pages\n- Change app name from \"榴莲挖矿\" to \"榴莲生态\"\n- Fix refreshTokenIfNeeded to properly throw on failure instead of\n silently calling logout \\(which caused Riverpod ref errors\\)\n- Clear local storage directly on refresh failure without remote API call\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")", "Bash(git -C \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\" commit -m \"$\\(cat <<''EOF''\nfix\\(mining-app\\): update splash page theme and fix token refresh\n\n- Update splash_page.dart to orange theme \\(#FF6B00\\) matching other pages\n- Change app name from \"榴莲挖矿\" to \"榴莲生态\"\n- Fix refreshTokenIfNeeded to properly throw on failure instead of\n silently calling logout \\(which caused Riverpod ref errors\\)\n- Clear local storage directly on refresh failure without remote API call\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(python3 -c \" import sys content = sys.stdin.read\\(\\) old = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' new = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' print\\(content.replace\\(old, new\\)\\) \")", "Bash(python3 -c \" import sys content = sys.stdin.read\\(\\) old = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' new = '''''' done # 清空 processed_cdc_events 表(因为 migration 时可能已经消费了一些消息) # 这是事务性幂等消费的关键:重置 Kafka offset 后必须同时清空幂等记录 log_info \"\"Truncating processed_cdc_events tables to allow re-consumption...\"\" for db in \"\"rwa_contribution\"\" \"\"rwa_auth\"\"; do if run_psql \"\"$db\"\" \"\"TRUNCATE TABLE processed_cdc_events;\"\" 2>/dev/null; then log_success \"\"Truncated processed_cdc_events in $db\"\" else log_warn \"\"Could not truncate processed_cdc_events in $db \\(table may not exist yet\\)\"\" fi done log_step \"\"Step 9/18: Starting 2.0 services...\"\"'''''' print\\(content.replace\\(old, new\\)\\) \")",
"Bash(git rm:*)", "Bash(git rm:*)",
"Bash(echo \"请在服务器运行以下命令检查 outbox 事件:\n\ndocker exec -it rwa-postgres psql -U rwa_user -d rwa_contribution -c \"\"\nSELECT id, event_type, aggregate_id, \n payload->>''sourceType'' as source_type,\n payload->>''accountSequence'' as account_seq,\n payload->>''sourceAccountSequence'' as source_account_seq,\n payload->>''bonusTier'' as bonus_tier\nFROM outbox_events \nWHERE payload->>''accountSequence'' = ''D25122900007''\nORDER BY id;\n\"\"\")" "Bash(echo \"请在服务器运行以下命令检查 outbox 事件:\n\ndocker exec -it rwa-postgres psql -U rwa_user -d rwa_contribution -c \"\"\nSELECT id, event_type, aggregate_id, \n payload->>''sourceType'' as source_type,\n payload->>''accountSequence'' as account_seq,\n payload->>''sourceAccountSequence'' as source_account_seq,\n payload->>''bonusTier'' as bonus_tier\nFROM outbox_events \nWHERE payload->>''accountSequence'' = ''D25122900007''\nORDER BY id;\n\"\"\")",
"Bash(ssh -o ConnectTimeout=10 ceshi@14.215.128.96 'find /home/ceshi/rwadurian/frontend/mining-admin-web -name \"\"*.tsx\"\" -o -name \"\"*.ts\"\" | xargs grep -l \"\"用户管理\\\\|users\"\" 2>/dev/null | head -10')"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@ -103,15 +103,15 @@ export class UsersService {
*/ */
private async getAdoptionStatsForUsers( private async getAdoptionStatsForUsers(
accountSequences: string[], accountSequences: string[],
): Promise<Map<string, { personalCount: number; teamCount: number }>> { ): Promise<Map<string, { personalCount: number; personalOrders: number; teamCount: number; teamOrders: number }>> {
const result = new Map< const result = new Map<
string, string,
{ personalCount: number; teamCount: number } { personalCount: number; personalOrders: number; teamCount: number; teamOrders: number }
>(); >();
if (accountSequences.length === 0) return result; if (accountSequences.length === 0) return result;
// 获取每个用户的个人认种数量(只统计 MINING_ENABLED 状态) // 获取每个用户的个人认种数量和订单数(只统计 MINING_ENABLED 状态)
const personalAdoptions = await this.prisma.syncedAdoption.groupBy({ const personalAdoptions = await this.prisma.syncedAdoption.groupBy({
by: ['accountSequence'], by: ['accountSequence'],
where: { where: {
@ -119,19 +119,22 @@ export class UsersService {
status: 'MINING_ENABLED', status: 'MINING_ENABLED',
}, },
_sum: { treeCount: true }, _sum: { treeCount: true },
_count: { id: true },
}); });
for (const stat of personalAdoptions) { for (const stat of personalAdoptions) {
result.set(stat.accountSequence, { result.set(stat.accountSequence, {
personalCount: stat._sum.treeCount || 0, personalCount: stat._sum.treeCount || 0,
personalOrders: stat._count.id || 0,
teamCount: 0, teamCount: 0,
teamOrders: 0,
}); });
} }
// 确保所有用户都有记录 // 确保所有用户都有记录
for (const seq of accountSequences) { for (const seq of accountSequences) {
if (!result.has(seq)) { if (!result.has(seq)) {
result.set(seq, { personalCount: 0, teamCount: 0 }); result.set(seq, { personalCount: 0, personalOrders: 0, teamCount: 0, teamOrders: 0 });
} }
} }
@ -159,10 +162,12 @@ export class UsersService {
status: 'MINING_ENABLED', status: 'MINING_ENABLED',
}, },
_sum: { treeCount: true }, _sum: { treeCount: true },
_count: { id: true },
}); });
const stats = result.get(ref.accountSequence); const stats = result.get(ref.accountSequence);
if (stats) { if (stats) {
stats.teamCount = teamAdoptionStats._sum.treeCount || 0; stats.teamCount = teamAdoptionStats._sum.treeCount || 0;
stats.teamOrders = teamAdoptionStats._count.id || 0;
} }
} }
} }
@ -883,7 +888,7 @@ export class UsersService {
private formatUserListItem( private formatUserListItem(
user: any, user: any,
extra?: { extra?: {
adoptionStats?: { personalCount: number; teamCount: number }; adoptionStats?: { personalCount: number; personalOrders: number; teamCount: number; teamOrders: number };
referrerInfo?: { nickname: string | null; phone: string } | null; referrerInfo?: { nickname: string | null; phone: string } | null;
}, },
) { ) {
@ -899,7 +904,9 @@ export class UsersService {
// 认种统计 // 认种统计
adoption: { adoption: {
personalAdoptionCount: extra?.adoptionStats?.personalCount || 0, personalAdoptionCount: extra?.adoptionStats?.personalCount || 0,
personalAdoptionOrders: extra?.adoptionStats?.personalOrders || 0,
teamAdoptions: extra?.adoptionStats?.teamCount || 0, teamAdoptions: extra?.adoptionStats?.teamCount || 0,
teamAdoptionOrders: extra?.adoptionStats?.teamOrders || 0,
}, },
// 推荐人信息 // 推荐人信息
referral: user.referral referral: user.referral

View File

@ -158,6 +158,11 @@ export default function UsersPage() {
<TreePine className="h-3 w-3 text-green-600" /> <TreePine className="h-3 w-3 text-green-600" />
<span className="font-mono text-sm"> <span className="font-mono text-sm">
{formatNumber(user.personalAdoptions ?? 0)} {formatNumber(user.personalAdoptions ?? 0)}
{(user.personalAdoptionOrders ?? 0) > 0 && (
<span className="text-muted-foreground ml-1">
({user.personalAdoptionOrders})
</span>
)}
</span> </span>
</div> </div>
</TableCell> </TableCell>
@ -167,6 +172,11 @@ export default function UsersPage() {
<Users className="h-3 w-3 text-blue-600" /> <Users className="h-3 w-3 text-blue-600" />
<span className="font-mono text-sm"> <span className="font-mono text-sm">
{formatNumber(user.teamAdoptions ?? 0)} {formatNumber(user.teamAdoptions ?? 0)}
{(user.teamAdoptionOrders ?? 0) > 0 && (
<span className="text-muted-foreground ml-1">
({user.teamAdoptionOrders})
</span>
)}
</span> </span>
</div> </div>
</TableCell> </TableCell>

View File

@ -27,7 +27,9 @@ function transformUserOverview(backendUser: any): UserOverview {
frozenBalance: '0', frozenBalance: '0',
// 认种数据 - 从后端 adoption 字段获取 // 认种数据 - 从后端 adoption 字段获取
personalAdoptions: backendUser.adoption?.personalAdoptionCount || 0, personalAdoptions: backendUser.adoption?.personalAdoptionCount || 0,
personalAdoptionOrders: backendUser.adoption?.personalAdoptionOrders || 0,
teamAdoptions: backendUser.adoption?.teamAdoptions || 0, teamAdoptions: backendUser.adoption?.teamAdoptions || 0,
teamAdoptionOrders: backendUser.adoption?.teamAdoptionOrders || 0,
// 推荐人 // 推荐人
referrerId: backendUser.referral?.referrerAccountSequence || null, referrerId: backendUser.referral?.referrerAccountSequence || null,
status: backendUser.status?.toLowerCase() as 'active' | 'frozen' | 'deactivated', status: backendUser.status?.toLowerCase() as 'active' | 'frozen' | 'deactivated',

View File

@ -13,7 +13,9 @@ export interface UserOverview {
frozenBalance: string; frozenBalance: string;
// 从 admin-web 复用的字段 // 从 admin-web 复用的字段
personalAdoptions?: number; personalAdoptions?: number;
personalAdoptionOrders?: number; // 个人认种订单数
teamAdoptions?: number; teamAdoptions?: number;
teamAdoptionOrders?: number; // 团队认种订单数
teamAddresses?: number; teamAddresses?: number;
provincialAdoptions?: { provincialAdoptions?: {
count: number; count: number;