feat(admin-service): 用户详情页统计数据改用真实数据

- 移除前端"活跃引荐"统计卡片
- 添加 getPersonalAdoptionCount 方法从 PlantingPositionQueryView 获取个人认种量
- 添加 getTeamStats 方法计算团队地址数和团队认种量
- 修改 getFullDetail 使用新方法获取真实统计数据
- 团队地址数通过 ancestorPath 查询所有下级用户
- 团队认种量汇总所有团队成员的 effectiveTreeCount

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-07 21:23:18 -08:00
parent eccc637a02
commit 7176bbd5c2
5 changed files with 102 additions and 16 deletions

View File

@ -638,7 +638,27 @@
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service, admin-web\\): 修复系统账户划转金额类型问题\n\n- wallet-service: 支持 amount 为字符串或数字类型,添加类型转换\n- admin-web: 改进错误处理,正确提取 Axios 错误消息\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")", "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service, admin-web\\): 修复系统账户划转金额类型问题\n\n- wallet-service: 支持 amount 为字符串或数字类型,添加类型转换\n- admin-web: 改进错误处理,正确提取 Axios 错误消息\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(mobile-app\\): 更新客服联系方式\n\n- 客服微信1: liulianhuanghou1\n- 客服微信2: liulianhuanghou2\n- 客服QQ1: 1502109619\n- 客服QQ2: 2171447109\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")", "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(mobile-app\\): 更新客服联系方式\n\n- 客服微信1: liulianhuanghou1\n- 客服微信2: liulianhuanghou2\n- 客服QQ1: 1502109619\n- 客服QQ2: 2171447109\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(wallet-service\\): 添加运营1和积分股池到系统划转账户列表\n\n- 添加 S0000000002 \\(运营1\\) 和 S0000000004 \\(积分股池\\) 到允许转出白名单\n- 更新系统账户名称映射与前端保持一致\n- 为 S0000000006 手续费归集账户添加兼容逻辑当余额为0时从提现订单表统计历史手续费\n- 优化过期奖励处理,按分配类型分别记录流水便于明细查看\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")", "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(wallet-service\\): 添加运营1和积分股池到系统划转账户列表\n\n- 添加 S0000000002 \\(运营1\\) 和 S0000000004 \\(积分股池\\) 到允许转出白名单\n- 更新系统账户名称映射与前端保持一致\n- 为 S0000000006 手续费归集账户添加兼容逻辑当余额为0时从提现订单表统计历史手续费\n- 优化过期奖励处理,按分配类型分别记录流水便于明细查看\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service\\): 修复系统账户余额统计不一致问题\n\n- 账户余额改为 usdtAvailable + settleableUsdt与累计收入统计保持一致\n- 解决社区权益进入 settleableUsdt 导致的余额与累计收入不匹配问题\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")" "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service\\): 修复系统账户余额统计不一致问题\n\n- 账户余额改为 usdtAvailable + settleableUsdt与累计收入统计保持一致\n- 解决社区权益进入 settleableUsdt 导致的余额与累计收入不匹配问题\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(npx eslint:*)",
"Bash(backend/services/admin-service/src/infrastructure/kafka/cdc-consumer.service.ts )",
"Bash(backend/services/admin-service/src/infrastructure/kafka/index.ts )",
"Bash(backend/services/admin-service/src/infrastructure/kafka/kafka.module.ts )",
"Bash(backend/services/deploy.sh )",
"Bash(backend/services/docker-compose.yml )",
"Bash(backend/services/scripts/init-databases.sh )",
"Bash(backend/services/scripts/debezium/)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(admin-service\\): 实现 Debezium CDC 数据同步\n\n- 新增 CdcConsumerService 消费 PostgreSQL WAL 变更事件\n- 配置 Debezium Connect 服务和 PostgreSQL 逻辑复制\n- 更新 deploy.sh 支持 Debezium 启动和连接器管理\n- 新增 identity-postgres-connector 配置同步 user_accounts 表\n- 保留原有 Outbox 机制用于业务领域事件\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(referral-service\\): 修复 Kafka 消费异常被吞掉的问题\n\n- kafka.service.ts: 抛出异常让 KafkaJS 触发重试\n- user-registered.handler.ts: 传播异常到 KafkaService\n\n修复前处理失败的消息不会重试导致推荐关系可能丢失\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(leaderboard-service\\): 修复健康检查 API 路径\n\n将 Dockerfile 和 docker-compose.yml 中的健康检查路径从\n/api/health 修改为 /api/v1/health与实际 API 路由保持一致\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(backend/services/admin-service/prisma/migrations/20250107100000_add_referral_query_view/ )",
"Bash(backend/services/admin-service/src/infrastructure/kafka/referral-cdc-consumer.service.ts )",
"Bash(backend/services/scripts/debezium/referral-connector.json)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(admin-service\\): 添加 referral-service CDC 数据同步\n\n- 新增 ReferralQueryView schema 和 migration\n- 新增 ReferralCdcConsumerService 消费推荐关系变更\n- 配置 referral-postgres-connector 用于 Debezium CDC\n- 更新 deploy.sh 自动注册 referral connector\n- 更新 init-databases.sh 配置 rwa_referral 逻辑复制权限\n\nCDC 同步的字段:\n- user_id, account_sequence, referrer_id\n- my_referral_code, used_referral_code\n- ancestor_path, depth\n- direct_referral_count, active_direct_count\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(admin-service\\): 添加 CDC 分类账流水同步\n\n新增 wallet/planting/authorization 服务的 CDC 数据同步:\n\n状态表同步:\n- WalletAccountQueryView: 钱包账户余额状态\n- WithdrawalOrderQueryView: 提现订单状态\n- FiatWithdrawalOrderQueryView: 法币提现订单\n- PlantingOrderQueryView: 认种订单状态\n- PlantingPositionQueryView: 持仓状态\n- ContractSigningTaskQueryView: 合同签约任务\n- AuthorizationRoleQueryView: 授权角色\n- MonthlyAssessmentQueryView: 月度考核\n- SystemAccountQueryView: 系统账户余额\n\n分类账流水同步:\n- WalletLedgerEntryView: 钱包流水分类账\n- FundAllocationView: 认种资金分配记录\n- SystemAccountLedgerView: 系统账户流水\n\n其他:\n- Debezium Connect 端口改为 8084 避免冲突\n- 更新连接器配置添加流水表\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
"Bash($env:DATABASE_URL=\"postgresql://test:test@localhost:5432/test\")",
"Bash(DATABASE_URL=\"postgresql://test:test@localhost:5432/test\" npx prisma validate:*)",
"Bash(DATABASE_URL=\"postgresql://test:test@localhost:5432/test\" npx prisma format:*)",
"Bash(timeout 60 npx tsc:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@ -57,8 +57,12 @@ export class UserDetailController {
throw new NotFoundException(`用户 ${accountSequence} 不存在`); throw new NotFoundException(`用户 ${accountSequence} 不存在`);
} }
// 获取推荐关系信息 // 并行获取所有相关数据
const referralInfo = await this.userDetailRepository.getReferralInfo(accountSequence); const [referralInfo, personalAdoptions, teamStats] = await Promise.all([
this.userDetailRepository.getReferralInfo(accountSequence),
this.userDetailRepository.getPersonalAdoptionCount(accountSequence),
this.userDetailRepository.getTeamStats(accountSequence),
]);
// 获取推荐人昵称 // 获取推荐人昵称
let referrerNickname: string | null = null; let referrerNickname: string | null = null;
@ -81,19 +85,19 @@ export class UserDetailController {
isOnline: user.isOnline, isOnline: user.isOnline,
registeredAt: user.registeredAt.toISOString(), registeredAt: user.registeredAt.toISOString(),
lastActiveAt: user.lastActiveAt?.toISOString() || null, lastActiveAt: user.lastActiveAt?.toISOString() || null,
personalAdoptions: user.personalAdoptionCount, personalAdoptions: personalAdoptions,
teamAddresses: user.teamAddressCount, teamAddresses: teamStats.teamAddressCount,
teamAdoptions: user.teamAdoptionCount, teamAdoptions: teamStats.teamAdoptionCount,
provincialAdoptions: { provincialAdoptions: {
count: user.provinceAdoptionCount, count: user.provinceAdoptionCount,
percentage: user.teamAdoptionCount > 0 percentage: teamStats.teamAdoptionCount > 0
? Math.round((user.provinceAdoptionCount / user.teamAdoptionCount) * 100) ? Math.round((user.provinceAdoptionCount / teamStats.teamAdoptionCount) * 100)
: 0, : 0,
}, },
cityAdoptions: { cityAdoptions: {
count: user.cityAdoptionCount, count: user.cityAdoptionCount,
percentage: user.teamAdoptionCount > 0 percentage: teamStats.teamAdoptionCount > 0
? Math.round((user.cityAdoptionCount / user.teamAdoptionCount) * 100) ? Math.round((user.cityAdoptionCount / teamStats.teamAdoptionCount) * 100)
: 0, : 0,
}, },
ranking: user.leaderboardRank, ranking: user.leaderboardRank,

View File

@ -245,4 +245,15 @@ export interface IUserDetailQueryRepository {
* *
*/ */
getSystemAccountLedger(accountSequence: string): Promise<SystemAccountLedger[]>; getSystemAccountLedger(accountSequence: string): Promise<SystemAccountLedger[]>;
/**
*
*/
getPersonalAdoptionCount(accountSequence: string): Promise<number>;
/**
*
*
*/
getTeamStats(accountSequence: string): Promise<{ teamAddressCount: number; teamAdoptionCount: number }>;
} }

View File

@ -427,6 +427,63 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository
return []; return [];
} }
async getPersonalAdoptionCount(accountSequence: string): Promise<number> {
// 先获取用户的 userId
const user = await this.prisma.userQueryView.findUnique({
where: { accountSequence },
select: { userId: true },
});
if (!user) return 0;
// 从 PlantingPositionQueryView 获取有效认种树数
const position = await this.prisma.plantingPositionQueryView.findUnique({
where: { userId: user.userId },
select: { effectiveTreeCount: true },
});
return position?.effectiveTreeCount || 0;
}
async getTeamStats(accountSequence: string): Promise<{ teamAddressCount: number; teamAdoptionCount: number }> {
// 先获取用户的 userId
const user = await this.prisma.userQueryView.findUnique({
where: { accountSequence },
select: { userId: true },
});
if (!user) return { teamAddressCount: 0, teamAdoptionCount: 0 };
// 1. 获取团队地址数:递归查找所有以当前用户为祖先的推荐关系
// ancestorPath 包含所有祖先 userId如果当前用户的 userId 在某用户的 ancestorPath 中,说明该用户是当前用户的下级
const teamMembers = await this.prisma.referralQueryView.findMany({
where: {
ancestorPath: {
has: user.userId,
},
},
select: { userId: true },
});
const teamAddressCount = teamMembers.length;
// 2. 获取团队认种量:汇总所有团队成员的有效认种树数
let teamAdoptionCount = 0;
if (teamMembers.length > 0) {
const teamUserIds = teamMembers.map((m) => m.userId);
const positions = await this.prisma.plantingPositionQueryView.findMany({
where: {
userId: { in: teamUserIds },
},
select: { effectiveTreeCount: true },
});
teamAdoptionCount = positions.reduce((sum, p) => sum + p.effectiveTreeCount, 0);
}
return { teamAddressCount, teamAdoptionCount };
}
// ============================================================================ // ============================================================================
// 辅助方法 // 辅助方法
// ============================================================================ // ============================================================================

View File

@ -253,12 +253,6 @@ export default function UserDetailPage() {
{formatNumber(userDetail.referralInfo.directReferralCount)} {formatNumber(userDetail.referralInfo.directReferralCount)}
</span> </span>
</div> </div>
<div className={styles.userDetail__statCard}>
<span className={styles.userDetail__statLabel}></span>
<span className={styles.userDetail__statValue}>
{formatNumber(userDetail.referralInfo.activeDirectCount)}
</span>
</div>
</div> </div>
{/* 引荐人信息 */} {/* 引荐人信息 */}