From 034fb536746f2d0c9cae322a729dbba4deddb30a Mon Sep 17 00:00:00 2001 From: hailin Date: Wed, 10 Dec 2025 13:55:03 -0800 Subject: [PATCH] refactor: use accountSequence as unified user identifier across all services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - planting-service: extract accountSequence from JWT, pass to referral-service - referral-service: query by accountSequence instead of userId - reward-service: add accountSequence field to schema and all layers - wallet-service: prioritize accountSequence lookup over userId - authorization-service: change userId from String to BigInt, add accountSequence This change ensures consistent cross-service user identification using accountSequence (8-digit unique business ID) instead of internal database IDs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/settings.local.json | 29 ++----- .../prisma/schema.prisma | 32 +++++--- .../controllers/authorization.controller.ts | 27 +++---- .../apply-auth-city-company.command.ts | 1 + .../apply-auth-province-company.command.ts | 1 + .../commands/apply-community-auth.command.ts | 1 + .../exempt-percentage-check.command.ts | 2 +- .../commands/grant-city-company.command.ts | 2 + .../commands/grant-monthly-bypass.command.ts | 2 +- .../grant-province-company.command.ts | 2 + .../commands/revoke-authorization.command.ts | 2 +- .../authorization-application.service.ts | 47 ++++++----- .../authorization-role.aggregate.ts | 7 +- .../monthly-assessment.aggregate.ts | 5 +- .../authorization-role.repository.ts | 2 + .../src/domain/value-objects/user-id.vo.ts | 28 +++++-- .../authorization-role.repository.impl.ts | 28 ++++++- .../monthly-assessment.repository.impl.ts | 6 +- .../controllers/planting-order.controller.ts | 4 +- .../src/api/guards/jwt-auth.guard.ts | 2 + .../services/planting-application.service.ts | 3 +- .../external/referral-service.client.ts | 6 +- .../api/controllers/referral.controller.ts | 14 ++-- .../queries/get-user-referral-info.query.ts | 2 +- .../application/services/referral.service.ts | 4 +- .../reward-service/prisma/schema.prisma | 7 ++ .../src/api/controllers/reward.controller.ts | 12 +-- .../api/controllers/settlement.controller.ts | 4 +- .../services/reward-application.service.ts | 35 +++++---- .../reward-ledger-entry.aggregate.ts | 10 +++ .../reward-summary.aggregate.ts | 12 ++- ...eward-ledger-entry.repository.interface.ts | 13 ++++ .../reward-summary.repository.interface.ts | 2 + .../mappers/reward-ledger-entry.mapper.ts | 2 + .../mappers/reward-summary.mapper.ts | 2 + .../reward-ledger-entry.repository.impl.ts | 74 ++++++++++++++++++ .../reward-summary.repository.impl.ts | 19 +++++ .../src/shared/strategies/jwt.strategy.ts | 1 + .../controllers/internal-wallet.controller.ts | 30 ++++--- .../services/wallet-application.service.ts | 78 +++++++++++++------ .../lib/core/services/planting_service.dart | 43 ++++++++++ 41 files changed, 440 insertions(+), 163 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index eac5afae..91d1a29b 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,30 +1,15 @@ { "permissions": { "allow": [ - "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" add frontend/mobile-app/lib/features/home/presentation/pages/home_shell_page.dart)", - "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"$(cat <<''EOF''\nfix(mobile): change update check interval from 24h to 30-90s random\n\nAllows faster detection of urgent updates while preventing excessive\nAPI calls with random cooldown period.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n)\")", - "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"$(cat <<''EOF''\nfix(mobile): change update check interval to 90-300s random\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n)\")", - "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" status --short backend/services/blockchain-service/)", - "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" log --oneline -5)", - "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" status backend/services/blockchain-service/)", - "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" log --oneline --all -- backend/services/blockchain-service/src/api/controllers/deposit-repair.controller.ts)", - "Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" log --oneline origin/main -10)", - "Bash(npx tsc:*)", - "Bash(flutter analyze:*)", + "Bash(git -C c:/Users/dong/Desktop/rwadurian add backend/services/reward-service/prisma/migrations/)", + "Bash(git -C c:/Users/dong/Desktop/rwadurian commit --amend --no-edit)", + "Bash(git -C c:/Users/dong/Desktop/rwadurian push)", + "Bash(ssh root@154.204.60.178 \"cd /opt/rwadurian && git pull && docker-compose build blockchain-service && docker-compose up -d blockchain-service\")", + "Bash(ssh:*)", + "Bash(cat:*)", "Bash(git add:*)", "Bash(git commit:*)", - "Bash(git push:*)", - "Bash(echo:*)", - "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" add frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart)", - "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"$(cat <<''EOF''\nfix(mobile): reduce direct referral list item spacing\n\n- Row gap: 8px → 4px\n- Vertical padding: 12px → 8px\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n)\")", - "Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" push)", - "Bash(git checkout:*)", - "Bash(find:*)", - "Bash(docker exec:*)", - "Bash(npx prisma migrate status)", - "Bash(DATABASE_URL=\"postgresql://rwa_user:your_secure_password_here@localhost:5432/rwa_referral\" npx prisma migrate status:*)", - "Bash(DATABASE_URL=\"postgresql://rwa_user:your_secure_password_here@localhost:5432/rwa_referral\" npx prisma migrate deploy:*)", - "Bash(DATABASE_URL=\"postgresql://rwa_user:your_secure_password_here@localhost:5432/rwa_referral\" npx prisma migrate resolve:*)" + "Bash(git push)" ], "deny": [], "ask": [] diff --git a/backend/services/authorization-service/prisma/schema.prisma b/backend/services/authorization-service/prisma/schema.prisma index 3f8bd97b..7c93ba51 100644 --- a/backend/services/authorization-service/prisma/schema.prisma +++ b/backend/services/authorization-service/prisma/schema.prisma @@ -13,7 +13,8 @@ datasource db { // ============ 授权角色表 ============ model AuthorizationRole { id String @id @default(uuid()) - userId String @map("user_id") + userId BigInt @map("user_id") + accountSequence BigInt @map("account_sequence") roleType RoleType @map("role_type") regionCode String @map("region_code") regionName String @map("region_name") @@ -22,9 +23,9 @@ model AuthorizationRole { // 授权信息 authorizedAt DateTime? @map("authorized_at") - authorizedBy String? @map("authorized_by") + authorizedBy BigInt? @map("authorized_by") revokedAt DateTime? @map("revoked_at") - revokedBy String? @map("revoked_by") + revokedBy BigInt? @map("revoked_by") revokeReason String? @map("revoke_reason") // 考核配置 @@ -51,7 +52,8 @@ model AuthorizationRole { assessments MonthlyAssessment[] bypassRecords MonthlyBypass[] - @@unique([userId, roleType, regionCode]) + @@unique([accountSequence, roleType, regionCode]) + @@index([accountSequence]) @@index([userId]) @@index([roleType, regionCode]) @@index([status]) @@ -63,7 +65,8 @@ model AuthorizationRole { model MonthlyAssessment { id String @id @default(uuid()) authorizationId String @map("authorization_id") - userId String @map("user_id") + userId BigInt @map("user_id") + accountSequence BigInt @map("account_sequence") roleType RoleType @map("role_type") regionCode String @map("region_code") @@ -98,7 +101,7 @@ model MonthlyAssessment { // 豁免 isBypassed Boolean @default(false) @map("is_bypassed") - bypassedBy String? @map("bypassed_by") + bypassedBy BigInt? @map("bypassed_by") bypassedAt DateTime? @map("bypassed_at") // 时间戳 @@ -110,6 +113,7 @@ model MonthlyAssessment { authorization AuthorizationRole @relation(fields: [authorizationId], references: [id]) @@unique([authorizationId, assessmentMonth]) + @@index([accountSequence, assessmentMonth]) @@index([userId, assessmentMonth]) @@index([roleType, regionCode, assessmentMonth]) @@index([assessmentMonth, result]) @@ -121,21 +125,22 @@ model MonthlyAssessment { model MonthlyBypass { id String @id @default(uuid()) authorizationId String @map("authorization_id") - userId String @map("user_id") + userId BigInt @map("user_id") + accountSequence BigInt @map("account_sequence") roleType RoleType @map("role_type") bypassMonth String @map("bypass_month") // YYYY-MM // 授权信息 - grantedBy String @map("granted_by") + grantedBy BigInt @map("granted_by") grantedAt DateTime @map("granted_at") reason String? // 审批信息(三人授权) - approver1Id String @map("approver1_id") + approver1Id BigInt @map("approver1_id") approver1At DateTime @map("approver1_at") - approver2Id String? @map("approver2_id") + approver2Id BigInt? @map("approver2_id") approver2At DateTime? @map("approver2_at") - approver3Id String? @map("approver3_id") + approver3Id BigInt? @map("approver3_id") approver3At DateTime? @map("approver3_at") approvalStatus ApprovalStatus @default(PENDING) @map("approval_status") @@ -144,6 +149,7 @@ model MonthlyBypass { authorization AuthorizationRole @relation(fields: [authorizationId], references: [id]) @@unique([authorizationId, bypassMonth]) + @@index([accountSequence, bypassMonth]) @@index([userId, bypassMonth]) @@map("monthly_bypasses") } @@ -275,7 +281,8 @@ model RegionHeatMap { // ============ 火柴人排名视图数据表 ============ model StickmanRanking { id String @id @default(uuid()) - userId String @map("user_id") + userId BigInt @map("user_id") + accountSequence BigInt @map("account_sequence") authorizationId String @map("authorization_id") roleType RoleType @map("role_type") regionCode String @map("region_code") @@ -303,6 +310,7 @@ model StickmanRanking { updatedAt DateTime @updatedAt @map("updated_at") @@unique([authorizationId, currentMonth]) + @@index([accountSequence, currentMonth]) @@index([roleType, regionCode, currentMonth]) @@map("stickman_rankings") } diff --git a/backend/services/authorization-service/src/api/controllers/authorization.controller.ts b/backend/services/authorization-service/src/api/controllers/authorization.controller.ts index 2685d425..9593dcdd 100644 --- a/backend/services/authorization-service/src/api/controllers/authorization.controller.ts +++ b/backend/services/authorization-service/src/api/controllers/authorization.controller.ts @@ -54,10 +54,10 @@ export class AuthorizationController { @ApiOperation({ summary: '申请社区授权' }) @ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) async applyCommunityAuth( - @CurrentUser() user: { userId: string }, + @CurrentUser() user: { userId: string; accountSequence: number }, @Body() dto: ApplyCommunityAuthDto, ): Promise { - const command = new ApplyCommunityAuthCommand(user.userId, dto.communityName) + const command = new ApplyCommunityAuthCommand(user.userId, user.accountSequence, dto.communityName) return await this.applicationService.applyCommunityAuth(command) } @@ -65,11 +65,12 @@ export class AuthorizationController { @ApiOperation({ summary: '申请授权省公司' }) @ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) async applyAuthProvinceCompany( - @CurrentUser() user: { userId: string }, + @CurrentUser() user: { userId: string; accountSequence: number }, @Body() dto: ApplyAuthProvinceDto, ): Promise { const command = new ApplyAuthProvinceCompanyCommand( user.userId, + user.accountSequence, dto.provinceCode, dto.provinceName, ) @@ -80,10 +81,10 @@ export class AuthorizationController { @ApiOperation({ summary: '申请授权市公司' }) @ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) async applyAuthCityCompany( - @CurrentUser() user: { userId: string }, + @CurrentUser() user: { userId: string; accountSequence: number }, @Body() dto: ApplyAuthCityDto, ): Promise { - const command = new ApplyAuthCityCompanyCommand(user.userId, dto.cityCode, dto.cityName) + const command = new ApplyAuthCityCompanyCommand(user.userId, user.accountSequence, dto.cityCode, dto.cityName) return await this.applicationService.applyAuthCityCompany(command) } @@ -91,9 +92,9 @@ export class AuthorizationController { @ApiOperation({ summary: '获取我的授权列表' }) @ApiResponse({ status: 200, type: [AuthorizationResponse] }) async getMyAuthorizations( - @CurrentUser() user: { userId: string }, + @CurrentUser() user: { userId: string; accountSequence: number }, ): Promise { - return await this.applicationService.getUserAuthorizations(user.userId) + return await this.applicationService.getUserAuthorizations(user.accountSequence) } @Get(':id') @@ -125,10 +126,10 @@ export class AuthorizationController { @ApiResponse({ status: 204 }) async revokeAuthorization( @Param('id') id: string, - @CurrentUser() user: { userId: string }, + @CurrentUser() user: { userId: string; accountSequence: number }, @Body() dto: RevokeAuthorizationDto, ): Promise { - const command = new RevokeAuthorizationCommand(id, user.userId, dto.reason) + const command = new RevokeAuthorizationCommand(id, user.accountSequence, dto.reason) await this.applicationService.revokeAuthorization(command) } @@ -139,10 +140,10 @@ export class AuthorizationController { @ApiResponse({ status: 204 }) async grantMonthlyBypass( @Param('id') id: string, - @CurrentUser() user: { userId: string }, + @CurrentUser() user: { userId: string; accountSequence: number }, @Body() dto: GrantMonthlyBypassDto, ): Promise { - const command = new GrantMonthlyBypassCommand(id, dto.month, user.userId, dto.reason) + const command = new GrantMonthlyBypassCommand(id, dto.month, user.accountSequence, dto.reason) await this.applicationService.grantMonthlyBypass(command) } @@ -153,9 +154,9 @@ export class AuthorizationController { @ApiResponse({ status: 204 }) async exemptLocalPercentageCheck( @Param('id') id: string, - @CurrentUser() user: { userId: string }, + @CurrentUser() user: { userId: string; accountSequence: number }, ): Promise { - const command = new ExemptLocalPercentageCheckCommand(id, user.userId) + const command = new ExemptLocalPercentageCheckCommand(id, user.accountSequence) await this.applicationService.exemptLocalPercentageCheck(command) } } diff --git a/backend/services/authorization-service/src/application/commands/apply-auth-city-company.command.ts b/backend/services/authorization-service/src/application/commands/apply-auth-city-company.command.ts index d5aacc85..25c88dff 100644 --- a/backend/services/authorization-service/src/application/commands/apply-auth-city-company.command.ts +++ b/backend/services/authorization-service/src/application/commands/apply-auth-city-company.command.ts @@ -1,6 +1,7 @@ export class ApplyAuthCityCompanyCommand { constructor( public readonly userId: string, + public readonly accountSequence: number, public readonly cityCode: string, public readonly cityName: string, ) {} diff --git a/backend/services/authorization-service/src/application/commands/apply-auth-province-company.command.ts b/backend/services/authorization-service/src/application/commands/apply-auth-province-company.command.ts index f627838c..482dd68b 100644 --- a/backend/services/authorization-service/src/application/commands/apply-auth-province-company.command.ts +++ b/backend/services/authorization-service/src/application/commands/apply-auth-province-company.command.ts @@ -1,6 +1,7 @@ export class ApplyAuthProvinceCompanyCommand { constructor( public readonly userId: string, + public readonly accountSequence: number, public readonly provinceCode: string, public readonly provinceName: string, ) {} diff --git a/backend/services/authorization-service/src/application/commands/apply-community-auth.command.ts b/backend/services/authorization-service/src/application/commands/apply-community-auth.command.ts index 64f2e659..f2615ed0 100644 --- a/backend/services/authorization-service/src/application/commands/apply-community-auth.command.ts +++ b/backend/services/authorization-service/src/application/commands/apply-community-auth.command.ts @@ -1,6 +1,7 @@ export class ApplyCommunityAuthCommand { constructor( public readonly userId: string, + public readonly accountSequence: number, public readonly communityName: string, ) {} } diff --git a/backend/services/authorization-service/src/application/commands/exempt-percentage-check.command.ts b/backend/services/authorization-service/src/application/commands/exempt-percentage-check.command.ts index a2fb57b1..17fe805b 100644 --- a/backend/services/authorization-service/src/application/commands/exempt-percentage-check.command.ts +++ b/backend/services/authorization-service/src/application/commands/exempt-percentage-check.command.ts @@ -1,6 +1,6 @@ export class ExemptLocalPercentageCheckCommand { constructor( public readonly authorizationId: string, - public readonly adminId: string, + public readonly adminAccountSequence: number, ) {} } diff --git a/backend/services/authorization-service/src/application/commands/grant-city-company.command.ts b/backend/services/authorization-service/src/application/commands/grant-city-company.command.ts index e00e99f8..10a44a86 100644 --- a/backend/services/authorization-service/src/application/commands/grant-city-company.command.ts +++ b/backend/services/authorization-service/src/application/commands/grant-city-company.command.ts @@ -1,8 +1,10 @@ export class GrantCityCompanyCommand { constructor( public readonly userId: string, + public readonly accountSequence: number, public readonly cityCode: string, public readonly cityName: string, public readonly adminId: string, + public readonly adminAccountSequence: number, ) {} } diff --git a/backend/services/authorization-service/src/application/commands/grant-monthly-bypass.command.ts b/backend/services/authorization-service/src/application/commands/grant-monthly-bypass.command.ts index f1e916d1..c0a9ecf3 100644 --- a/backend/services/authorization-service/src/application/commands/grant-monthly-bypass.command.ts +++ b/backend/services/authorization-service/src/application/commands/grant-monthly-bypass.command.ts @@ -2,7 +2,7 @@ export class GrantMonthlyBypassCommand { constructor( public readonly authorizationId: string, public readonly month: string, - public readonly adminId: string, + public readonly adminAccountSequence: number, public readonly reason?: string, ) {} } diff --git a/backend/services/authorization-service/src/application/commands/grant-province-company.command.ts b/backend/services/authorization-service/src/application/commands/grant-province-company.command.ts index dc2a309b..c4853177 100644 --- a/backend/services/authorization-service/src/application/commands/grant-province-company.command.ts +++ b/backend/services/authorization-service/src/application/commands/grant-province-company.command.ts @@ -1,8 +1,10 @@ export class GrantProvinceCompanyCommand { constructor( public readonly userId: string, + public readonly accountSequence: number, public readonly provinceCode: string, public readonly provinceName: string, public readonly adminId: string, + public readonly adminAccountSequence: number, ) {} } diff --git a/backend/services/authorization-service/src/application/commands/revoke-authorization.command.ts b/backend/services/authorization-service/src/application/commands/revoke-authorization.command.ts index cf46fab9..ffb2a647 100644 --- a/backend/services/authorization-service/src/application/commands/revoke-authorization.command.ts +++ b/backend/services/authorization-service/src/application/commands/revoke-authorization.command.ts @@ -1,7 +1,7 @@ export class RevokeAuthorizationCommand { constructor( public readonly authorizationId: string, - public readonly adminId: string, + public readonly adminAccountSequence: number, public readonly reason: string, ) {} } diff --git a/backend/services/authorization-service/src/application/services/authorization-application.service.ts b/backend/services/authorization-service/src/application/services/authorization-application.service.ts index 021e9f98..2c543a50 100644 --- a/backend/services/authorization-service/src/application/services/authorization-application.service.ts +++ b/backend/services/authorization-service/src/application/services/authorization-application.service.ts @@ -64,11 +64,11 @@ export class AuthorizationApplicationService { async applyCommunityAuth( command: ApplyCommunityAuthCommand, ): Promise { - const userId = UserId.create(command.userId) + const userId = UserId.create(command.userId, command.accountSequence) // 1. 检查是否已有社区授权 - const existing = await this.authorizationRepository.findByUserIdAndRoleType( - userId, + const existing = await this.authorizationRepository.findByAccountSequenceAndRoleType( + userId.accountSequence, RoleType.COMMUNITY, ) @@ -83,7 +83,7 @@ export class AuthorizationApplicationService { }) // 3. 检查初始考核(10棵) - const teamStats = await this.statsRepository.findByUserId(userId.value) + const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) const totalTreeCount = teamStats?.totalTeamPlantingCount || 0 if (totalTreeCount >= authorization.getInitialTarget()) { @@ -113,7 +113,7 @@ export class AuthorizationApplicationService { async applyAuthProvinceCompany( command: ApplyAuthProvinceCompanyCommand, ): Promise { - const userId = UserId.create(command.userId) + const userId = UserId.create(command.userId, command.accountSequence) const regionCode = RegionCode.create(command.provinceCode) // 1. 验证授权申请(团队内唯一性) @@ -137,7 +137,7 @@ export class AuthorizationApplicationService { }) // 3. 检查初始考核(500棵) - const teamStats = await this.statsRepository.findByUserId(userId.value) + const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) const totalTreeCount = teamStats?.totalTeamPlantingCount || 0 if (totalTreeCount >= authorization.getInitialTarget()) { @@ -169,7 +169,7 @@ export class AuthorizationApplicationService { async applyAuthCityCompany( command: ApplyAuthCityCompanyCommand, ): Promise { - const userId = UserId.create(command.userId) + const userId = UserId.create(command.userId, command.accountSequence) const regionCode = RegionCode.create(command.cityCode) // 1. 验证 @@ -193,7 +193,7 @@ export class AuthorizationApplicationService { }) // 3. 检查初始考核(100棵) - const teamStats = await this.statsRepository.findByUserId(userId.value) + const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) const totalTreeCount = teamStats?.totalTeamPlantingCount || 0 if (totalTreeCount >= authorization.getInitialTarget()) { @@ -222,8 +222,8 @@ export class AuthorizationApplicationService { * 管理员授权正式省公司 */ async grantProvinceCompany(command: GrantProvinceCompanyCommand): Promise { - const userId = UserId.create(command.userId) - const adminId = AdminUserId.create(command.adminId) + const userId = UserId.create(command.userId, command.accountSequence) + const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) const authorization = AuthorizationRole.createProvinceCompany({ userId, @@ -241,8 +241,8 @@ export class AuthorizationApplicationService { * 管理员授权正式市公司 */ async grantCityCompany(command: GrantCityCompanyCommand): Promise { - const userId = UserId.create(command.userId) - const adminId = AdminUserId.create(command.adminId) + const userId = UserId.create(command.userId, command.accountSequence) + const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) const authorization = AuthorizationRole.createCityCompany({ userId, @@ -268,7 +268,10 @@ export class AuthorizationApplicationService { throw new NotFoundError('授权不存在') } - authorization.revoke(AdminUserId.create(command.adminId), command.reason) + // Note: We need the adminId from somewhere, for now using a placeholder + // In a real scenario, we would need to fetch the admin's userId from the accountSequence + const adminId = AdminUserId.create('admin', command.adminAccountSequence) + authorization.revoke(adminId, command.reason) await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) @@ -288,7 +291,9 @@ export class AuthorizationApplicationService { throw new NotFoundError('考核记录不存在') } - assessment.grantBypass(AdminUserId.create(command.adminId)) + // Note: We need the adminId from somewhere, for now using a placeholder + const adminId = AdminUserId.create('admin', command.adminAccountSequence) + assessment.grantBypass(adminId) await this.assessmentRepository.save(assessment) await this.eventPublisher.publishAll(assessment.domainEvents) @@ -307,7 +312,9 @@ export class AuthorizationApplicationService { throw new NotFoundError('授权不存在') } - authorization.exemptLocalPercentageCheck(AdminUserId.create(command.adminId)) + // Note: We need the adminId from somewhere, for now using a placeholder + const adminId = AdminUserId.create('admin', command.adminAccountSequence) + authorization.exemptLocalPercentageCheck(adminId) await this.authorizationRepository.save(authorization) await this.eventPublisher.publishAll(authorization.domainEvents) @@ -317,13 +324,13 @@ export class AuthorizationApplicationService { /** * 查询用户授权列表 */ - async getUserAuthorizations(userId: string): Promise { - const authorizations = await this.authorizationRepository.findByUserId( - UserId.create(userId), + async getUserAuthorizations(accountSequence: number): Promise { + const authorizations = await this.authorizationRepository.findByAccountSequence( + BigInt(accountSequence), ) // 查询用户团队统计数据 - const teamStats = await this.statsRepository.findByUserId(UserId.create(userId).value) + const teamStats = await this.statsRepository.findByAccountSequence(BigInt(accountSequence)) const currentTreeCount = teamStats?.totalTeamPlantingCount || 0 return authorizations.map((auth) => this.toAuthorizationDTO(auth, currentTreeCount)) @@ -340,7 +347,7 @@ export class AuthorizationApplicationService { if (!authorization) return null // 查询用户团队统计数据 - const teamStats = await this.statsRepository.findByUserId(authorization.userId.value) + const teamStats = await this.statsRepository.findByAccountSequence(authorization.userId.accountSequence) const currentTreeCount = teamStats?.totalTeamPlantingCount || 0 return this.toAuthorizationDTO(authorization, currentTreeCount) diff --git a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts index 4fb2437c..fb29f81c 100644 --- a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts +++ b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.ts @@ -559,16 +559,17 @@ export class AuthorizationRole extends AggregateRoot { toPersistence(): Record { return { id: this._authorizationId.value, - userId: this._userId.value, + userId: this._userId.accountSequence, + accountSequence: this._userId.accountSequence, roleType: this._roleType, regionCode: this._regionCode.value, regionName: this._regionName, status: this._status, displayTitle: this._displayTitle, authorizedAt: this._authorizedAt, - authorizedBy: this._authorizedBy?.value || null, + authorizedBy: this._authorizedBy?.accountSequence || null, revokedAt: this._revokedAt, - revokedBy: this._revokedBy?.value || null, + revokedBy: this._revokedBy?.accountSequence || null, revokeReason: this._revokeReason, initialTargetTreeCount: this._assessmentConfig.initialTargetTreeCount, monthlyTargetType: this._assessmentConfig.monthlyTargetType, diff --git a/backend/services/authorization-service/src/domain/aggregates/monthly-assessment.aggregate.ts b/backend/services/authorization-service/src/domain/aggregates/monthly-assessment.aggregate.ts index b7e73f93..6ad9dbe5 100644 --- a/backend/services/authorization-service/src/domain/aggregates/monthly-assessment.aggregate.ts +++ b/backend/services/authorization-service/src/domain/aggregates/monthly-assessment.aggregate.ts @@ -398,7 +398,8 @@ export class MonthlyAssessment extends AggregateRoot { return { id: this._assessmentId.value, authorizationId: this._authorizationId.value, - userId: this._userId.value, + userId: this._userId.accountSequence, + accountSequence: this._userId.accountSequence, roleType: this._roleType, regionCode: this._regionCode.value, assessmentMonth: this._assessmentMonth.value, @@ -417,7 +418,7 @@ export class MonthlyAssessment extends AggregateRoot { rankingInRegion: this._rankingInRegion, isFirstPlace: this._isFirstPlace, isBypassed: this._isBypassed, - bypassedBy: this._bypassedBy?.value || null, + bypassedBy: this._bypassedBy?.accountSequence || null, bypassedAt: this._bypassedAt, assessedAt: this._assessedAt, createdAt: this._createdAt, diff --git a/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts b/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts index 8afaf12e..c7745e37 100644 --- a/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts +++ b/backend/services/authorization-service/src/domain/repositories/authorization-role.repository.ts @@ -8,12 +8,14 @@ export interface IAuthorizationRoleRepository { save(authorization: AuthorizationRole): Promise findById(authorizationId: AuthorizationId): Promise findByUserIdAndRoleType(userId: UserId, roleType: RoleType): Promise + findByAccountSequenceAndRoleType(accountSequence: bigint, roleType: RoleType): Promise findByUserIdRoleTypeAndRegion( userId: UserId, roleType: RoleType, regionCode: RegionCode, ): Promise findByUserId(userId: UserId): Promise + findByAccountSequence(accountSequence: bigint): Promise findActiveByRoleTypeAndRegion( roleType: RoleType, regionCode: RegionCode, diff --git a/backend/services/authorization-service/src/domain/value-objects/user-id.vo.ts b/backend/services/authorization-service/src/domain/value-objects/user-id.vo.ts index 118eab86..6f59b513 100644 --- a/backend/services/authorization-service/src/domain/value-objects/user-id.vo.ts +++ b/backend/services/authorization-service/src/domain/value-objects/user-id.vo.ts @@ -1,18 +1,24 @@ import { DomainError } from '@/shared/exceptions' export class UserId { - constructor(public readonly value: string) { + constructor( + public readonly value: string, + public readonly accountSequence: bigint, + ) { if (!value) { throw new DomainError('用户ID不能为空') } + if (accountSequence === undefined || accountSequence === null) { + throw new DomainError('账户序列号不能为空') + } } - static create(value: string): UserId { - return new UserId(value) + static create(value: string, accountSequence: number | bigint): UserId { + return new UserId(value, BigInt(accountSequence)) } equals(other: UserId): boolean { - return this.value === other.value + return this.value === other.value && this.accountSequence === other.accountSequence } toString(): string { @@ -21,18 +27,24 @@ export class UserId { } export class AdminUserId { - constructor(public readonly value: string) { + constructor( + public readonly value: string, + public readonly accountSequence: bigint, + ) { if (!value) { throw new DomainError('管理员ID不能为空') } + if (accountSequence === undefined || accountSequence === null) { + throw new DomainError('管理员账户序列号不能为空') + } } - static create(value: string): AdminUserId { - return new AdminUserId(value) + static create(value: string, accountSequence: number | bigint): AdminUserId { + return new AdminUserId(value, BigInt(accountSequence)) } equals(other: AdminUserId): boolean { - return this.value === other.value + return this.value === other.value && this.accountSequence === other.accountSequence } toString(): string { diff --git a/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts b/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts index ca31537b..f1ea450c 100644 --- a/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts +++ b/backend/services/authorization-service/src/infrastructure/persistence/repositories/authorization-role.repository.impl.ts @@ -25,6 +25,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi create: { id: data.id, userId: data.userId, + accountSequence: data.accountSequence, roleType: data.roleType, regionCode: data.regionCode, regionName: data.regionName, @@ -97,6 +98,19 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi return record ? this.toDomain(record) : null } + async findByAccountSequenceAndRoleType( + accountSequence: bigint, + roleType: RoleType, + ): Promise { + const record = await this.prisma.authorizationRole.findFirst({ + where: { + accountSequence: accountSequence, + roleType: roleType, + }, + }) + return record ? this.toDomain(record) : null + } + async findByUserId(userId: UserId): Promise { const records = await this.prisma.authorizationRole.findMany({ where: { userId: userId.value }, @@ -105,6 +119,14 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi return records.map((record) => this.toDomain(record)) } + async findByAccountSequence(accountSequence: bigint): Promise { + const records = await this.prisma.authorizationRole.findMany({ + where: { accountSequence: accountSequence }, + orderBy: { createdAt: 'desc' }, + }) + return records.map((record) => this.toDomain(record)) + } + async findActiveByRoleTypeAndRegion( roleType: RoleType, regionCode: RegionCode, @@ -157,16 +179,16 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi private toDomain(record: any): AuthorizationRole { const props: AuthorizationRoleProps = { authorizationId: AuthorizationId.create(record.id), - userId: UserId.create(record.userId), + userId: UserId.create(record.userId.toString(), record.accountSequence), roleType: record.roleType as RoleType, regionCode: RegionCode.create(record.regionCode), regionName: record.regionName, status: record.status as AuthorizationStatus, displayTitle: record.displayTitle, authorizedAt: record.authorizedAt, - authorizedBy: record.authorizedBy ? AdminUserId.create(record.authorizedBy) : null, + authorizedBy: record.authorizedBy ? AdminUserId.create(record.authorizedBy.toString(), record.authorizedBy) : null, revokedAt: record.revokedAt, - revokedBy: record.revokedBy ? AdminUserId.create(record.revokedBy) : null, + revokedBy: record.revokedBy ? AdminUserId.create(record.revokedBy.toString(), record.revokedBy) : null, revokeReason: record.revokeReason, assessmentConfig: new AssessmentConfig( record.initialTargetTreeCount, diff --git a/backend/services/authorization-service/src/infrastructure/persistence/repositories/monthly-assessment.repository.impl.ts b/backend/services/authorization-service/src/infrastructure/persistence/repositories/monthly-assessment.repository.impl.ts index 313cd4e6..c712fe02 100644 --- a/backend/services/authorization-service/src/infrastructure/persistence/repositories/monthly-assessment.repository.impl.ts +++ b/backend/services/authorization-service/src/infrastructure/persistence/repositories/monthly-assessment.repository.impl.ts @@ -27,6 +27,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi id: data.id, authorizationId: data.authorizationId, userId: data.userId, + accountSequence: data.accountSequence, roleType: data.roleType, regionCode: data.regionCode, assessmentMonth: data.assessmentMonth, @@ -79,6 +80,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi id: data.id, authorizationId: data.authorizationId, userId: data.userId, + accountSequence: data.accountSequence, roleType: data.roleType, regionCode: data.regionCode, assessmentMonth: data.assessmentMonth, @@ -212,7 +214,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi const props: MonthlyAssessmentProps = { assessmentId: AssessmentId.create(record.id), authorizationId: AuthorizationId.create(record.authorizationId), - userId: UserId.create(record.userId), + userId: UserId.create(record.userId.toString(), record.accountSequence), roleType: record.roleType as RoleType, regionCode: RegionCode.create(record.regionCode), assessmentMonth: Month.create(record.assessmentMonth), @@ -231,7 +233,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi rankingInRegion: record.rankingInRegion, isFirstPlace: record.isFirstPlace, isBypassed: record.isBypassed, - bypassedBy: record.bypassedBy ? AdminUserId.create(record.bypassedBy) : null, + bypassedBy: record.bypassedBy ? AdminUserId.create(record.bypassedBy.toString(), record.bypassedBy) : null, bypassedAt: record.bypassedAt, assessedAt: record.assessedAt, createdAt: record.createdAt, diff --git a/backend/services/planting-service/src/api/controllers/planting-order.controller.ts b/backend/services/planting-service/src/api/controllers/planting-order.controller.ts index 63c94c9a..4a3bf550 100644 --- a/backend/services/planting-service/src/api/controllers/planting-order.controller.ts +++ b/backend/services/planting-service/src/api/controllers/planting-order.controller.ts @@ -32,7 +32,7 @@ import { import { JwtAuthGuard } from '../guards/jwt-auth.guard'; interface AuthenticatedRequest { - user: { id: string }; + user: { id: string; accountSequence: number }; } @ApiTags('认种订单') @@ -124,7 +124,7 @@ export class PlantingOrderController { @Param('orderNo') orderNo: string, ): Promise { const userId = BigInt(req.user.id); - return this.plantingService.payOrder(orderNo, userId); + return this.plantingService.payOrder(orderNo, userId, req.user.accountSequence); } @Get('orders') diff --git a/backend/services/planting-service/src/api/guards/jwt-auth.guard.ts b/backend/services/planting-service/src/api/guards/jwt-auth.guard.ts index ab8385a4..9adac864 100644 --- a/backend/services/planting-service/src/api/guards/jwt-auth.guard.ts +++ b/backend/services/planting-service/src/api/guards/jwt-auth.guard.ts @@ -10,6 +10,7 @@ import * as jwt from 'jsonwebtoken'; export interface JwtPayload { sub: string; userId: string; + accountSequence: number; iat: number; exp: number; } @@ -35,6 +36,7 @@ export class JwtAuthGuard implements CanActivate { const payload = jwt.verify(token, secret) as JwtPayload; request.user = { id: payload.userId || payload.sub, + accountSequence: payload.accountSequence, }; return true; diff --git a/backend/services/planting-service/src/application/services/planting-application.service.ts b/backend/services/planting-service/src/application/services/planting-application.service.ts index 6da3c45f..745d272d 100644 --- a/backend/services/planting-service/src/application/services/planting-application.service.ts +++ b/backend/services/planting-service/src/application/services/planting-application.service.ts @@ -173,6 +173,7 @@ export class PlantingApplicationService { async payOrder( orderNo: string, userId: bigint, + accountSequence?: number, ): Promise<{ orderNo: string; status: string; @@ -208,7 +209,7 @@ export class PlantingApplicationService { // 3. 获取推荐链上下文 (先获取,确保服务可用) const referralContext = await this.referralService.getReferralContext( - userId.toString(), + accountSequence!, selection.provinceCode, selection.cityCode, ); diff --git a/backend/services/planting-service/src/infrastructure/external/referral-service.client.ts b/backend/services/planting-service/src/infrastructure/external/referral-service.client.ts index ea6846c1..8af78d05 100644 --- a/backend/services/planting-service/src/infrastructure/external/referral-service.client.ts +++ b/backend/services/planting-service/src/infrastructure/external/referral-service.client.ts @@ -30,14 +30,14 @@ export class ReferralServiceClient { * 获取用户的推荐链和权限上级信息 */ async getReferralContext( - userId: string, + accountSequence: number, provinceCode: string, cityCode: string, ): Promise { try { const response = await firstValueFrom( this.httpService.get( - `${this.baseUrl}/api/v1/referrals/${userId}/context`, + `${this.baseUrl}/api/v1/referrals/${accountSequence}/context`, { params: { provinceCode, cityCode }, }, @@ -52,7 +52,7 @@ export class ReferralServiceClient { }; } catch (error) { this.logger.error( - `Failed to get referral context for user ${userId}`, + `Failed to get referral context for accountSequence ${accountSequence}`, error, ); // 在开发环境返回默认空数据 diff --git a/backend/services/referral-service/src/api/controllers/referral.controller.ts b/backend/services/referral-service/src/api/controllers/referral.controller.ts index 5e585f38..f6ec4d93 100644 --- a/backend/services/referral-service/src/api/controllers/referral.controller.ts +++ b/backend/services/referral-service/src/api/controllers/referral.controller.ts @@ -41,7 +41,7 @@ export class ReferralController { @ApiOperation({ summary: '获取当前用户推荐信息' }) @ApiResponse({ status: 200, type: ReferralInfoResponseDto }) async getMyReferralInfo(@CurrentUser('userId') userId: bigint): Promise { - const query = new GetUserReferralInfoQuery(userId); + const query = new GetUserReferralInfoQuery(Number(userId)); return this.referralService.getUserReferralInfo(query); } @@ -104,7 +104,7 @@ export class ReferralController { @ApiParam({ name: 'userId', description: '用户ID' }) @ApiResponse({ status: 200, type: ReferralInfoResponseDto }) async getUserReferralInfo(@Param('userId') userId: string): Promise { - const query = new GetUserReferralInfoQuery(BigInt(userId)); + const query = new GetUserReferralInfoQuery(Number(userId)); return this.referralService.getUserReferralInfo(query); } } @@ -118,23 +118,23 @@ export class ReferralController { export class InternalReferralController { constructor(private readonly referralService: ReferralService) {} - @Get(':userId/context') + @Get(':accountSequence/context') @ApiOperation({ summary: '获取用户推荐上下文信息(内部API)' }) - @ApiParam({ name: 'userId', description: '用户ID' }) + @ApiParam({ name: 'accountSequence', description: '账户序列号' }) @ApiResponse({ status: 200, description: '推荐上下文' }) async getReferralContext( - @Param('userId') userId: string, + @Param('accountSequence') accountSequence: string, @Query('provinceCode') provinceCode: string, @Query('cityCode') cityCode: string, ) { // 获取用户的推荐链 - const query = new GetUserReferralInfoQuery(BigInt(userId)); + const query = new GetUserReferralInfoQuery(Number(accountSequence)); const referralInfo = await this.referralService.getUserReferralInfo(query); // 返回推荐上下文信息 // 目前返回基础信息,后续可以扩展省市授权等信息 return { - userId, + accountSequence, referralChain: referralInfo.referrerId ? [referralInfo.referrerId] : [], referrerId: referralInfo.referrerId, nearestProvinceAuth: null, // 省代账户ID - 需要后续实现 diff --git a/backend/services/referral-service/src/application/queries/get-user-referral-info.query.ts b/backend/services/referral-service/src/application/queries/get-user-referral-info.query.ts index d4944f69..bfb9af83 100644 --- a/backend/services/referral-service/src/application/queries/get-user-referral-info.query.ts +++ b/backend/services/referral-service/src/application/queries/get-user-referral-info.query.ts @@ -1,5 +1,5 @@ export class GetUserReferralInfoQuery { - constructor(public readonly userId: bigint) {} + constructor(public readonly accountSequence: number) {} } export interface UserReferralInfoResult { diff --git a/backend/services/referral-service/src/application/services/referral.service.ts b/backend/services/referral-service/src/application/services/referral.service.ts index d903adea..fcdf32bf 100644 --- a/backend/services/referral-service/src/application/services/referral.service.ts +++ b/backend/services/referral-service/src/application/services/referral.service.ts @@ -114,12 +114,12 @@ export class ReferralService { * 获取用户推荐信息 */ async getUserReferralInfo(query: GetUserReferralInfoQuery): Promise { - const relationship = await this.referralRepo.findByUserId(query.userId); + const relationship = await this.referralRepo.findByAccountSequence(query.accountSequence); if (!relationship) { throw new NotFoundException('用户推荐关系不存在'); } - const teamStats = await this.teamStatsRepo.findByUserId(query.userId); + const teamStats = await this.teamStatsRepo.findByUserId(relationship.userId); return { userId: relationship.userId.toString(), diff --git a/backend/services/reward-service/prisma/schema.prisma b/backend/services/reward-service/prisma/schema.prisma index 4af8c7ac..f63e14a1 100644 --- a/backend/services/reward-service/prisma/schema.prisma +++ b/backend/services/reward-service/prisma/schema.prisma @@ -14,6 +14,7 @@ datasource db { model RewardLedgerEntry { id BigInt @id @default(autoincrement()) @map("entry_id") userId BigInt @map("user_id") // 接收奖励的用户ID + accountSequence BigInt @map("account_sequence") // 账户序列号 // === 奖励来源 === sourceOrderNo String @map("source_order_no") @db.VarChar(50) // 来源认种订单号(字符串格式如PLT1765391584505Q0Q6QD) @@ -40,6 +41,8 @@ model RewardLedgerEntry { @@map("reward_ledger_entries") @@index([userId, rewardStatus], name: "idx_user_status") @@index([userId, createdAt(sort: Desc)], name: "idx_user_created") + @@index([accountSequence, rewardStatus], name: "idx_account_status") + @@index([accountSequence, createdAt(sort: Desc)], name: "idx_account_created") @@index([sourceOrderNo], name: "idx_source_order") @@index([sourceUserId], name: "idx_source_user") @@index([rightType], name: "idx_right_type") @@ -55,6 +58,7 @@ model RewardLedgerEntry { model RewardSummary { id BigInt @id @default(autoincrement()) @map("summary_id") userId BigInt @unique @map("user_id") + accountSequence BigInt @unique @map("account_sequence") // 账户序列号 // === 待领取收益 (24h倒计时) === pendingUsdt Decimal @default(0) @map("pending_usdt") @db.Decimal(20, 8) @@ -79,6 +83,7 @@ model RewardSummary { @@map("reward_summaries") @@index([userId], name: "idx_summary_user") + @@index([accountSequence], name: "idx_summary_account") @@index([settleableUsdt(sort: Desc)], name: "idx_settleable_desc") @@index([pendingExpireAt], name: "idx_pending_expire") } @@ -119,6 +124,7 @@ model RightDefinition { model SettlementRecord { id BigInt @id @default(autoincrement()) @map("settlement_id") userId BigInt @map("user_id") + accountSequence BigInt @map("account_sequence") // 账户序列号 // === 结算金额 === usdtAmount Decimal @map("usdt_amount") @db.Decimal(20, 8) @@ -144,6 +150,7 @@ model SettlementRecord { @@map("settlement_records") @@index([userId], name: "idx_settlement_user") + @@index([accountSequence], name: "idx_settlement_account") @@index([status], name: "idx_settlement_status") @@index([createdAt], name: "idx_settlement_created") } diff --git a/backend/services/reward-service/src/api/controllers/reward.controller.ts b/backend/services/reward-service/src/api/controllers/reward.controller.ts index 6c7534ef..b200a903 100644 --- a/backend/services/reward-service/src/api/controllers/reward.controller.ts +++ b/backend/services/reward-service/src/api/controllers/reward.controller.ts @@ -18,8 +18,8 @@ export class RewardController { @ApiOperation({ summary: '获取我的收益汇总' }) @ApiResponse({ status: 200, description: '成功', type: RewardSummaryDto }) async getSummary(@Request() req): Promise { - const userId = BigInt(req.user.sub); - const summary = await this.rewardService.getRewardSummary(userId); + const accountSequence = BigInt(req.user.accountSequence); + const summary = await this.rewardService.getRewardSummary(accountSequence); return { ...summary, @@ -43,19 +43,19 @@ export class RewardController { @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number = 1, @Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number = 20, ) { - const userId = BigInt(req.user.sub); + const accountSequence = BigInt(req.user.accountSequence); const filters: any = {}; if (status) filters.status = status; if (rightType) filters.rightType = rightType; - return this.rewardService.getRewardDetails(userId, filters, { page, pageSize }); + return this.rewardService.getRewardDetails(accountSequence, filters, { page, pageSize }); } @Get('pending') @ApiOperation({ summary: '获取待领取奖励(含倒计时)' }) @ApiResponse({ status: 200, description: '成功' }) async getPending(@Request() req) { - const userId = BigInt(req.user.sub); - return this.rewardService.getPendingRewards(userId); + const accountSequence = BigInt(req.user.accountSequence); + return this.rewardService.getPendingRewards(accountSequence); } } diff --git a/backend/services/reward-service/src/api/controllers/settlement.controller.ts b/backend/services/reward-service/src/api/controllers/settlement.controller.ts index 7c374ec3..9f0adce5 100644 --- a/backend/services/reward-service/src/api/controllers/settlement.controller.ts +++ b/backend/services/reward-service/src/api/controllers/settlement.controller.ts @@ -20,10 +20,10 @@ export class SettlementController { @Request() req, @Body() dto: SettleRewardsDto, ): Promise { - const userId = BigInt(req.user.sub); + const accountSequence = BigInt(req.user.accountSequence); return this.rewardService.settleRewards({ - userId, + accountSequence, settleCurrency: dto.settleCurrency, }); } diff --git a/backend/services/reward-service/src/application/services/reward-application.service.ts b/backend/services/reward-service/src/application/services/reward-application.service.ts index 7980ce8b..4bacd6ee 100644 --- a/backend/services/reward-service/src/application/services/reward-application.service.ts +++ b/backend/services/reward-service/src/application/services/reward-application.service.ts @@ -120,7 +120,7 @@ export class RewardApplicationService { * 结算可结算收益 */ async settleRewards(params: { - userId: bigint; + accountSequence: bigint; settleCurrency: string; // BNB/OG/USDT/DST }): Promise<{ success: boolean; @@ -130,10 +130,10 @@ export class RewardApplicationService { txHash?: string; error?: string; }> { - this.logger.log(`Settling rewards for user ${params.userId}`); + this.logger.log(`Settling rewards for accountSequence ${params.accountSequence}`); // 1. 获取可结算奖励 - const settleableRewards = await this.rewardLedgerEntryRepository.findSettleableByUserId(params.userId); + const settleableRewards = await this.rewardLedgerEntryRepository.findSettleableByAccountSequence(params.accountSequence); if (settleableRewards.length === 0) { return { @@ -149,9 +149,10 @@ export class RewardApplicationService { const totalUsdt = settleableRewards.reduce((sum, r) => sum + r.usdtAmount.amount, 0); const totalHashpower = settleableRewards.reduce((sum, r) => sum + r.hashpowerAmount.value, 0); - // 3. 调用钱包服务执行SWAP + // 3. 调用钱包服务执行SWAP (使用第一条记录的userId) + const userId = settleableRewards[0].userId; const swapResult = await this.walletService.executeSwap({ - userId: params.userId, + userId: userId, usdtAmount: totalUsdt, targetCurrency: params.settleCurrency, }); @@ -175,11 +176,13 @@ export class RewardApplicationService { } // 5. 更新汇总数据 - const summary = await this.rewardSummaryRepository.getOrCreate(params.userId); - summary.settle(Money.USDT(totalUsdt), Hashpower.create(totalHashpower)); - await this.rewardSummaryRepository.save(summary); + const summary = await this.rewardSummaryRepository.findByAccountSequence(params.accountSequence); + if (summary) { + summary.settle(Money.USDT(totalUsdt), Hashpower.create(totalHashpower)); + await this.rewardSummaryRepository.save(summary); + } - this.logger.log(`Settled ${totalUsdt} USDT for user ${params.userId}`); + this.logger.log(`Settled ${totalUsdt} USDT for accountSequence ${params.accountSequence}`); return { success: true, @@ -251,8 +254,8 @@ export class RewardApplicationService { /** * 获取用户奖励汇总 */ - async getRewardSummary(userId: bigint) { - const summary = await this.rewardSummaryRepository.findByUserId(userId); + async getRewardSummary(accountSequence: bigint) { + const summary = await this.rewardSummaryRepository.findByAccountSequence(accountSequence); if (!summary) { return { @@ -285,7 +288,7 @@ export class RewardApplicationService { * 获取用户奖励明细 */ async getRewardDetails( - userId: bigint, + accountSequence: bigint, filters?: { status?: RewardStatus; rightType?: RightType; @@ -294,8 +297,8 @@ export class RewardApplicationService { }, pagination?: { page: number; pageSize: number }, ) { - const rewards = await this.rewardLedgerEntryRepository.findByUserId(userId, filters, pagination); - const total = await this.rewardLedgerEntryRepository.countByUserId(userId, filters?.status); + const rewards = await this.rewardLedgerEntryRepository.findByAccountSequence(accountSequence, filters, pagination); + const total = await this.rewardLedgerEntryRepository.countByAccountSequence(accountSequence, filters?.status); return { data: rewards.map(r => ({ @@ -323,8 +326,8 @@ export class RewardApplicationService { /** * 获取待领取奖励(含倒计时) */ - async getPendingRewards(userId: bigint) { - const rewards = await this.rewardLedgerEntryRepository.findPendingByUserId(userId); + async getPendingRewards(accountSequence: bigint) { + const rewards = await this.rewardLedgerEntryRepository.findPendingByAccountSequence(accountSequence); return rewards.map(r => ({ id: r.id?.toString(), diff --git a/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.aggregate.ts b/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.aggregate.ts index ee171173..35728c28 100644 --- a/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.aggregate.ts +++ b/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.aggregate.ts @@ -20,6 +20,7 @@ import { Hashpower } from '../../value-objects/hashpower.vo'; export class RewardLedgerEntry { private _id: bigint | null = null; private readonly _userId: bigint; + private readonly _accountSequence: bigint; private readonly _rewardSource: RewardSource; private readonly _usdtAmount: Money; private readonly _hashpowerAmount: Hashpower; @@ -35,6 +36,7 @@ export class RewardLedgerEntry { private constructor( userId: bigint, + accountSequence: bigint, rewardSource: RewardSource, usdtAmount: Money, hashpowerAmount: Hashpower, @@ -44,6 +46,7 @@ export class RewardLedgerEntry { memo: string, ) { this._userId = userId; + this._accountSequence = accountSequence; this._rewardSource = rewardSource; this._usdtAmount = usdtAmount; this._hashpowerAmount = hashpowerAmount; @@ -59,6 +62,7 @@ export class RewardLedgerEntry { // ============ Getters ============ get id(): bigint | null { return this._id; } get userId(): bigint { return this._userId; } + get accountSequence(): bigint { return this._accountSequence; } get rewardSource(): RewardSource { return this._rewardSource; } get usdtAmount(): Money { return this._usdtAmount; } get hashpowerAmount(): Hashpower { return this._hashpowerAmount; } @@ -84,6 +88,7 @@ export class RewardLedgerEntry { */ static createPending(params: { userId: bigint; + accountSequence: bigint; rewardSource: RewardSource; usdtAmount: Money; hashpowerAmount: Hashpower; @@ -94,6 +99,7 @@ export class RewardLedgerEntry { const entry = new RewardLedgerEntry( params.userId, + params.accountSequence, params.rewardSource, params.usdtAmount, params.hashpowerAmount, @@ -124,6 +130,7 @@ export class RewardLedgerEntry { */ static createSettleable(params: { userId: bigint; + accountSequence: bigint; rewardSource: RewardSource; usdtAmount: Money; hashpowerAmount: Hashpower; @@ -131,6 +138,7 @@ export class RewardLedgerEntry { }): RewardLedgerEntry { const entry = new RewardLedgerEntry( params.userId, + params.accountSequence, params.rewardSource, params.usdtAmount, params.hashpowerAmount, @@ -252,6 +260,7 @@ export class RewardLedgerEntry { static reconstitute(data: { id: bigint; userId: bigint; + accountSequence: bigint; rewardSource: RewardSource; usdtAmount: number; hashpowerAmount: number; @@ -265,6 +274,7 @@ export class RewardLedgerEntry { }): RewardLedgerEntry { const entry = new RewardLedgerEntry( data.userId, + data.accountSequence, data.rewardSource, Money.USDT(data.usdtAmount), Hashpower.create(data.hashpowerAmount), diff --git a/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.aggregate.ts b/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.aggregate.ts index 3eabe31e..e0a8dc4e 100644 --- a/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.aggregate.ts +++ b/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.aggregate.ts @@ -8,6 +8,7 @@ import { Hashpower } from '../../value-objects/hashpower.vo'; export class RewardSummary { private _id: bigint | null = null; private readonly _userId: bigint; + private readonly _accountSequence: bigint; // 待领取收益 private _pendingUsdt: Money; @@ -29,8 +30,9 @@ export class RewardSummary { private _lastUpdateAt: Date; private readonly _createdAt: Date; - private constructor(userId: bigint) { + private constructor(userId: bigint, accountSequence: bigint) { this._userId = userId; + this._accountSequence = accountSequence; this._pendingUsdt = Money.zero(); this._pendingHashpower = Hashpower.zero(); this._pendingExpireAt = null; @@ -47,6 +49,7 @@ export class RewardSummary { // ============ Getters ============ get id(): bigint | null { return this._id; } get userId(): bigint { return this._userId; } + get accountSequence(): bigint { return this._accountSequence; } get pendingUsdt(): Money { return this._pendingUsdt; } get pendingHashpower(): Hashpower { return this._pendingHashpower; } get pendingExpireAt(): Date | null { return this._pendingExpireAt; } @@ -61,8 +64,8 @@ export class RewardSummary { // ============ 工厂方法 ============ - static create(userId: bigint): RewardSummary { - return new RewardSummary(userId); + static create(userId: bigint, accountSequence: bigint): RewardSummary { + return new RewardSummary(userId, accountSequence); } // ============ 领域行为 ============ @@ -140,6 +143,7 @@ export class RewardSummary { static reconstitute(data: { id: bigint; userId: bigint; + accountSequence: bigint; pendingUsdt: number; pendingHashpower: number; pendingExpireAt: Date | null; @@ -152,7 +156,7 @@ export class RewardSummary { lastUpdateAt: Date; createdAt: Date; }): RewardSummary { - const summary = new RewardSummary(data.userId); + const summary = new RewardSummary(data.userId, data.accountSequence); summary._id = data.id; summary._pendingUsdt = Money.USDT(data.pendingUsdt); summary._pendingHashpower = Hashpower.create(data.pendingHashpower); diff --git a/backend/services/reward-service/src/domain/repositories/reward-ledger-entry.repository.interface.ts b/backend/services/reward-service/src/domain/repositories/reward-ledger-entry.repository.interface.ts index 2287fd1d..523b6272 100644 --- a/backend/services/reward-service/src/domain/repositories/reward-ledger-entry.repository.interface.ts +++ b/backend/services/reward-service/src/domain/repositories/reward-ledger-entry.repository.interface.ts @@ -16,11 +16,24 @@ export interface IRewardLedgerEntryRepository { }, pagination?: { page: number; pageSize: number }, ): Promise; + findByAccountSequence( + accountSequence: bigint, + filters?: { + status?: RewardStatus; + rightType?: RightType; + startDate?: Date; + endDate?: Date; + }, + pagination?: { page: number; pageSize: number }, + ): Promise; findPendingByUserId(userId: bigint): Promise; + findPendingByAccountSequence(accountSequence: bigint): Promise; findSettleableByUserId(userId: bigint): Promise; + findSettleableByAccountSequence(accountSequence: bigint): Promise; findExpiredPending(beforeDate: Date): Promise; findBySourceOrderNo(sourceOrderNo: string): Promise; countByUserId(userId: bigint, status?: RewardStatus): Promise; + countByAccountSequence(accountSequence: bigint, status?: RewardStatus): Promise; } export const REWARD_LEDGER_ENTRY_REPOSITORY = Symbol('IRewardLedgerEntryRepository'); diff --git a/backend/services/reward-service/src/domain/repositories/reward-summary.repository.interface.ts b/backend/services/reward-service/src/domain/repositories/reward-summary.repository.interface.ts index 4f040f84..36ac75ae 100644 --- a/backend/services/reward-service/src/domain/repositories/reward-summary.repository.interface.ts +++ b/backend/services/reward-service/src/domain/repositories/reward-summary.repository.interface.ts @@ -3,7 +3,9 @@ import { RewardSummary } from '../aggregates/reward-summary/reward-summary.aggre export interface IRewardSummaryRepository { save(summary: RewardSummary): Promise; findByUserId(userId: bigint): Promise; + findByAccountSequence(accountSequence: bigint): Promise; getOrCreate(userId: bigint): Promise; + getOrCreateByAccountSequence(accountSequence: bigint): Promise; findByUserIds(userIds: bigint[]): Promise>; findTopSettleableUsers(limit: number): Promise; } diff --git a/backend/services/reward-service/src/infrastructure/persistence/mappers/reward-ledger-entry.mapper.ts b/backend/services/reward-service/src/infrastructure/persistence/mappers/reward-ledger-entry.mapper.ts index 27dd7cab..43ae9160 100644 --- a/backend/services/reward-service/src/infrastructure/persistence/mappers/reward-ledger-entry.mapper.ts +++ b/backend/services/reward-service/src/infrastructure/persistence/mappers/reward-ledger-entry.mapper.ts @@ -9,6 +9,7 @@ export class RewardLedgerEntryMapper { return RewardLedgerEntry.reconstitute({ id: raw.id, userId: raw.userId, + accountSequence: raw.accountSequence, rewardSource: RewardSource.create( raw.rightType as RightType, raw.sourceOrderNo, @@ -30,6 +31,7 @@ export class RewardLedgerEntryMapper { return { id: entry.id || undefined, userId: entry.userId, + accountSequence: entry.accountSequence, sourceOrderNo: entry.rewardSource.sourceOrderNo, sourceUserId: entry.rewardSource.sourceUserId, rightType: entry.rewardSource.rightType, diff --git a/backend/services/reward-service/src/infrastructure/persistence/mappers/reward-summary.mapper.ts b/backend/services/reward-service/src/infrastructure/persistence/mappers/reward-summary.mapper.ts index 0f1e1fe8..31526eae 100644 --- a/backend/services/reward-service/src/infrastructure/persistence/mappers/reward-summary.mapper.ts +++ b/backend/services/reward-service/src/infrastructure/persistence/mappers/reward-summary.mapper.ts @@ -6,6 +6,7 @@ export class RewardSummaryMapper { return RewardSummary.reconstitute({ id: raw.id, userId: raw.userId, + accountSequence: raw.accountSequence, pendingUsdt: Number(raw.pendingUsdt), pendingHashpower: Number(raw.pendingHashpower), pendingExpireAt: raw.pendingExpireAt, @@ -24,6 +25,7 @@ export class RewardSummaryMapper { return { id: summary.id || undefined, userId: summary.userId, + accountSequence: summary.accountSequence, pendingUsdt: new Prisma.Decimal(summary.pendingUsdt.amount), pendingHashpower: new Prisma.Decimal(summary.pendingHashpower.value), pendingExpireAt: summary.pendingExpireAt, diff --git a/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-ledger-entry.repository.impl.ts b/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-ledger-entry.repository.impl.ts index 94cc35c4..124dec5b 100644 --- a/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-ledger-entry.repository.impl.ts +++ b/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-ledger-entry.repository.impl.ts @@ -29,6 +29,7 @@ export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryReposi const created = await this.prisma.rewardLedgerEntry.create({ data: { userId: data.userId, + accountSequence: data.accountSequence, sourceOrderNo: data.sourceOrderNo, sourceUserId: data.sourceUserId, rightType: data.rightType, @@ -152,4 +153,77 @@ export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryReposi } return this.prisma.rewardLedgerEntry.count({ where }); } + + async findByAccountSequence( + accountSequence: bigint, + filters?: { + status?: RewardStatus; + rightType?: RightType; + startDate?: Date; + endDate?: Date; + }, + pagination?: { page: number; pageSize: number }, + ): Promise { + const where: any = { accountSequence }; + + if (filters?.status) { + where.rewardStatus = filters.status; + } + if (filters?.rightType) { + where.rightType = filters.rightType; + } + if (filters?.startDate || filters?.endDate) { + where.createdAt = {}; + if (filters.startDate) { + where.createdAt.gte = filters.startDate; + } + if (filters.endDate) { + where.createdAt.lte = filters.endDate; + } + } + + const skip = pagination ? (pagination.page - 1) * pagination.pageSize : undefined; + const take = pagination?.pageSize; + + const rawList = await this.prisma.rewardLedgerEntry.findMany({ + where, + orderBy: { createdAt: 'desc' }, + skip, + take, + }); + + return rawList.map(RewardLedgerEntryMapper.toDomain); + } + + async findPendingByAccountSequence(accountSequence: bigint): Promise { + const rawList = await this.prisma.rewardLedgerEntry.findMany({ + where: { + accountSequence, + rewardStatus: RewardStatus.PENDING, + }, + orderBy: { createdAt: 'desc' }, + }); + + return rawList.map(RewardLedgerEntryMapper.toDomain); + } + + async findSettleableByAccountSequence(accountSequence: bigint): Promise { + const rawList = await this.prisma.rewardLedgerEntry.findMany({ + where: { + accountSequence, + rewardStatus: RewardStatus.SETTLEABLE, + }, + orderBy: { createdAt: 'desc' }, + }); + + return rawList.map(RewardLedgerEntryMapper.toDomain); + } + + async countByAccountSequence(accountSequence: bigint, status?: RewardStatus): Promise { + const where: any = { accountSequence }; + if (status) { + where.rewardStatus = status; + } + return this.prisma.rewardLedgerEntry.count({ where }); + } } diff --git a/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-summary.repository.impl.ts b/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-summary.repository.impl.ts index 82dee60a..c9167897 100644 --- a/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-summary.repository.impl.ts +++ b/backend/services/reward-service/src/infrastructure/persistence/repositories/reward-summary.repository.impl.ts @@ -30,6 +30,7 @@ export class RewardSummaryRepositoryImpl implements IRewardSummaryRepository { const created = await this.prisma.rewardSummary.create({ data: { userId: data.userId, + accountSequence: data.accountSequence, pendingUsdt: data.pendingUsdt, pendingHashpower: data.pendingHashpower, pendingExpireAt: data.pendingExpireAt, @@ -88,4 +89,22 @@ export class RewardSummaryRepositoryImpl implements IRewardSummaryRepository { return rawList.map(RewardSummaryMapper.toDomain); } + + async findByAccountSequence(accountSequence: bigint): Promise { + const raw = await this.prisma.rewardSummary.findUnique({ + where: { accountSequence }, + }); + return raw ? RewardSummaryMapper.toDomain(raw) : null; + } + + async getOrCreateByAccountSequence(accountSequence: bigint): Promise { + const existing = await this.findByAccountSequence(accountSequence); + if (existing) { + return existing; + } + + // Need to find userId by accountSequence - this requires user service integration + // For now, we'll throw an error indicating this needs to be implemented + throw new Error('getOrCreateByAccountSequence requires userId mapping - use getOrCreate with userId instead'); + } } diff --git a/backend/services/reward-service/src/shared/strategies/jwt.strategy.ts b/backend/services/reward-service/src/shared/strategies/jwt.strategy.ts index ecbade5d..b3165efc 100644 --- a/backend/services/reward-service/src/shared/strategies/jwt.strategy.ts +++ b/backend/services/reward-service/src/shared/strategies/jwt.strategy.ts @@ -18,6 +18,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) { sub: payload.sub, username: payload.username, roles: payload.roles, + accountSequence: payload.accountSequence, }; } } diff --git a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts index e248e7e7..d6b53420 100644 --- a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts @@ -26,9 +26,10 @@ export class InternalWalletController { @Get(':userId/balance') @Public() @ApiOperation({ summary: '获取用户钱包余额(内部API)' }) - @ApiParam({ name: 'userId', description: '用户ID' }) + @ApiParam({ name: 'userId', description: '用户ID或accountSequence' }) @ApiResponse({ status: 200, description: '余额信息' }) async getBalance(@Param('userId') userId: string) { + // 优先使用 accountSequence,如果相同则使用 userId const query = new GetMyWalletQuery(userId, userId); const wallet = await this.walletService.getMyWallet(query); return { @@ -44,10 +45,12 @@ export class InternalWalletController { @ApiOperation({ summary: '认种扣款(内部API) - 直接扣款模式' }) @ApiResponse({ status: 200, description: '扣款结果' }) async deductForPlanting( - @Body() dto: { userId: string; amount: number; orderId: string }, + @Body() dto: { userId: string; accountSequence?: string; amount: number; orderId: string }, ) { + // 优先使用 accountSequence,如果未提供则使用 userId + const userIdentifier = dto.accountSequence || dto.userId; const command = new DeductForPlantingCommand( - dto.userId, + userIdentifier, dto.amount, dto.orderId, ); @@ -60,17 +63,22 @@ export class InternalWalletController { @ApiOperation({ summary: '认种冻结资金(内部API) - 预扣款模式第一步' }) @ApiResponse({ status: 200, description: '冻结结果' }) async freezeForPlanting( - @Body() dto: { userId: string; amount: number; orderId: string }, + @Body() dto: { userId: string; accountSequence?: string; amount: number; orderId: string }, ) { this.logger.log(`========== freeze-for-planting 请求 ==========`); this.logger.log(`请求参数: ${JSON.stringify(dto)}`); this.logger.log(` userId: ${dto.userId}`); + this.logger.log(` accountSequence: ${dto.accountSequence || '未提供'}`); this.logger.log(` amount: ${dto.amount}`); this.logger.log(` orderId: ${dto.orderId}`); try { + // 优先使用 accountSequence,如果未提供则使用 userId + const userIdentifier = dto.accountSequence || dto.userId; + this.logger.log(` 使用标识符: ${userIdentifier}`); + const command = new FreezeForPlantingCommand( - dto.userId, + userIdentifier, dto.amount, dto.orderId, ); @@ -89,10 +97,12 @@ export class InternalWalletController { @ApiOperation({ summary: '确认认种扣款(内部API) - 预扣款模式第二步' }) @ApiResponse({ status: 200, description: '确认结果' }) async confirmPlantingDeduction( - @Body() dto: { userId: string; orderId: string }, + @Body() dto: { userId: string; accountSequence?: string; orderId: string }, ) { + // 优先使用 accountSequence,如果未提供则使用 userId + const userIdentifier = dto.accountSequence || dto.userId; const command = new ConfirmPlantingDeductionCommand( - dto.userId, + userIdentifier, dto.orderId, ); const success = await this.walletService.confirmPlantingDeduction(command); @@ -104,10 +114,12 @@ export class InternalWalletController { @ApiOperation({ summary: '解冻认种资金(内部API) - 认种失败时回滚' }) @ApiResponse({ status: 200, description: '解冻结果' }) async unfreezeForPlanting( - @Body() dto: { userId: string; orderId: string }, + @Body() dto: { userId: string; accountSequence?: string; orderId: string }, ) { + // 优先使用 accountSequence,如果未提供则使用 userId + const userIdentifier = dto.accountSequence || dto.userId; const command = new UnfreezeForPlantingCommand( - dto.userId, + userIdentifier, dto.orderId, ); const success = await this.walletService.unfreezeForPlanting(command); diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index 10400971..e973c676 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -153,9 +153,13 @@ export class WalletApplicationService { return true; } - const wallet = await this.walletRepo.findByUserId(userId); + // 优先按 accountSequence 查找,如果未找到则按 userId 查找 + let wallet = await this.walletRepo.findByAccountSequence(userId); if (!wallet) { - throw new WalletNotFoundError(`userId: ${command.userId}`); + wallet = await this.walletRepo.findByUserId(userId); + } + if (!wallet) { + throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`); } // Deduct from wallet @@ -188,7 +192,7 @@ export class WalletApplicationService { frozenAmount: number; }> { this.logger.log(`[freezeForPlanting] ========== 开始处理 ==========`); - this.logger.log(`[freezeForPlanting] userId: ${command.userId}`); + this.logger.log(`[freezeForPlanting] userId/accountSequence: ${command.userId}`); this.logger.log(`[freezeForPlanting] amount: ${command.amount}`); this.logger.log(`[freezeForPlanting] orderId: ${command.orderId}`); @@ -209,10 +213,14 @@ export class WalletApplicationService { return { success: true, frozenAmount: command.amount }; } - const wallet = await this.walletRepo.findByUserId(userId); + // 优先按 accountSequence 查找,如果未找到则按 userId 查找 + let wallet = await this.walletRepo.findByAccountSequence(userId); if (!wallet) { - this.logger.error(`[freezeForPlanting] 钱包不存在: userId=${command.userId}`); - throw new WalletNotFoundError(`userId: ${command.userId}`); + wallet = await this.walletRepo.findByUserId(userId); + } + if (!wallet) { + this.logger.error(`[freezeForPlanting] 钱包不存在: userId/accountSequence=${command.userId}`); + throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`); } this.logger.log(`[freezeForPlanting] 钱包信息:`); @@ -289,9 +297,13 @@ export class WalletApplicationService { // 获取冻结金额(流水中是负数,取绝对值) const frozenAmount = Money.USDT(Math.abs(freezeEntry.amount.value)); - const wallet = await this.walletRepo.findByUserId(userId); + // 优先按 accountSequence 查找,如果未找到则按 userId 查找 + let wallet = await this.walletRepo.findByAccountSequence(userId); if (!wallet) { - throw new WalletNotFoundError(`userId: ${command.userId}`); + wallet = await this.walletRepo.findByUserId(userId); + } + if (!wallet) { + throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`); } // 从冻结金额扣款 @@ -363,9 +375,13 @@ export class WalletApplicationService { // 获取冻结金额 const frozenAmount = Money.USDT(Math.abs(freezeEntry.amount.value)); - const wallet = await this.walletRepo.findByUserId(userId); + // 优先按 accountSequence 查找,如果未找到则按 userId 查找 + let wallet = await this.walletRepo.findByAccountSequence(userId); if (!wallet) { - throw new WalletNotFoundError(`userId: ${command.userId}`); + wallet = await this.walletRepo.findByUserId(userId); + } + if (!wallet) { + throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`); } // 解冻资金 @@ -393,10 +409,13 @@ export class WalletApplicationService { async addRewards(command: AddRewardsCommand): Promise { const userId = BigInt(command.userId); - // 先通过 userId 查找钱包(addRewards 是内部调用,钱包应该已存在) - const wallet = await this.walletRepo.findByUserId(userId); + // 优先按 accountSequence 查找,如果未找到则按 userId 查找 + let wallet = await this.walletRepo.findByAccountSequence(userId); if (!wallet) { - throw new WalletNotFoundError(`userId: ${command.userId}`); + wallet = await this.walletRepo.findByUserId(userId); + } + if (!wallet) { + throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`); } const usdtAmount = Money.USDT(command.usdtAmount); @@ -440,9 +459,13 @@ export class WalletApplicationService { async claimRewards(command: ClaimRewardsCommand): Promise { const userId = BigInt(command.userId); - const wallet = await this.walletRepo.findByUserId(userId); + // 优先按 accountSequence 查找,如果未找到则按 userId 查找 + let wallet = await this.walletRepo.findByAccountSequence(userId); if (!wallet) { - throw new WalletNotFoundError(`userId: ${command.userId}`); + wallet = await this.walletRepo.findByUserId(userId); + } + if (!wallet) { + throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`); } const pendingUsdt = wallet.rewards.pendingUsdt.value; @@ -483,9 +506,13 @@ export class WalletApplicationService { const userId = BigInt(command.userId); const usdtAmount = Money.USDT(command.usdtAmount); - const wallet = await this.walletRepo.findByUserId(userId); + // 优先按 accountSequence 查找,如果未找到则按 userId 查找 + let wallet = await this.walletRepo.findByAccountSequence(userId); if (!wallet) { - throw new WalletNotFoundError(`userId: ${command.userId}`); + wallet = await this.walletRepo.findByUserId(userId); + } + if (!wallet) { + throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`); } // Create settlement order @@ -586,10 +613,14 @@ export class WalletApplicationService { orderId: string, ): Promise { const userId = BigInt(allocation.targetId); - const wallet = await this.walletRepo.findByUserId(userId); + // 优先按 accountSequence 查找,如果未找到则按 userId 查找 + let wallet = await this.walletRepo.findByAccountSequence(userId); if (!wallet) { - this.logger.warn(`Wallet not found for user ${allocation.targetId}, skipping allocation`); + wallet = await this.walletRepo.findByUserId(userId); + } + if (!wallet) { + this.logger.warn(`Wallet not found for user/accountSequence ${allocation.targetId}, skipping allocation`); return; } @@ -695,10 +726,13 @@ export class WalletApplicationService { throw new Error(`最小提现金额为 ${this.MIN_WITHDRAWAL_AMOUNT} USDT`); } - // 获取钱包 - const wallet = await this.walletRepo.findByUserId(userId); + // 优先按 accountSequence 查找,如果未找到则按 userId 查找 + let wallet = await this.walletRepo.findByAccountSequence(userId); if (!wallet) { - throw new WalletNotFoundError(`userId: ${command.userId}`); + wallet = await this.walletRepo.findByUserId(userId); + } + if (!wallet) { + throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`); } // 验证余额是否足够 diff --git a/frontend/mobile-app/lib/core/services/planting_service.dart b/frontend/mobile-app/lib/core/services/planting_service.dart index 81ea4f2d..58c86250 100644 --- a/frontend/mobile-app/lib/core/services/planting_service.dart +++ b/frontend/mobile-app/lib/core/services/planting_service.dart @@ -105,6 +105,27 @@ class CreateOrderResponse { } } +/// 用户持仓信息 +class PlantingPosition { + final int totalTreeCount; + final int effectiveTreeCount; + final int pendingTreeCount; + + PlantingPosition({ + required this.totalTreeCount, + required this.effectiveTreeCount, + required this.pendingTreeCount, + }); + + factory PlantingPosition.fromJson(Map json) { + return PlantingPosition( + totalTreeCount: json['totalTreeCount'] ?? 0, + effectiveTreeCount: json['effectiveTreeCount'] ?? 0, + pendingTreeCount: json['pendingTreeCount'] ?? 0, + ); + } +} + /// 认种服务 /// /// 提供认种订单创建、省市选择、支付等功能 @@ -113,6 +134,28 @@ class PlantingService { PlantingService({required ApiClient apiClient}) : _apiClient = apiClient; + /// 获取我的持仓信息 + /// + /// 返回用户的总认种数量、有效数量、待生效数量 + Future getMyPosition() async { + try { + debugPrint('[PlantingService] 获取我的持仓信息'); + + final response = await _apiClient.get('/planting/position'); + + if (response.statusCode == 200) { + final data = response.data as Map; + debugPrint('[PlantingService] 持仓信息: totalTreeCount=${data['totalTreeCount']}'); + return PlantingPosition.fromJson(data); + } + + throw Exception('获取持仓信息失败: ${response.statusCode}'); + } catch (e) { + debugPrint('[PlantingService] 获取持仓信息失败: $e'); + rethrow; + } + } + /// 创建认种订单 /// /// [treeCount] 认种数量