fix: 修复火柴人排名显示问题

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 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-23 01:48:16 -08:00
parent e4f2a61ecb
commit 5fa195e4bc
7 changed files with 101 additions and 8 deletions

View File

@ -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 <noreply@anthropic.com>\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": []

View File

@ -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(

View File

@ -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 {}

View File

@ -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<UserBasicInfo[]> {
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 [];
}
}
}

View File

@ -60,6 +60,9 @@ export interface UserAccountRepository {
kycStatus?: KYCStatus;
}): Promise<number>;
// 批量查询
findByUserIds(userIds: UserId[]): Promise<UserAccount[]>;
// 推荐相关
findByInviterSequence(
inviterSequence: AccountSequence,

View File

@ -376,6 +376,25 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
});
}
// ============ 批量查询 ============
async findByUserIds(userIds: UserId[]): Promise<UserAccount[]> {
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(

View File

@ -82,7 +82,7 @@ class _StickmanRaceWidgetState extends State<StickmanRaceWidget>
}
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<StickmanRaceWidget>
);
}
/// ++
static const double _minTrackHeight = 85.0;
/// +++
static const double _minTrackHeight = 100.0;
///
Widget _buildRaceTrack() {
@ -167,9 +167,9 @@ class _StickmanRaceWidgetState extends State<StickmanRaceWidget>
final sortedRankings = List<StickmanRankingData>.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,