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:
parent
eccc637a02
commit
7176bbd5c2
|
|
@ -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''\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''\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": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -245,4 +245,15 @@ export interface IUserDetailQueryRepository {
|
|||
* 获取系统账户流水(用户相关的授权角色账户流水)
|
||||
*/
|
||||
getSystemAccountLedger(accountSequence: string): Promise<SystemAccountLedger[]>;
|
||||
|
||||
/**
|
||||
* 获取用户个人认种量(有效树数)
|
||||
*/
|
||||
getPersonalAdoptionCount(accountSequence: string): Promise<number>;
|
||||
|
||||
/**
|
||||
* 获取用户团队统计(团队地址数、团队认种量)
|
||||
* 团队包括所有直推和间推用户
|
||||
*/
|
||||
getTeamStats(accountSequence: string): Promise<{ teamAddressCount: number; teamAdoptionCount: number }>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -427,6 +427,63 @@ export class UserDetailQueryRepositoryImpl implements IUserDetailQueryRepository
|
|||
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 };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助方法
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -253,12 +253,6 @@ export default function UserDetailPage() {
|
|||
{formatNumber(userDetail.referralInfo.directReferralCount)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.userDetail__statCard}>
|
||||
<span className={styles.userDetail__statLabel}>活跃引荐</span>
|
||||
<span className={styles.userDetail__statValue}>
|
||||
{formatNumber(userDetail.referralInfo.activeDirectCount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 引荐人信息 */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue