From 7176bbd5c221219dd41f5c55825522b8c47693ae Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 7 Jan 2026 21:23:18 -0800 Subject: [PATCH] =?UTF-8?q?feat(admin-service):=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5=E7=BB=9F=E8=AE=A1=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=94=B9=E7=94=A8=E7=9C=9F=E5=AE=9E=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除前端"活跃引荐"统计卡片 - 添加 getPersonalAdoptionCount 方法从 PlantingPositionQueryView 获取个人认种量 - 添加 getTeamStats 方法计算团队地址数和团队认种量 - 修改 getFullDetail 使用新方法获取真实统计数据 - 团队地址数通过 ancestorPath 查询所有下级用户 - 团队认种量汇总所有团队成员的 effectiveTreeCount 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/settings.local.json | 22 ++++++- .../api/controllers/user-detail.controller.ts | 22 ++++--- .../user-detail-query.repository.ts | 11 ++++ .../user-detail-query.repository.impl.ts | 57 +++++++++++++++++++ .../src/app/(dashboard)/users/[id]/page.tsx | 6 -- 5 files changed, 102 insertions(+), 16 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7eaceb38..6bae0722 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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 \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 \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 \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 \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 \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 \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 \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 \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 \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 \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": [], "ask": [] diff --git a/backend/services/admin-service/src/api/controllers/user-detail.controller.ts b/backend/services/admin-service/src/api/controllers/user-detail.controller.ts index dacb2b90..137da697 100644 --- a/backend/services/admin-service/src/api/controllers/user-detail.controller.ts +++ b/backend/services/admin-service/src/api/controllers/user-detail.controller.ts @@ -57,8 +57,12 @@ export class UserDetailController { 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; @@ -81,19 +85,19 @@ export class UserDetailController { isOnline: user.isOnline, registeredAt: user.registeredAt.toISOString(), lastActiveAt: user.lastActiveAt?.toISOString() || null, - personalAdoptions: user.personalAdoptionCount, - teamAddresses: user.teamAddressCount, - teamAdoptions: user.teamAdoptionCount, + personalAdoptions: personalAdoptions, + teamAddresses: teamStats.teamAddressCount, + teamAdoptions: teamStats.teamAdoptionCount, provincialAdoptions: { count: user.provinceAdoptionCount, - percentage: user.teamAdoptionCount > 0 - ? Math.round((user.provinceAdoptionCount / user.teamAdoptionCount) * 100) + percentage: teamStats.teamAdoptionCount > 0 + ? Math.round((user.provinceAdoptionCount / teamStats.teamAdoptionCount) * 100) : 0, }, cityAdoptions: { count: user.cityAdoptionCount, - percentage: user.teamAdoptionCount > 0 - ? Math.round((user.cityAdoptionCount / user.teamAdoptionCount) * 100) + percentage: teamStats.teamAdoptionCount > 0 + ? Math.round((user.cityAdoptionCount / teamStats.teamAdoptionCount) * 100) : 0, }, ranking: user.leaderboardRank, diff --git a/backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts b/backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts index 0fdd8ab3..21823e3d 100644 --- a/backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts +++ b/backend/services/admin-service/src/domain/repositories/user-detail-query.repository.ts @@ -245,4 +245,15 @@ export interface IUserDetailQueryRepository { * 获取系统账户流水(用户相关的授权角色账户流水) */ getSystemAccountLedger(accountSequence: string): Promise; + + /** + * 获取用户个人认种量(有效树数) + */ + getPersonalAdoptionCount(accountSequence: string): Promise; + + /** + * 获取用户团队统计(团队地址数、团队认种量) + * 团队包括所有直推和间推用户 + */ + getTeamStats(accountSequence: string): Promise<{ teamAddressCount: number; teamAdoptionCount: number }>; } diff --git a/backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts b/backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts index 4b1b61ea..9c5175a8 100644 --- a/backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts +++ b/backend/services/admin-service/src/infrastructure/persistence/repositories/user-detail-query.repository.impl.ts @@ -427,6 +427,63 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository return []; } + async getPersonalAdoptionCount(accountSequence: string): Promise { + // 先获取用户的 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 }; + } + // ============================================================================ // 辅助方法 // ============================================================================ diff --git a/frontend/admin-web/src/app/(dashboard)/users/[id]/page.tsx b/frontend/admin-web/src/app/(dashboard)/users/[id]/page.tsx index 39be2e3e..05a1b762 100644 --- a/frontend/admin-web/src/app/(dashboard)/users/[id]/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/users/[id]/page.tsx @@ -253,12 +253,6 @@ export default function UserDetailPage() { {formatNumber(userDetail.referralInfo.directReferralCount)} -
- 活跃引荐 - - {formatNumber(userDetail.referralInfo.activeDirectCount)} - -
{/* 引荐人信息 */}