From 5fa195e4bccaf6c94c1cc6954f16774aba873159 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 23 Dec 2025 01:48:16 -0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=81=AB=E6=9F=B4?= =?UTF-8?q?=E4=BA=BA=E6=8E=92=E5=90=8D=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. identity-service: 添加批量获取用户信息内部接口 - 新增 InternalController 提供 POST /internal/users/batch - repository 添加 findByUserIds 批量查询方法 2. authorization-service: 修复 cumulativeCompleted=0 问题 - assessAndRankRegion 改用 findByAccountSequence 查询团队统计 - referral-service 使用 accountSequence 作为主键 3. mobile-app: 修复火柴人UI显示问题 - 容器边距调整为16px与其他组件一致 - 行高增加到100px避免火柴人重叠 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/settings.local.json | 3 +- .../services/assessment-calculator.service.ts | 3 +- .../identity-service/src/api/api.module.ts | 5 +- .../api/controllers/internal.controller.ts | 66 +++++++++++++++++++ .../user-account.repository.interface.ts | 3 + .../user-account.repository.impl.ts | 19 ++++++ .../widgets/stickman_race_widget.dart | 10 +-- 7 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 backend/services/identity-service/src/api/controllers/internal.controller.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ec169563..67becd94 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -328,7 +328,8 @@ "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(blockchain-service\\): 过滤热钱包发出的转账避免内部转账重复入账\n\n内部转账时,wallet-service 已经处理了接收方入账,\n需要过滤掉 blockchain-service 扫描到的热钱包转出交易,\n避免将其当作充值重复处理导致双倍入账\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", "Bash(node -e \"\nconst { ethers } = require\\(''ethers''\\);\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\n\nasync function mint\\(\\) {\n const provider = new ethers.JsonRpcProvider\\(KAVA_TESTNET_RPC\\);\n const wallet = new ethers.Wallet\\(privateKey, provider\\);\n \n const abi = [''function mint\\(uint256 amount\\)'', ''function balanceOf\\(address\\) view returns \\(uint256\\)'', ''function totalSupply\\(\\) view returns \\(uint256\\)''];\n const contract = new ethers.Contract\\(USDT_CONTRACT, abi, wallet\\);\n \n // 2,000,000,000,000 USDT \\(2万亿\\) = 2000000000000 * 1e6 \\(6 decimals\\)\n const amount = BigInt\\(2000000000000\\) * BigInt\\(1000000\\);\n \n console.log\\(''Minting 2,000,000,000,000 USDT \\(2万亿\\)...''\\);\n const tx = await contract.mint\\(amount, { gasLimit: 100000 }\\);\n console.log\\(''TX Hash:'', tx.hash\\);\n await tx.wait\\(\\);\n \n const totalSupply = await contract.totalSupply\\(\\);\n const balance = await contract.balanceOf\\(wallet.address\\);\n console.log\\(''New Total Supply:'', Number\\(totalSupply\\) / 1e6, ''USDT''\\);\n console.log\\(''Deployer Balance:'', Number\\(balance\\) / 1e6, ''USDT''\\);\n}\n\nmint\\(\\).catch\\(e => console.error\\(''Error:'', e.message\\)\\);\n\")", "Bash(npx prisma migrate diff:*)", - "Bash(git revert:*)" + "Bash(git revert:*)", + "Bash(node -e \"\nconst { ethers } = require\\(''ethers''\\);\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0x0ec001ed6233b7959d7a251e2792621e4707c35f'';\n\nasync function transfer\\(\\) {\n const provider = new ethers.JsonRpcProvider\\(KAVA_TESTNET_RPC\\);\n const wallet = new ethers.Wallet\\(privateKey, provider\\);\n \n const abi = [''function transfer\\(address to, uint256 amount\\) returns \\(bool\\)'', ''function balanceOf\\(address\\) view returns \\(uint256\\)''];\n const contract = new ethers.Contract\\(USDT_CONTRACT, abi, wallet\\);\n \n // 1,020,000,000 USDT \\(10亿2千万\\) = 1020000000 * 1e6 \\(6 decimals\\)\n const amount = BigInt\\(1020000000\\) * BigInt\\(1000000\\);\n \n console.log\\(''Transferring 1,020,000,000 USDT to'', TO_ADDRESS\\);\n const tx = await contract.transfer\\(TO_ADDRESS, amount, { gasLimit: 100000 }\\);\n console.log\\(''TX Hash:'', tx.hash\\);\n await tx.wait\\(\\);\n \n const newBalance = await contract.balanceOf\\(TO_ADDRESS\\);\n console.log\\(''New balance:'', Number\\(newBalance\\) / 1e6, ''USDT''\\);\n}\n\ntransfer\\(\\).catch\\(e => console.error\\(''Error:'', e.message\\)\\);\n\")" ], "deny": [], "ask": [] diff --git a/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts b/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts index b878367c..7bfd37af 100644 --- a/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts +++ b/backend/services/authorization-service/src/domain/services/assessment-calculator.service.ts @@ -94,7 +94,8 @@ export class AssessmentCalculatorService { const assessments: MonthlyAssessment[] = [] for (const auth of authorizations) { - const teamStats = await statsRepository.findByUserId(auth.userId.value) + // 使用 accountSequence 查询团队统计(referral-service 使用 accountSequence 作为主键) + const teamStats = await statsRepository.findByAccountSequence(auth.userId.accountSequence) if (!teamStats) continue const assessment = await this.calculateMonthlyAssessment( diff --git a/backend/services/identity-service/src/api/api.module.ts b/backend/services/identity-service/src/api/api.module.ts index 40ccfa51..bfe707b8 100644 --- a/backend/services/identity-service/src/api/api.module.ts +++ b/backend/services/identity-service/src/api/api.module.ts @@ -3,15 +3,18 @@ import { UserAccountController } from './controllers/user-account.controller'; import { AuthController } from './controllers/auth.controller'; import { ReferralsController } from './controllers/referrals.controller'; import { TotpController } from './controllers/totp.controller'; +import { InternalController } from './controllers/internal.controller'; import { ApplicationModule } from '@/application/application.module'; +import { InfrastructureModule } from '@/infrastructure/infrastructure.module'; @Module({ - imports: [ApplicationModule], + imports: [ApplicationModule, InfrastructureModule], controllers: [ UserAccountController, AuthController, ReferralsController, TotpController, + InternalController, ], }) export class ApiModule {} diff --git a/backend/services/identity-service/src/api/controllers/internal.controller.ts b/backend/services/identity-service/src/api/controllers/internal.controller.ts new file mode 100644 index 00000000..f08c9bd7 --- /dev/null +++ b/backend/services/identity-service/src/api/controllers/internal.controller.ts @@ -0,0 +1,66 @@ +import { Controller, Post, Body, Logger } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { Public } from '@/shared/guards/jwt-auth.guard'; +import { UserAccountRepositoryImpl } from '@/infrastructure/persistence/repositories/user-account.repository.impl'; +import { UserId } from '@/domain/value-objects'; + +/** + * 批量获取用户信息请求 DTO + */ +class BatchGetUsersDto { + userIds: string[]; +} + +/** + * 用户基本信息响应 + */ +interface UserBasicInfo { + userId: string; + accountSequence: string; + nickname: string; + avatarUrl?: string; +} + +/** + * 内部服务调用控制器 + * 用于微服务间的内部通信,不需要JWT认证 + */ +@ApiTags('Internal') +@Controller('internal') +export class InternalController { + private readonly logger = new Logger(InternalController.name); + + constructor( + private readonly userRepository: UserAccountRepositoryImpl, + ) {} + + @Public() + @Post('users/batch') + @ApiOperation({ summary: '批量获取用户信息(内部调用)' }) + @ApiResponse({ status: 200, description: '返回用户信息列表' }) + async batchGetUsers(@Body() dto: BatchGetUsersDto): Promise { + this.logger.debug(`[batchGetUsers] 请求用户数量: ${dto.userIds?.length || 0}`); + + if (!dto.userIds || dto.userIds.length === 0) { + return []; + } + + try { + const userIds = dto.userIds.map((id) => UserId.create(id)); + const users = await this.userRepository.findByUserIds(userIds); + + const result = users.map((user) => ({ + userId: user.userId.value.toString(), + accountSequence: user.accountSequence.value, + nickname: user.nickname, + avatarUrl: user.avatarUrl || undefined, + })); + + this.logger.debug(`[batchGetUsers] 返回用户数量: ${result.length}`); + return result; + } catch (error) { + this.logger.error(`[batchGetUsers] 查询失败:`, error); + return []; + } + } +} diff --git a/backend/services/identity-service/src/domain/repositories/user-account.repository.interface.ts b/backend/services/identity-service/src/domain/repositories/user-account.repository.interface.ts index bd7447f9..ec8ef29a 100644 --- a/backend/services/identity-service/src/domain/repositories/user-account.repository.interface.ts +++ b/backend/services/identity-service/src/domain/repositories/user-account.repository.interface.ts @@ -60,6 +60,9 @@ export interface UserAccountRepository { kycStatus?: KYCStatus; }): Promise; + // 批量查询 + findByUserIds(userIds: UserId[]): Promise; + // 推荐相关 findByInviterSequence( inviterSequence: AccountSequence, diff --git a/backend/services/identity-service/src/infrastructure/persistence/repositories/user-account.repository.impl.ts b/backend/services/identity-service/src/infrastructure/persistence/repositories/user-account.repository.impl.ts index 85343702..dc74ea9d 100644 --- a/backend/services/identity-service/src/infrastructure/persistence/repositories/user-account.repository.impl.ts +++ b/backend/services/identity-service/src/infrastructure/persistence/repositories/user-account.repository.impl.ts @@ -376,6 +376,25 @@ export class UserAccountRepositoryImpl implements UserAccountRepository { }); } + // ============ 批量查询 ============ + + async findByUserIds(userIds: UserId[]): Promise { + if (userIds.length === 0) { + return []; + } + + const data = await this.prisma.userAccount.findMany({ + where: { + userId: { + in: userIds.map((id) => BigInt(id.value)), + }, + }, + include: { devices: true, walletAddresses: true }, + }); + + return data.map((d) => this.toDomain(d)); + } + // ============ 推荐相关 ============ async findByInviterSequence( diff --git a/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart b/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart index 99797925..a5620a40 100644 --- a/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart +++ b/frontend/mobile-app/lib/features/authorization/presentation/widgets/stickman_race_widget.dart @@ -82,7 +82,7 @@ class _StickmanRaceWidgetState extends State } return Container( - margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: const Color(0x80FFFFFF), @@ -158,8 +158,8 @@ class _StickmanRaceWidgetState extends State ); } - /// 每个火柴人需要的最小高度(标签+动画+昵称) - static const double _minTrackHeight = 85.0; + /// 每个火柴人需要的最小高度(标签+动画+昵称+间距) + static const double _minTrackHeight = 100.0; /// 构建赛道区域 Widget _buildRaceTrack() { @@ -167,9 +167,9 @@ class _StickmanRaceWidgetState extends State final sortedRankings = List.from(widget.rankings) ..sort((a, b) => b.completedCount.compareTo(a.completedCount)); - // 根据火柴人数量动态计算赛道高度 + // 根据火柴人数量动态计算赛道高度(无上限) final trackCount = sortedRankings.length; - final raceTrackHeight = (trackCount * _minTrackHeight).clamp(160.0, 500.0); + final raceTrackHeight = (trackCount * _minTrackHeight).clamp(160.0, double.infinity); return SizedBox( height: raceTrackHeight,