diff --git a/.claude/settings.local.json b/.claude/settings.local.json index dc179fe1..8ae78e3e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -32,7 +32,20 @@ "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 = ''0xd110112e057d269b41f7dc7dbf1f8eabb896f51a'';\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 // 300,000 USDT = 300000 * 1e6 (6 decimals)\n const amount = BigInt(300000) * BigInt(1000000);\n \n console.log(''Transferring 300,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\")", "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 = ''0x6a664488d000e094baa8a055961921bf495c1152'';\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 // 880,000 USDT = 880000 * 1e6 (6 decimals)\n const amount = BigInt(880000) * BigInt(1000000);\n \n console.log(''Transferring 880,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\")", "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 = ''0x53fd262ef1a707b80f87581cc64e09800fdbd690'';\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 // 360,000 USDT = 360000 * 1e6 (6 decimals)\n const amount = BigInt(360000) * BigInt(1000000);\n \n console.log(''Transferring 360,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\")", - "Bash(docker exec:*)" + "Bash(docker exec:*)", + "Bash(node -e:*)", + "Bash(dir /s /b c:UsersdongDesktoprwadurianbackendservicesreward-servicesrc*.ts)", + "Bash(git tag:*)", + "Bash(dir:*)", + "Bash(grep:*)", + "Bash(npx prisma format)", + "Bash(DATABASE_URL=\"postgresql://dummy:dummy@localhost:5432/dummy\" npx prisma generate:*)", + "Bash(npx prisma generate)", + "Bash(for file in grant-*.dto.ts)", + "Bash(do sed -i '/@ApiProperty.*账户序列号/,/accountSequence:/ s/@IsNumber()/@IsString()/' \"$file\")", + "Bash(done)", + "Bash(git diff:*)", + "Bash(npm install:*)" ], "deny": [], "ask": [] diff --git a/backend/services/authorization-service/prisma/schema.prisma b/backend/services/authorization-service/prisma/schema.prisma index 7c93ba51..70ba3023 100644 --- a/backend/services/authorization-service/prisma/schema.prisma +++ b/backend/services/authorization-service/prisma/schema.prisma @@ -13,8 +13,8 @@ datasource db { // ============ 授权角色表 ============ model AuthorizationRole { id String @id @default(uuid()) - userId BigInt @map("user_id") - accountSequence BigInt @map("account_sequence") + userId String @map("user_id") + accountSequence String @map("account_sequence") roleType RoleType @map("role_type") regionCode String @map("region_code") regionName String @map("region_name") @@ -23,9 +23,9 @@ model AuthorizationRole { // 授权信息 authorizedAt DateTime? @map("authorized_at") - authorizedBy BigInt? @map("authorized_by") + authorizedBy String? @map("authorized_by") revokedAt DateTime? @map("revoked_at") - revokedBy BigInt? @map("revoked_by") + revokedBy String? @map("revoked_by") revokeReason String? @map("revoke_reason") // 考核配置 @@ -65,8 +65,8 @@ model AuthorizationRole { model MonthlyAssessment { id String @id @default(uuid()) authorizationId String @map("authorization_id") - userId BigInt @map("user_id") - accountSequence BigInt @map("account_sequence") + userId String @map("user_id") + accountSequence String @map("account_sequence") roleType RoleType @map("role_type") regionCode String @map("region_code") @@ -101,7 +101,7 @@ model MonthlyAssessment { // 豁免 isBypassed Boolean @default(false) @map("is_bypassed") - bypassedBy BigInt? @map("bypassed_by") + bypassedBy String? @map("bypassed_by") bypassedAt DateTime? @map("bypassed_at") // 时间戳 @@ -125,22 +125,22 @@ model MonthlyAssessment { model MonthlyBypass { id String @id @default(uuid()) authorizationId String @map("authorization_id") - userId BigInt @map("user_id") - accountSequence BigInt @map("account_sequence") + userId String @map("user_id") + accountSequence String @map("account_sequence") roleType RoleType @map("role_type") bypassMonth String @map("bypass_month") // YYYY-MM // 授权信息 - grantedBy BigInt @map("granted_by") + grantedBy String @map("granted_by") grantedAt DateTime @map("granted_at") reason String? // 审批信息(三人授权) - approver1Id BigInt @map("approver1_id") + approver1Id String @map("approver1_id") approver1At DateTime @map("approver1_at") - approver2Id BigInt? @map("approver2_id") + approver2Id String? @map("approver2_id") approver2At DateTime? @map("approver2_at") - approver3Id BigInt? @map("approver3_id") + approver3Id String? @map("approver3_id") approver3At DateTime? @map("approver3_at") approvalStatus ApprovalStatus @default(PENDING) @map("approval_status") @@ -281,8 +281,8 @@ model RegionHeatMap { // ============ 火柴人排名视图数据表 ============ model StickmanRanking { id String @id @default(uuid()) - userId BigInt @map("user_id") - accountSequence BigInt @map("account_sequence") + userId String @map("user_id") + accountSequence String @map("account_sequence") authorizationId String @map("authorization_id") roleType RoleType @map("role_type") regionCode String @map("region_code") diff --git a/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts b/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts index 22b418df..59436214 100644 --- a/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts +++ b/backend/services/authorization-service/src/api/controllers/admin-authorization.controller.ts @@ -1,133 +1,133 @@ -import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common' -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger' -import { AuthorizationApplicationService } from '@/application/services' -import { - GrantCommunityCommand, - GrantProvinceCompanyCommand, - GrantCityCompanyCommand, - GrantAuthProvinceCompanyCommand, - GrantAuthCityCompanyCommand, -} from '@/application/commands' -import { - GrantCommunityDto, - GrantProvinceCompanyDto, - GrantCityCompanyDto, - GrantAuthProvinceCompanyDto, - GrantAuthCityCompanyDto, -} from '@/api/dto/request' -import { CurrentUser } from '@/shared/decorators' -import { JwtAuthGuard } from '@/shared/guards' - -@ApiTags('Admin Authorization') -@Controller('admin/authorizations') -@UseGuards(JwtAuthGuard) -@ApiBearerAuth() -export class AdminAuthorizationController { - constructor(private readonly applicationService: AuthorizationApplicationService) {} - - @Post('community') - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: '授权社区(管理员)' }) - @ApiResponse({ status: 201, description: '授权成功' }) - async grantCommunity( - @CurrentUser() user: { userId: string; accountSequence: number }, - @Body() dto: GrantCommunityDto, - ): Promise<{ message: string }> { - const command = new GrantCommunityCommand( - dto.userId, - dto.accountSequence, - dto.communityName, - user.userId, - user.accountSequence, - dto.skipAssessment ?? false, - ) - await this.applicationService.grantCommunity(command) - return { message: '社区授权成功' } - } - - @Post('province-company') - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: '授权正式省公司(管理员)' }) - @ApiResponse({ status: 201, description: '授权成功' }) - async grantProvinceCompany( - @CurrentUser() user: { userId: string; accountSequence: number }, - @Body() dto: GrantProvinceCompanyDto, - ): Promise<{ message: string }> { - const command = new GrantProvinceCompanyCommand( - dto.userId, - dto.accountSequence, - dto.provinceCode, - dto.provinceName, - user.userId, - user.accountSequence, - dto.skipAssessment ?? false, - ) - await this.applicationService.grantProvinceCompany(command) - return { message: '正式省公司授权成功' } - } - - @Post('city-company') - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: '授权正式市公司(管理员)' }) - @ApiResponse({ status: 201, description: '授权成功' }) - async grantCityCompany( - @CurrentUser() user: { userId: string; accountSequence: number }, - @Body() dto: GrantCityCompanyDto, - ): Promise<{ message: string }> { - const command = new GrantCityCompanyCommand( - dto.userId, - dto.accountSequence, - dto.cityCode, - dto.cityName, - user.userId, - user.accountSequence, - dto.skipAssessment ?? false, - ) - await this.applicationService.grantCityCompany(command) - return { message: '正式市公司授权成功' } - } - - @Post('auth-province-company') - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: '授权省团队(管理员)' }) - @ApiResponse({ status: 201, description: '授权成功' }) - @ApiResponse({ status: 400, description: '验证失败(如团队内已存在相同省份授权)' }) - async grantAuthProvinceCompany( - @CurrentUser() user: { userId: string; accountSequence: number }, - @Body() dto: GrantAuthProvinceCompanyDto, - ): Promise<{ message: string }> { - const command = new GrantAuthProvinceCompanyCommand( - dto.userId, - dto.accountSequence, - dto.provinceCode, - dto.provinceName, - user.userId, - user.accountSequence, - dto.skipAssessment ?? false, - ) - await this.applicationService.grantAuthProvinceCompany(command) - return { message: '省团队授权成功' } - } - - @Post('auth-city-company') - @HttpCode(HttpStatus.CREATED) - @ApiOperation({ summary: '授权市团队(管理员)' }) - @ApiResponse({ status: 201, description: '授权成功' }) - @ApiResponse({ status: 400, description: '验证失败(如团队内已存在相同城市授权)' }) - async grantAuthCityCompany( - @CurrentUser() user: { userId: string; accountSequence: number }, - @Body() dto: GrantAuthCityCompanyDto, - ): Promise<{ message: string }> { - const command = new GrantAuthCityCompanyCommand( - dto.userId, - dto.accountSequence, - dto.cityCode, - dto.cityName, - user.userId, - user.accountSequence, - dto.skipAssessment ?? false, - ) - await this.applicationService.grantAuthCityCompany(command) - return { message: '市团队授权成功' } - } -} +import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common' +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger' +import { AuthorizationApplicationService } from '@/application/services' +import { + GrantCommunityCommand, + GrantProvinceCompanyCommand, + GrantCityCompanyCommand, + GrantAuthProvinceCompanyCommand, + GrantAuthCityCompanyCommand, +} from '@/application/commands' +import { + GrantCommunityDto, + GrantProvinceCompanyDto, + GrantCityCompanyDto, + GrantAuthProvinceCompanyDto, + GrantAuthCityCompanyDto, +} from '@/api/dto/request' +import { CurrentUser } from '@/shared/decorators' +import { JwtAuthGuard } from '@/shared/guards' + +@ApiTags('Admin Authorization') +@Controller('admin/authorizations') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth() +export class AdminAuthorizationController { + constructor(private readonly applicationService: AuthorizationApplicationService) {} + + @Post('community') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: '授权社区(管理员)' }) + @ApiResponse({ status: 201, description: '授权成功' }) + async grantCommunity( + @CurrentUser() user: { userId: string; accountSequence: string }, + @Body() dto: GrantCommunityDto, + ): Promise<{ message: string }> { + const command = new GrantCommunityCommand( + dto.userId, + dto.accountSequence, + dto.communityName, + user.userId, + user.accountSequence, + dto.skipAssessment ?? false, + ) + await this.applicationService.grantCommunity(command) + return { message: '社区授权成功' } + } + + @Post('province-company') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: '授权正式省公司(管理员)' }) + @ApiResponse({ status: 201, description: '授权成功' }) + async grantProvinceCompany( + @CurrentUser() user: { userId: string; accountSequence: string }, + @Body() dto: GrantProvinceCompanyDto, + ): Promise<{ message: string }> { + const command = new GrantProvinceCompanyCommand( + dto.userId, + dto.accountSequence, + dto.provinceCode, + dto.provinceName, + user.userId, + user.accountSequence, + dto.skipAssessment ?? false, + ) + await this.applicationService.grantProvinceCompany(command) + return { message: '正式省公司授权成功' } + } + + @Post('city-company') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: '授权正式市公司(管理员)' }) + @ApiResponse({ status: 201, description: '授权成功' }) + async grantCityCompany( + @CurrentUser() user: { userId: string; accountSequence: string }, + @Body() dto: GrantCityCompanyDto, + ): Promise<{ message: string }> { + const command = new GrantCityCompanyCommand( + dto.userId, + dto.accountSequence, + dto.cityCode, + dto.cityName, + user.userId, + user.accountSequence, + dto.skipAssessment ?? false, + ) + await this.applicationService.grantCityCompany(command) + return { message: '正式市公司授权成功' } + } + + @Post('auth-province-company') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: '授权省团队(管理员)' }) + @ApiResponse({ status: 201, description: '授权成功' }) + @ApiResponse({ status: 400, description: '验证失败(如团队内已存在相同省份授权)' }) + async grantAuthProvinceCompany( + @CurrentUser() user: { userId: string; accountSequence: string }, + @Body() dto: GrantAuthProvinceCompanyDto, + ): Promise<{ message: string }> { + const command = new GrantAuthProvinceCompanyCommand( + dto.userId, + dto.accountSequence, + dto.provinceCode, + dto.provinceName, + user.userId, + user.accountSequence, + dto.skipAssessment ?? false, + ) + await this.applicationService.grantAuthProvinceCompany(command) + return { message: '省团队授权成功' } + } + + @Post('auth-city-company') + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ summary: '授权市团队(管理员)' }) + @ApiResponse({ status: 201, description: '授权成功' }) + @ApiResponse({ status: 400, description: '验证失败(如团队内已存在相同城市授权)' }) + async grantAuthCityCompany( + @CurrentUser() user: { userId: string; accountSequence: string }, + @Body() dto: GrantAuthCityCompanyDto, + ): Promise<{ message: string }> { + const command = new GrantAuthCityCompanyCommand( + dto.userId, + dto.accountSequence, + dto.cityCode, + dto.cityName, + user.userId, + user.accountSequence, + dto.skipAssessment ?? false, + ) + await this.applicationService.grantAuthCityCompany(command) + return { message: '市团队授权成功' } + } +} 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 ab826ca0..73986392 100644 --- a/backend/services/authorization-service/src/api/controllers/authorization.controller.ts +++ b/backend/services/authorization-service/src/api/controllers/authorization.controller.ts @@ -1,172 +1,172 @@ -import { - Controller, - Get, - Post, - Delete, - Param, - Body, - Query, - UseGuards, - HttpCode, - HttpStatus, -} from '@nestjs/common' -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiBearerAuth, - ApiParam, - ApiQuery, -} from '@nestjs/swagger' -import { AuthorizationApplicationService } from '@/application/services' -import { - ApplyCommunityAuthCommand, - ApplyAuthProvinceCompanyCommand, - ApplyAuthCityCompanyCommand, - RevokeAuthorizationCommand, - GrantMonthlyBypassCommand, - ExemptLocalPercentageCheckCommand, -} from '@/application/commands' -import { - ApplyCommunityAuthDto, - ApplyAuthProvinceDto, - ApplyAuthCityDto, - RevokeAuthorizationDto, - GrantMonthlyBypassDto, -} from '@/api/dto/request' -import { - AuthorizationResponse, - ApplyAuthorizationResponse, - StickmanRankingResponse, - CommunityHierarchyResponse, -} from '@/api/dto/response' -import { CurrentUser } from '@/shared/decorators' -import { JwtAuthGuard } from '@/shared/guards' -import { RoleType } from '@/domain/enums' - -@ApiTags('Authorization') -@Controller('authorizations') -@UseGuards(JwtAuthGuard) -@ApiBearerAuth() -export class AuthorizationController { - constructor(private readonly applicationService: AuthorizationApplicationService) {} - - @Post('community') - @ApiOperation({ summary: '申请社区授权' }) - @ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) - async applyCommunityAuth( - @CurrentUser() user: { userId: string; accountSequence: number }, - @Body() dto: ApplyCommunityAuthDto, - ): Promise { - const command = new ApplyCommunityAuthCommand(user.userId, user.accountSequence, dto.communityName) - return await this.applicationService.applyCommunityAuth(command) - } - - @Post('province') - @ApiOperation({ summary: '申请授权省公司' }) - @ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) - async applyAuthProvinceCompany( - @CurrentUser() user: { userId: string; accountSequence: number }, - @Body() dto: ApplyAuthProvinceDto, - ): Promise { - const command = new ApplyAuthProvinceCompanyCommand( - user.userId, - user.accountSequence, - dto.provinceCode, - dto.provinceName, - ) - return await this.applicationService.applyAuthProvinceCompany(command) - } - - @Post('city') - @ApiOperation({ summary: '申请授权市公司' }) - @ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) - async applyAuthCityCompany( - @CurrentUser() user: { userId: string; accountSequence: number }, - @Body() dto: ApplyAuthCityDto, - ): Promise { - const command = new ApplyAuthCityCompanyCommand(user.userId, user.accountSequence, dto.cityCode, dto.cityName) - return await this.applicationService.applyAuthCityCompany(command) - } - - @Get('my') - @ApiOperation({ summary: '获取我的授权列表' }) - @ApiResponse({ status: 200, type: [AuthorizationResponse] }) - async getMyAuthorizations( - @CurrentUser() user: { userId: string; accountSequence: number }, - ): Promise { - return await this.applicationService.getUserAuthorizations(user.accountSequence) - } - - @Get('my/community-hierarchy') - @ApiOperation({ summary: '获取我的社区层级(上级社区和下级社区)' }) - @ApiResponse({ status: 200, type: CommunityHierarchyResponse }) - async getMyCommunityHierarchy( - @CurrentUser() user: { userId: string; accountSequence: number }, - ): Promise { - return await this.applicationService.getCommunityHierarchy(user.accountSequence) - } - - @Get(':id') - @ApiOperation({ summary: '获取授权详情' }) - @ApiParam({ name: 'id', description: '授权ID' }) - @ApiResponse({ status: 200, type: AuthorizationResponse }) - async getAuthorizationById(@Param('id') id: string): Promise { - return await this.applicationService.getAuthorizationById(id) - } - - @Get('ranking/stickman') - @ApiOperation({ summary: '获取火柴人排名' }) - @ApiQuery({ name: 'month', description: '月份 (YYYY-MM)', example: '2024-01' }) - @ApiQuery({ name: 'roleType', description: '角色类型', enum: RoleType }) - @ApiQuery({ name: 'regionCode', description: '区域代码' }) - @ApiResponse({ status: 200, type: [StickmanRankingResponse] }) - async getStickmanRanking( - @Query('month') month: string, - @Query('roleType') roleType: RoleType, - @Query('regionCode') regionCode: string, - ): Promise { - return await this.applicationService.getStickmanRanking(month, roleType, regionCode) - } - - @Delete(':id') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: '撤销授权(管理员)' }) - @ApiParam({ name: 'id', description: '授权ID' }) - @ApiResponse({ status: 204 }) - async revokeAuthorization( - @Param('id') id: string, - @CurrentUser() user: { userId: string; accountSequence: number }, - @Body() dto: RevokeAuthorizationDto, - ): Promise { - const command = new RevokeAuthorizationCommand(id, user.accountSequence, dto.reason) - await this.applicationService.revokeAuthorization(command) - } - - @Post(':id/bypass') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: '授予单月豁免(管理员)' }) - @ApiParam({ name: 'id', description: '授权ID' }) - @ApiResponse({ status: 204 }) - async grantMonthlyBypass( - @Param('id') id: string, - @CurrentUser() user: { userId: string; accountSequence: number }, - @Body() dto: GrantMonthlyBypassDto, - ): Promise { - const command = new GrantMonthlyBypassCommand(id, dto.month, user.accountSequence, dto.reason) - await this.applicationService.grantMonthlyBypass(command) - } - - @Post(':id/exempt-percentage') - @HttpCode(HttpStatus.NO_CONTENT) - @ApiOperation({ summary: '豁免占比考核(管理员)' }) - @ApiParam({ name: 'id', description: '授权ID' }) - @ApiResponse({ status: 204 }) - async exemptLocalPercentageCheck( - @Param('id') id: string, - @CurrentUser() user: { userId: string; accountSequence: number }, - ): Promise { - const command = new ExemptLocalPercentageCheckCommand(id, user.accountSequence) - await this.applicationService.exemptLocalPercentageCheck(command) - } -} +import { + Controller, + Get, + Post, + Delete, + Param, + Body, + Query, + UseGuards, + HttpCode, + HttpStatus, +} from '@nestjs/common' +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, + ApiParam, + ApiQuery, +} from '@nestjs/swagger' +import { AuthorizationApplicationService } from '@/application/services' +import { + ApplyCommunityAuthCommand, + ApplyAuthProvinceCompanyCommand, + ApplyAuthCityCompanyCommand, + RevokeAuthorizationCommand, + GrantMonthlyBypassCommand, + ExemptLocalPercentageCheckCommand, +} from '@/application/commands' +import { + ApplyCommunityAuthDto, + ApplyAuthProvinceDto, + ApplyAuthCityDto, + RevokeAuthorizationDto, + GrantMonthlyBypassDto, +} from '@/api/dto/request' +import { + AuthorizationResponse, + ApplyAuthorizationResponse, + StickmanRankingResponse, + CommunityHierarchyResponse, +} from '@/api/dto/response' +import { CurrentUser } from '@/shared/decorators' +import { JwtAuthGuard } from '@/shared/guards' +import { RoleType } from '@/domain/enums' + +@ApiTags('Authorization') +@Controller('authorizations') +@UseGuards(JwtAuthGuard) +@ApiBearerAuth() +export class AuthorizationController { + constructor(private readonly applicationService: AuthorizationApplicationService) {} + + @Post('community') + @ApiOperation({ summary: '申请社区授权' }) + @ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) + async applyCommunityAuth( + @CurrentUser() user: { userId: string; accountSequence: string }, + @Body() dto: ApplyCommunityAuthDto, + ): Promise { + const command = new ApplyCommunityAuthCommand(user.userId, user.accountSequence, dto.communityName) + return await this.applicationService.applyCommunityAuth(command) + } + + @Post('province') + @ApiOperation({ summary: '申请授权省公司' }) + @ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) + async applyAuthProvinceCompany( + @CurrentUser() user: { userId: string; accountSequence: string }, + @Body() dto: ApplyAuthProvinceDto, + ): Promise { + const command = new ApplyAuthProvinceCompanyCommand( + user.userId, + user.accountSequence, + dto.provinceCode, + dto.provinceName, + ) + return await this.applicationService.applyAuthProvinceCompany(command) + } + + @Post('city') + @ApiOperation({ summary: '申请授权市公司' }) + @ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) + async applyAuthCityCompany( + @CurrentUser() user: { userId: string; accountSequence: string }, + @Body() dto: ApplyAuthCityDto, + ): Promise { + const command = new ApplyAuthCityCompanyCommand(user.userId, user.accountSequence, dto.cityCode, dto.cityName) + return await this.applicationService.applyAuthCityCompany(command) + } + + @Get('my') + @ApiOperation({ summary: '获取我的授权列表' }) + @ApiResponse({ status: 200, type: [AuthorizationResponse] }) + async getMyAuthorizations( + @CurrentUser() user: { userId: string; accountSequence: string }, + ): Promise { + return await this.applicationService.getUserAuthorizations(user.accountSequence) + } + + @Get('my/community-hierarchy') + @ApiOperation({ summary: '获取我的社区层级(上级社区和下级社区)' }) + @ApiResponse({ status: 200, type: CommunityHierarchyResponse }) + async getMyCommunityHierarchy( + @CurrentUser() user: { userId: string; accountSequence: string }, + ): Promise { + return await this.applicationService.getCommunityHierarchy(user.accountSequence) + } + + @Get(':id') + @ApiOperation({ summary: '获取授权详情' }) + @ApiParam({ name: 'id', description: '授权ID' }) + @ApiResponse({ status: 200, type: AuthorizationResponse }) + async getAuthorizationById(@Param('id') id: string): Promise { + return await this.applicationService.getAuthorizationById(id) + } + + @Get('ranking/stickman') + @ApiOperation({ summary: '获取火柴人排名' }) + @ApiQuery({ name: 'month', description: '月份 (YYYY-MM)', example: '2024-01' }) + @ApiQuery({ name: 'roleType', description: '角色类型', enum: RoleType }) + @ApiQuery({ name: 'regionCode', description: '区域代码' }) + @ApiResponse({ status: 200, type: [StickmanRankingResponse] }) + async getStickmanRanking( + @Query('month') month: string, + @Query('roleType') roleType: RoleType, + @Query('regionCode') regionCode: string, + ): Promise { + return await this.applicationService.getStickmanRanking(month, roleType, regionCode) + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: '撤销授权(管理员)' }) + @ApiParam({ name: 'id', description: '授权ID' }) + @ApiResponse({ status: 204 }) + async revokeAuthorization( + @Param('id') id: string, + @CurrentUser() user: { userId: string; accountSequence: string }, + @Body() dto: RevokeAuthorizationDto, + ): Promise { + const command = new RevokeAuthorizationCommand(id, user.accountSequence, dto.reason) + await this.applicationService.revokeAuthorization(command) + } + + @Post(':id/bypass') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: '授予单月豁免(管理员)' }) + @ApiParam({ name: 'id', description: '授权ID' }) + @ApiResponse({ status: 204 }) + async grantMonthlyBypass( + @Param('id') id: string, + @CurrentUser() user: { userId: string; accountSequence: string }, + @Body() dto: GrantMonthlyBypassDto, + ): Promise { + const command = new GrantMonthlyBypassCommand(id, dto.month, user.accountSequence, dto.reason) + await this.applicationService.grantMonthlyBypass(command) + } + + @Post(':id/exempt-percentage') + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ summary: '豁免占比考核(管理员)' }) + @ApiParam({ name: 'id', description: '授权ID' }) + @ApiResponse({ status: 204 }) + async exemptLocalPercentageCheck( + @Param('id') id: string, + @CurrentUser() user: { userId: string; accountSequence: string }, + ): Promise { + const command = new ExemptLocalPercentageCheckCommand(id, user.accountSequence) + await this.applicationService.exemptLocalPercentageCheck(command) + } +} diff --git a/backend/services/authorization-service/src/api/controllers/health.controller.ts b/backend/services/authorization-service/src/api/controllers/health.controller.ts index 254d72bf..3ca0be2b 100644 --- a/backend/services/authorization-service/src/api/controllers/health.controller.ts +++ b/backend/services/authorization-service/src/api/controllers/health.controller.ts @@ -1,16 +1,16 @@ -import { Controller, Get } from '@nestjs/common' -import { ApiTags, ApiOperation } from '@nestjs/swagger' - -@ApiTags('Health') -@Controller('health') -export class HealthController { - @Get() - @ApiOperation({ summary: '健康检查' }) - check() { - return { - status: 'ok', - service: 'authorization-service', - timestamp: new Date().toISOString(), - } - } -} +import { Controller, Get } from '@nestjs/common' +import { ApiTags, ApiOperation } from '@nestjs/swagger' + +@ApiTags('Health') +@Controller('health') +export class HealthController { + @Get() + @ApiOperation({ summary: '健康检查' }) + check() { + return { + status: 'ok', + service: 'authorization-service', + timestamp: new Date().toISOString(), + } + } +} diff --git a/backend/services/authorization-service/src/api/controllers/internal-authorization.controller.ts b/backend/services/authorization-service/src/api/controllers/internal-authorization.controller.ts index 505cbe80..d880a910 100644 --- a/backend/services/authorization-service/src/api/controllers/internal-authorization.controller.ts +++ b/backend/services/authorization-service/src/api/controllers/internal-authorization.controller.ts @@ -26,21 +26,21 @@ export class InternalAuthorizationController { schema: { type: 'object', properties: { - accountSequence: { type: 'number', nullable: true }, + accountSequence: { type: 'string', nullable: true }, }, }, }) async findNearestCommunity( @Query('accountSequence') accountSequence: string, - ): Promise<{ accountSequence: number | null }> { + ): Promise<{ accountSequence: string | null }> { this.logger.debug(`[INTERNAL] findNearestCommunity: accountSequence=${accountSequence}`) const result = await this.applicationService.findNearestAuthorizedCommunity( - Number(accountSequence), + accountSequence, ) return { - accountSequence: result ? Number(result) : null, + accountSequence: result, } } @@ -58,25 +58,25 @@ export class InternalAuthorizationController { schema: { type: 'object', properties: { - accountSequence: { type: 'number', nullable: true }, + accountSequence: { type: 'string', nullable: true }, }, }, }) async findNearestProvince( @Query('accountSequence') accountSequence: string, @Query('provinceCode') provinceCode: string, - ): Promise<{ accountSequence: number | null }> { + ): Promise<{ accountSequence: string | null }> { this.logger.debug( `[INTERNAL] findNearestProvince: accountSequence=${accountSequence}, provinceCode=${provinceCode}`, ) const result = await this.applicationService.findNearestAuthorizedProvince( - Number(accountSequence), + accountSequence, provinceCode, ) return { - accountSequence: result ? Number(result) : null, + accountSequence: result ? result : null, } } @@ -94,25 +94,25 @@ export class InternalAuthorizationController { schema: { type: 'object', properties: { - accountSequence: { type: 'number', nullable: true }, + accountSequence: { type: 'string', nullable: true }, }, }, }) async findNearestCity( @Query('accountSequence') accountSequence: string, @Query('cityCode') cityCode: string, - ): Promise<{ accountSequence: number | null }> { + ): Promise<{ accountSequence: string | null }> { this.logger.debug( `[INTERNAL] findNearestCity: accountSequence=${accountSequence}, cityCode=${cityCode}`, ) const result = await this.applicationService.findNearestAuthorizedCity( - Number(accountSequence), + accountSequence, cityCode, ) return { - accountSequence: result ? Number(result) : null, + accountSequence: result ? result : null, } } @@ -136,8 +136,8 @@ export class InternalAuthorizationController { items: { type: 'object', properties: { - accountSequence: { type: 'number', description: '接收者账号' }, - treeCount: { type: 'number', description: '分配棵数' }, + accountSequence: { type: 'string', description: '接收者账号' }, + treeCount: { type: 'string', description: '分配棵数' }, reason: { type: 'string', description: '分配原因' }, }, }, @@ -150,7 +150,7 @@ export class InternalAuthorizationController { @Query('treeCount') treeCount: string, ): Promise<{ distributions: Array<{ - accountSequence: number + accountSequence: string treeCount: number reason: string }> @@ -160,7 +160,7 @@ export class InternalAuthorizationController { ) return this.applicationService.getCommunityRewardDistribution( - Number(accountSequence), + accountSequence, Number(treeCount), ) } @@ -179,7 +179,7 @@ export class InternalAuthorizationController { @Query('treeCount') treeCount: string, ): Promise<{ distributions: Array<{ - accountSequence: number + accountSequence: string treeCount: number reason: string }> @@ -189,7 +189,7 @@ export class InternalAuthorizationController { ) return this.applicationService.getProvinceTeamRewardDistribution( - Number(accountSequence), + accountSequence, provinceCode, Number(treeCount), ) @@ -207,7 +207,7 @@ export class InternalAuthorizationController { @Query('treeCount') treeCount: string, ): Promise<{ distributions: Array<{ - accountSequence: number + accountSequence: string treeCount: number reason: string isSystemAccount: boolean @@ -237,7 +237,7 @@ export class InternalAuthorizationController { @Query('treeCount') treeCount: string, ): Promise<{ distributions: Array<{ - accountSequence: number + accountSequence: string treeCount: number reason: string }> @@ -247,7 +247,7 @@ export class InternalAuthorizationController { ) return this.applicationService.getCityTeamRewardDistribution( - Number(accountSequence), + accountSequence, cityCode, Number(treeCount), ) @@ -265,7 +265,7 @@ export class InternalAuthorizationController { @Query('treeCount') treeCount: string, ): Promise<{ distributions: Array<{ - accountSequence: number + accountSequence: string treeCount: number reason: string isSystemAccount: boolean diff --git a/backend/services/authorization-service/src/api/dto/request/apply-auth-city.dto.ts b/backend/services/authorization-service/src/api/dto/request/apply-auth-city.dto.ts index 69bed050..1df91881 100644 --- a/backend/services/authorization-service/src/api/dto/request/apply-auth-city.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/apply-auth-city.dto.ts @@ -1,16 +1,16 @@ -import { IsString, IsNotEmpty, MaxLength } from 'class-validator' -import { ApiProperty } from '@nestjs/swagger' - -export class ApplyAuthCityDto { - @ApiProperty({ description: '城市代码', example: '430100' }) - @IsString() - @IsNotEmpty({ message: '城市代码不能为空' }) - @MaxLength(20, { message: '城市代码最大20字符' }) - cityCode: string - - @ApiProperty({ description: '城市名称', example: '长沙市' }) - @IsString() - @IsNotEmpty({ message: '城市名称不能为空' }) - @MaxLength(50, { message: '城市名称最大50字符' }) - cityName: string -} +import { IsString, IsNotEmpty, MaxLength } from 'class-validator' +import { ApiProperty } from '@nestjs/swagger' + +export class ApplyAuthCityDto { + @ApiProperty({ description: '城市代码', example: '430100' }) + @IsString() + @IsNotEmpty({ message: '城市代码不能为空' }) + @MaxLength(20, { message: '城市代码最大20字符' }) + cityCode: string + + @ApiProperty({ description: '城市名称', example: '长沙市' }) + @IsString() + @IsNotEmpty({ message: '城市名称不能为空' }) + @MaxLength(50, { message: '城市名称最大50字符' }) + cityName: string +} diff --git a/backend/services/authorization-service/src/api/dto/request/apply-auth-province.dto.ts b/backend/services/authorization-service/src/api/dto/request/apply-auth-province.dto.ts index 02156f4b..df4623ec 100644 --- a/backend/services/authorization-service/src/api/dto/request/apply-auth-province.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/apply-auth-province.dto.ts @@ -1,16 +1,16 @@ -import { IsString, IsNotEmpty, MaxLength } from 'class-validator' -import { ApiProperty } from '@nestjs/swagger' - -export class ApplyAuthProvinceDto { - @ApiProperty({ description: '省份代码', example: '430000' }) - @IsString() - @IsNotEmpty({ message: '省份代码不能为空' }) - @MaxLength(20, { message: '省份代码最大20字符' }) - provinceCode: string - - @ApiProperty({ description: '省份名称', example: '湖南省' }) - @IsString() - @IsNotEmpty({ message: '省份名称不能为空' }) - @MaxLength(50, { message: '省份名称最大50字符' }) - provinceName: string -} +import { IsString, IsNotEmpty, MaxLength } from 'class-validator' +import { ApiProperty } from '@nestjs/swagger' + +export class ApplyAuthProvinceDto { + @ApiProperty({ description: '省份代码', example: '430000' }) + @IsString() + @IsNotEmpty({ message: '省份代码不能为空' }) + @MaxLength(20, { message: '省份代码最大20字符' }) + provinceCode: string + + @ApiProperty({ description: '省份名称', example: '湖南省' }) + @IsString() + @IsNotEmpty({ message: '省份名称不能为空' }) + @MaxLength(50, { message: '省份名称最大50字符' }) + provinceName: string +} diff --git a/backend/services/authorization-service/src/api/dto/request/apply-community-auth.dto.ts b/backend/services/authorization-service/src/api/dto/request/apply-community-auth.dto.ts index 7390a566..e1972bea 100644 --- a/backend/services/authorization-service/src/api/dto/request/apply-community-auth.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/apply-community-auth.dto.ts @@ -1,10 +1,10 @@ -import { IsString, IsNotEmpty, MaxLength } from 'class-validator' -import { ApiProperty } from '@nestjs/swagger' - -export class ApplyCommunityAuthDto { - @ApiProperty({ description: '社区名称', example: '量子社区' }) - @IsString() - @IsNotEmpty({ message: '社区名称不能为空' }) - @MaxLength(50, { message: '社区名称最大50字符' }) - communityName: string -} +import { IsString, IsNotEmpty, MaxLength } from 'class-validator' +import { ApiProperty } from '@nestjs/swagger' + +export class ApplyCommunityAuthDto { + @ApiProperty({ description: '社区名称', example: '量子社区' }) + @IsString() + @IsNotEmpty({ message: '社区名称不能为空' }) + @MaxLength(50, { message: '社区名称最大50字符' }) + communityName: string +} diff --git a/backend/services/authorization-service/src/api/dto/request/grant-auth-city-company.dto.ts b/backend/services/authorization-service/src/api/dto/request/grant-auth-city-company.dto.ts index 55edd97a..21349cc5 100644 --- a/backend/services/authorization-service/src/api/dto/request/grant-auth-city-company.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/grant-auth-city-company.dto.ts @@ -8,9 +8,9 @@ export class GrantAuthCityCompanyDto { userId: string @ApiProperty({ description: '账户序列号' }) - @IsNumber() + @IsString() @IsNotEmpty({ message: '账户序列号不能为空' }) - accountSequence: number + accountSequence: string @ApiProperty({ description: '城市代码', example: '430100' }) @IsString() diff --git a/backend/services/authorization-service/src/api/dto/request/grant-auth-province-company.dto.ts b/backend/services/authorization-service/src/api/dto/request/grant-auth-province-company.dto.ts index 1081ab26..2458a469 100644 --- a/backend/services/authorization-service/src/api/dto/request/grant-auth-province-company.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/grant-auth-province-company.dto.ts @@ -8,9 +8,9 @@ export class GrantAuthProvinceCompanyDto { userId: string @ApiProperty({ description: '账户序列号' }) - @IsNumber() + @IsString() @IsNotEmpty({ message: '账户序列号不能为空' }) - accountSequence: number + accountSequence: string @ApiProperty({ description: '省份代码', example: '430000' }) @IsString() diff --git a/backend/services/authorization-service/src/api/dto/request/grant-city-company.dto.ts b/backend/services/authorization-service/src/api/dto/request/grant-city-company.dto.ts index d774b175..7a375239 100644 --- a/backend/services/authorization-service/src/api/dto/request/grant-city-company.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/grant-city-company.dto.ts @@ -1,31 +1,31 @@ -import { IsString, IsNotEmpty, MaxLength, IsNumber, IsBoolean, IsOptional } from 'class-validator' -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' - -export class GrantCityCompanyDto { - @ApiProperty({ description: '用户ID' }) - @IsString() - @IsNotEmpty({ message: '用户ID不能为空' }) - userId: string - - @ApiProperty({ description: '账户序列号' }) - @IsNumber() - @IsNotEmpty({ message: '账户序列号不能为空' }) - accountSequence: number - - @ApiProperty({ description: '城市代码', example: '430100' }) - @IsString() - @IsNotEmpty({ message: '城市代码不能为空' }) - @MaxLength(20, { message: '城市代码最大20字符' }) - cityCode: string - - @ApiProperty({ description: '城市名称', example: '长沙市' }) - @IsString() - @IsNotEmpty({ message: '城市名称不能为空' }) - @MaxLength(50, { message: '城市名称最大50字符' }) - cityName: string - - @ApiPropertyOptional({ description: '是否跳过考核直接激活权益', default: false }) - @IsBoolean() - @IsOptional() - skipAssessment?: boolean -} +import { IsString, IsNotEmpty, MaxLength, IsNumber, IsBoolean, IsOptional } from 'class-validator' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' + +export class GrantCityCompanyDto { + @ApiProperty({ description: '用户ID' }) + @IsString() + @IsNotEmpty({ message: '用户ID不能为空' }) + userId: string + + @ApiProperty({ description: '账户序列号' }) + @IsString() + @IsNotEmpty({ message: '账户序列号不能为空' }) + accountSequence: string + + @ApiProperty({ description: '城市代码', example: '430100' }) + @IsString() + @IsNotEmpty({ message: '城市代码不能为空' }) + @MaxLength(20, { message: '城市代码最大20字符' }) + cityCode: string + + @ApiProperty({ description: '城市名称', example: '长沙市' }) + @IsString() + @IsNotEmpty({ message: '城市名称不能为空' }) + @MaxLength(50, { message: '城市名称最大50字符' }) + cityName: string + + @ApiPropertyOptional({ description: '是否跳过考核直接激活权益', default: false }) + @IsBoolean() + @IsOptional() + skipAssessment?: boolean +} diff --git a/backend/services/authorization-service/src/api/dto/request/grant-community.dto.ts b/backend/services/authorization-service/src/api/dto/request/grant-community.dto.ts index f5d82bdc..52a36528 100644 --- a/backend/services/authorization-service/src/api/dto/request/grant-community.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/grant-community.dto.ts @@ -8,9 +8,9 @@ export class GrantCommunityDto { userId: string @ApiProperty({ description: '账户序列号' }) - @IsNumber() + @IsString() @IsNotEmpty({ message: '账户序列号不能为空' }) - accountSequence: number + accountSequence: string @ApiProperty({ description: '社区名称', example: '深圳社区' }) @IsString() diff --git a/backend/services/authorization-service/src/api/dto/request/grant-monthly-bypass.dto.ts b/backend/services/authorization-service/src/api/dto/request/grant-monthly-bypass.dto.ts index 53431a01..fa898c36 100644 --- a/backend/services/authorization-service/src/api/dto/request/grant-monthly-bypass.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/grant-monthly-bypass.dto.ts @@ -1,16 +1,16 @@ -import { IsString, IsNotEmpty, IsOptional, MaxLength, Matches } from 'class-validator' -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' - -export class GrantMonthlyBypassDto { - @ApiProperty({ description: '豁免月份', example: '2024-01' }) - @IsString() - @IsNotEmpty({ message: '豁免月份不能为空' }) - @Matches(/^\d{4}-\d{2}$/, { message: '月份格式应为YYYY-MM' }) - month: string - - @ApiPropertyOptional({ description: '豁免原因', example: '特殊情况' }) - @IsOptional() - @IsString() - @MaxLength(200, { message: '豁免原因最大200字符' }) - reason?: string -} +import { IsString, IsNotEmpty, IsOptional, MaxLength, Matches } from 'class-validator' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' + +export class GrantMonthlyBypassDto { + @ApiProperty({ description: '豁免月份', example: '2024-01' }) + @IsString() + @IsNotEmpty({ message: '豁免月份不能为空' }) + @Matches(/^\d{4}-\d{2}$/, { message: '月份格式应为YYYY-MM' }) + month: string + + @ApiPropertyOptional({ description: '豁免原因', example: '特殊情况' }) + @IsOptional() + @IsString() + @MaxLength(200, { message: '豁免原因最大200字符' }) + reason?: string +} diff --git a/backend/services/authorization-service/src/api/dto/request/grant-province-company.dto.ts b/backend/services/authorization-service/src/api/dto/request/grant-province-company.dto.ts index 3b5cec09..7ab24908 100644 --- a/backend/services/authorization-service/src/api/dto/request/grant-province-company.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/grant-province-company.dto.ts @@ -1,31 +1,31 @@ -import { IsString, IsNotEmpty, MaxLength, IsNumber, IsBoolean, IsOptional } from 'class-validator' -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' - -export class GrantProvinceCompanyDto { - @ApiProperty({ description: '用户ID' }) - @IsString() - @IsNotEmpty({ message: '用户ID不能为空' }) - userId: string - - @ApiProperty({ description: '账户序列号' }) - @IsNumber() - @IsNotEmpty({ message: '账户序列号不能为空' }) - accountSequence: number - - @ApiProperty({ description: '省份代码', example: '430000' }) - @IsString() - @IsNotEmpty({ message: '省份代码不能为空' }) - @MaxLength(20, { message: '省份代码最大20字符' }) - provinceCode: string - - @ApiProperty({ description: '省份名称', example: '湖南省' }) - @IsString() - @IsNotEmpty({ message: '省份名称不能为空' }) - @MaxLength(50, { message: '省份名称最大50字符' }) - provinceName: string - - @ApiPropertyOptional({ description: '是否跳过考核直接激活权益', default: false }) - @IsBoolean() - @IsOptional() - skipAssessment?: boolean -} +import { IsString, IsNotEmpty, MaxLength, IsNumber, IsBoolean, IsOptional } from 'class-validator' +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' + +export class GrantProvinceCompanyDto { + @ApiProperty({ description: '用户ID' }) + @IsString() + @IsNotEmpty({ message: '用户ID不能为空' }) + userId: string + + @ApiProperty({ description: '账户序列号' }) + @IsString() + @IsNotEmpty({ message: '账户序列号不能为空' }) + accountSequence: string + + @ApiProperty({ description: '省份代码', example: '430000' }) + @IsString() + @IsNotEmpty({ message: '省份代码不能为空' }) + @MaxLength(20, { message: '省份代码最大20字符' }) + provinceCode: string + + @ApiProperty({ description: '省份名称', example: '湖南省' }) + @IsString() + @IsNotEmpty({ message: '省份名称不能为空' }) + @MaxLength(50, { message: '省份名称最大50字符' }) + provinceName: string + + @ApiPropertyOptional({ description: '是否跳过考核直接激活权益', default: false }) + @IsBoolean() + @IsOptional() + skipAssessment?: boolean +} diff --git a/backend/services/authorization-service/src/api/dto/request/revoke-authorization.dto.ts b/backend/services/authorization-service/src/api/dto/request/revoke-authorization.dto.ts index 381aae88..2a2442e5 100644 --- a/backend/services/authorization-service/src/api/dto/request/revoke-authorization.dto.ts +++ b/backend/services/authorization-service/src/api/dto/request/revoke-authorization.dto.ts @@ -1,10 +1,10 @@ -import { IsString, IsNotEmpty, MaxLength } from 'class-validator' -import { ApiProperty } from '@nestjs/swagger' - -export class RevokeAuthorizationDto { - @ApiProperty({ description: '撤销原因', example: '违规操作' }) - @IsString() - @IsNotEmpty({ message: '撤销原因不能为空' }) - @MaxLength(200, { message: '撤销原因最大200字符' }) - reason: string -} +import { IsString, IsNotEmpty, MaxLength } from 'class-validator' +import { ApiProperty } from '@nestjs/swagger' + +export class RevokeAuthorizationDto { + @ApiProperty({ description: '撤销原因', example: '违规操作' }) + @IsString() + @IsNotEmpty({ message: '撤销原因不能为空' }) + @MaxLength(200, { message: '撤销原因最大200字符' }) + reason: string +} diff --git a/backend/services/authorization-service/src/api/dto/response/authorization.response.ts b/backend/services/authorization-service/src/api/dto/response/authorization.response.ts index 781703dc..a1259b06 100644 --- a/backend/services/authorization-service/src/api/dto/response/authorization.response.ts +++ b/backend/services/authorization-service/src/api/dto/response/authorization.response.ts @@ -1,122 +1,122 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' -import { RoleType, AuthorizationStatus } from '@/domain/enums' - -export class AuthorizationResponse { - @ApiProperty({ description: '授权ID' }) - authorizationId: string - - @ApiProperty({ description: '用户ID' }) - userId: string - - @ApiProperty({ description: '角色类型', enum: RoleType }) - roleType: RoleType - - @ApiProperty({ description: '区域代码' }) - regionCode: string - - @ApiProperty({ description: '区域名称' }) - regionName: string - - @ApiProperty({ description: '授权状态', enum: AuthorizationStatus }) - status: AuthorizationStatus - - @ApiProperty({ description: '显示标题' }) - displayTitle: string - - @ApiProperty({ description: '权益是否激活' }) - benefitActive: boolean - - @ApiProperty({ description: '当前考核月份索引' }) - currentMonthIndex: number - - @ApiProperty({ description: '本地占比要求' }) - requireLocalPercentage: number - - @ApiProperty({ description: '是否豁免占比考核' }) - exemptFromPercentageCheck: boolean - - @ApiProperty({ description: '初始考核目标(社区10,市100,省500)' }) - initialTargetTreeCount: number - - @ApiProperty({ description: '当前团队认种数量' }) - currentTreeCount: number - - @ApiProperty({ description: '月度考核目标' }) - monthlyTargetTreeCount: number - - @ApiProperty({ description: '创建时间' }) - createdAt: Date - - @ApiProperty({ description: '更新时间' }) - updatedAt: Date -} - -export class ApplyAuthorizationResponse { - @ApiProperty({ description: '授权ID' }) - authorizationId: string - - @ApiProperty({ description: '授权状态' }) - status: string - - @ApiProperty({ description: '权益是否激活' }) - benefitActive: boolean - - @ApiPropertyOptional({ description: '显示标题' }) - displayTitle?: string - - @ApiProperty({ description: '消息提示' }) - message: string - - @ApiProperty({ description: '当前认种数量' }) - currentTreeCount: number - - @ApiProperty({ description: '所需认种数量' }) - requiredTreeCount: number -} - -export class StickmanRankingResponse { - @ApiProperty({ description: '用户ID' }) - userId: string - - @ApiProperty({ description: '授权ID' }) - authorizationId: string - - @ApiProperty({ description: '角色类型', enum: RoleType }) - roleType: RoleType - - @ApiProperty({ description: '区域代码' }) - regionCode: string - - @ApiPropertyOptional({ description: '昵称' }) - nickname?: string - - @ApiPropertyOptional({ description: '头像URL' }) - avatarUrl?: string - - @ApiProperty({ description: '排名' }) - ranking: number - - @ApiProperty({ description: '是否第一名' }) - isFirstPlace: boolean - - @ApiProperty({ description: '累计完成数量' }) - cumulativeCompleted: number - - @ApiProperty({ description: '累计目标数量' }) - cumulativeTarget: number - - @ApiProperty({ description: '最终目标数量' }) - finalTarget: number - - @ApiProperty({ description: '进度百分比' }) - progressPercentage: number - - @ApiProperty({ description: '超越比例' }) - exceedRatio: number - - @ApiProperty({ description: '本月USDT收益' }) - monthlyRewardUsdt: number - - @ApiProperty({ description: '本月RWAD收益' }) - monthlyRewardRwad: number -} +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' +import { RoleType, AuthorizationStatus } from '@/domain/enums' + +export class AuthorizationResponse { + @ApiProperty({ description: '授权ID' }) + authorizationId: string + + @ApiProperty({ description: '用户ID' }) + userId: string + + @ApiProperty({ description: '角色类型', enum: RoleType }) + roleType: RoleType + + @ApiProperty({ description: '区域代码' }) + regionCode: string + + @ApiProperty({ description: '区域名称' }) + regionName: string + + @ApiProperty({ description: '授权状态', enum: AuthorizationStatus }) + status: AuthorizationStatus + + @ApiProperty({ description: '显示标题' }) + displayTitle: string + + @ApiProperty({ description: '权益是否激活' }) + benefitActive: boolean + + @ApiProperty({ description: '当前考核月份索引' }) + currentMonthIndex: number + + @ApiProperty({ description: '本地占比要求' }) + requireLocalPercentage: number + + @ApiProperty({ description: '是否豁免占比考核' }) + exemptFromPercentageCheck: boolean + + @ApiProperty({ description: '初始考核目标(社区10,市100,省500)' }) + initialTargetTreeCount: number + + @ApiProperty({ description: '当前团队认种数量' }) + currentTreeCount: number + + @ApiProperty({ description: '月度考核目标' }) + monthlyTargetTreeCount: number + + @ApiProperty({ description: '创建时间' }) + createdAt: Date + + @ApiProperty({ description: '更新时间' }) + updatedAt: Date +} + +export class ApplyAuthorizationResponse { + @ApiProperty({ description: '授权ID' }) + authorizationId: string + + @ApiProperty({ description: '授权状态' }) + status: string + + @ApiProperty({ description: '权益是否激活' }) + benefitActive: boolean + + @ApiPropertyOptional({ description: '显示标题' }) + displayTitle?: string + + @ApiProperty({ description: '消息提示' }) + message: string + + @ApiProperty({ description: '当前认种数量' }) + currentTreeCount: number + + @ApiProperty({ description: '所需认种数量' }) + requiredTreeCount: number +} + +export class StickmanRankingResponse { + @ApiProperty({ description: '用户ID' }) + userId: string + + @ApiProperty({ description: '授权ID' }) + authorizationId: string + + @ApiProperty({ description: '角色类型', enum: RoleType }) + roleType: RoleType + + @ApiProperty({ description: '区域代码' }) + regionCode: string + + @ApiPropertyOptional({ description: '昵称' }) + nickname?: string + + @ApiPropertyOptional({ description: '头像URL' }) + avatarUrl?: string + + @ApiProperty({ description: '排名' }) + ranking: number + + @ApiProperty({ description: '是否第一名' }) + isFirstPlace: boolean + + @ApiProperty({ description: '累计完成数量' }) + cumulativeCompleted: number + + @ApiProperty({ description: '累计目标数量' }) + cumulativeTarget: number + + @ApiProperty({ description: '最终目标数量' }) + finalTarget: number + + @ApiProperty({ description: '进度百分比' }) + progressPercentage: number + + @ApiProperty({ description: '超越比例' }) + exceedRatio: number + + @ApiProperty({ description: '本月USDT收益' }) + monthlyRewardUsdt: number + + @ApiProperty({ description: '本月RWAD收益' }) + monthlyRewardRwad: number +} diff --git a/backend/services/authorization-service/src/api/dto/response/community-hierarchy.response.ts b/backend/services/authorization-service/src/api/dto/response/community-hierarchy.response.ts index 2edf66c6..16680153 100644 --- a/backend/services/authorization-service/src/api/dto/response/community-hierarchy.response.ts +++ b/backend/services/authorization-service/src/api/dto/response/community-hierarchy.response.ts @@ -8,7 +8,7 @@ export class CommunityInfo { authorizationId: string @ApiProperty({ description: '账户序列号' }) - accountSequence: number + accountSequence: string @ApiProperty({ description: '社区名称' }) communityName: string @@ -45,7 +45,7 @@ export class CommunityHierarchyResponse { */ export const HEADQUARTERS_COMMUNITY: CommunityInfo = { authorizationId: 'headquarters', - accountSequence: 0, + accountSequence: '', communityName: '总部社区', userId: undefined, isHeadquarters: true, 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 25c88dff..8716cb6a 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,18 +1,18 @@ -export class ApplyAuthCityCompanyCommand { - constructor( - public readonly userId: string, - public readonly accountSequence: number, - public readonly cityCode: string, - public readonly cityName: string, - ) {} -} - -export interface ApplyAuthCityCompanyResult { - authorizationId: string - status: string - benefitActive: boolean - displayTitle: string - message: string - currentTreeCount: number - requiredTreeCount: number -} +export class ApplyAuthCityCompanyCommand { + constructor( + public readonly userId: string, + public readonly accountSequence: string, + public readonly cityCode: string, + public readonly cityName: string, + ) {} +} + +export interface ApplyAuthCityCompanyResult { + authorizationId: string + status: string + benefitActive: boolean + displayTitle: string + message: string + currentTreeCount: number + requiredTreeCount: number +} 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 482dd68b..464cb8e8 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,18 +1,18 @@ -export class ApplyAuthProvinceCompanyCommand { - constructor( - public readonly userId: string, - public readonly accountSequence: number, - public readonly provinceCode: string, - public readonly provinceName: string, - ) {} -} - -export interface ApplyAuthProvinceCompanyResult { - authorizationId: string - status: string - benefitActive: boolean - displayTitle: string - message: string - currentTreeCount: number - requiredTreeCount: number -} +export class ApplyAuthProvinceCompanyCommand { + constructor( + public readonly userId: string, + public readonly accountSequence: string, + public readonly provinceCode: string, + public readonly provinceName: string, + ) {} +} + +export interface ApplyAuthProvinceCompanyResult { + authorizationId: string + status: string + benefitActive: boolean + displayTitle: string + message: string + currentTreeCount: number + requiredTreeCount: number +} 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 f2615ed0..8f07f95b 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,16 +1,16 @@ -export class ApplyCommunityAuthCommand { - constructor( - public readonly userId: string, - public readonly accountSequence: number, - public readonly communityName: string, - ) {} -} - -export interface ApplyCommunityAuthResult { - authorizationId: string - status: string - benefitActive: boolean - message: string - currentTreeCount: number - requiredTreeCount: number -} +export class ApplyCommunityAuthCommand { + constructor( + public readonly userId: string, + public readonly accountSequence: string, + public readonly communityName: string, + ) {} +} + +export interface ApplyCommunityAuthResult { + authorizationId: string + status: string + benefitActive: boolean + message: string + currentTreeCount: number + requiredTreeCount: number +} 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 17fe805b..988e3941 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 adminAccountSequence: number, - ) {} -} +export class ExemptLocalPercentageCheckCommand { + constructor( + public readonly authorizationId: string, + public readonly adminAccountSequence: string, + ) {} +} diff --git a/backend/services/authorization-service/src/application/commands/grant-auth-city-company.command.ts b/backend/services/authorization-service/src/application/commands/grant-auth-city-company.command.ts index 145363ce..4100e125 100644 --- a/backend/services/authorization-service/src/application/commands/grant-auth-city-company.command.ts +++ b/backend/services/authorization-service/src/application/commands/grant-auth-city-company.command.ts @@ -1,11 +1,11 @@ export class GrantAuthCityCompanyCommand { constructor( public readonly userId: string, - public readonly accountSequence: number, + public readonly accountSequence: string, public readonly cityCode: string, public readonly cityName: string, public readonly adminId: string, - public readonly adminAccountSequence: number, + public readonly adminAccountSequence: string, public readonly skipAssessment: boolean = false, ) {} } diff --git a/backend/services/authorization-service/src/application/commands/grant-auth-province-company.command.ts b/backend/services/authorization-service/src/application/commands/grant-auth-province-company.command.ts index f706f050..5a42faa1 100644 --- a/backend/services/authorization-service/src/application/commands/grant-auth-province-company.command.ts +++ b/backend/services/authorization-service/src/application/commands/grant-auth-province-company.command.ts @@ -1,11 +1,11 @@ export class GrantAuthProvinceCompanyCommand { constructor( public readonly userId: string, - public readonly accountSequence: number, + public readonly accountSequence: string, public readonly provinceCode: string, public readonly provinceName: string, public readonly adminId: string, - public readonly adminAccountSequence: number, + public readonly adminAccountSequence: string, public readonly skipAssessment: boolean = false, ) {} } 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 19a9e780..49281cf2 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,11 +1,11 @@ -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, - public readonly skipAssessment: boolean = false, - ) {} -} +export class GrantCityCompanyCommand { + constructor( + public readonly userId: string, + public readonly accountSequence: string, + public readonly cityCode: string, + public readonly cityName: string, + public readonly adminId: string, + public readonly adminAccountSequence: string, + public readonly skipAssessment: boolean = false, + ) {} +} diff --git a/backend/services/authorization-service/src/application/commands/grant-community.command.ts b/backend/services/authorization-service/src/application/commands/grant-community.command.ts index 306018b7..da7aa7a2 100644 --- a/backend/services/authorization-service/src/application/commands/grant-community.command.ts +++ b/backend/services/authorization-service/src/application/commands/grant-community.command.ts @@ -1,10 +1,10 @@ export class GrantCommunityCommand { constructor( public readonly userId: string, - public readonly accountSequence: number, + public readonly accountSequence: string, public readonly communityName: string, public readonly adminId: string, - public readonly adminAccountSequence: number, + public readonly adminAccountSequence: string, public readonly skipAssessment: boolean = false, ) {} } 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 c0a9ecf3..93047d8d 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 @@ -1,8 +1,8 @@ -export class GrantMonthlyBypassCommand { - constructor( - public readonly authorizationId: string, - public readonly month: string, - public readonly adminAccountSequence: number, - public readonly reason?: string, - ) {} -} +export class GrantMonthlyBypassCommand { + constructor( + public readonly authorizationId: string, + public readonly month: string, + public readonly adminAccountSequence: string, + 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 7aa16d72..a89bd7ad 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,11 +1,11 @@ -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, - public readonly skipAssessment: boolean = false, - ) {} -} +export class GrantProvinceCompanyCommand { + constructor( + public readonly userId: string, + public readonly accountSequence: string, + public readonly provinceCode: string, + public readonly provinceName: string, + public readonly adminId: string, + public readonly adminAccountSequence: string, + public readonly skipAssessment: boolean = false, + ) {} +} 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 ffb2a647..0c18b782 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 adminAccountSequence: number, - public readonly reason: string, - ) {} -} +export class RevokeAuthorizationCommand { + constructor( + public readonly authorizationId: string, + public readonly adminAccountSequence: string, + public readonly reason: string, + ) {} +} diff --git a/backend/services/authorization-service/src/application/dto/authorization.dto.ts b/backend/services/authorization-service/src/application/dto/authorization.dto.ts index 7e0a95f1..6f5a5e19 100644 --- a/backend/services/authorization-service/src/application/dto/authorization.dto.ts +++ b/backend/services/authorization-service/src/application/dto/authorization.dto.ts @@ -1,60 +1,60 @@ -import { RoleType, AuthorizationStatus } from '@/domain/enums' - -export interface AuthorizationDTO { - authorizationId: string - userId: string - roleType: RoleType - regionCode: string - regionName: string - status: AuthorizationStatus - displayTitle: string - benefitActive: boolean - currentMonthIndex: number - requireLocalPercentage: number - exemptFromPercentageCheck: boolean - // 考核进度字段 - initialTargetTreeCount: number // 初始考核目标(社区10,市100,省500) - currentTreeCount: number // 当前团队认种数量 - monthlyTargetTreeCount: number // 月度考核目标(社区固定10) - createdAt: Date - updatedAt: Date -} - -export interface StickmanRankingDTO { - userId: string - authorizationId: string - roleType: RoleType - regionCode: string - nickname?: string - avatarUrl?: string - ranking: number - isFirstPlace: boolean - cumulativeCompleted: number - cumulativeTarget: number - finalTarget: number - progressPercentage: number - exceedRatio: number - monthlyRewardUsdt: number - monthlyRewardRwad: number -} - -export interface MonthlyAssessmentDTO { - assessmentId: string - authorizationId: string - userId: string - roleType: RoleType - regionCode: string - assessmentMonth: string - monthIndex: number - monthlyTarget: number - cumulativeTarget: number - monthlyCompleted: number - cumulativeCompleted: number - localPercentage: number - localPercentagePass: boolean - exceedRatio: number - result: string - rankingInRegion: number | null - isFirstPlace: boolean - isBypassed: boolean -} +import { RoleType, AuthorizationStatus } from '@/domain/enums' + +export interface AuthorizationDTO { + authorizationId: string + userId: string + roleType: RoleType + regionCode: string + regionName: string + status: AuthorizationStatus + displayTitle: string + benefitActive: boolean + currentMonthIndex: number + requireLocalPercentage: number + exemptFromPercentageCheck: boolean + // 考核进度字段 + initialTargetTreeCount: number // 初始考核目标(社区10,市100,省500) + currentTreeCount: number // 当前团队认种数量 + monthlyTargetTreeCount: number // 月度考核目标(社区固定10) + createdAt: Date + updatedAt: Date +} + +export interface StickmanRankingDTO { + userId: string + authorizationId: string + roleType: RoleType + regionCode: string + nickname?: string + avatarUrl?: string + ranking: number + isFirstPlace: boolean + cumulativeCompleted: number + cumulativeTarget: number + finalTarget: number + progressPercentage: number + exceedRatio: number + monthlyRewardUsdt: number + monthlyRewardRwad: number +} + +export interface MonthlyAssessmentDTO { + assessmentId: string + authorizationId: string + userId: string + roleType: RoleType + regionCode: string + assessmentMonth: string + monthIndex: number + monthlyTarget: number + cumulativeTarget: number + monthlyCompleted: number + cumulativeCompleted: number + localPercentage: number + localPercentagePass: boolean + exceedRatio: number + result: string + rankingInRegion: number | null + isFirstPlace: boolean + isBypassed: boolean +} diff --git a/backend/services/authorization-service/src/application/dto/community-hierarchy.dto.ts b/backend/services/authorization-service/src/application/dto/community-hierarchy.dto.ts index 12a105c1..fbd6607a 100644 --- a/backend/services/authorization-service/src/application/dto/community-hierarchy.dto.ts +++ b/backend/services/authorization-service/src/application/dto/community-hierarchy.dto.ts @@ -3,7 +3,7 @@ */ export interface CommunityInfoDTO { authorizationId: string - accountSequence: number + accountSequence: string communityName: string userId?: string isHeadquarters: boolean 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 c6ea2222..3df3b06d 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 @@ -1,1680 +1,1680 @@ -import { Injectable, Inject, Logger } from '@nestjs/common' -import { AuthorizationRole, MonthlyAssessment } from '@/domain/aggregates' -import { LadderTargetRule } from '@/domain/entities' -import { - UserId, - AdminUserId, - RegionCode, - AuthorizationId, - Month, -} from '@/domain/value-objects' -import { RoleType, AuthorizationStatus } from '@/domain/enums' -import { - IAuthorizationRoleRepository, - AUTHORIZATION_ROLE_REPOSITORY, - IMonthlyAssessmentRepository, - MONTHLY_ASSESSMENT_REPOSITORY, -} from '@/domain/repositories' -import { - AuthorizationValidatorService, - IReferralRepository, - ITeamStatisticsRepository, - TeamStatistics, -} from '@/domain/services' -import { EventPublisherService } from '@/infrastructure/kafka' -import { ReferralServiceClient } from '@/infrastructure/external' -import { ApplicationError, NotFoundError } from '@/shared/exceptions' -import { - ApplyCommunityAuthCommand, - ApplyCommunityAuthResult, - ApplyAuthProvinceCompanyCommand, - ApplyAuthProvinceCompanyResult, - ApplyAuthCityCompanyCommand, - ApplyAuthCityCompanyResult, - GrantCommunityCommand, - GrantProvinceCompanyCommand, - GrantCityCompanyCommand, - GrantAuthProvinceCompanyCommand, - GrantAuthCityCompanyCommand, - RevokeAuthorizationCommand, - GrantMonthlyBypassCommand, - ExemptLocalPercentageCheckCommand, -} from '@/application/commands' -import { AuthorizationDTO, StickmanRankingDTO, CommunityHierarchyDTO } from '@/application/dto' - -export const REFERRAL_REPOSITORY = Symbol('IReferralRepository') -export const TEAM_STATISTICS_REPOSITORY = Symbol('ITeamStatisticsRepository') - -@Injectable() -export class AuthorizationApplicationService { - private readonly logger = new Logger(AuthorizationApplicationService.name) - private readonly validatorService = new AuthorizationValidatorService() - - constructor( - @Inject(AUTHORIZATION_ROLE_REPOSITORY) - private readonly authorizationRepository: IAuthorizationRoleRepository, - @Inject(MONTHLY_ASSESSMENT_REPOSITORY) - private readonly assessmentRepository: IMonthlyAssessmentRepository, - @Inject(REFERRAL_REPOSITORY) - private readonly referralRepository: IReferralRepository, - @Inject(TEAM_STATISTICS_REPOSITORY) - private readonly statsRepository: ITeamStatisticsRepository, - private readonly eventPublisher: EventPublisherService, - private readonly referralServiceClient: ReferralServiceClient, - ) {} - - /** - * 申请社区授权 - */ - async applyCommunityAuth( - command: ApplyCommunityAuthCommand, - ): Promise { - const userId = UserId.create(command.userId, command.accountSequence) - - // 1. 检查是否已有社区授权 - const existing = await this.authorizationRepository.findByAccountSequenceAndRoleType( - userId.accountSequence, - RoleType.COMMUNITY, - ) - - if (existing && existing.status !== AuthorizationStatus.REVOKED) { - throw new ApplicationError('您已申请过社区授权') - } - - // 2. 创建社区授权 - const authorization = AuthorizationRole.createCommunityAuth({ - userId, - communityName: command.communityName, - }) - - // 3. 检查初始考核(10棵)- 使用下级团队认种数(不含自己) - const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) - const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 - - if (subordinateTreeCount >= authorization.getInitialTarget()) { - // 达标,激活权益 - authorization.activateBenefit() - } - - await this.authorizationRepository.save(authorization) - await this.eventPublisher.publishAll(authorization.domainEvents) - authorization.clearDomainEvents() - - return { - authorizationId: authorization.authorizationId.value, - status: authorization.status, - benefitActive: authorization.benefitActive, - message: authorization.benefitActive - ? '社区权益已激活' - : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, - currentTreeCount: subordinateTreeCount, - requiredTreeCount: authorization.getInitialTarget(), - } - } - - /** - * 申请授权省公司 - */ - async applyAuthProvinceCompany( - command: ApplyAuthProvinceCompanyCommand, - ): Promise { - const userId = UserId.create(command.userId, command.accountSequence) - const regionCode = RegionCode.create(command.provinceCode) - - // 1. 验证授权申请(团队内唯一性) - const validation = await this.validatorService.validateAuthorizationRequest( - userId, - RoleType.AUTH_PROVINCE_COMPANY, - regionCode, - this.referralRepository, - this.authorizationRepository, - ) - - if (!validation.isValid) { - throw new ApplicationError(validation.errorMessage!) - } - - // 2. 创建授权 - const authorization = AuthorizationRole.createAuthProvinceCompany({ - userId, - provinceCode: command.provinceCode, - provinceName: command.provinceName, - }) - - // 3. 检查初始考核(500棵)- 使用下级团队认种数(不含自己) - const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) - const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 - - if (subordinateTreeCount >= authorization.getInitialTarget()) { - // 达标,激活权益并创建首月考核 - authorization.activateBenefit() - await this.createInitialAssessment(authorization, teamStats!) - } - - await this.authorizationRepository.save(authorization) - await this.eventPublisher.publishAll(authorization.domainEvents) - authorization.clearDomainEvents() - - return { - authorizationId: authorization.authorizationId.value, - status: authorization.status, - benefitActive: authorization.benefitActive, - displayTitle: authorization.displayTitle, - message: authorization.benefitActive - ? '授权省公司权益已激活,开始阶梯考核' - : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, - currentTreeCount: subordinateTreeCount, - requiredTreeCount: authorization.getInitialTarget(), - } - } - - /** - * 申请授权市公司 - */ - async applyAuthCityCompany( - command: ApplyAuthCityCompanyCommand, - ): Promise { - const userId = UserId.create(command.userId, command.accountSequence) - const regionCode = RegionCode.create(command.cityCode) - - // 1. 验证 - const validation = await this.validatorService.validateAuthorizationRequest( - userId, - RoleType.AUTH_CITY_COMPANY, - regionCode, - this.referralRepository, - this.authorizationRepository, - ) - - if (!validation.isValid) { - throw new ApplicationError(validation.errorMessage!) - } - - // 2. 创建授权 - const authorization = AuthorizationRole.createAuthCityCompany({ - userId, - cityCode: command.cityCode, - cityName: command.cityName, - }) - - // 3. 检查初始考核(100棵)- 使用下级团队认种数(不含自己) - const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) - const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 - - if (subordinateTreeCount >= authorization.getInitialTarget()) { - authorization.activateBenefit() - await this.createInitialAssessment(authorization, teamStats!) - } - - await this.authorizationRepository.save(authorization) - await this.eventPublisher.publishAll(authorization.domainEvents) - authorization.clearDomainEvents() - - return { - authorizationId: authorization.authorizationId.value, - status: authorization.status, - benefitActive: authorization.benefitActive, - displayTitle: authorization.displayTitle, - message: authorization.benefitActive - ? '授权市公司权益已激活,开始阶梯考核' - : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, - currentTreeCount: subordinateTreeCount, - requiredTreeCount: authorization.getInitialTarget(), - } - } - - /** - * 管理员直接授权社区 - */ - async grantCommunity(command: GrantCommunityCommand): Promise { - const userId = UserId.create(command.userId, command.accountSequence) - const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) - - const authorization = AuthorizationRole.createCommunity({ - userId, - communityName: command.communityName, - adminId, - skipAssessment: command.skipAssessment, - }) - - await this.authorizationRepository.save(authorization) - await this.eventPublisher.publishAll(authorization.domainEvents) - authorization.clearDomainEvents() - } - - /** - * 管理员授权正式省公司(省区域) - * 需要验证团队内唯一性:同一推荐链上不能有重复的相同省份授权 - */ - async grantProvinceCompany(command: GrantProvinceCompanyCommand): Promise { - const userId = UserId.create(command.userId, command.accountSequence) - const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) - const regionCode = RegionCode.create(command.provinceCode) - - // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同省份授权) - const validation = await this.validatorService.validateAuthorizationRequest( - userId, - RoleType.PROVINCE_COMPANY, - regionCode, - this.referralRepository, - this.authorizationRepository, - ) - - if (!validation.isValid) { - throw new ApplicationError(validation.errorMessage!) - } - - // 2. 创建授权 - const authorization = AuthorizationRole.createProvinceCompany({ - userId, - provinceCode: command.provinceCode, - provinceName: command.provinceName, - adminId, - skipAssessment: command.skipAssessment, - }) - - await this.authorizationRepository.save(authorization) - await this.eventPublisher.publishAll(authorization.domainEvents) - authorization.clearDomainEvents() - } - - /** - * 管理员授权正式市公司(市区域) - * 需要验证团队内唯一性:同一推荐链上不能有重复的相同城市授权 - */ - async grantCityCompany(command: GrantCityCompanyCommand): Promise { - const userId = UserId.create(command.userId, command.accountSequence) - const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) - const regionCode = RegionCode.create(command.cityCode) - - // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同城市授权) - const validation = await this.validatorService.validateAuthorizationRequest( - userId, - RoleType.CITY_COMPANY, - regionCode, - this.referralRepository, - this.authorizationRepository, - ) - - if (!validation.isValid) { - throw new ApplicationError(validation.errorMessage!) - } - - // 2. 创建授权 - const authorization = AuthorizationRole.createCityCompany({ - userId, - cityCode: command.cityCode, - cityName: command.cityName, - adminId, - skipAssessment: command.skipAssessment, - }) - - await this.authorizationRepository.save(authorization) - await this.eventPublisher.publishAll(authorization.domainEvents) - authorization.clearDomainEvents() - } - - /** - * 管理员授权授权省公司(省团队) - * Admin直接授权,跳过用户申请流程 - * 需要验证团队内唯一性:同一推荐链上不能有重复的相同省份授权 - */ - async grantAuthProvinceCompany(command: GrantAuthProvinceCompanyCommand): Promise { - const userId = UserId.create(command.userId, command.accountSequence) - const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) - const regionCode = RegionCode.create(command.provinceCode) - - // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同省份授权) - const validation = await this.validatorService.validateAuthorizationRequest( - userId, - RoleType.AUTH_PROVINCE_COMPANY, - regionCode, - this.referralRepository, - this.authorizationRepository, - ) - - if (!validation.isValid) { - throw new ApplicationError(validation.errorMessage!) - } - - // 2. 创建授权 - const authorization = AuthorizationRole.createAuthProvinceCompanyByAdmin({ - userId, - provinceCode: command.provinceCode, - provinceName: command.provinceName, - adminId, - skipAssessment: command.skipAssessment, - }) - - await this.authorizationRepository.save(authorization) - await this.eventPublisher.publishAll(authorization.domainEvents) - authorization.clearDomainEvents() - } - - /** - * 管理员授权授权市公司(市团队) - * Admin直接授权,跳过用户申请流程 - * 需要验证团队内唯一性:同一推荐链上不能有重复的相同城市授权 - */ - async grantAuthCityCompany(command: GrantAuthCityCompanyCommand): Promise { - const userId = UserId.create(command.userId, command.accountSequence) - const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) - const regionCode = RegionCode.create(command.cityCode) - - // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同城市授权) - const validation = await this.validatorService.validateAuthorizationRequest( - userId, - RoleType.AUTH_CITY_COMPANY, - regionCode, - this.referralRepository, - this.authorizationRepository, - ) - - if (!validation.isValid) { - throw new ApplicationError(validation.errorMessage!) - } - - // 2. 创建授权 - const authorization = AuthorizationRole.createAuthCityCompanyByAdmin({ - userId, - cityCode: command.cityCode, - cityName: command.cityName, - adminId, - skipAssessment: command.skipAssessment, - }) - - await this.authorizationRepository.save(authorization) - await this.eventPublisher.publishAll(authorization.domainEvents) - authorization.clearDomainEvents() - } - - /** - * 撤销授权 - */ - async revokeAuthorization(command: RevokeAuthorizationCommand): Promise { - const authorization = await this.authorizationRepository.findById( - AuthorizationId.create(command.authorizationId), - ) - - if (!authorization) { - throw new NotFoundError('授权不存在') - } - - // 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) - authorization.clearDomainEvents() - } - - /** - * 授予单月豁免 - */ - async grantMonthlyBypass(command: GrantMonthlyBypassCommand): Promise { - const assessment = await this.assessmentRepository.findByAuthorizationAndMonth( - AuthorizationId.create(command.authorizationId), - Month.create(command.month), - ) - - if (!assessment) { - throw new NotFoundError('考核记录不存在') - } - - // 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) - assessment.clearDomainEvents() - } - - /** - * 豁免占比考核 - */ - async exemptLocalPercentageCheck(command: ExemptLocalPercentageCheckCommand): Promise { - const authorization = await this.authorizationRepository.findById( - AuthorizationId.create(command.authorizationId), - ) - - if (!authorization) { - throw new NotFoundError('授权不存在') - } - - // 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) - authorization.clearDomainEvents() - } - - /** - * 查询用户授权列表 - */ - async getUserAuthorizations(accountSequence: number): Promise { - const authorizations = await this.authorizationRepository.findByAccountSequence( - BigInt(accountSequence), - ) - - // 查询用户团队统计数据 - const teamStats = await this.statsRepository.findByAccountSequence(BigInt(accountSequence)) - const currentTreeCount = teamStats?.totalTeamPlantingCount || 0 - - return authorizations.map((auth) => this.toAuthorizationDTO(auth, currentTreeCount)) - } - - /** - * 查询用户授权详情 - */ - async getAuthorizationById(authorizationId: string): Promise { - const authorization = await this.authorizationRepository.findById( - AuthorizationId.create(authorizationId), - ) - - if (!authorization) return null - - // 查询用户团队统计数据 - const teamStats = await this.statsRepository.findByAccountSequence(authorization.userId.accountSequence) - const currentTreeCount = teamStats?.totalTeamPlantingCount || 0 - - return this.toAuthorizationDTO(authorization, currentTreeCount) - } - - /** - * 查询火柴人排名数据 - */ - async getStickmanRanking( - month: string, - roleType: RoleType, - regionCode: string, - ): Promise { - const assessments = await this.assessmentRepository.findRankingsByMonthAndRegion( - Month.create(month), - roleType, - RegionCode.create(regionCode), - ) - - const rankings: StickmanRankingDTO[] = [] - const finalTarget = LadderTargetRule.getFinalTarget(roleType) - - for (const assessment of assessments) { - rankings.push({ - userId: assessment.userId.value, - authorizationId: assessment.authorizationId.value, - roleType: assessment.roleType, - regionCode: assessment.regionCode.value, - ranking: assessment.rankingInRegion || 0, - isFirstPlace: assessment.isFirstPlace, - cumulativeCompleted: assessment.cumulativeCompleted, - cumulativeTarget: assessment.cumulativeTarget, - finalTarget, - progressPercentage: (assessment.cumulativeCompleted / finalTarget) * 100, - exceedRatio: assessment.exceedRatio, - monthlyRewardUsdt: 0, // TODO: 从奖励服务获取 - monthlyRewardRwad: 0, - }) - } - - return rankings - } - - // 辅助方法 - private async createInitialAssessment( - authorization: AuthorizationRole, - teamStats: TeamStatistics, - ): Promise { - const currentMonth = Month.current() - const target = LadderTargetRule.getTarget(authorization.roleType, 1) - - const assessment = MonthlyAssessment.create({ - authorizationId: authorization.authorizationId, - userId: authorization.userId, - roleType: authorization.roleType, - regionCode: authorization.regionCode, - assessmentMonth: currentMonth, - monthIndex: 1, - monthlyTarget: target.monthlyTarget, - cumulativeTarget: target.cumulativeTarget, - }) - - // 立即评估首月 - const localTeamCount = this.getLocalTeamCount( - teamStats, - authorization.roleType, - authorization.regionCode, - ) - - assessment.assess({ - cumulativeCompleted: teamStats.totalTeamPlantingCount, - localTeamCount, - totalTeamCount: teamStats.totalTeamPlantingCount, - requireLocalPercentage: authorization.requireLocalPercentage, - exemptFromPercentageCheck: authorization.exemptFromPercentageCheck, - }) - - await this.assessmentRepository.save(assessment) - await this.eventPublisher.publishAll(assessment.domainEvents) - assessment.clearDomainEvents() - } - - private getLocalTeamCount( - teamStats: TeamStatistics, - roleType: RoleType, - regionCode: RegionCode, - ): number { - if (roleType === RoleType.AUTH_PROVINCE_COMPANY) { - return teamStats.getProvinceTeamCount(regionCode.value) - } else if (roleType === RoleType.AUTH_CITY_COMPANY) { - return teamStats.getCityTeamCount(regionCode.value) - } - return 0 - } - - private toAuthorizationDTO(auth: AuthorizationRole, currentTreeCount: number): AuthorizationDTO { - // 获取月度考核目标(社区固定10,其他类型根据阶梯规则) - let monthlyTargetTreeCount = 0 - if (auth.roleType === RoleType.COMMUNITY) { - monthlyTargetTreeCount = 10 // 社区固定每月新增10棵 - } else if (auth.benefitActive && auth.currentMonthIndex > 0) { - // 省/市公司使用阶梯目标 - const target = LadderTargetRule.getTarget(auth.roleType, auth.currentMonthIndex) - monthlyTargetTreeCount = target.monthlyTarget - } - - return { - authorizationId: auth.authorizationId.value, - userId: auth.userId.value, - roleType: auth.roleType, - regionCode: auth.regionCode.value, - regionName: auth.regionName, - status: auth.status, - displayTitle: auth.displayTitle, - benefitActive: auth.benefitActive, - currentMonthIndex: auth.currentMonthIndex, - requireLocalPercentage: auth.requireLocalPercentage, - exemptFromPercentageCheck: auth.exemptFromPercentageCheck, - // 考核进度字段 - initialTargetTreeCount: auth.getInitialTarget(), - currentTreeCount, - monthlyTargetTreeCount, - createdAt: auth.createdAt, - updatedAt: auth.updatedAt, - } - } - - /** - * 获取用户的社区层级信息 - * - myCommunity: 我的社区授权(如果有) - * - parentCommunity: 上级社区(沿推荐链往上找最近的,如果没有则返回总部社区) - * - childCommunities: 下级社区(在我的团队中找最近的社区) - */ - async getCommunityHierarchy(accountSequence: number): Promise { - this.logger.debug(`[getCommunityHierarchy] accountSequence=${accountSequence}`) - - // 1. 查询我的社区授权 - const myCommunity = await this.authorizationRepository.findByAccountSequenceAndRoleType( - BigInt(accountSequence), - RoleType.COMMUNITY, - ) - - // 2. 获取我的祖先链(推荐链) - const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence)) - this.logger.debug(`[getCommunityHierarchy] ancestorPath: ${ancestorAccountSequences.join(',')}`) - - // 3. 查找上级社区(在祖先链中找最近的有社区授权的用户) - let parentCommunityAuth: AuthorizationRole | null = null - if (ancestorAccountSequences.length > 0) { - const ancestorCommunities = await this.authorizationRepository.findActiveCommunityByAccountSequences( - ancestorAccountSequences.map((seq) => BigInt(seq)), - ) - - // 找最近的(ancestorAccountSequences 是从直接推荐人到根节点的顺序) - if (ancestorCommunities.length > 0) { - // 按祖先链顺序找第一个匹配的 - for (const ancestorSeq of ancestorAccountSequences) { - const found = ancestorCommunities.find( - (auth) => Number(auth.userId.accountSequence) === ancestorSeq, - ) - if (found) { - parentCommunityAuth = found - break - } - } - } - } - - // 4. 获取我的团队成员 - const teamMemberAccountSequences = await this.referralServiceClient.getTeamMembers(BigInt(accountSequence)) - this.logger.debug(`[getCommunityHierarchy] teamMembers count: ${teamMemberAccountSequences.length}`) - - // 5. 查找下级社区(在团队成员中找最近的有社区授权的用户) - // "最近" 的定义:直接下级优先,然后是下级的下级,以此类推 - // 由于 getTeamMembers 返回的是广度优先遍历结果,可以直接使用顺序 - let childCommunityAuths: AuthorizationRole[] = [] - if (teamMemberAccountSequences.length > 0) { - const teamCommunities = await this.authorizationRepository.findActiveCommunityByAccountSequences( - teamMemberAccountSequences.map((seq) => BigInt(seq)), - ) - - // 只保留"最近的"下级社区 - // 如果一个社区的上级不在我的直接团队成员中,或者其上级就是我,则它是"最近的" - // 简化实现:返回所有团队中的社区,前端可以根据需要过滤 - // 但按用户要求"只计算最近的那个",这里需要做过滤 - // 算法:如果某个社区 A 的祖先中有另一个社区 B 也在团队中,则 A 不是最近的 - - const communityAccountSeqs = new Set(teamCommunities.map((c) => Number(c.userId.accountSequence))) - - for (const comm of teamCommunities) { - // 获取这个社区成员的祖先链 - const commAncestors = await this.referralServiceClient.getReferralChain(comm.userId.accountSequence) - - // 检查这个社区是否有"更近"的祖先社区 - let hasCloserAncestorCommunity = false - for (const ancestorSeq of commAncestors) { - // 如果祖先是我,停止检查 - if (ancestorSeq === accountSequence) { - break - } - // 如果祖先也是社区且在我的团队中,则当前社区不是最近的 - if (communityAccountSeqs.has(ancestorSeq)) { - hasCloserAncestorCommunity = true - break - } - } - - if (!hasCloserAncestorCommunity) { - childCommunityAuths.push(comm) - } - } - } - - // 6. 构建响应 - const HEADQUARTERS_COMMUNITY = { - authorizationId: 'headquarters', - accountSequence: 0, - communityName: '总部社区', - userId: undefined, - isHeadquarters: true, - } - - return { - myCommunity: myCommunity && myCommunity.status === AuthorizationStatus.AUTHORIZED - ? { - authorizationId: myCommunity.authorizationId.value, - accountSequence: Number(myCommunity.userId.accountSequence), - communityName: myCommunity.displayTitle, - userId: myCommunity.userId.value, - isHeadquarters: false, - } - : null, - parentCommunity: parentCommunityAuth - ? { - authorizationId: parentCommunityAuth.authorizationId.value, - accountSequence: Number(parentCommunityAuth.userId.accountSequence), - communityName: parentCommunityAuth.displayTitle, - userId: parentCommunityAuth.userId.value, - isHeadquarters: false, - } - : HEADQUARTERS_COMMUNITY, - childCommunities: childCommunityAuths.map((auth) => ({ - authorizationId: auth.authorizationId.value, - accountSequence: Number(auth.userId.accountSequence), - communityName: auth.displayTitle, - userId: auth.userId.value, - isHeadquarters: false, - })), - hasParentCommunity: parentCommunityAuth !== null, - childCommunityCount: childCommunityAuths.length, - } - } - - /** - * 查找用户推荐链中最近的社区授权用户 - * 用于 reward-service 分配社区权益 - * @returns accountSequence of nearest community authorization holder, or null - */ - async findNearestAuthorizedCommunity(accountSequence: number): Promise { - this.logger.debug(`[findNearestAuthorizedCommunity] accountSequence=${accountSequence}`) - - // 获取用户的祖先链(推荐链) - const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence)) - - if (ancestorAccountSequences.length === 0) { - return null - } - - // 在祖先链中找最近的有社区授权的用户 - const ancestorCommunities = await this.authorizationRepository.findActiveCommunityByAccountSequences( - ancestorAccountSequences.map((seq) => BigInt(seq)), - ) - - if (ancestorCommunities.length === 0) { - return null - } - - // 按祖先链顺序找第一个匹配的 - for (const ancestorSeq of ancestorAccountSequences) { - const found = ancestorCommunities.find( - (auth) => Number(auth.userId.accountSequence) === ancestorSeq, - ) - if (found) { - return found.userId.accountSequence - } - } - - return null - } - - /** - * 查找用户推荐链中最近的省公司授权用户(匹配指定省份) - * 用于 reward-service 分配省团队权益 - * @returns accountSequence of nearest province authorization holder, or null - */ - async findNearestAuthorizedProvince( - accountSequence: number, - provinceCode: string, - ): Promise { - this.logger.debug( - `[findNearestAuthorizedProvince] accountSequence=${accountSequence}, provinceCode=${provinceCode}`, - ) - - // 获取用户的祖先链(推荐链) - const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence)) - - if (ancestorAccountSequences.length === 0) { - return null - } - - // 在祖先链中找最近的有省公司授权且匹配省份代码的用户 - const ancestorProvinces = await this.authorizationRepository.findActiveProvinceByAccountSequencesAndRegion( - ancestorAccountSequences.map((seq) => BigInt(seq)), - provinceCode, - ) - - if (ancestorProvinces.length === 0) { - return null - } - - // 按祖先链顺序找第一个匹配的 - for (const ancestorSeq of ancestorAccountSequences) { - const found = ancestorProvinces.find( - (auth) => Number(auth.userId.accountSequence) === ancestorSeq, - ) - if (found) { - return found.userId.accountSequence - } - } - - return null - } - - /** - * 查找用户推荐链中最近的市公司授权用户(匹配指定城市) - * 用于 reward-service 分配市团队权益 - * @returns accountSequence of nearest city authorization holder, or null - */ - async findNearestAuthorizedCity( - accountSequence: number, - cityCode: string, - ): Promise { - this.logger.debug( - `[findNearestAuthorizedCity] accountSequence=${accountSequence}, cityCode=${cityCode}`, - ) - - // 获取用户的祖先链(推荐链) - const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence)) - - if (ancestorAccountSequences.length === 0) { - return null - } - - // 在祖先链中找最近的有市公司授权且匹配城市代码的用户 - const ancestorCities = await this.authorizationRepository.findActiveCityByAccountSequencesAndRegion( - ancestorAccountSequences.map((seq) => BigInt(seq)), - cityCode, - ) - - if (ancestorCities.length === 0) { - return null - } - - // 按祖先链顺序找第一个匹配的 - for (const ancestorSeq of ancestorAccountSequences) { - const found = ancestorCities.find( - (auth) => Number(auth.userId.accountSequence) === ancestorSeq, - ) - if (found) { - return found.userId.accountSequence - } - } - - return null - } - - /** - * 尝试激活授权权益 - * 仅当权益未激活时执行激活操作 - */ - private async tryActivateBenefit(authorization: AuthorizationRole): Promise { - if (authorization.benefitActive) { - return // 已激活,无需操作 - } - - this.logger.log( - `[tryActivateBenefit] Activating benefit for authorization ${authorization.authorizationId.value}, ` + - `role=${authorization.roleType}, accountSequence=${authorization.userId.accountSequence}`, - ) - - authorization.activateBenefit() - await this.authorizationRepository.save(authorization) - await this.eventPublisher.publishAll(authorization.domainEvents) - authorization.clearDomainEvents() - } - - /** - * 获取社区权益分配方案 - * 根据考核规则计算每棵树的社区权益应该分配给谁 - * - * 规则: - * 1. 找到认种用户推荐链上最近的社区 - * 2. 如果该社区 benefitActive=true,全部权益给该社区 - * 3. 如果该社区 benefitActive=false: - * - 计算该社区还需要多少棵才能达到初始考核(10棵) - * - 考核前的部分给上级社区或总部 - * - 考核后的部分给该社区(同时激活权益) - * 4. 如果没有社区,全部给总部 - */ - async getCommunityRewardDistribution( - accountSequence: number, - treeCount: number, - ): Promise<{ - distributions: Array<{ - accountSequence: number - treeCount: number - reason: string - }> - }> { - this.logger.debug( - `[getCommunityRewardDistribution] accountSequence=${accountSequence}, treeCount=${treeCount}`, - ) - - const HEADQUARTERS_ACCOUNT_SEQUENCE = 1 // 总部社区账号 - - // 1. 获取用户的祖先链(推荐链) - const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence)) - - if (ancestorAccountSequences.length === 0) { - // 无推荐链,全部给总部 - return { - distributions: [ - { - accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, - treeCount, - reason: '无推荐链,进总部社区', - }, - ], - } - } - - // 2. 查找祖先链中所有社区授权(包括 benefitActive=false 的) - const ancestorCommunities = await this.authorizationRepository.findCommunityByAccountSequences( - ancestorAccountSequences.map((seq) => BigInt(seq)), - ) - - if (ancestorCommunities.length === 0) { - // 推荐链上没有社区,全部给总部 - return { - distributions: [ - { - accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, - treeCount, - reason: '推荐链上无社区授权,进总部社区', - }, - ], - } - } - - // 3. 按祖先链顺序找最近的社区 - let nearestCommunity: typeof ancestorCommunities[0] | null = null - let nearestCommunityIndex = -1 - - for (let i = 0; i < ancestorAccountSequences.length; i++) { - const ancestorSeq = ancestorAccountSequences[i] - const found = ancestorCommunities.find( - (auth) => Number(auth.userId.accountSequence) === ancestorSeq, - ) - if (found) { - nearestCommunity = found - nearestCommunityIndex = i - break - } - } - - if (!nearestCommunity) { - // 这种情况理论上不应该发生,但作为兜底 - return { - distributions: [ - { - accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, - treeCount, - reason: '未找到匹配的社区,进总部社区', - }, - ], - } - } - - // 4. 检查最近社区的权益状态 - if (nearestCommunity.benefitActive) { - // 权益已激活,全部给该社区 - return { - distributions: [ - { - accountSequence: Number(nearestCommunity.userId.accountSequence), - treeCount, - reason: '社区权益已激活', - }, - ], - } - } - - // 5. 权益未激活,需要计算考核分配 - // 获取该社区的团队统计数据 - 使用下级团队认种数(不含自己) - const communityStats = await this.statsRepository.findByAccountSequence( - nearestCommunity.userId.accountSequence, - ) - const rawSubordinateCount = communityStats?.subordinateTeamPlantingCount ?? 0 - - // 重要:由于 referral-service 和 reward-service 都消费同一个 Kafka 事件, - // 存在竞态条件,此时查询到的 subordinateTeamPlantingCount 可能已经包含了本次认种。 - // 因此需要减去本次认种数量来还原"认种前"的下级团队数。 - // 注意:如果 referral-service 还没处理完,rawSubordinateCount 可能还是旧值, - // 此时 currentTeamCount 可能为负数,需要取 max(0, ...) - const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) - const initialTarget = nearestCommunity.getInitialTarget() // 社区初始考核目标:10棵 - - this.logger.debug( - `[getCommunityRewardDistribution] Community ${nearestCommunity.userId.accountSequence} ` + - `benefitActive=false, rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, ` + - `currentTeamCount(before)=${currentTeamCount}, initialTarget=${initialTarget}`, - ) - - // 6. 查找上级社区(用于接收考核前的权益) - let parentCommunityAccountSequence: number = HEADQUARTERS_ACCOUNT_SEQUENCE - let parentCommunityReason = '上级为总部社区' - - // 从最近社区之后继续查找上级社区 - for (let i = nearestCommunityIndex + 1; i < ancestorAccountSequences.length; i++) { - const ancestorSeq = ancestorAccountSequences[i] - const found = ancestorCommunities.find( - (auth) => Number(auth.userId.accountSequence) === ancestorSeq && auth.benefitActive, - ) - if (found) { - parentCommunityAccountSequence = Number(found.userId.accountSequence) - parentCommunityReason = '上级社区权益已激活' - break - } - } - - // 7. 计算分配方案 - const distributions: Array<{ - accountSequence: number - treeCount: number - reason: string - }> = [] - - if (currentTeamCount >= initialTarget) { - // 已达标但权益未激活(可能是月度考核失败),全部给该社区 - // 注:这种情况下应该由系统自动激活权益,但这里作为兜底处理 - distributions.push({ - accountSequence: Number(nearestCommunity.userId.accountSequence), - treeCount, - reason: '已达初始考核目标', - }) - - // 自动激活权益 - await this.tryActivateBenefit(nearestCommunity) - } else { - // 未达标,需要拆分 - // remaining: 还差多少棵达标(不包括达标那一棵本身) - // 例如:目标10棵,当前0棵 -> remaining = 9(前9棵给上级,第10棵给自己) - const remaining = Math.max(0, initialTarget - currentTeamCount - 1) - const afterPlantingCount = currentTeamCount + treeCount // 本次认种后的总数 - - if (afterPlantingCount < initialTarget) { - // 本次认种后仍未达标,全部给上级/总部 - distributions.push({ - accountSequence: parentCommunityAccountSequence, - treeCount, - reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),${parentCommunityReason}`, - }) - } else { - // 本次认种达到或跨越考核达标点 (afterPlantingCount >= initialTarget) - // 考核前的部分给上级/总部(remaining 可能为0,此时不添加分配记录) - if (remaining > 0) { - distributions.push({ - accountSequence: parentCommunityAccountSequence, - treeCount: remaining, - reason: `初始考核(${currentTeamCount}+${remaining}=${currentTeamCount + remaining}/${initialTarget}),${parentCommunityReason}`, - }) - } - // 达标的那一棵 + 之后多出来的,全部给该社区 - const afterTargetCount = treeCount - remaining - if (afterTargetCount > 0) { - distributions.push({ - accountSequence: Number(nearestCommunity.userId.accountSequence), - treeCount: afterTargetCount, - reason: `考核达标后权益生效`, - }) - } - - // 自动激活权益(本次认种使其达标) - await this.tryActivateBenefit(nearestCommunity) - } - } - - this.logger.debug( - `[getCommunityRewardDistribution] Result: ${JSON.stringify(distributions)}`, - ) - - return { distributions } - } - - /** - * 获取省团队权益分配方案 (20 USDT) - * - * 规则: - * 1. 找到认种用户推荐链上最近的授权省公司(AUTH_PROVINCE_COMPANY) - * 2. 如果该授权省公司 benefitActive=true,全部权益给该用户 - * 3. 如果该授权省公司 benefitActive=false: - * - 计算还需要多少棵才能达到初始考核(500棵) - * - 考核前的部分给上级/总部 - * - 考核后的部分给该用户 - * 4. 如果没有授权省公司,全部给总部 - */ - async getProvinceTeamRewardDistribution( - accountSequence: number, - provinceCode: string, - treeCount: number, - ): Promise<{ - distributions: Array<{ - accountSequence: number - treeCount: number - reason: string - }> - }> { - this.logger.debug( - `[getProvinceTeamRewardDistribution] accountSequence=${accountSequence}, provinceCode=${provinceCode}, treeCount=${treeCount}`, - ) - - const HEADQUARTERS_ACCOUNT_SEQUENCE = 1 - - // 1. 获取用户的祖先链 - const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence)) - - if (ancestorAccountSequences.length === 0) { - return { - distributions: [ - { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '无推荐链,进总部社区' }, - ], - } - } - - // 2. 查找祖先链中所有授权省公司(包括 benefitActive=false) - // 注意:省团队收益不再要求省份匹配,只要推荐链上有省团队授权即可获得收益 - const ancestorAuthProvinces = await this.authorizationRepository.findAuthProvinceByAccountSequences( - ancestorAccountSequences.map((seq) => BigInt(seq)), - ) - - if (ancestorAuthProvinces.length === 0) { - return { - distributions: [ - { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '推荐链上无授权省公司,进总部社区' }, - ], - } - } - - // 3. 按祖先链顺序找最近的授权省公司 - let nearestAuthProvince: typeof ancestorAuthProvinces[0] | null = null - let nearestIndex = -1 - - for (let i = 0; i < ancestorAccountSequences.length; i++) { - const ancestorSeq = ancestorAccountSequences[i] - const found = ancestorAuthProvinces.find( - (auth) => Number(auth.userId.accountSequence) === ancestorSeq, - ) - if (found) { - nearestAuthProvince = found - nearestIndex = i - break - } - } - - if (!nearestAuthProvince) { - return { - distributions: [ - { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '未找到匹配的授权省公司,进总部社区' }, - ], - } - } - - // 4. 检查权益状态 - if (nearestAuthProvince.benefitActive) { - return { - distributions: [ - { accountSequence: Number(nearestAuthProvince.userId.accountSequence), treeCount, reason: '省团队权益已激活' }, - ], - } - } - - // 5. 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) - const stats = await this.statsRepository.findByAccountSequence(nearestAuthProvince.userId.accountSequence) - const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 - // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 - const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) - const initialTarget = nearestAuthProvince.getInitialTarget() // 500棵 - - this.logger.debug( - `[getProvinceTeamRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}`, - ) - - // 6. 查找上级(用于接收考核前的权益) - let parentAccountSequence: number = HEADQUARTERS_ACCOUNT_SEQUENCE - let parentReason = '上级为总部社区' - - for (let i = nearestIndex + 1; i < ancestorAccountSequences.length; i++) { - const ancestorSeq = ancestorAccountSequences[i] - const found = ancestorAuthProvinces.find( - (auth) => Number(auth.userId.accountSequence) === ancestorSeq && auth.benefitActive, - ) - if (found) { - parentAccountSequence = Number(found.userId.accountSequence) - parentReason = '上级授权省公司权益已激活' - break - } - } - - // 7. 计算分配 - const distributions: Array<{ accountSequence: number; treeCount: number; reason: string }> = [] - - if (currentTeamCount >= initialTarget) { - distributions.push({ - accountSequence: Number(nearestAuthProvince.userId.accountSequence), - treeCount, - reason: '已达初始考核目标', - }) - // 自动激活权益 - await this.tryActivateBenefit(nearestAuthProvince) - } else { - // remaining: 还差多少棵达标(不包括达标那一棵本身) - const remaining = Math.max(0, initialTarget - currentTeamCount - 1) - const afterPlantingCount = currentTeamCount + treeCount - - if (afterPlantingCount < initialTarget) { - // 本次认种后仍未达标 - distributions.push({ - accountSequence: parentAccountSequence, - treeCount, - reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),${parentReason}`, - }) - } else { - // 本次认种达到或跨越达标点 (afterPlantingCount >= initialTarget) - // 考核前的部分给上级/总部(remaining 可能为0,此时不添加分配记录) - if (remaining > 0) { - distributions.push({ - accountSequence: parentAccountSequence, - treeCount: remaining, - reason: `初始考核(${currentTeamCount}+${remaining}=${currentTeamCount + remaining}/${initialTarget}),${parentReason}`, - }) - } - // 达标的那一棵 + 之后多出来的,全部给该省团队 - const afterTargetCount = treeCount - remaining - if (afterTargetCount > 0) { - distributions.push({ - accountSequence: Number(nearestAuthProvince.userId.accountSequence), - treeCount: afterTargetCount, - reason: '考核达标后权益生效', - }) - } - // 自动激活权益(本次认种使其达标) - await this.tryActivateBenefit(nearestAuthProvince) - } - } - - this.logger.debug(`[getProvinceTeamRewardDistribution] Result: ${JSON.stringify(distributions)}`) - return { distributions } - } - - /** - * 获取省区域权益分配方案 (15 USDT + 1%算力) - * - * 规则: - * 1. 查找该省份是否有正式省公司(PROVINCE_COMPANY) - * 2. 如果有且 benefitActive=true,权益进该省公司自己的账户 - * 3. 如果有但 benefitActive=false(考核中): - * - 计算还需要多少棵才能达到初始考核(5万棵) - * - 考核前的部分进系统省账户 - * - 考核后的部分给该省公司 - * 4. 如果没有正式省公司,全部进系统省账户 - */ - async getProvinceAreaRewardDistribution( - provinceCode: string, - treeCount: number, - ): Promise<{ - distributions: Array<{ - accountSequence: number - treeCount: number - reason: string - isSystemAccount: boolean - }> - }> { - this.logger.debug( - `[getProvinceAreaRewardDistribution] provinceCode=${provinceCode}, treeCount=${treeCount}`, - ) - - // 系统省账户ID格式: 9 + 省份代码 - const systemProvinceAccountId = Number(`9${provinceCode.padStart(6, '0')}`) - - // 查找该省份的正式省公司 - const provinceCompany = await this.authorizationRepository.findProvinceCompanyByRegion(provinceCode) - - if (!provinceCompany) { - // 无正式省公司,全部进系统省账户 - return { - distributions: [ - { - accountSequence: systemProvinceAccountId, - treeCount, - reason: '无正式省公司授权,进系统省账户', - isSystemAccount: true, - }, - ], - } - } - - if (provinceCompany.benefitActive) { - // 正式省公司权益已激活,进该省公司账户 - return { - distributions: [ - { - accountSequence: Number(provinceCompany.userId.accountSequence), - treeCount, - reason: '省区域权益已激活', - isSystemAccount: false, - }, - ], - } - } - - // 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) - const stats = await this.statsRepository.findByAccountSequence(provinceCompany.userId.accountSequence) - const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 - // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 - const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) - const initialTarget = provinceCompany.getInitialTarget() // 5万棵 - - this.logger.debug( - `[getProvinceAreaRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}, initialTarget=${initialTarget}`, - ) - - const distributions: Array<{ - accountSequence: number - treeCount: number - reason: string - isSystemAccount: boolean - }> = [] - - if (currentTeamCount >= initialTarget) { - // 已达标但权益未激活,全部给该省公司 - distributions.push({ - accountSequence: Number(provinceCompany.userId.accountSequence), - treeCount, - reason: '已达初始考核目标', - isSystemAccount: false, - }) - // 自动激活权益 - await this.tryActivateBenefit(provinceCompany) - } else { - // remaining: 还差多少棵达标(不包括达标那一棵本身) - const remaining = Math.max(0, initialTarget - currentTeamCount - 1) - const afterPlantingCount = currentTeamCount + treeCount - - if (afterPlantingCount < initialTarget) { - // 本次认种后仍未达标,全部进系统省账户 - distributions.push({ - accountSequence: systemProvinceAccountId, - treeCount, - reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),进系统省账户`, - isSystemAccount: true, - }) - } else { - // 本次认种达到或跨越考核达标点 (afterPlantingCount >= initialTarget) - // 考核前的部分进系统省账户(remaining 可能为0,此时不添加分配记录) - if (remaining > 0) { - distributions.push({ - accountSequence: systemProvinceAccountId, - treeCount: remaining, - reason: `初始考核(${currentTeamCount}+${remaining}=${currentTeamCount + remaining}/${initialTarget}),进系统省账户`, - isSystemAccount: true, - }) - } - // 达标的那一棵 + 之后多出来的,全部给该省公司 - const afterTargetCount = treeCount - remaining - if (afterTargetCount > 0) { - distributions.push({ - accountSequence: Number(provinceCompany.userId.accountSequence), - treeCount: afterTargetCount, - reason: '考核达标后权益生效', - isSystemAccount: false, - }) - } - // 自动激活权益 - await this.tryActivateBenefit(provinceCompany) - } - } - - this.logger.debug(`[getProvinceAreaRewardDistribution] Result: ${JSON.stringify(distributions)}`) - return { distributions } - } - - /** - * 获取市团队权益分配方案 (40 USDT) - * - * 规则: - * 1. 找到认种用户推荐链上最近的授权市公司(AUTH_CITY_COMPANY) - * 2. 如果该授权市公司 benefitActive=true,全部权益给该用户 - * 3. 如果该授权市公司 benefitActive=false: - * - 计算还需要多少棵才能达到初始考核(100棵) - * - 考核前的部分给上级/总部 - * - 考核后的部分给该用户 - * 4. 如果没有授权市公司,全部给总部 - */ - async getCityTeamRewardDistribution( - accountSequence: number, - cityCode: string, - treeCount: number, - ): Promise<{ - distributions: Array<{ - accountSequence: number - treeCount: number - reason: string - }> - }> { - this.logger.debug( - `[getCityTeamRewardDistribution] accountSequence=${accountSequence}, cityCode=${cityCode}, treeCount=${treeCount}`, - ) - - const HEADQUARTERS_ACCOUNT_SEQUENCE = 1 - - // 1. 获取用户的祖先链 - const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(BigInt(accountSequence)) - - if (ancestorAccountSequences.length === 0) { - return { - distributions: [ - { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '无推荐链,进总部社区' }, - ], - } - } - - // 2. 查找祖先链中所有授权市公司(包括 benefitActive=false) - // 注意:市团队收益不再要求城市匹配,只要推荐链上有市团队授权即可获得收益 - const ancestorAuthCities = await this.authorizationRepository.findAuthCityByAccountSequences( - ancestorAccountSequences.map((seq) => BigInt(seq)), - ) - - if (ancestorAuthCities.length === 0) { - return { - distributions: [ - { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '推荐链上无授权市公司,进总部社区' }, - ], - } - } - - // 3. 按祖先链顺序找最近的授权市公司 - let nearestAuthCity: typeof ancestorAuthCities[0] | null = null - let nearestIndex = -1 - - for (let i = 0; i < ancestorAccountSequences.length; i++) { - const ancestorSeq = ancestorAccountSequences[i] - const found = ancestorAuthCities.find( - (auth) => Number(auth.userId.accountSequence) === ancestorSeq, - ) - if (found) { - nearestAuthCity = found - nearestIndex = i - break - } - } - - if (!nearestAuthCity) { - return { - distributions: [ - { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '未找到匹配的授权市公司,进总部社区' }, - ], - } - } - - // 4. 检查权益状态 - if (nearestAuthCity.benefitActive) { - return { - distributions: [ - { accountSequence: Number(nearestAuthCity.userId.accountSequence), treeCount, reason: '市团队权益已激活' }, - ], - } - } - - // 5. 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) - const stats = await this.statsRepository.findByAccountSequence(nearestAuthCity.userId.accountSequence) - const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 - // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 - const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) - const initialTarget = nearestAuthCity.getInitialTarget() // 100棵 - - this.logger.debug( - `[getCityTeamRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}`, - ) - - // 6. 查找上级 - let parentAccountSequence: number = HEADQUARTERS_ACCOUNT_SEQUENCE - let parentReason = '上级为总部社区' - - for (let i = nearestIndex + 1; i < ancestorAccountSequences.length; i++) { - const ancestorSeq = ancestorAccountSequences[i] - const found = ancestorAuthCities.find( - (auth) => Number(auth.userId.accountSequence) === ancestorSeq && auth.benefitActive, - ) - if (found) { - parentAccountSequence = Number(found.userId.accountSequence) - parentReason = '上级授权市公司权益已激活' - break - } - } - - // 7. 计算分配 - const distributions: Array<{ accountSequence: number; treeCount: number; reason: string }> = [] - - if (currentTeamCount >= initialTarget) { - distributions.push({ - accountSequence: Number(nearestAuthCity.userId.accountSequence), - treeCount, - reason: '已达初始考核目标', - }) - // 自动激活权益 - await this.tryActivateBenefit(nearestAuthCity) - } else { - // remaining: 还差多少棵达标(不包括达标那一棵本身) - const remaining = Math.max(0, initialTarget - currentTeamCount - 1) - const afterPlantingCount = currentTeamCount + treeCount - - if (afterPlantingCount < initialTarget) { - // 本次认种后仍未达标 - distributions.push({ - accountSequence: parentAccountSequence, - treeCount, - reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),${parentReason}`, - }) - } else { - // 本次认种达到或跨越达标点 (afterPlantingCount >= initialTarget) - // 考核前的部分给上级/总部(remaining 可能为0,此时不添加分配记录) - if (remaining > 0) { - distributions.push({ - accountSequence: parentAccountSequence, - treeCount: remaining, - reason: `初始考核(${currentTeamCount}+${remaining}=${currentTeamCount + remaining}/${initialTarget}),${parentReason}`, - }) - } - // 达标的那一棵 + 之后多出来的,全部给该市团队 - const afterTargetCount = treeCount - remaining - if (afterTargetCount > 0) { - distributions.push({ - accountSequence: Number(nearestAuthCity.userId.accountSequence), - treeCount: afterTargetCount, - reason: '考核达标后权益生效', - }) - } - // 自动激活权益(本次认种使其达标) - await this.tryActivateBenefit(nearestAuthCity) - } - } - - this.logger.debug(`[getCityTeamRewardDistribution] Result: ${JSON.stringify(distributions)}`) - return { distributions } - } - - /** - * 获取市区域权益分配方案 (35 USDT + 2%算力) - * - * 规则: - * 1. 查找该城市是否有正式市公司(CITY_COMPANY) - * 2. 如果有且 benefitActive=true,权益进该市公司自己的账户 - * 3. 如果有但 benefitActive=false(考核中): - * - 计算还需要多少棵才能达到初始考核(1万棵) - * - 考核前的部分进系统市账户 - * - 考核后的部分给该市公司 - * 4. 如果没有正式市公司,全部进系统市账户 - */ - async getCityAreaRewardDistribution( - cityCode: string, - treeCount: number, - ): Promise<{ - distributions: Array<{ - accountSequence: number - treeCount: number - reason: string - isSystemAccount: boolean - }> - }> { - this.logger.debug( - `[getCityAreaRewardDistribution] cityCode=${cityCode}, treeCount=${treeCount}`, - ) - - // 系统市账户ID格式: 8 + 城市代码 - const systemCityAccountId = Number(`8${cityCode.padStart(6, '0')}`) - - // 查找该城市的正式市公司 - const cityCompany = await this.authorizationRepository.findCityCompanyByRegion(cityCode) - - if (!cityCompany) { - // 无正式市公司,全部进系统市账户 - return { - distributions: [ - { - accountSequence: systemCityAccountId, - treeCount, - reason: '无正式市公司授权,进系统市账户', - isSystemAccount: true, - }, - ], - } - } - - if (cityCompany.benefitActive) { - // 正式市公司权益已激活,进该市公司账户 - return { - distributions: [ - { - accountSequence: Number(cityCompany.userId.accountSequence), - treeCount, - reason: '市区域权益已激活', - isSystemAccount: false, - }, - ], - } - } - - // 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) - const stats = await this.statsRepository.findByAccountSequence(cityCompany.userId.accountSequence) - const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 - // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 - const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) - const initialTarget = cityCompany.getInitialTarget() // 1万棵 - - this.logger.debug( - `[getCityAreaRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}, initialTarget=${initialTarget}`, - ) - - const distributions: Array<{ - accountSequence: number - treeCount: number - reason: string - isSystemAccount: boolean - }> = [] - - if (currentTeamCount >= initialTarget) { - // 已达标但权益未激活,全部给该市公司 - distributions.push({ - accountSequence: Number(cityCompany.userId.accountSequence), - treeCount, - reason: '已达初始考核目标', - isSystemAccount: false, - }) - // 自动激活权益 - await this.tryActivateBenefit(cityCompany) - } else { - // remaining: 还差多少棵达标(不包括达标那一棵本身) - const remaining = Math.max(0, initialTarget - currentTeamCount - 1) - const afterPlantingCount = currentTeamCount + treeCount - - if (afterPlantingCount < initialTarget) { - // 本次认种后仍未达标,全部进系统市账户 - distributions.push({ - accountSequence: systemCityAccountId, - treeCount, - reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),进系统市账户`, - isSystemAccount: true, - }) - } else { - // 本次认种达到或跨越考核达标点 (afterPlantingCount >= initialTarget) - // 考核前的部分进系统市账户(remaining 可能为0,此时不添加分配记录) - if (remaining > 0) { - distributions.push({ - accountSequence: systemCityAccountId, - treeCount: remaining, - reason: `初始考核(${currentTeamCount}+${remaining}=${currentTeamCount + remaining}/${initialTarget}),进系统市账户`, - isSystemAccount: true, - }) - } - // 达标的那一棵 + 之后多出来的,全部给该市公司 - const afterTargetCount = treeCount - remaining - if (afterTargetCount > 0) { - distributions.push({ - accountSequence: Number(cityCompany.userId.accountSequence), - treeCount: afterTargetCount, - reason: '考核达标后权益生效', - isSystemAccount: false, - }) - } - // 自动激活权益 - await this.tryActivateBenefit(cityCompany) - } - } - - this.logger.debug(`[getCityAreaRewardDistribution] Result: ${JSON.stringify(distributions)}`) - return { distributions } - } -} +import { Injectable, Inject, Logger } from '@nestjs/common' +import { AuthorizationRole, MonthlyAssessment } from '@/domain/aggregates' +import { LadderTargetRule } from '@/domain/entities' +import { + UserId, + AdminUserId, + RegionCode, + AuthorizationId, + Month, +} from '@/domain/value-objects' +import { RoleType, AuthorizationStatus } from '@/domain/enums' +import { + IAuthorizationRoleRepository, + AUTHORIZATION_ROLE_REPOSITORY, + IMonthlyAssessmentRepository, + MONTHLY_ASSESSMENT_REPOSITORY, +} from '@/domain/repositories' +import { + AuthorizationValidatorService, + IReferralRepository, + ITeamStatisticsRepository, + TeamStatistics, +} from '@/domain/services' +import { EventPublisherService } from '@/infrastructure/kafka' +import { ReferralServiceClient } from '@/infrastructure/external' +import { ApplicationError, NotFoundError } from '@/shared/exceptions' +import { + ApplyCommunityAuthCommand, + ApplyCommunityAuthResult, + ApplyAuthProvinceCompanyCommand, + ApplyAuthProvinceCompanyResult, + ApplyAuthCityCompanyCommand, + ApplyAuthCityCompanyResult, + GrantCommunityCommand, + GrantProvinceCompanyCommand, + GrantCityCompanyCommand, + GrantAuthProvinceCompanyCommand, + GrantAuthCityCompanyCommand, + RevokeAuthorizationCommand, + GrantMonthlyBypassCommand, + ExemptLocalPercentageCheckCommand, +} from '@/application/commands' +import { AuthorizationDTO, StickmanRankingDTO, CommunityHierarchyDTO } from '@/application/dto' + +export const REFERRAL_REPOSITORY = Symbol('IReferralRepository') +export const TEAM_STATISTICS_REPOSITORY = Symbol('ITeamStatisticsRepository') + +@Injectable() +export class AuthorizationApplicationService { + private readonly logger = new Logger(AuthorizationApplicationService.name) + private readonly validatorService = new AuthorizationValidatorService() + + constructor( + @Inject(AUTHORIZATION_ROLE_REPOSITORY) + private readonly authorizationRepository: IAuthorizationRoleRepository, + @Inject(MONTHLY_ASSESSMENT_REPOSITORY) + private readonly assessmentRepository: IMonthlyAssessmentRepository, + @Inject(REFERRAL_REPOSITORY) + private readonly referralRepository: IReferralRepository, + @Inject(TEAM_STATISTICS_REPOSITORY) + private readonly statsRepository: ITeamStatisticsRepository, + private readonly eventPublisher: EventPublisherService, + private readonly referralServiceClient: ReferralServiceClient, + ) {} + + /** + * 申请社区授权 + */ + async applyCommunityAuth( + command: ApplyCommunityAuthCommand, + ): Promise { + const userId = UserId.create(command.userId, command.accountSequence) + + // 1. 检查是否已有社区授权 + const existing = await this.authorizationRepository.findByAccountSequenceAndRoleType( + userId.accountSequence, + RoleType.COMMUNITY, + ) + + if (existing && existing.status !== AuthorizationStatus.REVOKED) { + throw new ApplicationError('您已申请过社区授权') + } + + // 2. 创建社区授权 + const authorization = AuthorizationRole.createCommunityAuth({ + userId, + communityName: command.communityName, + }) + + // 3. 检查初始考核(10棵)- 使用下级团队认种数(不含自己) + const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) + const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 + + if (subordinateTreeCount >= authorization.getInitialTarget()) { + // 达标,激活权益 + authorization.activateBenefit() + } + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + + return { + authorizationId: authorization.authorizationId.value, + status: authorization.status, + benefitActive: authorization.benefitActive, + message: authorization.benefitActive + ? '社区权益已激活' + : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, + currentTreeCount: subordinateTreeCount, + requiredTreeCount: authorization.getInitialTarget(), + } + } + + /** + * 申请授权省公司 + */ + async applyAuthProvinceCompany( + command: ApplyAuthProvinceCompanyCommand, + ): Promise { + const userId = UserId.create(command.userId, command.accountSequence) + const regionCode = RegionCode.create(command.provinceCode) + + // 1. 验证授权申请(团队内唯一性) + const validation = await this.validatorService.validateAuthorizationRequest( + userId, + RoleType.AUTH_PROVINCE_COMPANY, + regionCode, + this.referralRepository, + this.authorizationRepository, + ) + + if (!validation.isValid) { + throw new ApplicationError(validation.errorMessage!) + } + + // 2. 创建授权 + const authorization = AuthorizationRole.createAuthProvinceCompany({ + userId, + provinceCode: command.provinceCode, + provinceName: command.provinceName, + }) + + // 3. 检查初始考核(500棵)- 使用下级团队认种数(不含自己) + const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) + const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 + + if (subordinateTreeCount >= authorization.getInitialTarget()) { + // 达标,激活权益并创建首月考核 + authorization.activateBenefit() + await this.createInitialAssessment(authorization, teamStats!) + } + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + + return { + authorizationId: authorization.authorizationId.value, + status: authorization.status, + benefitActive: authorization.benefitActive, + displayTitle: authorization.displayTitle, + message: authorization.benefitActive + ? '授权省公司权益已激活,开始阶梯考核' + : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, + currentTreeCount: subordinateTreeCount, + requiredTreeCount: authorization.getInitialTarget(), + } + } + + /** + * 申请授权市公司 + */ + async applyAuthCityCompany( + command: ApplyAuthCityCompanyCommand, + ): Promise { + const userId = UserId.create(command.userId, command.accountSequence) + const regionCode = RegionCode.create(command.cityCode) + + // 1. 验证 + const validation = await this.validatorService.validateAuthorizationRequest( + userId, + RoleType.AUTH_CITY_COMPANY, + regionCode, + this.referralRepository, + this.authorizationRepository, + ) + + if (!validation.isValid) { + throw new ApplicationError(validation.errorMessage!) + } + + // 2. 创建授权 + const authorization = AuthorizationRole.createAuthCityCompany({ + userId, + cityCode: command.cityCode, + cityName: command.cityName, + }) + + // 3. 检查初始考核(100棵)- 使用下级团队认种数(不含自己) + const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence) + const subordinateTreeCount = teamStats?.subordinateTeamPlantingCount || 0 + + if (subordinateTreeCount >= authorization.getInitialTarget()) { + authorization.activateBenefit() + await this.createInitialAssessment(authorization, teamStats!) + } + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + + return { + authorizationId: authorization.authorizationId.value, + status: authorization.status, + benefitActive: authorization.benefitActive, + displayTitle: authorization.displayTitle, + message: authorization.benefitActive + ? '授权市公司权益已激活,开始阶梯考核' + : `需要下级团队累计认种达到${authorization.getInitialTarget()}棵才能激活`, + currentTreeCount: subordinateTreeCount, + requiredTreeCount: authorization.getInitialTarget(), + } + } + + /** + * 管理员直接授权社区 + */ + async grantCommunity(command: GrantCommunityCommand): Promise { + const userId = UserId.create(command.userId, command.accountSequence) + const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) + + const authorization = AuthorizationRole.createCommunity({ + userId, + communityName: command.communityName, + adminId, + skipAssessment: command.skipAssessment, + }) + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + } + + /** + * 管理员授权正式省公司(省区域) + * 需要验证团队内唯一性:同一推荐链上不能有重复的相同省份授权 + */ + async grantProvinceCompany(command: GrantProvinceCompanyCommand): Promise { + const userId = UserId.create(command.userId, command.accountSequence) + const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) + const regionCode = RegionCode.create(command.provinceCode) + + // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同省份授权) + const validation = await this.validatorService.validateAuthorizationRequest( + userId, + RoleType.PROVINCE_COMPANY, + regionCode, + this.referralRepository, + this.authorizationRepository, + ) + + if (!validation.isValid) { + throw new ApplicationError(validation.errorMessage!) + } + + // 2. 创建授权 + const authorization = AuthorizationRole.createProvinceCompany({ + userId, + provinceCode: command.provinceCode, + provinceName: command.provinceName, + adminId, + skipAssessment: command.skipAssessment, + }) + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + } + + /** + * 管理员授权正式市公司(市区域) + * 需要验证团队内唯一性:同一推荐链上不能有重复的相同城市授权 + */ + async grantCityCompany(command: GrantCityCompanyCommand): Promise { + const userId = UserId.create(command.userId, command.accountSequence) + const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) + const regionCode = RegionCode.create(command.cityCode) + + // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同城市授权) + const validation = await this.validatorService.validateAuthorizationRequest( + userId, + RoleType.CITY_COMPANY, + regionCode, + this.referralRepository, + this.authorizationRepository, + ) + + if (!validation.isValid) { + throw new ApplicationError(validation.errorMessage!) + } + + // 2. 创建授权 + const authorization = AuthorizationRole.createCityCompany({ + userId, + cityCode: command.cityCode, + cityName: command.cityName, + adminId, + skipAssessment: command.skipAssessment, + }) + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + } + + /** + * 管理员授权授权省公司(省团队) + * Admin直接授权,跳过用户申请流程 + * 需要验证团队内唯一性:同一推荐链上不能有重复的相同省份授权 + */ + async grantAuthProvinceCompany(command: GrantAuthProvinceCompanyCommand): Promise { + const userId = UserId.create(command.userId, command.accountSequence) + const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) + const regionCode = RegionCode.create(command.provinceCode) + + // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同省份授权) + const validation = await this.validatorService.validateAuthorizationRequest( + userId, + RoleType.AUTH_PROVINCE_COMPANY, + regionCode, + this.referralRepository, + this.authorizationRepository, + ) + + if (!validation.isValid) { + throw new ApplicationError(validation.errorMessage!) + } + + // 2. 创建授权 + const authorization = AuthorizationRole.createAuthProvinceCompanyByAdmin({ + userId, + provinceCode: command.provinceCode, + provinceName: command.provinceName, + adminId, + skipAssessment: command.skipAssessment, + }) + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + } + + /** + * 管理员授权授权市公司(市团队) + * Admin直接授权,跳过用户申请流程 + * 需要验证团队内唯一性:同一推荐链上不能有重复的相同城市授权 + */ + async grantAuthCityCompany(command: GrantAuthCityCompanyCommand): Promise { + const userId = UserId.create(command.userId, command.accountSequence) + const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence) + const regionCode = RegionCode.create(command.cityCode) + + // 1. 验证团队内唯一性(同一推荐链上不能有重复的相同城市授权) + const validation = await this.validatorService.validateAuthorizationRequest( + userId, + RoleType.AUTH_CITY_COMPANY, + regionCode, + this.referralRepository, + this.authorizationRepository, + ) + + if (!validation.isValid) { + throw new ApplicationError(validation.errorMessage!) + } + + // 2. 创建授权 + const authorization = AuthorizationRole.createAuthCityCompanyByAdmin({ + userId, + cityCode: command.cityCode, + cityName: command.cityName, + adminId, + skipAssessment: command.skipAssessment, + }) + + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + } + + /** + * 撤销授权 + */ + async revokeAuthorization(command: RevokeAuthorizationCommand): Promise { + const authorization = await this.authorizationRepository.findById( + AuthorizationId.create(command.authorizationId), + ) + + if (!authorization) { + throw new NotFoundError('授权不存在') + } + + // 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) + authorization.clearDomainEvents() + } + + /** + * 授予单月豁免 + */ + async grantMonthlyBypass(command: GrantMonthlyBypassCommand): Promise { + const assessment = await this.assessmentRepository.findByAuthorizationAndMonth( + AuthorizationId.create(command.authorizationId), + Month.create(command.month), + ) + + if (!assessment) { + throw new NotFoundError('考核记录不存在') + } + + // 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) + assessment.clearDomainEvents() + } + + /** + * 豁免占比考核 + */ + async exemptLocalPercentageCheck(command: ExemptLocalPercentageCheckCommand): Promise { + const authorization = await this.authorizationRepository.findById( + AuthorizationId.create(command.authorizationId), + ) + + if (!authorization) { + throw new NotFoundError('授权不存在') + } + + // 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) + authorization.clearDomainEvents() + } + + /** + * 查询用户授权列表 + */ + async getUserAuthorizations(accountSequence: string): Promise { + const authorizations = await this.authorizationRepository.findByAccountSequence( + accountSequence, + ) + + // 查询用户团队统计数据 + const teamStats = await this.statsRepository.findByAccountSequence(accountSequence) + const currentTreeCount = teamStats?.totalTeamPlantingCount || 0 + + return authorizations.map((auth) => this.toAuthorizationDTO(auth, currentTreeCount)) + } + + /** + * 查询用户授权详情 + */ + async getAuthorizationById(authorizationId: string): Promise { + const authorization = await this.authorizationRepository.findById( + AuthorizationId.create(authorizationId), + ) + + if (!authorization) return null + + // 查询用户团队统计数据 + const teamStats = await this.statsRepository.findByAccountSequence(authorization.userId.accountSequence) + const currentTreeCount = teamStats?.totalTeamPlantingCount || 0 + + return this.toAuthorizationDTO(authorization, currentTreeCount) + } + + /** + * 查询火柴人排名数据 + */ + async getStickmanRanking( + month: string, + roleType: RoleType, + regionCode: string, + ): Promise { + const assessments = await this.assessmentRepository.findRankingsByMonthAndRegion( + Month.create(month), + roleType, + RegionCode.create(regionCode), + ) + + const rankings: StickmanRankingDTO[] = [] + const finalTarget = LadderTargetRule.getFinalTarget(roleType) + + for (const assessment of assessments) { + rankings.push({ + userId: assessment.userId.value, + authorizationId: assessment.authorizationId.value, + roleType: assessment.roleType, + regionCode: assessment.regionCode.value, + ranking: assessment.rankingInRegion || 0, + isFirstPlace: assessment.isFirstPlace, + cumulativeCompleted: assessment.cumulativeCompleted, + cumulativeTarget: assessment.cumulativeTarget, + finalTarget, + progressPercentage: (assessment.cumulativeCompleted / finalTarget) * 100, + exceedRatio: assessment.exceedRatio, + monthlyRewardUsdt: 0, // TODO: 从奖励服务获取 + monthlyRewardRwad: 0, + }) + } + + return rankings + } + + // 辅助方法 + private async createInitialAssessment( + authorization: AuthorizationRole, + teamStats: TeamStatistics, + ): Promise { + const currentMonth = Month.current() + const target = LadderTargetRule.getTarget(authorization.roleType, 1) + + const assessment = MonthlyAssessment.create({ + authorizationId: authorization.authorizationId, + userId: authorization.userId, + roleType: authorization.roleType, + regionCode: authorization.regionCode, + assessmentMonth: currentMonth, + monthIndex: 1, + monthlyTarget: target.monthlyTarget, + cumulativeTarget: target.cumulativeTarget, + }) + + // 立即评估首月 + const localTeamCount = this.getLocalTeamCount( + teamStats, + authorization.roleType, + authorization.regionCode, + ) + + assessment.assess({ + cumulativeCompleted: teamStats.totalTeamPlantingCount, + localTeamCount, + totalTeamCount: teamStats.totalTeamPlantingCount, + requireLocalPercentage: authorization.requireLocalPercentage, + exemptFromPercentageCheck: authorization.exemptFromPercentageCheck, + }) + + await this.assessmentRepository.save(assessment) + await this.eventPublisher.publishAll(assessment.domainEvents) + assessment.clearDomainEvents() + } + + private getLocalTeamCount( + teamStats: TeamStatistics, + roleType: RoleType, + regionCode: RegionCode, + ): number { + if (roleType === RoleType.AUTH_PROVINCE_COMPANY) { + return teamStats.getProvinceTeamCount(regionCode.value) + } else if (roleType === RoleType.AUTH_CITY_COMPANY) { + return teamStats.getCityTeamCount(regionCode.value) + } + return 0 + } + + private toAuthorizationDTO(auth: AuthorizationRole, currentTreeCount: number): AuthorizationDTO { + // 获取月度考核目标(社区固定10,其他类型根据阶梯规则) + let monthlyTargetTreeCount = 0 + if (auth.roleType === RoleType.COMMUNITY) { + monthlyTargetTreeCount = 10 // 社区固定每月新增10棵 + } else if (auth.benefitActive && auth.currentMonthIndex > 0) { + // 省/市公司使用阶梯目标 + const target = LadderTargetRule.getTarget(auth.roleType, auth.currentMonthIndex) + monthlyTargetTreeCount = target.monthlyTarget + } + + return { + authorizationId: auth.authorizationId.value, + userId: auth.userId.value, + roleType: auth.roleType, + regionCode: auth.regionCode.value, + regionName: auth.regionName, + status: auth.status, + displayTitle: auth.displayTitle, + benefitActive: auth.benefitActive, + currentMonthIndex: auth.currentMonthIndex, + requireLocalPercentage: auth.requireLocalPercentage, + exemptFromPercentageCheck: auth.exemptFromPercentageCheck, + // 考核进度字段 + initialTargetTreeCount: auth.getInitialTarget(), + currentTreeCount, + monthlyTargetTreeCount, + createdAt: auth.createdAt, + updatedAt: auth.updatedAt, + } + } + + /** + * 获取用户的社区层级信息 + * - myCommunity: 我的社区授权(如果有) + * - parentCommunity: 上级社区(沿推荐链往上找最近的,如果没有则返回总部社区) + * - childCommunities: 下级社区(在我的团队中找最近的社区) + */ + async getCommunityHierarchy(accountSequence: string): Promise { + this.logger.debug(`[getCommunityHierarchy] accountSequence=${accountSequence}`) + + // 1. 查询我的社区授权 + const myCommunity = await this.authorizationRepository.findByAccountSequenceAndRoleType( + accountSequence, + RoleType.COMMUNITY, + ) + + // 2. 获取我的祖先链(推荐链) + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) + this.logger.debug(`[getCommunityHierarchy] ancestorPath: ${ancestorAccountSequences.join(',')}`) + + // 3. 查找上级社区(在祖先链中找最近的有社区授权的用户) + let parentCommunityAuth: AuthorizationRole | null = null + if (ancestorAccountSequences.length > 0) { + const ancestorCommunities = await this.authorizationRepository.findActiveCommunityByAccountSequences( + ancestorAccountSequences, + ) + + // 找最近的(ancestorAccountSequences 是从直接推荐人到根节点的顺序) + if (ancestorCommunities.length > 0) { + // 按祖先链顺序找第一个匹配的 + for (const ancestorSeq of ancestorAccountSequences) { + const found = ancestorCommunities.find( + (auth) => auth.userId.accountSequence === ancestorSeq, + ) + if (found) { + parentCommunityAuth = found + break + } + } + } + } + + // 4. 获取我的团队成员 + const teamMemberAccountSequences = await this.referralServiceClient.getTeamMembers(accountSequence) + this.logger.debug(`[getCommunityHierarchy] teamMembers count: ${teamMemberAccountSequences.length}`) + + // 5. 查找下级社区(在团队成员中找最近的有社区授权的用户) + // "最近" 的定义:直接下级优先,然后是下级的下级,以此类推 + // 由于 getTeamMembers 返回的是广度优先遍历结果,可以直接使用顺序 + let childCommunityAuths: AuthorizationRole[] = [] + if (teamMemberAccountSequences.length > 0) { + const teamCommunities = await this.authorizationRepository.findActiveCommunityByAccountSequences( + teamMemberAccountSequences, + ) + + // 只保留"最近的"下级社区 + // 如果一个社区的上级不在我的直接团队成员中,或者其上级就是我,则它是"最近的" + // 简化实现:返回所有团队中的社区,前端可以根据需要过滤 + // 但按用户要求"只计算最近的那个",这里需要做过滤 + // 算法:如果某个社区 A 的祖先中有另一个社区 B 也在团队中,则 A 不是最近的 + + const communityAccountSeqs = new Set(teamCommunities.map((c) => c.userId.accountSequence)) + + for (const comm of teamCommunities) { + // 获取这个社区成员的祖先链 + const commAncestors = await this.referralServiceClient.getReferralChain(comm.userId.accountSequence) + + // 检查这个社区是否有"更近"的祖先社区 + let hasCloserAncestorCommunity = false + for (const ancestorSeq of commAncestors) { + // 如果祖先是我,停止检查 + if (ancestorSeq === accountSequence) { + break + } + // 如果祖先也是社区且在我的团队中,则当前社区不是最近的 + if (communityAccountSeqs.has(ancestorSeq)) { + hasCloserAncestorCommunity = true + break + } + } + + if (!hasCloserAncestorCommunity) { + childCommunityAuths.push(comm) + } + } + } + + // 6. 构建响应 + const HEADQUARTERS_COMMUNITY = { + authorizationId: 'headquarters', + accountSequence: '0', + communityName: '总部社区', + userId: undefined, + isHeadquarters: true, + } + + return { + myCommunity: myCommunity && myCommunity.status === AuthorizationStatus.AUTHORIZED + ? { + authorizationId: myCommunity.authorizationId.value, + accountSequence: myCommunity.userId.accountSequence, + communityName: myCommunity.displayTitle, + userId: myCommunity.userId.value, + isHeadquarters: false, + } + : null, + parentCommunity: parentCommunityAuth + ? { + authorizationId: parentCommunityAuth.authorizationId.value, + accountSequence: parentCommunityAuth.userId.accountSequence, + communityName: parentCommunityAuth.displayTitle, + userId: parentCommunityAuth.userId.value, + isHeadquarters: false, + } + : HEADQUARTERS_COMMUNITY, + childCommunities: childCommunityAuths.map((auth) => ({ + authorizationId: auth.authorizationId.value, + accountSequence: auth.userId.accountSequence, + communityName: auth.displayTitle, + userId: auth.userId.value, + isHeadquarters: false, + })), + hasParentCommunity: parentCommunityAuth !== null, + childCommunityCount: childCommunityAuths.length, + } + } + + /** + * 查找用户推荐链中最近的社区授权用户 + * 用于 reward-service 分配社区权益 + * @returns accountSequence of nearest community authorization holder, or null + */ + async findNearestAuthorizedCommunity(accountSequence: string): Promise { + this.logger.debug(`[findNearestAuthorizedCommunity] accountSequence=${accountSequence}`) + + // 获取用户的祖先链(推荐链) + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) + + if (ancestorAccountSequences.length === 0) { + return null + } + + // 在祖先链中找最近的有社区授权的用户 + const ancestorCommunities = await this.authorizationRepository.findActiveCommunityByAccountSequences( + ancestorAccountSequences, + ) + + if (ancestorCommunities.length === 0) { + return null + } + + // 按祖先链顺序找第一个匹配的 + for (const ancestorSeq of ancestorAccountSequences) { + const found = ancestorCommunities.find( + (auth) => auth.userId.accountSequence === ancestorSeq, + ) + if (found) { + return found.userId.accountSequence + } + } + + return null + } + + /** + * 查找用户推荐链中最近的省公司授权用户(匹配指定省份) + * 用于 reward-service 分配省团队权益 + * @returns accountSequence of nearest province authorization holder, or null + */ + async findNearestAuthorizedProvince( + accountSequence: string, + provinceCode: string, + ): Promise { + this.logger.debug( + `[findNearestAuthorizedProvince] accountSequence=${accountSequence}, provinceCode=${provinceCode}`, + ) + + // 获取用户的祖先链(推荐链) + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) + + if (ancestorAccountSequences.length === 0) { + return null + } + + // 在祖先链中找最近的有省公司授权且匹配省份代码的用户 + const ancestorProvinces = await this.authorizationRepository.findActiveProvinceByAccountSequencesAndRegion( + ancestorAccountSequences, + provinceCode, + ) + + if (ancestorProvinces.length === 0) { + return null + } + + // 按祖先链顺序找第一个匹配的 + for (const ancestorSeq of ancestorAccountSequences) { + const found = ancestorProvinces.find( + (auth) => auth.userId.accountSequence === ancestorSeq, + ) + if (found) { + return found.userId.accountSequence + } + } + + return null + } + + /** + * 查找用户推荐链中最近的市公司授权用户(匹配指定城市) + * 用于 reward-service 分配市团队权益 + * @returns accountSequence of nearest city authorization holder, or null + */ + async findNearestAuthorizedCity( + accountSequence: string, + cityCode: string, + ): Promise { + this.logger.debug( + `[findNearestAuthorizedCity] accountSequence=${accountSequence}, cityCode=${cityCode}`, + ) + + // 获取用户的祖先链(推荐链) + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) + + if (ancestorAccountSequences.length === 0) { + return null + } + + // 在祖先链中找最近的有市公司授权且匹配城市代码的用户 + const ancestorCities = await this.authorizationRepository.findActiveCityByAccountSequencesAndRegion( + ancestorAccountSequences, + cityCode, + ) + + if (ancestorCities.length === 0) { + return null + } + + // 按祖先链顺序找第一个匹配的 + for (const ancestorSeq of ancestorAccountSequences) { + const found = ancestorCities.find( + (auth) => auth.userId.accountSequence === ancestorSeq, + ) + if (found) { + return found.userId.accountSequence + } + } + + return null + } + + /** + * 尝试激活授权权益 + * 仅当权益未激活时执行激活操作 + */ + private async tryActivateBenefit(authorization: AuthorizationRole): Promise { + if (authorization.benefitActive) { + return // 已激活,无需操作 + } + + this.logger.log( + `[tryActivateBenefit] Activating benefit for authorization ${authorization.authorizationId.value}, ` + + `role=${authorization.roleType}, accountSequence=${authorization.userId.accountSequence}`, + ) + + authorization.activateBenefit() + await this.authorizationRepository.save(authorization) + await this.eventPublisher.publishAll(authorization.domainEvents) + authorization.clearDomainEvents() + } + + /** + * 获取社区权益分配方案 + * 根据考核规则计算每棵树的社区权益应该分配给谁 + * + * 规则: + * 1. 找到认种用户推荐链上最近的社区 + * 2. 如果该社区 benefitActive=true,全部权益给该社区 + * 3. 如果该社区 benefitActive=false: + * - 计算该社区还需要多少棵才能达到初始考核(10棵) + * - 考核前的部分给上级社区或总部 + * - 考核后的部分给该社区(同时激活权益) + * 4. 如果没有社区,全部给总部 + */ + async getCommunityRewardDistribution( + accountSequence: string, + treeCount: number, + ): Promise<{ + distributions: Array<{ + accountSequence: string + treeCount: number + reason: string + }> + }> { + this.logger.debug( + `[getCommunityRewardDistribution] accountSequence=${accountSequence}, treeCount=${treeCount}`, + ) + + const HEADQUARTERS_ACCOUNT_SEQUENCE = '1' // 总部社区账号 + + // 1. 获取用户的祖先链(推荐链) + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) + + if (ancestorAccountSequences.length === 0) { + // 无推荐链,全部给总部 + return { + distributions: [ + { + accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, + treeCount, + reason: '无推荐链,进总部社区', + }, + ], + } + } + + // 2. 查找祖先链中所有社区授权(包括 benefitActive=false 的) + const ancestorCommunities = await this.authorizationRepository.findCommunityByAccountSequences( + ancestorAccountSequences, + ) + + if (ancestorCommunities.length === 0) { + // 推荐链上没有社区,全部给总部 + return { + distributions: [ + { + accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, + treeCount, + reason: '推荐链上无社区授权,进总部社区', + }, + ], + } + } + + // 3. 按祖先链顺序找最近的社区 + let nearestCommunity: typeof ancestorCommunities[0] | null = null + let nearestCommunityIndex = -1 + + for (let i = 0; i < ancestorAccountSequences.length; i++) { + const ancestorSeq = ancestorAccountSequences[i] + const found = ancestorCommunities.find( + (auth) => auth.userId.accountSequence === ancestorSeq, + ) + if (found) { + nearestCommunity = found + nearestCommunityIndex = i + break + } + } + + if (!nearestCommunity) { + // 这种情况理论上不应该发生,但作为兜底 + return { + distributions: [ + { + accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, + treeCount, + reason: '未找到匹配的社区,进总部社区', + }, + ], + } + } + + // 4. 检查最近社区的权益状态 + if (nearestCommunity.benefitActive) { + // 权益已激活,全部给该社区 + return { + distributions: [ + { + accountSequence: nearestCommunity.userId.accountSequence, + treeCount, + reason: '社区权益已激活', + }, + ], + } + } + + // 5. 权益未激活,需要计算考核分配 + // 获取该社区的团队统计数据 - 使用下级团队认种数(不含自己) + const communityStats = await this.statsRepository.findByAccountSequence( + nearestCommunity.userId.accountSequence, + ) + const rawSubordinateCount = communityStats?.subordinateTeamPlantingCount ?? 0 + + // 重要:由于 referral-service 和 reward-service 都消费同一个 Kafka 事件, + // 存在竞态条件,此时查询到的 subordinateTeamPlantingCount 可能已经包含了本次认种。 + // 因此需要减去本次认种数量来还原"认种前"的下级团队数。 + // 注意:如果 referral-service 还没处理完,rawSubordinateCount 可能还是旧值, + // 此时 currentTeamCount 可能为负数,需要取 max(0, ...) + const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) + const initialTarget = nearestCommunity.getInitialTarget() // 社区初始考核目标:10棵 + + this.logger.debug( + `[getCommunityRewardDistribution] Community ${nearestCommunity.userId.accountSequence} ` + + `benefitActive=false, rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, ` + + `currentTeamCount(before)=${currentTeamCount}, initialTarget=${initialTarget}`, + ) + + // 6. 查找上级社区(用于接收考核前的权益) + let parentCommunityAccountSequence: string = HEADQUARTERS_ACCOUNT_SEQUENCE + let parentCommunityReason = '上级为总部社区' + + // 从最近社区之后继续查找上级社区 + for (let i = nearestCommunityIndex + 1; i < ancestorAccountSequences.length; i++) { + const ancestorSeq = ancestorAccountSequences[i] + const found = ancestorCommunities.find( + (auth) => auth.userId.accountSequence === ancestorSeq && auth.benefitActive, + ) + if (found) { + parentCommunityAccountSequence = found.userId.accountSequence + parentCommunityReason = '上级社区权益已激活' + break + } + } + + // 7. 计算分配方案 + const distributions: Array<{ + accountSequence: string + treeCount: number + reason: string + }> = [] + + if (currentTeamCount >= initialTarget) { + // 已达标但权益未激活(可能是月度考核失败),全部给该社区 + // 注:这种情况下应该由系统自动激活权益,但这里作为兜底处理 + distributions.push({ + accountSequence: nearestCommunity.userId.accountSequence, + treeCount, + reason: '已达初始考核目标', + }) + + // 自动激活权益 + await this.tryActivateBenefit(nearestCommunity) + } else { + // 未达标,需要拆分 + // remaining: 还差多少棵达标(不包括达标那一棵本身) + // 例如:目标10棵,当前0棵 -> remaining = 9(前9棵给上级,第10棵给自己) + const remaining = Math.max(0, initialTarget - currentTeamCount - 1) + const afterPlantingCount = currentTeamCount + treeCount // 本次认种后的总数 + + if (afterPlantingCount < initialTarget) { + // 本次认种后仍未达标,全部给上级/总部 + distributions.push({ + accountSequence: parentCommunityAccountSequence, + treeCount, + reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),${parentCommunityReason}`, + }) + } else { + // 本次认种达到或跨越考核达标点 (afterPlantingCount >= initialTarget) + // 考核前的部分给上级/总部(remaining 可能为0,此时不添加分配记录) + if (remaining > 0) { + distributions.push({ + accountSequence: parentCommunityAccountSequence, + treeCount: remaining, + reason: `初始考核(${currentTeamCount}+${remaining}=${currentTeamCount + remaining}/${initialTarget}),${parentCommunityReason}`, + }) + } + // 达标的那一棵 + 之后多出来的,全部给该社区 + const afterTargetCount = treeCount - remaining + if (afterTargetCount > 0) { + distributions.push({ + accountSequence: nearestCommunity.userId.accountSequence, + treeCount: afterTargetCount, + reason: `考核达标后权益生效`, + }) + } + + // 自动激活权益(本次认种使其达标) + await this.tryActivateBenefit(nearestCommunity) + } + } + + this.logger.debug( + `[getCommunityRewardDistribution] Result: ${JSON.stringify(distributions)}`, + ) + + return { distributions } + } + + /** + * 获取省团队权益分配方案 (20 USDT) + * + * 规则: + * 1. 找到认种用户推荐链上最近的授权省公司(AUTH_PROVINCE_COMPANY) + * 2. 如果该授权省公司 benefitActive=true,全部权益给该用户 + * 3. 如果该授权省公司 benefitActive=false: + * - 计算还需要多少棵才能达到初始考核(500棵) + * - 考核前的部分给上级/总部 + * - 考核后的部分给该用户 + * 4. 如果没有授权省公司,全部给总部 + */ + async getProvinceTeamRewardDistribution( + accountSequence: string, + provinceCode: string, + treeCount: number, + ): Promise<{ + distributions: Array<{ + accountSequence: string + treeCount: number + reason: string + }> + }> { + this.logger.debug( + `[getProvinceTeamRewardDistribution] accountSequence=${accountSequence}, provinceCode=${provinceCode}, treeCount=${treeCount}`, + ) + + const HEADQUARTERS_ACCOUNT_SEQUENCE = '1' + + // 1. 获取用户的祖先链 + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) + + if (ancestorAccountSequences.length === 0) { + return { + distributions: [ + { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '无推荐链,进总部社区' }, + ], + } + } + + // 2. 查找祖先链中所有授权省公司(包括 benefitActive=false) + // 注意:省团队收益不再要求省份匹配,只要推荐链上有省团队授权即可获得收益 + const ancestorAuthProvinces = await this.authorizationRepository.findAuthProvinceByAccountSequences( + ancestorAccountSequences, + ) + + if (ancestorAuthProvinces.length === 0) { + return { + distributions: [ + { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '推荐链上无授权省公司,进总部社区' }, + ], + } + } + + // 3. 按祖先链顺序找最近的授权省公司 + let nearestAuthProvince: typeof ancestorAuthProvinces[0] | null = null + let nearestIndex = -1 + + for (let i = 0; i < ancestorAccountSequences.length; i++) { + const ancestorSeq = ancestorAccountSequences[i] + const found = ancestorAuthProvinces.find( + (auth) => auth.userId.accountSequence === ancestorSeq, + ) + if (found) { + nearestAuthProvince = found + nearestIndex = i + break + } + } + + if (!nearestAuthProvince) { + return { + distributions: [ + { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '未找到匹配的授权省公司,进总部社区' }, + ], + } + } + + // 4. 检查权益状态 + if (nearestAuthProvince.benefitActive) { + return { + distributions: [ + { accountSequence: nearestAuthProvince.userId.accountSequence, treeCount, reason: '省团队权益已激活' }, + ], + } + } + + // 5. 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) + const stats = await this.statsRepository.findByAccountSequence(nearestAuthProvince.userId.accountSequence) + const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 + // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 + const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) + const initialTarget = nearestAuthProvince.getInitialTarget() // 500棵 + + this.logger.debug( + `[getProvinceTeamRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}`, + ) + + // 6. 查找上级(用于接收考核前的权益) + let parentAccountSequence: string = HEADQUARTERS_ACCOUNT_SEQUENCE + let parentReason = '上级为总部社区' + + for (let i = nearestIndex + 1; i < ancestorAccountSequences.length; i++) { + const ancestorSeq = ancestorAccountSequences[i] + const found = ancestorAuthProvinces.find( + (auth) => auth.userId.accountSequence === ancestorSeq && auth.benefitActive, + ) + if (found) { + parentAccountSequence = found.userId.accountSequence + parentReason = '上级授权省公司权益已激活' + break + } + } + + // 7. 计算分配 + const distributions: Array<{ accountSequence: string; treeCount: number; reason: string }> = [] + + if (currentTeamCount >= initialTarget) { + distributions.push({ + accountSequence: nearestAuthProvince.userId.accountSequence, + treeCount, + reason: '已达初始考核目标', + }) + // 自动激活权益 + await this.tryActivateBenefit(nearestAuthProvince) + } else { + // remaining: 还差多少棵达标(不包括达标那一棵本身) + const remaining = Math.max(0, initialTarget - currentTeamCount - 1) + const afterPlantingCount = currentTeamCount + treeCount + + if (afterPlantingCount < initialTarget) { + // 本次认种后仍未达标 + distributions.push({ + accountSequence: parentAccountSequence, + treeCount, + reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),${parentReason}`, + }) + } else { + // 本次认种达到或跨越达标点 (afterPlantingCount >= initialTarget) + // 考核前的部分给上级/总部(remaining 可能为0,此时不添加分配记录) + if (remaining > 0) { + distributions.push({ + accountSequence: parentAccountSequence, + treeCount: remaining, + reason: `初始考核(${currentTeamCount}+${remaining}=${currentTeamCount + remaining}/${initialTarget}),${parentReason}`, + }) + } + // 达标的那一棵 + 之后多出来的,全部给该省团队 + const afterTargetCount = treeCount - remaining + if (afterTargetCount > 0) { + distributions.push({ + accountSequence: nearestAuthProvince.userId.accountSequence, + treeCount: afterTargetCount, + reason: '考核达标后权益生效', + }) + } + // 自动激活权益(本次认种使其达标) + await this.tryActivateBenefit(nearestAuthProvince) + } + } + + this.logger.debug(`[getProvinceTeamRewardDistribution] Result: ${JSON.stringify(distributions)}`) + return { distributions } + } + + /** + * 获取省区域权益分配方案 (15 USDT + 1%算力) + * + * 规则: + * 1. 查找该省份是否有正式省公司(PROVINCE_COMPANY) + * 2. 如果有且 benefitActive=true,权益进该省公司自己的账户 + * 3. 如果有但 benefitActive=false(考核中): + * - 计算还需要多少棵才能达到初始考核(5万棵) + * - 考核前的部分进系统省账户 + * - 考核后的部分给该省公司 + * 4. 如果没有正式省公司,全部进系统省账户 + */ + async getProvinceAreaRewardDistribution( + provinceCode: string, + treeCount: number, + ): Promise<{ + distributions: Array<{ + accountSequence: string + treeCount: number + reason: string + isSystemAccount: boolean + }> + }> { + this.logger.debug( + `[getProvinceAreaRewardDistribution] provinceCode=${provinceCode}, treeCount=${treeCount}`, + ) + + // 系统省账户ID格式: 9 + 省份代码 + const systemProvinceAccountId = `9${provinceCode.padStart(6, '0')}` + + // 查找该省份的正式省公司 + const provinceCompany = await this.authorizationRepository.findProvinceCompanyByRegion(provinceCode) + + if (!provinceCompany) { + // 无正式省公司,全部进系统省账户 + return { + distributions: [ + { + accountSequence: systemProvinceAccountId, + treeCount, + reason: '无正式省公司授权,进系统省账户', + isSystemAccount: true, + }, + ], + } + } + + if (provinceCompany.benefitActive) { + // 正式省公司权益已激活,进该省公司账户 + return { + distributions: [ + { + accountSequence: provinceCompany.userId.accountSequence, + treeCount, + reason: '省区域权益已激活', + isSystemAccount: false, + }, + ], + } + } + + // 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) + const stats = await this.statsRepository.findByAccountSequence(provinceCompany.userId.accountSequence) + const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 + // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 + const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) + const initialTarget = provinceCompany.getInitialTarget() // 5万棵 + + this.logger.debug( + `[getProvinceAreaRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}, initialTarget=${initialTarget}`, + ) + + const distributions: Array<{ + accountSequence: string + treeCount: number + reason: string + isSystemAccount: boolean + }> = [] + + if (currentTeamCount >= initialTarget) { + // 已达标但权益未激活,全部给该省公司 + distributions.push({ + accountSequence: provinceCompany.userId.accountSequence, + treeCount, + reason: '已达初始考核目标', + isSystemAccount: false, + }) + // 自动激活权益 + await this.tryActivateBenefit(provinceCompany) + } else { + // remaining: 还差多少棵达标(不包括达标那一棵本身) + const remaining = Math.max(0, initialTarget - currentTeamCount - 1) + const afterPlantingCount = currentTeamCount + treeCount + + if (afterPlantingCount < initialTarget) { + // 本次认种后仍未达标,全部进系统省账户 + distributions.push({ + accountSequence: systemProvinceAccountId, + treeCount, + reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),进系统省账户`, + isSystemAccount: true, + }) + } else { + // 本次认种达到或跨越考核达标点 (afterPlantingCount >= initialTarget) + // 考核前的部分进系统省账户(remaining 可能为0,此时不添加分配记录) + if (remaining > 0) { + distributions.push({ + accountSequence: systemProvinceAccountId, + treeCount: remaining, + reason: `初始考核(${currentTeamCount}+${remaining}=${currentTeamCount + remaining}/${initialTarget}),进系统省账户`, + isSystemAccount: true, + }) + } + // 达标的那一棵 + 之后多出来的,全部给该省公司 + const afterTargetCount = treeCount - remaining + if (afterTargetCount > 0) { + distributions.push({ + accountSequence: provinceCompany.userId.accountSequence, + treeCount: afterTargetCount, + reason: '考核达标后权益生效', + isSystemAccount: false, + }) + } + // 自动激活权益 + await this.tryActivateBenefit(provinceCompany) + } + } + + this.logger.debug(`[getProvinceAreaRewardDistribution] Result: ${JSON.stringify(distributions)}`) + return { distributions } + } + + /** + * 获取市团队权益分配方案 (40 USDT) + * + * 规则: + * 1. 找到认种用户推荐链上最近的授权市公司(AUTH_CITY_COMPANY) + * 2. 如果该授权市公司 benefitActive=true,全部权益给该用户 + * 3. 如果该授权市公司 benefitActive=false: + * - 计算还需要多少棵才能达到初始考核(100棵) + * - 考核前的部分给上级/总部 + * - 考核后的部分给该用户 + * 4. 如果没有授权市公司,全部给总部 + */ + async getCityTeamRewardDistribution( + accountSequence: string, + cityCode: string, + treeCount: number, + ): Promise<{ + distributions: Array<{ + accountSequence: string + treeCount: number + reason: string + }> + }> { + this.logger.debug( + `[getCityTeamRewardDistribution] accountSequence=${accountSequence}, cityCode=${cityCode}, treeCount=${treeCount}`, + ) + + const HEADQUARTERS_ACCOUNT_SEQUENCE = '1' + + // 1. 获取用户的祖先链 + const ancestorAccountSequences = await this.referralServiceClient.getReferralChain(accountSequence) + + if (ancestorAccountSequences.length === 0) { + return { + distributions: [ + { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '无推荐链,进总部社区' }, + ], + } + } + + // 2. 查找祖先链中所有授权市公司(包括 benefitActive=false) + // 注意:市团队收益不再要求城市匹配,只要推荐链上有市团队授权即可获得收益 + const ancestorAuthCities = await this.authorizationRepository.findAuthCityByAccountSequences( + ancestorAccountSequences, + ) + + if (ancestorAuthCities.length === 0) { + return { + distributions: [ + { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '推荐链上无授权市公司,进总部社区' }, + ], + } + } + + // 3. 按祖先链顺序找最近的授权市公司 + let nearestAuthCity: typeof ancestorAuthCities[0] | null = null + let nearestIndex = -1 + + for (let i = 0; i < ancestorAccountSequences.length; i++) { + const ancestorSeq = ancestorAccountSequences[i] + const found = ancestorAuthCities.find( + (auth) => auth.userId.accountSequence === ancestorSeq, + ) + if (found) { + nearestAuthCity = found + nearestIndex = i + break + } + } + + if (!nearestAuthCity) { + return { + distributions: [ + { accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, treeCount, reason: '未找到匹配的授权市公司,进总部社区' }, + ], + } + } + + // 4. 检查权益状态 + if (nearestAuthCity.benefitActive) { + return { + distributions: [ + { accountSequence: nearestAuthCity.userId.accountSequence, treeCount, reason: '市团队权益已激活' }, + ], + } + } + + // 5. 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) + const stats = await this.statsRepository.findByAccountSequence(nearestAuthCity.userId.accountSequence) + const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 + // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 + const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) + const initialTarget = nearestAuthCity.getInitialTarget() // 100棵 + + this.logger.debug( + `[getCityTeamRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}`, + ) + + // 6. 查找上级 + let parentAccountSequence: string = HEADQUARTERS_ACCOUNT_SEQUENCE + let parentReason = '上级为总部社区' + + for (let i = nearestIndex + 1; i < ancestorAccountSequences.length; i++) { + const ancestorSeq = ancestorAccountSequences[i] + const found = ancestorAuthCities.find( + (auth) => auth.userId.accountSequence === ancestorSeq && auth.benefitActive, + ) + if (found) { + parentAccountSequence = found.userId.accountSequence + parentReason = '上级授权市公司权益已激活' + break + } + } + + // 7. 计算分配 + const distributions: Array<{ accountSequence: string; treeCount: number; reason: string }> = [] + + if (currentTeamCount >= initialTarget) { + distributions.push({ + accountSequence: nearestAuthCity.userId.accountSequence, + treeCount, + reason: '已达初始考核目标', + }) + // 自动激活权益 + await this.tryActivateBenefit(nearestAuthCity) + } else { + // remaining: 还差多少棵达标(不包括达标那一棵本身) + const remaining = Math.max(0, initialTarget - currentTeamCount - 1) + const afterPlantingCount = currentTeamCount + treeCount + + if (afterPlantingCount < initialTarget) { + // 本次认种后仍未达标 + distributions.push({ + accountSequence: parentAccountSequence, + treeCount, + reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),${parentReason}`, + }) + } else { + // 本次认种达到或跨越达标点 (afterPlantingCount >= initialTarget) + // 考核前的部分给上级/总部(remaining 可能为0,此时不添加分配记录) + if (remaining > 0) { + distributions.push({ + accountSequence: parentAccountSequence, + treeCount: remaining, + reason: `初始考核(${currentTeamCount}+${remaining}=${currentTeamCount + remaining}/${initialTarget}),${parentReason}`, + }) + } + // 达标的那一棵 + 之后多出来的,全部给该市团队 + const afterTargetCount = treeCount - remaining + if (afterTargetCount > 0) { + distributions.push({ + accountSequence: nearestAuthCity.userId.accountSequence, + treeCount: afterTargetCount, + reason: '考核达标后权益生效', + }) + } + // 自动激活权益(本次认种使其达标) + await this.tryActivateBenefit(nearestAuthCity) + } + } + + this.logger.debug(`[getCityTeamRewardDistribution] Result: ${JSON.stringify(distributions)}`) + return { distributions } + } + + /** + * 获取市区域权益分配方案 (35 USDT + 2%算力) + * + * 规则: + * 1. 查找该城市是否有正式市公司(CITY_COMPANY) + * 2. 如果有且 benefitActive=true,权益进该市公司自己的账户 + * 3. 如果有但 benefitActive=false(考核中): + * - 计算还需要多少棵才能达到初始考核(1万棵) + * - 考核前的部分进系统市账户 + * - 考核后的部分给该市公司 + * 4. 如果没有正式市公司,全部进系统市账户 + */ + async getCityAreaRewardDistribution( + cityCode: string, + treeCount: number, + ): Promise<{ + distributions: Array<{ + accountSequence: string + treeCount: number + reason: string + isSystemAccount: boolean + }> + }> { + this.logger.debug( + `[getCityAreaRewardDistribution] cityCode=${cityCode}, treeCount=${treeCount}`, + ) + + // 系统市账户ID格式: 8 + 城市代码 + const systemCityAccountId = `8${cityCode.padStart(6, '0')}` + + // 查找该城市的正式市公司 + const cityCompany = await this.authorizationRepository.findCityCompanyByRegion(cityCode) + + if (!cityCompany) { + // 无正式市公司,全部进系统市账户 + return { + distributions: [ + { + accountSequence: systemCityAccountId, + treeCount, + reason: '无正式市公司授权,进系统市账户', + isSystemAccount: true, + }, + ], + } + } + + if (cityCompany.benefitActive) { + // 正式市公司权益已激活,进该市公司账户 + return { + distributions: [ + { + accountSequence: cityCompany.userId.accountSequence, + treeCount, + reason: '市区域权益已激活', + isSystemAccount: false, + }, + ], + } + } + + // 权益未激活,计算考核分配 - 使用下级团队认种数(不含自己) + const stats = await this.statsRepository.findByAccountSequence(cityCompany.userId.accountSequence) + const rawSubordinateCount = stats?.subordinateTeamPlantingCount ?? 0 + // 修复竞态条件:减去本次认种数量来还原"认种前"的下级团队数 + const currentTeamCount = Math.max(0, rawSubordinateCount - treeCount) + const initialTarget = cityCompany.getInitialTarget() // 1万棵 + + this.logger.debug( + `[getCityAreaRewardDistribution] rawSubordinateCount=${rawSubordinateCount}, treeCount=${treeCount}, currentTeamCount(before)=${currentTeamCount}, initialTarget=${initialTarget}`, + ) + + const distributions: Array<{ + accountSequence: string + treeCount: number + reason: string + isSystemAccount: boolean + }> = [] + + if (currentTeamCount >= initialTarget) { + // 已达标但权益未激活,全部给该市公司 + distributions.push({ + accountSequence: cityCompany.userId.accountSequence, + treeCount, + reason: '已达初始考核目标', + isSystemAccount: false, + }) + // 自动激活权益 + await this.tryActivateBenefit(cityCompany) + } else { + // remaining: 还差多少棵达标(不包括达标那一棵本身) + const remaining = Math.max(0, initialTarget - currentTeamCount - 1) + const afterPlantingCount = currentTeamCount + treeCount + + if (afterPlantingCount < initialTarget) { + // 本次认种后仍未达标,全部进系统市账户 + distributions.push({ + accountSequence: systemCityAccountId, + treeCount, + reason: `初始考核中(${currentTeamCount}+${treeCount}=${afterPlantingCount}/${initialTarget}),进系统市账户`, + isSystemAccount: true, + }) + } else { + // 本次认种达到或跨越考核达标点 (afterPlantingCount >= initialTarget) + // 考核前的部分进系统市账户(remaining 可能为0,此时不添加分配记录) + if (remaining > 0) { + distributions.push({ + accountSequence: systemCityAccountId, + treeCount: remaining, + reason: `初始考核(${currentTeamCount}+${remaining}=${currentTeamCount + remaining}/${initialTarget}),进系统市账户`, + isSystemAccount: true, + }) + } + // 达标的那一棵 + 之后多出来的,全部给该市公司 + const afterTargetCount = treeCount - remaining + if (afterTargetCount > 0) { + distributions.push({ + accountSequence: cityCompany.userId.accountSequence, + treeCount: afterTargetCount, + reason: '考核达标后权益生效', + isSystemAccount: false, + }) + } + // 自动激活权益 + await this.tryActivateBenefit(cityCompany) + } + } + + this.logger.debug(`[getCityAreaRewardDistribution] Result: ${JSON.stringify(distributions)}`) + return { distributions } + } +} diff --git a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.spec.ts b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.spec.ts index 53290fda..46804cae 100644 --- a/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.spec.ts +++ b/backend/services/authorization-service/src/domain/aggregates/authorization-role.aggregate.spec.ts @@ -1,193 +1,193 @@ -import { AuthorizationRole } from './authorization-role.aggregate' -import { UserId, AdminUserId } from '@/domain/value-objects' -import { RoleType, AuthorizationStatus, MonthlyTargetType } from '@/domain/enums' -import { DomainError } from '@/shared/exceptions' - -describe('AuthorizationRole Aggregate', () => { - describe('createCommunityAuth', () => { - it('should create community authorization', () => { - const auth = AuthorizationRole.createCommunityAuth({ - userId: UserId.create('user-1', BigInt(1)), - communityName: '量子社区', - }) - - expect(auth.roleType).toBe(RoleType.COMMUNITY) - expect(auth.status).toBe(AuthorizationStatus.PENDING) - expect(auth.displayTitle).toBe('量子社区') - expect(auth.benefitActive).toBe(false) - expect(auth.getInitialTarget()).toBe(10) - expect(auth.domainEvents.length).toBe(1) - expect(auth.domainEvents[0].eventType).toBe('authorization.community.requested') - }) - }) - - describe('createAuthProvinceCompany', () => { - it('should create auth province company authorization', () => { - const auth = AuthorizationRole.createAuthProvinceCompany({ - userId: UserId.create('user-1', BigInt(1)), - provinceCode: '430000', - provinceName: '湖南省', - }) - - expect(auth.roleType).toBe(RoleType.AUTH_PROVINCE_COMPANY) - expect(auth.status).toBe(AuthorizationStatus.PENDING) - expect(auth.displayTitle).toBe('授权湖南省') - expect(auth.benefitActive).toBe(false) - expect(auth.getInitialTarget()).toBe(500) - expect(auth.requireLocalPercentage).toBe(5.0) - expect(auth.needsLadderAssessment()).toBe(true) - }) - }) - - describe('createAuthCityCompany', () => { - it('should create auth city company authorization', () => { - const auth = AuthorizationRole.createAuthCityCompany({ - userId: UserId.create('user-1', BigInt(1)), - cityCode: '430100', - cityName: '长沙市', - }) - - expect(auth.roleType).toBe(RoleType.AUTH_CITY_COMPANY) - expect(auth.status).toBe(AuthorizationStatus.PENDING) - expect(auth.displayTitle).toBe('授权长沙市') - expect(auth.benefitActive).toBe(false) - expect(auth.getInitialTarget()).toBe(100) - }) - }) - - describe('createProvinceCompany', () => { - it('should create official province company with active benefits', () => { - const adminId = AdminUserId.create('admin-1', BigInt(101)) - const auth = AuthorizationRole.createProvinceCompany({ - userId: UserId.create('user-1', BigInt(1)), - provinceCode: '430000', - provinceName: '湖南省', - adminId, - }) - - expect(auth.roleType).toBe(RoleType.PROVINCE_COMPANY) - expect(auth.status).toBe(AuthorizationStatus.AUTHORIZED) - expect(auth.displayTitle).toBe('湖南省') - expect(auth.benefitActive).toBe(true) - expect(auth.getInitialTarget()).toBe(0) // No initial target for official company - }) - }) - - describe('activateBenefit', () => { - it('should activate benefit and emit event', () => { - const auth = AuthorizationRole.createCommunityAuth({ - userId: UserId.create('user-1', BigInt(1)), - communityName: '量子社区', - }) - auth.clearDomainEvents() - - auth.activateBenefit() - - expect(auth.benefitActive).toBe(true) - expect(auth.status).toBe(AuthorizationStatus.AUTHORIZED) - expect(auth.currentMonthIndex).toBe(1) - expect(auth.domainEvents.length).toBe(1) - expect(auth.domainEvents[0].eventType).toBe('authorization.benefit.activated') - }) - - it('should throw error if already active', () => { - const auth = AuthorizationRole.createProvinceCompany({ - userId: UserId.create('user-1', BigInt(1)), - provinceCode: '430000', - provinceName: '湖南省', - adminId: AdminUserId.create('admin-1', BigInt(101)), - }) - - expect(() => auth.activateBenefit()).toThrow(DomainError) - }) - }) - - describe('deactivateBenefit', () => { - it('should deactivate benefit and reset month index', () => { - const auth = AuthorizationRole.createProvinceCompany({ - userId: UserId.create('user-1', BigInt(1)), - provinceCode: '430000', - provinceName: '湖南省', - adminId: AdminUserId.create('admin-1', BigInt(101)), - }) - auth.clearDomainEvents() - - auth.deactivateBenefit('考核不达标') - - expect(auth.benefitActive).toBe(false) - expect(auth.currentMonthIndex).toBe(0) - expect(auth.domainEvents.length).toBe(1) - expect(auth.domainEvents[0].eventType).toBe('authorization.benefit.deactivated') - }) - }) - - describe('revoke', () => { - it('should revoke authorization', () => { - const auth = AuthorizationRole.createProvinceCompany({ - userId: UserId.create('user-1', BigInt(1)), - provinceCode: '430000', - provinceName: '湖南省', - adminId: AdminUserId.create('admin-1', BigInt(101)), - }) - auth.clearDomainEvents() - - auth.revoke(AdminUserId.create('admin-2', BigInt(102)), '违规操作') - - expect(auth.status).toBe(AuthorizationStatus.REVOKED) - expect(auth.benefitActive).toBe(false) - expect(auth.revokeReason).toBe('违规操作') - expect(auth.domainEvents.length).toBe(1) - expect(auth.domainEvents[0].eventType).toBe('authorization.role.revoked') - }) - - it('should throw error if already revoked', () => { - const auth = AuthorizationRole.createProvinceCompany({ - userId: UserId.create('user-1', BigInt(1)), - provinceCode: '430000', - provinceName: '湖南省', - adminId: AdminUserId.create('admin-1', BigInt(101)), - }) - auth.revoke(AdminUserId.create('admin-2', BigInt(102)), '违规操作') - - expect(() => auth.revoke(AdminUserId.create('admin-3', BigInt(103)), '再次撤销')).toThrow( - DomainError, - ) - }) - }) - - describe('exemptLocalPercentageCheck', () => { - it('should exempt from percentage check', () => { - const auth = AuthorizationRole.createAuthProvinceCompany({ - userId: UserId.create('user-1', BigInt(1)), - provinceCode: '430000', - provinceName: '湖南省', - }) - - expect(auth.exemptFromPercentageCheck).toBe(false) - expect(auth.needsLocalPercentageCheck()).toBe(true) - - auth.exemptLocalPercentageCheck(AdminUserId.create('admin-1', BigInt(101))) - - expect(auth.exemptFromPercentageCheck).toBe(true) - expect(auth.needsLocalPercentageCheck()).toBe(false) - }) - }) - - describe('incrementMonthIndex', () => { - it('should increment month index', () => { - const auth = AuthorizationRole.createCommunityAuth({ - userId: UserId.create('user-1', BigInt(1)), - communityName: '量子社区', - }) - auth.activateBenefit() - - expect(auth.currentMonthIndex).toBe(1) - - auth.incrementMonthIndex() - expect(auth.currentMonthIndex).toBe(2) - - auth.incrementMonthIndex() - expect(auth.currentMonthIndex).toBe(3) - }) - }) -}) +import { AuthorizationRole } from './authorization-role.aggregate' +import { UserId, AdminUserId } from '@/domain/value-objects' +import { RoleType, AuthorizationStatus, MonthlyTargetType } from '@/domain/enums' +import { DomainError } from '@/shared/exceptions' + +describe('AuthorizationRole Aggregate', () => { + describe('createCommunityAuth', () => { + it('should create community authorization', () => { + const auth = AuthorizationRole.createCommunityAuth({ + userId: UserId.create('user-1', '1'), + communityName: '量子社区', + }) + + expect(auth.roleType).toBe(RoleType.COMMUNITY) + expect(auth.status).toBe(AuthorizationStatus.PENDING) + expect(auth.displayTitle).toBe('量子社区') + expect(auth.benefitActive).toBe(false) + expect(auth.getInitialTarget()).toBe(10) + expect(auth.domainEvents.length).toBe(1) + expect(auth.domainEvents[0].eventType).toBe('authorization.community.requested') + }) + }) + + describe('createAuthProvinceCompany', () => { + it('should create auth province company authorization', () => { + const auth = AuthorizationRole.createAuthProvinceCompany({ + userId: UserId.create('user-1', '1'), + provinceCode: '430000', + provinceName: '湖南省', + }) + + expect(auth.roleType).toBe(RoleType.AUTH_PROVINCE_COMPANY) + expect(auth.status).toBe(AuthorizationStatus.PENDING) + expect(auth.displayTitle).toBe('授权湖南省') + expect(auth.benefitActive).toBe(false) + expect(auth.getInitialTarget()).toBe(500) + expect(auth.requireLocalPercentage).toBe(5.0) + expect(auth.needsLadderAssessment()).toBe(true) + }) + }) + + describe('createAuthCityCompany', () => { + it('should create auth city company authorization', () => { + const auth = AuthorizationRole.createAuthCityCompany({ + userId: UserId.create('user-1', '1'), + cityCode: '430100', + cityName: '长沙市', + }) + + expect(auth.roleType).toBe(RoleType.AUTH_CITY_COMPANY) + expect(auth.status).toBe(AuthorizationStatus.PENDING) + expect(auth.displayTitle).toBe('授权长沙市') + expect(auth.benefitActive).toBe(false) + expect(auth.getInitialTarget()).toBe(100) + }) + }) + + describe('createProvinceCompany', () => { + it('should create official province company with active benefits', () => { + const adminId = AdminUserId.create('admin-1', '101') + const auth = AuthorizationRole.createProvinceCompany({ + userId: UserId.create('user-1', '1'), + provinceCode: '430000', + provinceName: '湖南省', + adminId, + }) + + expect(auth.roleType).toBe(RoleType.PROVINCE_COMPANY) + expect(auth.status).toBe(AuthorizationStatus.AUTHORIZED) + expect(auth.displayTitle).toBe('湖南省') + expect(auth.benefitActive).toBe(true) + expect(auth.getInitialTarget()).toBe(0) // No initial target for official company + }) + }) + + describe('activateBenefit', () => { + it('should activate benefit and emit event', () => { + const auth = AuthorizationRole.createCommunityAuth({ + userId: UserId.create('user-1', '1'), + communityName: '量子社区', + }) + auth.clearDomainEvents() + + auth.activateBenefit() + + expect(auth.benefitActive).toBe(true) + expect(auth.status).toBe(AuthorizationStatus.AUTHORIZED) + expect(auth.currentMonthIndex).toBe(1) + expect(auth.domainEvents.length).toBe(1) + expect(auth.domainEvents[0].eventType).toBe('authorization.benefit.activated') + }) + + it('should throw error if already active', () => { + const auth = AuthorizationRole.createProvinceCompany({ + userId: UserId.create('user-1', '1'), + provinceCode: '430000', + provinceName: '湖南省', + adminId: AdminUserId.create('admin-1', '101'), + }) + + expect(() => auth.activateBenefit()).toThrow(DomainError) + }) + }) + + describe('deactivateBenefit', () => { + it('should deactivate benefit and reset month index', () => { + const auth = AuthorizationRole.createProvinceCompany({ + userId: UserId.create('user-1', '1'), + provinceCode: '430000', + provinceName: '湖南省', + adminId: AdminUserId.create('admin-1', '101'), + }) + auth.clearDomainEvents() + + auth.deactivateBenefit('考核不达标') + + expect(auth.benefitActive).toBe(false) + expect(auth.currentMonthIndex).toBe(0) + expect(auth.domainEvents.length).toBe(1) + expect(auth.domainEvents[0].eventType).toBe('authorization.benefit.deactivated') + }) + }) + + describe('revoke', () => { + it('should revoke authorization', () => { + const auth = AuthorizationRole.createProvinceCompany({ + userId: UserId.create('user-1', '1'), + provinceCode: '430000', + provinceName: '湖南省', + adminId: AdminUserId.create('admin-1', '101'), + }) + auth.clearDomainEvents() + + auth.revoke(AdminUserId.create('admin-2', '102'), '违规操作') + + expect(auth.status).toBe(AuthorizationStatus.REVOKED) + expect(auth.benefitActive).toBe(false) + expect(auth.revokeReason).toBe('违规操作') + expect(auth.domainEvents.length).toBe(1) + expect(auth.domainEvents[0].eventType).toBe('authorization.role.revoked') + }) + + it('should throw error if already revoked', () => { + const auth = AuthorizationRole.createProvinceCompany({ + userId: UserId.create('user-1', '1'), + provinceCode: '430000', + provinceName: '湖南省', + adminId: AdminUserId.create('admin-1', '101'), + }) + auth.revoke(AdminUserId.create('admin-2', '102'), '违规操作') + + expect(() => auth.revoke(AdminUserId.create('admin-3', '103'), '再次撤销')).toThrow( + DomainError, + ) + }) + }) + + describe('exemptLocalPercentageCheck', () => { + it('should exempt from percentage check', () => { + const auth = AuthorizationRole.createAuthProvinceCompany({ + userId: UserId.create('user-1', '1'), + provinceCode: '430000', + provinceName: '湖南省', + }) + + expect(auth.exemptFromPercentageCheck).toBe(false) + expect(auth.needsLocalPercentageCheck()).toBe(true) + + auth.exemptLocalPercentageCheck(AdminUserId.create('admin-1', '101')) + + expect(auth.exemptFromPercentageCheck).toBe(true) + expect(auth.needsLocalPercentageCheck()).toBe(false) + }) + }) + + describe('incrementMonthIndex', () => { + it('should increment month index', () => { + const auth = AuthorizationRole.createCommunityAuth({ + userId: UserId.create('user-1', '1'), + communityName: '量子社区', + }) + auth.activateBenefit() + + expect(auth.currentMonthIndex).toBe(1) + + auth.incrementMonthIndex() + expect(auth.currentMonthIndex).toBe(2) + + auth.incrementMonthIndex() + expect(auth.currentMonthIndex).toBe(3) + }) + }) +}) 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 da4fe71f..4c3d3c78 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 @@ -1,86 +1,86 @@ -import { AuthorizationRole } from '@/domain/aggregates' -import { AuthorizationId, UserId, RegionCode } from '@/domain/value-objects' -import { RoleType, AuthorizationStatus } from '@/domain/enums' - -export const AUTHORIZATION_ROLE_REPOSITORY = Symbol('IAuthorizationRoleRepository') - -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, - ): Promise - findAllActive(roleType?: RoleType): Promise - findPendingByUserId(userId: UserId): Promise - findByStatus(status: AuthorizationStatus): Promise - delete(authorizationId: AuthorizationId): Promise - /** - * 批量查询指定 accountSequence 列表中具有活跃社区授权的用户 - */ - findActiveCommunityByAccountSequences(accountSequences: bigint[]): Promise - /** - * 批量查询指定 accountSequence 列表中具有活跃省公司授权(且匹配省份代码)的用户 - */ - findActiveProvinceByAccountSequencesAndRegion( - accountSequences: bigint[], - provinceCode: string, - ): Promise - /** - * 批量查询指定 accountSequence 列表中具有活跃市公司授权(且匹配城市代码)的用户 - */ - findActiveCityByAccountSequencesAndRegion( - accountSequences: bigint[], - cityCode: string, - ): Promise - /** - * 批量查询指定 accountSequence 列表中具有社区授权的用户(包括 benefitActive=false) - * 用于社区权益分配计算 - */ - findCommunityByAccountSequences(accountSequences: bigint[]): Promise - /** - * 批量查询指定 accountSequence 列表中具有授权省公司授权的用户(包括 benefitActive=false) - * 用于省团队权益分配计算 - * @deprecated 使用 findAuthProvinceByAccountSequences 替代,省团队收益不再要求省份匹配 - */ - findAuthProvinceByAccountSequencesAndRegion( - accountSequences: bigint[], - provinceCode: string, - ): Promise - /** - * 批量查询指定 accountSequence 列表中具有授权市公司授权的用户(包括 benefitActive=false) - * 用于市团队权益分配计算 - * @deprecated 使用 findAuthCityByAccountSequences 替代,市团队收益不再要求城市匹配 - */ - findAuthCityByAccountSequencesAndRegion( - accountSequences: bigint[], - cityCode: string, - ): Promise - /** - * 批量查询指定 accountSequence 列表中具有授权省公司(省团队)授权的用户(包括 benefitActive=false) - * 用于省团队权益分配计算,不要求省份匹配 - */ - findAuthProvinceByAccountSequences(accountSequences: bigint[]): Promise - /** - * 批量查询指定 accountSequence 列表中具有授权市公司(市团队)授权的用户(包括 benefitActive=false) - * 用于市团队权益分配计算,不要求城市匹配 - */ - findAuthCityByAccountSequences(accountSequences: bigint[]): Promise - /** - * 查找指定省份的正式省公司授权(用于省区域权益分配) - */ - findProvinceCompanyByRegion(provinceCode: string): Promise - /** - * 查找指定城市的正式市公司授权(用于市区域权益分配) - */ - findCityCompanyByRegion(cityCode: string): Promise -} +import { AuthorizationRole } from '@/domain/aggregates' +import { AuthorizationId, UserId, RegionCode } from '@/domain/value-objects' +import { RoleType, AuthorizationStatus } from '@/domain/enums' + +export const AUTHORIZATION_ROLE_REPOSITORY = Symbol('IAuthorizationRoleRepository') + +export interface IAuthorizationRoleRepository { + save(authorization: AuthorizationRole): Promise + findById(authorizationId: AuthorizationId): Promise + findByUserIdAndRoleType(userId: UserId, roleType: RoleType): Promise + findByAccountSequenceAndRoleType(accountSequence: string, roleType: RoleType): Promise + findByUserIdRoleTypeAndRegion( + userId: UserId, + roleType: RoleType, + regionCode: RegionCode, + ): Promise + findByUserId(userId: UserId): Promise + findByAccountSequence(accountSequence: string): Promise + findActiveByRoleTypeAndRegion( + roleType: RoleType, + regionCode: RegionCode, + ): Promise + findAllActive(roleType?: RoleType): Promise + findPendingByUserId(userId: UserId): Promise + findByStatus(status: AuthorizationStatus): Promise + delete(authorizationId: AuthorizationId): Promise + /** + * 批量查询指定 accountSequence 列表中具有活跃社区授权的用户 + */ + findActiveCommunityByAccountSequences(accountSequences: string[]): Promise + /** + * 批量查询指定 accountSequence 列表中具有活跃省公司授权(且匹配省份代码)的用户 + */ + findActiveProvinceByAccountSequencesAndRegion( + accountSequences: string[], + provinceCode: string, + ): Promise + /** + * 批量查询指定 accountSequence 列表中具有活跃市公司授权(且匹配城市代码)的用户 + */ + findActiveCityByAccountSequencesAndRegion( + accountSequences: string[], + cityCode: string, + ): Promise + /** + * 批量查询指定 accountSequence 列表中具有社区授权的用户(包括 benefitActive=false) + * 用于社区权益分配计算 + */ + findCommunityByAccountSequences(accountSequences: string[]): Promise + /** + * 批量查询指定 accountSequence 列表中具有授权省公司授权的用户(包括 benefitActive=false) + * 用于省团队权益分配计算 + * @deprecated 使用 findAuthProvinceByAccountSequences 替代,省团队收益不再要求省份匹配 + */ + findAuthProvinceByAccountSequencesAndRegion( + accountSequences: string[], + provinceCode: string, + ): Promise + /** + * 批量查询指定 accountSequence 列表中具有授权市公司授权的用户(包括 benefitActive=false) + * 用于市团队权益分配计算 + * @deprecated 使用 findAuthCityByAccountSequences 替代,市团队收益不再要求城市匹配 + */ + findAuthCityByAccountSequencesAndRegion( + accountSequences: string[], + cityCode: string, + ): Promise + /** + * 批量查询指定 accountSequence 列表中具有授权省公司(省团队)授权的用户(包括 benefitActive=false) + * 用于省团队权益分配计算,不要求省份匹配 + */ + findAuthProvinceByAccountSequences(accountSequences: string[]): Promise + /** + * 批量查询指定 accountSequence 列表中具有授权市公司(市团队)授权的用户(包括 benefitActive=false) + * 用于市团队权益分配计算,不要求城市匹配 + */ + findAuthCityByAccountSequences(accountSequences: string[]): Promise + /** + * 查找指定省份的正式省公司授权(用于省区域权益分配) + */ + findProvinceCompanyByRegion(provinceCode: string): Promise + /** + * 查找指定城市的正式市公司授权(用于市区域权益分配) + */ + findCityCompanyByRegion(cityCode: string): Promise +} 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 3a3bcc9d..acc2532e 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 @@ -6,7 +6,7 @@ import { IMonthlyAssessmentRepository, IAuthorizationRoleRepository } from '@/do export interface TeamStatistics { userId: string - accountSequence: bigint + accountSequence: string totalTeamPlantingCount: number selfPlantingCount: number /** 下级团队认种数(不包括自己)= totalTeamPlantingCount - selfPlantingCount */ @@ -17,7 +17,7 @@ export interface TeamStatistics { export interface ITeamStatisticsRepository { findByUserId(userId: string): Promise - findByAccountSequence(accountSequence: bigint): Promise + findByAccountSequence(accountSequence: string): Promise } export class AssessmentCalculatorService { 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 6f59b513..600bace6 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 @@ -3,7 +3,7 @@ import { DomainError } from '@/shared/exceptions' export class UserId { constructor( public readonly value: string, - public readonly accountSequence: bigint, + public readonly accountSequence: string, ) { if (!value) { throw new DomainError('用户ID不能为空') @@ -13,8 +13,8 @@ export class UserId { } } - static create(value: string, accountSequence: number | bigint): UserId { - return new UserId(value, BigInt(accountSequence)) + static create(value: string, accountSequence: string): UserId { + return new UserId(value, accountSequence) } equals(other: UserId): boolean { @@ -29,7 +29,7 @@ export class UserId { export class AdminUserId { constructor( public readonly value: string, - public readonly accountSequence: bigint, + public readonly accountSequence: string, ) { if (!value) { throw new DomainError('管理员ID不能为空') @@ -39,8 +39,8 @@ export class AdminUserId { } } - static create(value: string, accountSequence: number | bigint): AdminUserId { - return new AdminUserId(value, BigInt(accountSequence)) + static create(value: string, accountSequence: string): AdminUserId { + return new AdminUserId(value, accountSequence) } equals(other: AdminUserId): boolean { diff --git a/backend/services/authorization-service/src/infrastructure/external/referral-service.client.ts b/backend/services/authorization-service/src/infrastructure/external/referral-service.client.ts index e2806f78..487f0f95 100644 --- a/backend/services/authorization-service/src/infrastructure/external/referral-service.client.ts +++ b/backend/services/authorization-service/src/infrastructure/external/referral-service.client.ts @@ -21,7 +21,7 @@ interface ReferralTeamStatsResponse { * 推荐链数据接口 */ interface ReferralChainResponse { - accountSequence: number; + accountSequence: string; userId: string | null; ancestorPath: string[]; referrerId: string | null; @@ -31,8 +31,8 @@ interface ReferralChainResponse { * 团队成员数据接口 */ interface TeamMembersResponse { - accountSequence: number; - teamMembers: number[]; + accountSequence: string; + teamMembers: string[]; } /** @@ -41,7 +41,7 @@ interface TeamMembersResponse { class TeamStatisticsAdapter implements TeamStatistics { constructor( public readonly userId: string, - public readonly accountSequence: bigint, + public readonly accountSequence: string, public readonly totalTeamPlantingCount: number, public readonly selfPlantingCount: number, private readonly provinceCityDistribution: Record> | null, @@ -110,7 +110,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul async findByUserId(userId: string): Promise { if (!this.enabled) { this.logger.debug('[DISABLED] Referral service integration is disabled'); - return this.createEmptyStats(userId, BigInt(0)); + return this.createEmptyStats(userId, '0'); } try { @@ -122,7 +122,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul if (!response.data) { this.logger.debug(`[HTTP] No stats found for userId: ${userId}`); - return this.createEmptyStats(userId, BigInt(0)); + return this.createEmptyStats(userId, '0'); } const data = response.data; @@ -130,7 +130,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul return new TeamStatisticsAdapter( data.userId, - BigInt(data.accountSequence || 0), + data.accountSequence || '0', data.totalTeamPlantingCount, data.selfPlantingCount || 0, data.provinceCityDistribution, @@ -138,14 +138,14 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul } catch (error) { this.logger.error(`[HTTP] Failed to get stats for userId ${userId}:`, error); // 返回空数据而不是抛出错误,避免影响主流程 - return this.createEmptyStats(userId, BigInt(0)); + return this.createEmptyStats(userId, '0'); } } /** * 根据 accountSequence 查询团队统计 */ - async findByAccountSequence(accountSequence: bigint): Promise { + async findByAccountSequence(accountSequence: string): Promise { if (!this.enabled) { this.logger.debug('[DISABLED] Referral service integration is disabled'); return this.createEmptyStats('', accountSequence); @@ -168,7 +168,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul return new TeamStatisticsAdapter( data.userId, - BigInt(data.accountSequence || accountSequence.toString()), + data.accountSequence || accountSequence, data.totalTeamPlantingCount, data.selfPlantingCount || 0, data.provinceCityDistribution, @@ -183,7 +183,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul /** * 创建空的统计数据 */ - private createEmptyStats(userId: string, accountSequence: bigint): TeamStatistics { + private createEmptyStats(userId: string, accountSequence: string): TeamStatistics { return new TeamStatisticsAdapter(userId, accountSequence, 0, 0, null); } @@ -191,7 +191,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul * 获取用户的祖先链(推荐链) * 返回从直接推荐人到根节点的 accountSequence 列表 */ - async getReferralChain(accountSequence: bigint): Promise { + async getReferralChain(accountSequence: string): Promise { if (!this.enabled) { this.logger.debug('[DISABLED] Referral service integration is disabled'); return []; @@ -208,9 +208,8 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul return []; } - // ancestorPath 存储的是 userId (bigint string),我们需要映射到 accountSequence - // 由于 referral-service 中 userId = BigInt(accountSequence),可以直接转换 - return response.data.ancestorPath.map((id) => Number(id)); + // ancestorPath 存储的是 accountSequence string + return response.data.ancestorPath; } catch (error) { this.logger.error(`[HTTP] Failed to get referral chain for accountSequence ${accountSequence}:`, error); return []; @@ -220,7 +219,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul /** * 获取用户的团队成员 accountSequence 列表 */ - async getTeamMembers(accountSequence: bigint): Promise { + async getTeamMembers(accountSequence: string): Promise { if (!this.enabled) { this.logger.debug('[DISABLED] Referral service integration is disabled'); return []; 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 d6852c2e..f6072840 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 @@ -76,7 +76,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi ): Promise { const record = await this.prisma.authorizationRole.findFirst({ where: { - userId: BigInt(userId.value), + userId: userId.value, roleType: roleType, }, }) @@ -90,7 +90,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi ): Promise { const record = await this.prisma.authorizationRole.findFirst({ where: { - userId: BigInt(userId.value), + userId: userId.value, roleType: roleType, regionCode: regionCode.value, }, @@ -99,7 +99,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi } async findByAccountSequenceAndRoleType( - accountSequence: bigint, + accountSequence: string, roleType: RoleType, ): Promise { const record = await this.prisma.authorizationRole.findFirst({ @@ -113,13 +113,13 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi async findByUserId(userId: UserId): Promise { const records = await this.prisma.authorizationRole.findMany({ - where: { userId: BigInt(userId.value) }, + where: { userId: userId.value }, orderBy: { createdAt: 'desc' }, }) return records.map((record) => this.toDomain(record)) } - async findByAccountSequence(accountSequence: bigint): Promise { + async findByAccountSequence(accountSequence: string): Promise { const records = await this.prisma.authorizationRole.findMany({ where: { accountSequence: accountSequence }, orderBy: { createdAt: 'desc' }, @@ -156,7 +156,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi async findPendingByUserId(userId: UserId): Promise { const records = await this.prisma.authorizationRole.findMany({ where: { - userId: BigInt(userId.value), + userId: userId.value, status: AuthorizationStatus.PENDING, }, }) @@ -177,7 +177,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi } async findActiveCommunityByAccountSequences( - accountSequences: bigint[], + accountSequences: string[], ): Promise { if (accountSequences.length === 0) { return [] @@ -197,7 +197,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi } async findActiveProvinceByAccountSequencesAndRegion( - accountSequences: bigint[], + accountSequences: string[], provinceCode: string, ): Promise { if (accountSequences.length === 0) { @@ -218,7 +218,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi } async findActiveCityByAccountSequencesAndRegion( - accountSequences: bigint[], + accountSequences: string[], cityCode: string, ): Promise { if (accountSequences.length === 0) { @@ -239,7 +239,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi } async findCommunityByAccountSequences( - accountSequences: bigint[], + accountSequences: string[], ): Promise { if (accountSequences.length === 0) { return [] @@ -258,7 +258,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi } async findAuthProvinceByAccountSequencesAndRegion( - accountSequences: bigint[], + accountSequences: string[], provinceCode: string, ): Promise { if (accountSequences.length === 0) { @@ -279,7 +279,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi } async findAuthCityByAccountSequencesAndRegion( - accountSequences: bigint[], + accountSequences: string[], cityCode: string, ): Promise { if (accountSequences.length === 0) { @@ -300,7 +300,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi } async findAuthProvinceByAccountSequences( - accountSequences: bigint[], + accountSequences: string[], ): Promise { if (accountSequences.length === 0) { return [] @@ -320,7 +320,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi } async findAuthCityByAccountSequences( - accountSequences: bigint[], + accountSequences: string[], ): Promise { if (accountSequences.length === 0) { return [] @@ -364,16 +364,16 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi private toDomain(record: any): AuthorizationRole { const props: AuthorizationRoleProps = { authorizationId: AuthorizationId.create(record.id), - userId: UserId.create(record.userId.toString(), record.accountSequence), + userId: UserId.create(record.userId, 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.toString(), record.authorizedBy) : null, + authorizedBy: record.authorizedBy ? AdminUserId.create(record.authorizedBy, record.authorizedBy) : null, revokedAt: record.revokedAt, - revokedBy: record.revokedBy ? AdminUserId.create(record.revokedBy.toString(), record.revokedBy) : null, + revokedBy: record.revokedBy ? AdminUserId.create(record.revokedBy, 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 e9b5cb75..c5b0ead4 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 @@ -148,7 +148,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi async findByUserAndMonth(userId: UserId, month: Month): Promise { const records = await this.prisma.monthlyAssessment.findMany({ where: { - userId: BigInt(userId.value), + userId: userId.value, assessmentMonth: month.value, }, }) @@ -214,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.toString(), record.accountSequence), + userId: UserId.create(record.userId, record.accountSequence), roleType: record.roleType as RoleType, regionCode: RegionCode.create(record.regionCode), assessmentMonth: Month.create(record.assessmentMonth), @@ -233,7 +233,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi rankingInRegion: record.rankingInRegion, isFirstPlace: record.isFirstPlace, isBypassed: record.isBypassed, - bypassedBy: record.bypassedBy ? AdminUserId.create(record.bypassedBy.toString(), record.bypassedBy) : null, + bypassedBy: record.bypassedBy ? AdminUserId.create(record.bypassedBy, record.bypassedBy) : null, bypassedAt: record.bypassedAt, assessedAt: record.assessedAt, createdAt: record.createdAt, diff --git a/backend/services/authorization-service/src/shared/decorators/current-user.decorator.ts b/backend/services/authorization-service/src/shared/decorators/current-user.decorator.ts index a5770a16..85bcd4ff 100644 --- a/backend/services/authorization-service/src/shared/decorators/current-user.decorator.ts +++ b/backend/services/authorization-service/src/shared/decorators/current-user.decorator.ts @@ -1,15 +1,15 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common' - -export interface CurrentUserData { - userId: string - accountSequence?: number - walletAddress?: string - roles?: string[] -} - -export const CurrentUser = createParamDecorator( - (data: unknown, ctx: ExecutionContext): CurrentUserData => { - const request = ctx.switchToHttp().getRequest() - return request.user - }, -) +import { createParamDecorator, ExecutionContext } from '@nestjs/common' + +export interface CurrentUserData { + userId: string + accountSequence?: string + walletAddress?: string + roles?: string[] +} + +export const CurrentUser = createParamDecorator( + (data: unknown, ctx: ExecutionContext): CurrentUserData => { + const request = ctx.switchToHttp().getRequest() + return request.user + }, +) diff --git a/backend/services/authorization-service/src/shared/decorators/index.ts b/backend/services/authorization-service/src/shared/decorators/index.ts index 8510bdb3..8394bd7b 100644 --- a/backend/services/authorization-service/src/shared/decorators/index.ts +++ b/backend/services/authorization-service/src/shared/decorators/index.ts @@ -1,2 +1,2 @@ -export * from './current-user.decorator' -export * from './public.decorator' +export * from './current-user.decorator' +export * from './public.decorator' diff --git a/backend/services/authorization-service/src/shared/decorators/public.decorator.ts b/backend/services/authorization-service/src/shared/decorators/public.decorator.ts index b8f9e049..91c2399b 100644 --- a/backend/services/authorization-service/src/shared/decorators/public.decorator.ts +++ b/backend/services/authorization-service/src/shared/decorators/public.decorator.ts @@ -1,4 +1,4 @@ -import { SetMetadata } from '@nestjs/common' - -export const IS_PUBLIC_KEY = 'isPublic' -export const Public = () => SetMetadata(IS_PUBLIC_KEY, true) +import { SetMetadata } from '@nestjs/common' + +export const IS_PUBLIC_KEY = 'isPublic' +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true) diff --git a/backend/services/authorization-service/src/shared/exceptions/application.exception.ts b/backend/services/authorization-service/src/shared/exceptions/application.exception.ts index 24517dd6..b205742e 100644 --- a/backend/services/authorization-service/src/shared/exceptions/application.exception.ts +++ b/backend/services/authorization-service/src/shared/exceptions/application.exception.ts @@ -1,31 +1,31 @@ -import { HttpException, HttpStatus } from '@nestjs/common' - -export class ApplicationException extends HttpException { - constructor(message: string, status: HttpStatus = HttpStatus.BAD_REQUEST) { - super(message, status) - } -} - -export class ApplicationError extends ApplicationException { - constructor(message: string) { - super(message, HttpStatus.BAD_REQUEST) - } -} - -export class NotFoundError extends ApplicationException { - constructor(message: string) { - super(message, HttpStatus.NOT_FOUND) - } -} - -export class UnauthorizedError extends ApplicationException { - constructor(message: string) { - super(message, HttpStatus.UNAUTHORIZED) - } -} - -export class ForbiddenError extends ApplicationException { - constructor(message: string) { - super(message, HttpStatus.FORBIDDEN) - } -} +import { HttpException, HttpStatus } from '@nestjs/common' + +export class ApplicationException extends HttpException { + constructor(message: string, status: HttpStatus = HttpStatus.BAD_REQUEST) { + super(message, status) + } +} + +export class ApplicationError extends ApplicationException { + constructor(message: string) { + super(message, HttpStatus.BAD_REQUEST) + } +} + +export class NotFoundError extends ApplicationException { + constructor(message: string) { + super(message, HttpStatus.NOT_FOUND) + } +} + +export class UnauthorizedError extends ApplicationException { + constructor(message: string) { + super(message, HttpStatus.UNAUTHORIZED) + } +} + +export class ForbiddenError extends ApplicationException { + constructor(message: string) { + super(message, HttpStatus.FORBIDDEN) + } +} diff --git a/backend/services/authorization-service/src/shared/exceptions/domain.exception.ts b/backend/services/authorization-service/src/shared/exceptions/domain.exception.ts index d11bdc1d..fce7c547 100644 --- a/backend/services/authorization-service/src/shared/exceptions/domain.exception.ts +++ b/backend/services/authorization-service/src/shared/exceptions/domain.exception.ts @@ -1,13 +1,13 @@ -export class DomainException extends Error { - constructor(message: string) { - super(message) - this.name = 'DomainException' - } -} - -export class DomainError extends DomainException { - constructor(message: string) { - super(message) - this.name = 'DomainError' - } -} +export class DomainException extends Error { + constructor(message: string) { + super(message) + this.name = 'DomainException' + } +} + +export class DomainError extends DomainException { + constructor(message: string) { + super(message) + this.name = 'DomainError' + } +} diff --git a/backend/services/authorization-service/src/shared/exceptions/index.ts b/backend/services/authorization-service/src/shared/exceptions/index.ts index 8696c79c..91a36fc6 100644 --- a/backend/services/authorization-service/src/shared/exceptions/index.ts +++ b/backend/services/authorization-service/src/shared/exceptions/index.ts @@ -1,2 +1,2 @@ -export * from './domain.exception' -export * from './application.exception' +export * from './domain.exception' +export * from './application.exception' diff --git a/backend/services/authorization-service/src/shared/filters/global-exception.filter.ts b/backend/services/authorization-service/src/shared/filters/global-exception.filter.ts index 1dd73748..745b8adc 100644 --- a/backend/services/authorization-service/src/shared/filters/global-exception.filter.ts +++ b/backend/services/authorization-service/src/shared/filters/global-exception.filter.ts @@ -1,63 +1,63 @@ -import { - ExceptionFilter, - Catch, - ArgumentsHost, - HttpException, - HttpStatus, - Logger, -} from '@nestjs/common' -import { Request, Response } from 'express' -import { DomainException } from '@/shared/exceptions' - -@Catch() -export class GlobalExceptionFilter implements ExceptionFilter { - private readonly logger = new Logger(GlobalExceptionFilter.name) - - catch(exception: unknown, host: ArgumentsHost) { - const ctx = host.switchToHttp() - const response = ctx.getResponse() - const request = ctx.getRequest() - - let status: number - let message: string - let error: string - - if (exception instanceof HttpException) { - status = exception.getStatus() - const exceptionResponse = exception.getResponse() - message = - typeof exceptionResponse === 'string' - ? exceptionResponse - : (exceptionResponse as any).message || exception.message - error = exception.name - } else if (exception instanceof DomainException) { - status = HttpStatus.BAD_REQUEST - message = exception.message - error = 'DomainError' - } else if (exception instanceof Error) { - status = HttpStatus.INTERNAL_SERVER_ERROR - message = 'Internal server error' - error = exception.name - - this.logger.error( - `Unhandled exception: ${exception.message}`, - exception.stack, - ) - } else { - status = HttpStatus.INTERNAL_SERVER_ERROR - message = 'Internal server error' - error = 'UnknownError' - } - - const responseBody = { - statusCode: status, - timestamp: new Date().toISOString(), - path: request.url, - method: request.method, - error, - message: Array.isArray(message) ? message : [message], - } - - response.status(status).json(responseBody) - } -} +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common' +import { Request, Response } from 'express' +import { DomainException } from '@/shared/exceptions' + +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(GlobalExceptionFilter.name) + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp() + const response = ctx.getResponse() + const request = ctx.getRequest() + + let status: number + let message: string + let error: string + + if (exception instanceof HttpException) { + status = exception.getStatus() + const exceptionResponse = exception.getResponse() + message = + typeof exceptionResponse === 'string' + ? exceptionResponse + : (exceptionResponse as any).message || exception.message + error = exception.name + } else if (exception instanceof DomainException) { + status = HttpStatus.BAD_REQUEST + message = exception.message + error = 'DomainError' + } else if (exception instanceof Error) { + status = HttpStatus.INTERNAL_SERVER_ERROR + message = 'Internal server error' + error = exception.name + + this.logger.error( + `Unhandled exception: ${exception.message}`, + exception.stack, + ) + } else { + status = HttpStatus.INTERNAL_SERVER_ERROR + message = 'Internal server error' + error = 'UnknownError' + } + + const responseBody = { + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + method: request.method, + error, + message: Array.isArray(message) ? message : [message], + } + + response.status(status).json(responseBody) + } +} diff --git a/backend/services/authorization-service/src/shared/filters/index.ts b/backend/services/authorization-service/src/shared/filters/index.ts index 77a28e5c..554ea9d9 100644 --- a/backend/services/authorization-service/src/shared/filters/index.ts +++ b/backend/services/authorization-service/src/shared/filters/index.ts @@ -1 +1 @@ -export * from './global-exception.filter' +export * from './global-exception.filter' diff --git a/backend/services/authorization-service/src/shared/guards/index.ts b/backend/services/authorization-service/src/shared/guards/index.ts index af25359c..3a377e96 100644 --- a/backend/services/authorization-service/src/shared/guards/index.ts +++ b/backend/services/authorization-service/src/shared/guards/index.ts @@ -1 +1 @@ -export * from './jwt-auth.guard' +export * from './jwt-auth.guard' diff --git a/backend/services/authorization-service/src/shared/guards/jwt-auth.guard.ts b/backend/services/authorization-service/src/shared/guards/jwt-auth.guard.ts index 407e4605..550437c9 100644 --- a/backend/services/authorization-service/src/shared/guards/jwt-auth.guard.ts +++ b/backend/services/authorization-service/src/shared/guards/jwt-auth.guard.ts @@ -1,31 +1,31 @@ -import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common' -import { AuthGuard } from '@nestjs/passport' -import { Reflector } from '@nestjs/core' -import { IS_PUBLIC_KEY } from '@/shared/decorators' - -@Injectable() -export class JwtAuthGuard extends AuthGuard('jwt') { - constructor(private reflector: Reflector) { - super() - } - - canActivate(context: ExecutionContext) { - const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ - context.getHandler(), - context.getClass(), - ]) - - if (isPublic) { - return true - } - - return super.canActivate(context) - } - - handleRequest(err: any, user: any, info: any) { - if (err || !user) { - throw err || new UnauthorizedException('未授权访问') - } - return user - } -} +import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common' +import { AuthGuard } from '@nestjs/passport' +import { Reflector } from '@nestjs/core' +import { IS_PUBLIC_KEY } from '@/shared/decorators' + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor(private reflector: Reflector) { + super() + } + + canActivate(context: ExecutionContext) { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]) + + if (isPublic) { + return true + } + + return super.canActivate(context) + } + + handleRequest(err: any, user: any, info: any) { + if (err || !user) { + throw err || new UnauthorizedException('未授权访问') + } + return user + } +} diff --git a/backend/services/authorization-service/src/shared/interceptors/index.ts b/backend/services/authorization-service/src/shared/interceptors/index.ts index 79fcd5d1..7272128b 100644 --- a/backend/services/authorization-service/src/shared/interceptors/index.ts +++ b/backend/services/authorization-service/src/shared/interceptors/index.ts @@ -1 +1 @@ -export * from './transform.interceptor' +export * from './transform.interceptor' diff --git a/backend/services/authorization-service/src/shared/interceptors/transform.interceptor.ts b/backend/services/authorization-service/src/shared/interceptors/transform.interceptor.ts index 9df20cfa..3c96c86f 100644 --- a/backend/services/authorization-service/src/shared/interceptors/transform.interceptor.ts +++ b/backend/services/authorization-service/src/shared/interceptors/transform.interceptor.ts @@ -1,27 +1,27 @@ -import { - Injectable, - NestInterceptor, - ExecutionContext, - CallHandler, -} from '@nestjs/common' -import { Observable } from 'rxjs' -import { map } from 'rxjs/operators' - -export interface ApiResponse { - success: boolean - data: T - timestamp: string -} - -@Injectable() -export class TransformInterceptor implements NestInterceptor> { - intercept(context: ExecutionContext, next: CallHandler): Observable> { - return next.handle().pipe( - map((data) => ({ - success: true, - data, - timestamp: new Date().toISOString(), - })), - ) - } -} +import { + Injectable, + NestInterceptor, + ExecutionContext, + CallHandler, +} from '@nestjs/common' +import { Observable } from 'rxjs' +import { map } from 'rxjs/operators' + +export interface ApiResponse { + success: boolean + data: T + timestamp: string +} + +@Injectable() +export class TransformInterceptor implements NestInterceptor> { + intercept(context: ExecutionContext, next: CallHandler): Observable> { + return next.handle().pipe( + map((data) => ({ + success: true, + data, + timestamp: new Date().toISOString(), + })), + ) + } +} diff --git a/backend/services/authorization-service/src/shared/strategies/index.ts b/backend/services/authorization-service/src/shared/strategies/index.ts index 8033d64f..b742f6b4 100644 --- a/backend/services/authorization-service/src/shared/strategies/index.ts +++ b/backend/services/authorization-service/src/shared/strategies/index.ts @@ -1 +1 @@ -export * from './jwt.strategy' +export * from './jwt.strategy' diff --git a/backend/services/authorization-service/src/shared/strategies/jwt.strategy.ts b/backend/services/authorization-service/src/shared/strategies/jwt.strategy.ts index 6dfc09c7..2e75f61d 100644 --- a/backend/services/authorization-service/src/shared/strategies/jwt.strategy.ts +++ b/backend/services/authorization-service/src/shared/strategies/jwt.strategy.ts @@ -1,41 +1,41 @@ -import { Injectable } from '@nestjs/common' -import { PassportStrategy } from '@nestjs/passport' -import { ExtractJwt, Strategy } from 'passport-jwt' -import { ConfigService } from '@nestjs/config' - -export interface JwtPayload { - // Identity-service uses 'userId' field - userId: string - accountSequence?: number - deviceId?: string - type?: string - // Legacy support for 'sub' field - sub?: string - walletAddress?: string - roles?: string[] - iat?: number - exp?: number -} - -@Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(private configService: ConfigService) { - super({ - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - ignoreExpiration: false, - secretOrKey: configService.get('JWT_SECRET'), - }) - } - - async validate(payload: JwtPayload) { - // Support both 'userId' (from identity-service) and 'sub' (legacy) - const userId = payload.userId || payload.sub - return { - userId, - accountSequence: payload.accountSequence, - deviceId: payload.deviceId, - walletAddress: payload.walletAddress, - roles: payload.roles, - } - } -} +import { Injectable } from '@nestjs/common' +import { PassportStrategy } from '@nestjs/passport' +import { ExtractJwt, Strategy } from 'passport-jwt' +import { ConfigService } from '@nestjs/config' + +export interface JwtPayload { + // Identity-service uses 'userId' field + userId: string + accountSequence?: string + deviceId?: string + type?: string + // Legacy support for 'sub' field + sub?: string + walletAddress?: string + roles?: string[] + iat?: number + exp?: number +} + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor(private configService: ConfigService) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.get('JWT_SECRET'), + }) + } + + async validate(payload: JwtPayload) { + // Support both 'userId' (from identity-service) and 'sub' (legacy) + const userId = payload.userId || payload.sub + return { + userId, + accountSequence: payload.accountSequence, + deviceId: payload.deviceId, + walletAddress: payload.walletAddress, + roles: payload.roles, + } + } +} diff --git a/backend/services/backup-service/prisma/schema.prisma b/backend/services/backup-service/prisma/schema.prisma index 8f173d65..5a82da15 100644 --- a/backend/services/backup-service/prisma/schema.prisma +++ b/backend/services/backup-service/prisma/schema.prisma @@ -14,7 +14,7 @@ model BackupShare { // 用户标识 (来自 identity-service) userId BigInt @unique @map("user_id") - accountSequence BigInt @unique @map("account_sequence") + accountSequence String @unique @map("account_sequence") // 格式: D + YYMMDD + 5位序号 // MPC 密钥信息 publicKey String @unique @map("public_key") @db.VarChar(130) diff --git a/backend/services/backup-service/src/api/dto/request/store-share.dto.ts b/backend/services/backup-service/src/api/dto/request/store-share.dto.ts index 528dd051..ac7d6c90 100644 --- a/backend/services/backup-service/src/api/dto/request/store-share.dto.ts +++ b/backend/services/backup-service/src/api/dto/request/store-share.dto.ts @@ -14,9 +14,8 @@ export class StoreShareDto { userId: string; @IsNotEmpty() - @IsNumber() - @Min(1) - accountSequence: number; + @IsString() + accountSequence: string; @IsNotEmpty() @IsString() diff --git a/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.command.ts b/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.command.ts index 6cda7077..3a9bd658 100644 --- a/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.command.ts +++ b/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.command.ts @@ -1,7 +1,7 @@ export class StoreBackupShareCommand { constructor( public readonly userId: string, - public readonly accountSequence: number, + public readonly accountSequence: string, public readonly publicKey: string, public readonly encryptedShareData: string, public readonly sourceService: string, diff --git a/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.handler.ts b/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.handler.ts index 4c9e9825..6a354e13 100644 --- a/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.handler.ts +++ b/backend/services/backup-service/src/application/commands/store-backup-share/store-backup-share.handler.ts @@ -53,7 +53,7 @@ export class StoreBackupShareHandler { // Create domain entity const share = BackupShare.create({ userId, - accountSequence: BigInt(command.accountSequence), + accountSequence: command.accountSequence, publicKey: command.publicKey, encryptedShareData: encrypted, encryptionKeyId: keyId, diff --git a/backend/services/backup-service/src/domain/entities/backup-share.entity.ts b/backend/services/backup-service/src/domain/entities/backup-share.entity.ts index 3dfdeba1..c8f9b07b 100644 --- a/backend/services/backup-service/src/domain/entities/backup-share.entity.ts +++ b/backend/services/backup-service/src/domain/entities/backup-share.entity.ts @@ -9,7 +9,7 @@ export enum BackupShareStatus { export interface BackupShareProps { shareId: bigint | null; userId: bigint; - accountSequence: bigint; + accountSequence: string; publicKey: string; partyIndex: number; threshold: number; @@ -27,7 +27,7 @@ export interface BackupShareProps { export class BackupShare { private _shareId: bigint | null; private readonly _userId: bigint; - private readonly _accountSequence: bigint; + private readonly _accountSequence: string; private readonly _publicKey: string; private readonly _partyIndex: number; private readonly _threshold: number; @@ -61,7 +61,7 @@ export class BackupShare { static create(params: { userId: bigint; - accountSequence: bigint; + accountSequence: string; publicKey: string; encryptedShareData: string; encryptionKeyId: string; @@ -131,7 +131,7 @@ export class BackupShare { get userId(): bigint { return this._userId; } - get accountSequence(): bigint { + get accountSequence(): string { return this._accountSequence; } get publicKey(): string { diff --git a/backend/services/backup-service/src/domain/repositories/backup-share.repository.interface.ts b/backend/services/backup-service/src/domain/repositories/backup-share.repository.interface.ts index 9f8164a7..71db41b8 100644 --- a/backend/services/backup-service/src/domain/repositories/backup-share.repository.interface.ts +++ b/backend/services/backup-service/src/domain/repositories/backup-share.repository.interface.ts @@ -11,6 +11,6 @@ export interface BackupShareRepository { userId: bigint, publicKey: string, ): Promise; - findByAccountSequence(accountSequence: bigint): Promise; + findByAccountSequence(accountSequence: string): Promise; delete(shareId: bigint): Promise; } diff --git a/backend/services/backup-service/src/infrastructure/persistence/repositories/backup-share.repository.impl.ts b/backend/services/backup-service/src/infrastructure/persistence/repositories/backup-share.repository.impl.ts index 20f01722..c6cb28ea 100644 --- a/backend/services/backup-service/src/infrastructure/persistence/repositories/backup-share.repository.impl.ts +++ b/backend/services/backup-service/src/infrastructure/persistence/repositories/backup-share.repository.impl.ts @@ -86,7 +86,7 @@ export class BackupShareRepositoryImpl implements BackupShareRepository { } async findByAccountSequence( - accountSequence: bigint, + accountSequence: string, ): Promise { const record = await this.prisma.backupShare.findUnique({ where: { accountSequence }, diff --git a/backend/services/blockchain-service/prisma/schema.prisma b/backend/services/blockchain-service/prisma/schema.prisma index 2f01e786..22f13989 100644 --- a/backend/services/blockchain-service/prisma/schema.prisma +++ b/backend/services/blockchain-service/prisma/schema.prisma @@ -15,7 +15,7 @@ datasource db { // 存储需要监听充值的地址(用户地址和系统账户地址) // ============================================ model MonitoredAddress { - id BigInt @id @default(autoincrement()) @map("address_id") + id BigInt @id @default(autoincrement()) @map("address_id") chainType String @map("chain_type") @db.VarChar(20) // KAVA, BSC address String @db.VarChar(42) // 0x地址 @@ -24,7 +24,7 @@ model MonitoredAddress { addressType String @default("USER") @map("address_type") @db.VarChar(20) // 用户地址关联 (addressType = USER 时使用) - accountSequence BigInt? @map("account_sequence") // 跨服务关联标识 + accountSequence String? @map("account_sequence") @db.VarChar(20) // 跨服务关联标识 (格式: D + YYMMDD + 5位序号) userId BigInt? @map("user_id") // 保留兼容 // 系统账户关联 (addressType = SYSTEM 时使用) @@ -74,11 +74,11 @@ model DepositTransaction { status String @default("DETECTED") @db.VarChar(20) // DETECTED, CONFIRMING, CONFIRMED, NOTIFIED // 关联 - 使用 accountSequence 作为跨服务主键 - addressId BigInt @map("address_id") - addressType String @default("USER") @map("address_type") @db.VarChar(20) // USER 或 SYSTEM + addressId BigInt @map("address_id") + addressType String @default("USER") @map("address_type") @db.VarChar(20) // USER 或 SYSTEM // 用户地址关联 - accountSequence BigInt? @map("account_sequence") // 跨服务关联标识 + accountSequence String? @map("account_sequence") @db.VarChar(20) // 跨服务关联标识 (格式: D + YYMMDD + 5位序号) userId BigInt? @map("user_id") // 保留兼容 // 系统账户关联(当 addressType = SYSTEM 时) @@ -174,26 +174,26 @@ model TransactionRequest { // 与账户序列号关联,用于账户恢复验证 // ============================================ model RecoveryMnemonic { - id BigInt @id @default(autoincrement()) - accountSequence Int @map("account_sequence") // 8位账户序列号 - publicKey String @map("public_key") @db.VarChar(130) // 关联的钱包公钥 + id BigInt @id @default(autoincrement()) + accountSequence String @map("account_sequence") @db.VarChar(20) // 账户序列号 (格式: D + YYMMDD + 5位序号) + publicKey String @map("public_key") @db.VarChar(130) // 关联的钱包公钥 // 助记词存储 (加密) - encryptedMnemonic String @map("encrypted_mnemonic") @db.Text // AES加密的助记词 - mnemonicHash String @map("mnemonic_hash") @db.VarChar(64) // SHA256哈希(用于验证) + encryptedMnemonic String @map("encrypted_mnemonic") @db.Text // AES加密的助记词 + mnemonicHash String @map("mnemonic_hash") @db.VarChar(64) // SHA256哈希(用于验证) // 状态管理 - status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, REPLACED - isBackedUp Boolean @default(false) @map("is_backed_up") // 用户是否已备份 + status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, REPLACED + isBackedUp Boolean @default(false) @map("is_backed_up") // 用户是否已备份 // 挂失/更换相关 - revokedAt DateTime? @map("revoked_at") - revokedReason String? @map("revoked_reason") @db.VarChar(200) - replacedById BigInt? @map("replaced_by_id") // 被哪个新助记词替代 + revokedAt DateTime? @map("revoked_at") + revokedReason String? @map("revoked_reason") @db.VarChar(200) + replacedById BigInt? @map("replaced_by_id") // 被哪个新助记词替代 - createdAt DateTime @default(now()) @map("created_at") + createdAt DateTime @default(now()) @map("created_at") - @@unique([accountSequence, status], name: "uk_account_active_mnemonic") // 一个账户只有一个ACTIVE助记词 + @@unique([accountSequence, status], name: "uk_account_active_mnemonic") // 一个账户只有一个ACTIVE助记词 @@index([accountSequence], name: "idx_recovery_account") @@index([publicKey], name: "idx_recovery_public_key") @@index([status], name: "idx_recovery_status") @@ -217,15 +217,15 @@ model OutboxEvent { status String @default("PENDING") @db.VarChar(20) // 重试信息 - retryCount Int @default(0) @map("retry_count") - maxRetries Int @default(10) @map("max_retries") - lastError String? @map("last_error") @db.Text + retryCount Int @default(0) @map("retry_count") + maxRetries Int @default(10) @map("max_retries") + lastError String? @map("last_error") @db.Text nextRetryAt DateTime? @map("next_retry_at") // 时间戳 - createdAt DateTime @default(now()) @map("created_at") - sentAt DateTime? @map("sent_at") - ackedAt DateTime? @map("acked_at") + createdAt DateTime @default(now()) @map("created_at") + sentAt DateTime? @map("sent_at") + ackedAt DateTime? @map("acked_at") @@index([status, nextRetryAt], name: "idx_outbox_pending") @@index([aggregateType, aggregateId], name: "idx_outbox_aggregate") diff --git a/backend/services/blockchain-service/src/api/dto/request/derive-address.dto.ts b/backend/services/blockchain-service/src/api/dto/request/derive-address.dto.ts index 31d27e3e..81639c07 100644 --- a/backend/services/blockchain-service/src/api/dto/request/derive-address.dto.ts +++ b/backend/services/blockchain-service/src/api/dto/request/derive-address.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsNumberString, IsInt } from 'class-validator'; +import { IsString, IsNumberString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class DeriveAddressDto { @@ -6,9 +6,9 @@ export class DeriveAddressDto { @IsNumberString() userId: string; - @ApiProperty({ description: '账户序列号 (8位数字)', example: 10000001 }) - @IsInt() - accountSequence: number; + @ApiProperty({ description: '账户序列号 (格式: D + YYMMDD + 5位序号)', example: 'D2512110008' }) + @IsString() + accountSequence: string; @ApiProperty({ description: '压缩公钥 (33 bytes, 0x02/0x03 开头)', diff --git a/backend/services/blockchain-service/src/api/dto/request/mark-mnemonic-backup.dto.ts b/backend/services/blockchain-service/src/api/dto/request/mark-mnemonic-backup.dto.ts index f078d8a8..063bc11d 100644 --- a/backend/services/blockchain-service/src/api/dto/request/mark-mnemonic-backup.dto.ts +++ b/backend/services/blockchain-service/src/api/dto/request/mark-mnemonic-backup.dto.ts @@ -1,8 +1,8 @@ -import { IsInt } from 'class-validator'; +import { IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class MarkMnemonicBackupDto { - @ApiProperty({ description: '账户序列号 (8位数字)', example: 10000001 }) - @IsInt() - accountSequence: number; + @ApiProperty({ description: '账户序列号 (格式: D + YYMMDD + 5位序号)', example: 'D2512110008' }) + @IsString() + accountSequence: string; } diff --git a/backend/services/blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts b/backend/services/blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts index 3ed3e6ac..b95a741b 100644 --- a/backend/services/blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts +++ b/backend/services/blockchain-service/src/api/dto/request/verify-mnemonic-hash.dto.ts @@ -1,13 +1,13 @@ -import { IsString, IsInt } from 'class-validator'; +import { IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; export class VerifyMnemonicHashDto { @ApiProperty({ - description: '账户序列号 (8位数字)', - example: 10000001, + description: '账户序列号 (格式: D + YYMMDD + 5位序号)', + example: 'D2512110008', }) - @IsInt() - accountSequence: number; + @IsString() + accountSequence: string; @ApiProperty({ description: '助记词 (12个单词,空格分隔)', diff --git a/backend/services/blockchain-service/src/application/event-handlers/mpc-keygen-completed.handler.ts b/backend/services/blockchain-service/src/application/event-handlers/mpc-keygen-completed.handler.ts index b8eb486f..201f444c 100644 --- a/backend/services/blockchain-service/src/application/event-handlers/mpc-keygen-completed.handler.ts +++ b/backend/services/blockchain-service/src/application/event-handlers/mpc-keygen-completed.handler.ts @@ -58,7 +58,7 @@ export class MpcKeygenCompletedHandler implements OnModuleInit { const result = await this.addressDerivationService.deriveAndRegister({ userId: BigInt(userId), - accountSequence: Number(accountSequence), + accountSequence: accountSequence, publicKey, }); diff --git a/backend/services/blockchain-service/src/application/services/address-derivation.service.ts b/backend/services/blockchain-service/src/application/services/address-derivation.service.ts index 6e07cd20..1f6c738f 100644 --- a/backend/services/blockchain-service/src/application/services/address-derivation.service.ts +++ b/backend/services/blockchain-service/src/application/services/address-derivation.service.ts @@ -18,13 +18,13 @@ import { ChainTypeEnum } from '@/domain/enums'; export interface DeriveAddressParams { userId: bigint; - accountSequence: number; + accountSequence: string; publicKey: string; } export interface DeriveAddressResult { userId: bigint; - accountSequence: number; + accountSequence: string; publicKey: string; addresses: DerivedAddress[]; } @@ -146,7 +146,7 @@ export class AddressDerivationService { */ private async registerEvmAddressForMonitoring( userId: bigint, - accountSequence: number, + accountSequence: string, derived: DerivedAddress, ): Promise { const chainType = ChainType.fromEnum(derived.chainType); @@ -159,7 +159,7 @@ export class AddressDerivationService { const monitored = MonitoredAddress.create({ chainType, address, - accountSequence: BigInt(accountSequence), + accountSequence, userId, }); diff --git a/backend/services/blockchain-service/src/application/services/deposit-repair.service.ts b/backend/services/blockchain-service/src/application/services/deposit-repair.service.ts index 4c7655ee..9a98b6d7 100644 --- a/backend/services/blockchain-service/src/application/services/deposit-repair.service.ts +++ b/backend/services/blockchain-service/src/application/services/deposit-repair.service.ts @@ -60,7 +60,7 @@ export class DepositRepairService { id: d.id?.toString() ?? '', txHash: d.txHash.toString(), userId: d.userId.toString(), - accountSequence: d.accountSequence.toString(), + accountSequence: d.accountSequence, amount: d.amount.toFixed(6), confirmedAt: d.createdAt?.toISOString() ?? '', })), @@ -99,7 +99,7 @@ export class DepositRepairService { amount: deposit.amount.raw.toString(), amountFormatted: deposit.amount.toFixed(8), confirmations: deposit.confirmations, - accountSequence: deposit.accountSequence.toString(), + accountSequence: deposit.accountSequence, userId: deposit.userId.toString(), }); diff --git a/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts b/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts index 8965da00..163a8e56 100644 --- a/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts +++ b/backend/services/blockchain-service/src/application/services/mnemonic-verification.service.ts @@ -3,7 +3,7 @@ import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.servic import { RecoveryMnemonicAdapter } from '@/infrastructure/blockchain/recovery-mnemonic.adapter'; export interface VerifyMnemonicByAccountParams { - accountSequence: number; + accountSequence: string; mnemonic: string; } @@ -64,7 +64,7 @@ export class MnemonicVerificationService { * 保存助记词记录(创建账户时调用) */ async saveRecoveryMnemonic(params: { - accountSequence: number; + accountSequence: string; publicKey: string; encryptedMnemonic: string; mnemonicHash: string; @@ -88,7 +88,7 @@ export class MnemonicVerificationService { /** * 标记助记词已备份 */ - async markAsBackedUp(accountSequence: number): Promise { + async markAsBackedUp(accountSequence: string): Promise { await this.prisma.recoveryMnemonic.updateMany({ where: { accountSequence, diff --git a/backend/services/blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts b/backend/services/blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts index 5a8a9881..1d5ef625 100644 --- a/backend/services/blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts +++ b/backend/services/blockchain-service/src/domain/aggregates/deposit-transaction/deposit-transaction.aggregate.ts @@ -17,7 +17,7 @@ export interface DepositTransactionProps { confirmations: number; status: DepositStatus; addressId: bigint; - accountSequence: bigint; // 跨服务关联标识 + accountSequence: string; // 跨服务关联标识 (格式: D + YYMMDD + 5位序号) userId: bigint; // 保留兼容 notifiedAt?: Date; notifyAttempts: number; @@ -74,7 +74,7 @@ export class DepositTransaction extends AggregateRoot { get addressId(): bigint { return this.props.addressId; } - get accountSequence(): bigint { + get accountSequence(): string { return this.props.accountSequence; } get userId(): bigint { @@ -117,7 +117,7 @@ export class DepositTransaction extends AggregateRoot { blockTimestamp: Date; logIndex: number; addressId: bigint; - accountSequence: bigint; + accountSequence: string; userId: bigint; }): DepositTransaction { const deposit = new DepositTransaction({ @@ -139,7 +139,7 @@ export class DepositTransaction extends AggregateRoot { amountFormatted: params.amount.toFixed(8), blockNumber: params.blockNumber.toString(), blockTimestamp: params.blockTimestamp.toISOString(), - accountSequence: params.accountSequence.toString(), + accountSequence: params.accountSequence, userId: params.userId.toString(), }), ); @@ -188,7 +188,7 @@ export class DepositTransaction extends AggregateRoot { amount: this.props.amount.raw.toString(), amountFormatted: this.props.amount.toFixed(8), confirmations: this.props.confirmations, - accountSequence: this.props.accountSequence.toString(), + accountSequence: this.props.accountSequence, userId: this.props.userId.toString(), }), ); diff --git a/backend/services/blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts b/backend/services/blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts index cdecd0d4..d79c3406 100644 --- a/backend/services/blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts +++ b/backend/services/blockchain-service/src/domain/aggregates/monitored-address/monitored-address.aggregate.ts @@ -5,7 +5,7 @@ export interface MonitoredAddressProps { id?: bigint; chainType: ChainType; address: EvmAddress; - accountSequence: bigint; // 跨服务关联标识 (全局唯一业务ID) + accountSequence: string; // 跨服务关联标识 (格式: D + YYMMDD + 5位序号) userId: bigint; // 保留兼容 isActive: boolean; createdAt?: Date; @@ -30,7 +30,7 @@ export class MonitoredAddress extends AggregateRoot { get address(): EvmAddress { return this.props.address; } - get accountSequence(): bigint { + get accountSequence(): string { return this.props.accountSequence; } get userId(): bigint { @@ -52,7 +52,7 @@ export class MonitoredAddress extends AggregateRoot { static create(params: { chainType: ChainType; address: EvmAddress; - accountSequence: bigint; + accountSequence: string; userId: bigint; }): MonitoredAddress { return new MonitoredAddress({ diff --git a/backend/services/blockchain-service/src/domain/events/wallet-address-created.event.ts b/backend/services/blockchain-service/src/domain/events/wallet-address-created.event.ts index 4aefac7e..8157a613 100644 --- a/backend/services/blockchain-service/src/domain/events/wallet-address-created.event.ts +++ b/backend/services/blockchain-service/src/domain/events/wallet-address-created.event.ts @@ -2,7 +2,7 @@ import { DomainEvent } from './domain-event.base'; export interface WalletAddressCreatedPayload { userId: string; - accountSequence: number; // 8位账户序列号 + accountSequence: string; // 账户序列号 (格式: D + YYMMDD + 5位序号) publicKey: string; addresses: { chainType: string; diff --git a/backend/services/blockchain-service/src/infrastructure/kafka/mpc-event-consumer.service.ts b/backend/services/blockchain-service/src/infrastructure/kafka/mpc-event-consumer.service.ts index 92f37097..36c99e88 100644 --- a/backend/services/blockchain-service/src/infrastructure/kafka/mpc-event-consumer.service.ts +++ b/backend/services/blockchain-service/src/infrastructure/kafka/mpc-event-consumer.service.ts @@ -23,7 +23,7 @@ export interface KeygenCompletedPayload { threshold: string; extraPayload?: { userId: string; - accountSequence: number; // 8位账户序列号,用于关联恢复助记词 + accountSequence: string; // 账户序列号 (格式: D + YYMMDD + 5位序号) username: string; delegateShare?: { partyId: string; diff --git a/backend/services/blockchain-service/src/shared/strategies/jwt.strategy.ts b/backend/services/blockchain-service/src/shared/strategies/jwt.strategy.ts index e03998b5..1bd12a12 100644 --- a/backend/services/blockchain-service/src/shared/strategies/jwt.strategy.ts +++ b/backend/services/blockchain-service/src/shared/strategies/jwt.strategy.ts @@ -5,7 +5,7 @@ import { ConfigService } from '@nestjs/config'; interface JwtPayload { userId: string; - accountSequence: number; + accountSequence: string; deviceId: string; type: 'access' | 'refresh'; iat: number; diff --git a/backend/services/identity-service/prisma/schema.prisma b/backend/services/identity-service/prisma/schema.prisma index 84c26503..b8e5e01d 100644 --- a/backend/services/identity-service/prisma/schema.prisma +++ b/backend/services/identity-service/prisma/schema.prisma @@ -9,13 +9,13 @@ datasource db { model UserAccount { userId BigInt @id @default(autoincrement()) @map("user_id") - accountSequence BigInt @unique @map("account_sequence") + accountSequence String @unique @map("account_sequence") @db.VarChar(12) // 格式: D + YYMMDD + 5位序号 phoneNumber String? @unique @map("phone_number") @db.VarChar(20) nickname String @db.VarChar(100) avatarUrl String? @map("avatar_url") @db.Text - inviterSequence BigInt? @map("inviter_sequence") + inviterSequence String? @map("inviter_sequence") @db.VarChar(12) // 推荐人序列号 referralCode String @unique @map("referral_code") @db.VarChar(10) kycStatus String @default("NOT_VERIFIED") @map("kyc_status") @db.VarChar(20) @@ -102,10 +102,11 @@ model WalletAddress { } model AccountSequenceGenerator { - id Int @id @default(1) - currentSequence BigInt @default(0) @map("current_sequence") + id Int @id @default(autoincrement()) + dateKey String @unique @map("date_key") @db.VarChar(6) // 格式: YYMMDD + currentSequence Int @default(0) @map("current_sequence") // 当日序号 (0-99999) updatedAt DateTime @updatedAt @map("updated_at") - + @@map("account_sequence_generator") } diff --git a/backend/services/identity-service/prisma/seed.ts b/backend/services/identity-service/prisma/seed.ts index 67aa0c94..07623050 100644 --- a/backend/services/identity-service/prisma/seed.ts +++ b/backend/services/identity-service/prisma/seed.ts @@ -1,91 +1,91 @@ -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -// ============================================ -// 系统账户定义 -// ============================================ -const SYSTEM_ACCOUNTS = [ - { - userId: BigInt(1), - accountSequence: BigInt(1), - nickname: '总部社区', - referralCode: 'HQ000001', - provinceCode: '000000', - cityCode: '000000', - status: 'SYSTEM', - }, - { - userId: BigInt(2), - accountSequence: BigInt(2), - nickname: '成本费账户', - referralCode: 'COST0002', - provinceCode: '000000', - cityCode: '000000', - status: 'SYSTEM', - }, - { - userId: BigInt(3), - accountSequence: BigInt(3), - nickname: '运营费账户', - referralCode: 'OPER0003', - provinceCode: '000000', - cityCode: '000000', - status: 'SYSTEM', - }, - { - userId: BigInt(4), - accountSequence: BigInt(4), - nickname: 'RWAD底池账户', - referralCode: 'POOL0004', - provinceCode: '000000', - cityCode: '000000', - status: 'SYSTEM', - }, -]; - -async function main() { - console.log('Seeding database...'); - - // 清理现有数据 - await prisma.deadLetterEvent.deleteMany(); - await prisma.smsCode.deleteMany(); - await prisma.userEvent.deleteMany(); - await prisma.deviceToken.deleteMany(); - await prisma.walletAddress.deleteMany(); - await prisma.userDevice.deleteMany(); - await prisma.userAccount.deleteMany(); - - // 初始化账户序列号生成器 (从100000开始,系统账户使用1-99) - await prisma.accountSequenceGenerator.deleteMany(); - await prisma.accountSequenceGenerator.create({ - data: { - id: 1, - currentSequence: BigInt(100000), // 普通用户从100000开始 - }, - }); - - // 创建系统账户 - console.log('Creating system accounts...'); - for (const account of SYSTEM_ACCOUNTS) { - await prisma.userAccount.upsert({ - where: { userId: account.userId }, - update: account, - create: account, - }); - console.log(` - Created system account: ${account.nickname} (userId=${account.userId})`); - } - - console.log('Database seeded successfully!'); - console.log('- Initialized account sequence generator starting at 100000'); - console.log(`- Created ${SYSTEM_ACCOUNTS.length} system accounts (userId 1-4)`); -} - -main() - .catch((e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +// ============================================ +// 系统账户定义 +// 系统账户使用特殊序列号格式: S + 00000 + 序号 (S0000000001 ~ S0000000004) +// ============================================ +const SYSTEM_ACCOUNTS = [ + { + userId: BigInt(1), + accountSequence: 'S0000000001', // 总部社区 + nickname: '总部社区', + referralCode: 'HQ000001', + status: 'SYSTEM', + }, + { + userId: BigInt(2), + accountSequence: 'S0000000002', // 成本费账户 + nickname: '成本费账户', + referralCode: 'COST0002', + status: 'SYSTEM', + }, + { + userId: BigInt(3), + accountSequence: 'S0000000003', // 运营费账户 + nickname: '运营费账户', + referralCode: 'OPER0003', + status: 'SYSTEM', + }, + { + userId: BigInt(4), + accountSequence: 'S0000000004', // RWAD底池账户 + nickname: 'RWAD底池账户', + referralCode: 'POOL0004', + status: 'SYSTEM', + }, +]; + +async function main() { + console.log('Seeding database...'); + + // 清理现有数据 + await prisma.deadLetterEvent.deleteMany(); + await prisma.smsCode.deleteMany(); + await prisma.userEvent.deleteMany(); + await prisma.deviceToken.deleteMany(); + await prisma.walletAddress.deleteMany(); + await prisma.userDevice.deleteMany(); + await prisma.userAccount.deleteMany(); + + // 初始化账户序列号生成器 (新格式: D + YYMMDD + 5位序号) + await prisma.accountSequenceGenerator.deleteMany(); + const today = new Date(); + const year = String(today.getFullYear()).slice(-2); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + const dateKey = `${year}${month}${day}`; + + await prisma.accountSequenceGenerator.create({ + data: { + id: 1, + dateKey: dateKey, + currentSequence: 0, + }, + }); + + // 创建系统账户 + console.log('Creating system accounts...'); + for (const account of SYSTEM_ACCOUNTS) { + await prisma.userAccount.upsert({ + where: { userId: account.userId }, + update: account, + create: account, + }); + console.log(` - Created system account: ${account.nickname} (accountSequence=${account.accountSequence})`); + } + + console.log('Database seeded successfully!'); + console.log(`- Initialized account sequence generator for date ${dateKey}`); + console.log(`- Created ${SYSTEM_ACCOUNTS.length} system accounts (S0000000001-S0000000004)`); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/backend/services/identity-service/src/api/dto/index.ts b/backend/services/identity-service/src/api/dto/index.ts index 2e521c34..43568eca 100644 --- a/backend/services/identity-service/src/api/dto/index.ts +++ b/backend/services/identity-service/src/api/dto/index.ts @@ -112,8 +112,8 @@ export class RemoveDeviceDto { // Response DTOs export class AutoCreateAccountResponseDto { - @ApiProperty({ example: 100001, description: '用户序列号 (唯一标识)' }) - userSerialNum: number; + @ApiProperty({ example: 'D2512110001', description: '用户序列号 (格式: D + YYMMDD + 5位序号)' }) + userSerialNum: string; @ApiProperty({ example: 'ABC123', description: '推荐码' }) referralCode: string; @@ -135,8 +135,8 @@ export class RecoverAccountResponseDto { @ApiProperty() userId: string; - @ApiProperty() - accountSequence: number; + @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' }) + accountSequence: string; @ApiProperty() nickname: string; @@ -188,8 +188,8 @@ export class LoginResponseDto { @ApiProperty() userId: string; - @ApiProperty() - accountSequence: number; + @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' }) + accountSequence: string; @ApiProperty() accessToken: string; @@ -216,8 +216,8 @@ export class MeResponseDto { @ApiProperty() userId: string; - @ApiProperty({ description: '账户序列号' }) - accountSequence: number; + @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' }) + accountSequence: string; @ApiProperty({ nullable: true }) phoneNumber: string | null; @@ -234,8 +234,8 @@ export class MeResponseDto { @ApiProperty({ description: '完整推荐链接' }) referralLink: string; - @ApiProperty({ description: '推荐人序列号', nullable: true }) - inviterSequence: number | null; + @ApiProperty({ example: 'D2512110001', description: '推荐人序列号', nullable: true }) + inviterSequence: string | null; @ApiProperty({ description: '钱包地址列表' }) walletAddresses: Array<{ chainType: string; address: string }>; @@ -259,7 +259,7 @@ export class ReferralValidationResponseDto { @ApiPropertyOptional({ description: '邀请人信息' }) inviterInfo?: { - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 nickname: string; avatarUrl: string | null; }; @@ -292,8 +292,8 @@ export class ReferralLinkResponseDto { } export class InviteRecordDto { - @ApiProperty() - accountSequence: number; + @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' }) + accountSequence: string; @ApiProperty() nickname: string; diff --git a/backend/services/identity-service/src/api/dto/request/recover-by-mnemonic.dto.ts b/backend/services/identity-service/src/api/dto/request/recover-by-mnemonic.dto.ts index 411c6fae..fa5c1265 100644 --- a/backend/services/identity-service/src/api/dto/request/recover-by-mnemonic.dto.ts +++ b/backend/services/identity-service/src/api/dto/request/recover-by-mnemonic.dto.ts @@ -1,10 +1,11 @@ -import { IsString, IsOptional, IsNotEmpty, IsNumber } from 'class-validator'; +import { IsString, IsOptional, IsNotEmpty, Matches } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class RecoverByMnemonicDto { - @ApiProperty({ example: 10001 }) - @IsNumber() - accountSequence: number; + @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' }) + @IsString() + @Matches(/^D\d{11}$/, { message: '账户序列号格式错误,应为 D + 年月日(6位) + 序号(5位)' }) + accountSequence: string; @ApiProperty({ example: 'abandon ability able about above absent absorb abstract absurd abuse access accident' }) @IsString() diff --git a/backend/services/identity-service/src/api/dto/request/recover-by-phone.dto.ts b/backend/services/identity-service/src/api/dto/request/recover-by-phone.dto.ts index bc8eb9d2..cb2e5323 100644 --- a/backend/services/identity-service/src/api/dto/request/recover-by-phone.dto.ts +++ b/backend/services/identity-service/src/api/dto/request/recover-by-phone.dto.ts @@ -1,10 +1,11 @@ -import { IsString, IsOptional, IsNotEmpty, IsNumber, Matches } from 'class-validator'; +import { IsString, IsOptional, IsNotEmpty, Matches } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class RecoverByPhoneDto { - @ApiProperty({ example: 10001 }) - @IsNumber() - accountSequence: number; + @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' }) + @IsString() + @Matches(/^D\d{11}$/, { message: '账户序列号格式错误,应为 D + 年月日(6位) + 序号(5位)' }) + accountSequence: string; @ApiProperty({ example: '13800138000' }) @IsString() diff --git a/backend/services/identity-service/src/api/dto/response/user-profile.dto.ts b/backend/services/identity-service/src/api/dto/response/user-profile.dto.ts index 96d420e4..4ce10113 100644 --- a/backend/services/identity-service/src/api/dto/response/user-profile.dto.ts +++ b/backend/services/identity-service/src/api/dto/response/user-profile.dto.ts @@ -20,8 +20,8 @@ export class UserProfileDto { @ApiProperty() userId: string; - @ApiProperty() - accountSequence: number; + @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' }) + accountSequence: string; @ApiProperty({ nullable: true }) phoneNumber: string | null; diff --git a/backend/services/identity-service/src/application/commands/index.ts b/backend/services/identity-service/src/application/commands/index.ts index dcaa8697..4cfb9cde 100644 --- a/backend/services/identity-service/src/application/commands/index.ts +++ b/backend/services/identity-service/src/application/commands/index.ts @@ -18,7 +18,7 @@ export class AutoCreateAccountCommand { export class RecoverByMnemonicCommand { constructor( - public readonly accountSequence: number, + public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号 public readonly mnemonic: string, public readonly newDeviceId: string, public readonly deviceName?: string, @@ -27,7 +27,7 @@ export class RecoverByMnemonicCommand { export class RecoverByPhoneCommand { constructor( - public readonly accountSequence: number, + public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号 public readonly phoneNumber: string, public readonly smsCode: string, public readonly newDeviceId: string, @@ -150,7 +150,7 @@ export class GenerateReferralLinkCommand { } export class GetWalletStatusQuery { - constructor(public readonly userSerialNum: number) {} + constructor(public readonly userSerialNum: string) {} // 格式: D + YYMMDD + 5位序号 } export class MarkMnemonicBackedUpCommand { @@ -173,7 +173,7 @@ export interface WalletStatusResult { errorMessage?: string; // 失败原因 (failed 状态时返回) } export interface AutoCreateAccountResult { - userSerialNum: number; // 用户序列号 + userSerialNum: string; // 用户序列号 (格式: D + YYMMDD + 5位序号) referralCode: string; // 推荐码 username: string; // 随机用户名 avatarSvg: string; // 随机SVG头像 @@ -183,7 +183,7 @@ export interface AutoCreateAccountResult { export interface RecoverAccountResult { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 nickname: string; avatarUrl: string | null; referralCode: string; @@ -193,14 +193,14 @@ export interface RecoverAccountResult { export interface AutoLoginResult { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 accessToken: string; refreshToken: string; } export interface RegisterResult { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 referralCode: string; accessToken: string; refreshToken: string; @@ -208,14 +208,14 @@ export interface RegisterResult { export interface LoginResult { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 accessToken: string; refreshToken: string; } export interface UserProfileDTO { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 phoneNumber: string | null; nickname: string; avatarUrl: string | null; @@ -238,7 +238,7 @@ export interface DeviceDTO { export interface UserBriefDTO { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 nickname: string; avatarUrl: string | null; } @@ -247,7 +247,7 @@ export interface ReferralCodeValidationResult { valid: boolean; referralCode?: string; inviterInfo?: { - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 nickname: string; avatarUrl: string | null; }; @@ -273,7 +273,7 @@ export interface ReferralStatsResult { thisWeekInvites: number; // 本周邀请 thisMonthInvites: number; // 本月邀请 recentInvites: Array<{ // 最近邀请记录 - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 nickname: string; avatarUrl: string | null; registeredAt: Date; @@ -283,13 +283,13 @@ export interface ReferralStatsResult { export interface MeResult { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 phoneNumber: string | null; nickname: string; avatarUrl: string | null; referralCode: string; referralLink: string; // 完整推荐链接 - inviterSequence: number | null; // 推荐人序列号 + inviterSequence: string | null; // 推荐人序列号 (格式: D + YYMMDD + 5位序号) walletAddresses: Array<{ chainType: string; address: string }>; kycStatus: string; status: string; diff --git a/backend/services/identity-service/src/application/commands/recover-by-mnemonic/recover-by-mnemonic.command.ts b/backend/services/identity-service/src/application/commands/recover-by-mnemonic/recover-by-mnemonic.command.ts index d3e795bf..d1e0fc03 100644 --- a/backend/services/identity-service/src/application/commands/recover-by-mnemonic/recover-by-mnemonic.command.ts +++ b/backend/services/identity-service/src/application/commands/recover-by-mnemonic/recover-by-mnemonic.command.ts @@ -1,6 +1,6 @@ export class RecoverByMnemonicCommand { constructor( - public readonly accountSequence: number, + public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号 public readonly mnemonic: string, public readonly newDeviceId: string, public readonly deviceName?: string, diff --git a/backend/services/identity-service/src/application/commands/recover-by-phone/recover-by-phone.command.ts b/backend/services/identity-service/src/application/commands/recover-by-phone/recover-by-phone.command.ts index 60e483e4..fdbe862b 100644 --- a/backend/services/identity-service/src/application/commands/recover-by-phone/recover-by-phone.command.ts +++ b/backend/services/identity-service/src/application/commands/recover-by-phone/recover-by-phone.command.ts @@ -1,6 +1,6 @@ export class RecoverByPhoneCommand { constructor( - public readonly accountSequence: number, + public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号 public readonly phoneNumber: string, public readonly smsCode: string, public readonly newDeviceId: string, diff --git a/backend/services/identity-service/src/application/services/token.service.ts b/backend/services/identity-service/src/application/services/token.service.ts index d1076303..9ef770f7 100644 --- a/backend/services/identity-service/src/application/services/token.service.ts +++ b/backend/services/identity-service/src/application/services/token.service.ts @@ -7,7 +7,7 @@ import { ApplicationError } from '@/shared/exceptions/domain.exception'; export interface TokenPayload { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 deviceId: string; type: 'access' | 'refresh'; } @@ -22,7 +22,7 @@ export class TokenService { async generateTokenPair(payload: { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 deviceId: string; }): Promise<{ accessToken: string; refreshToken: string }> { const accessToken = this.jwtService.sign( @@ -51,7 +51,7 @@ export class TokenService { async verifyRefreshToken(token: string): Promise<{ userId: string; - accountSequence: number; + accountSequence: string; deviceId: string; }> { try { diff --git a/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts b/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts index 4ab744a0..546df145 100644 --- a/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts +++ b/backend/services/identity-service/src/domain/aggregates/user-account/user-account.aggregate.ts @@ -147,9 +147,9 @@ export class UserAccount { } static reconstruct(params: { - userId: string; accountSequence: number; devices: DeviceInfo[]; + userId: string; accountSequence: string; devices: DeviceInfo[]; phoneNumber: string | null; nickname: string; avatarUrl: string | null; - inviterSequence: number | null; referralCode: string; + inviterSequence: string | null; referralCode: string; walletAddresses: WalletAddress[]; kycInfo: KYCInfo | null; kycStatus: KYCStatus; status: AccountStatus; registeredAt: Date; lastLoginAt: Date | null; updatedAt: Date; diff --git a/backend/services/identity-service/src/domain/events/index.ts b/backend/services/identity-service/src/domain/events/index.ts index 066b86d3..7c45aa0f 100644 --- a/backend/services/identity-service/src/domain/events/index.ts +++ b/backend/services/identity-service/src/domain/events/index.ts @@ -14,9 +14,9 @@ export class UserAccountAutoCreatedEvent extends DomainEvent { constructor( public readonly payload: { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 initialDeviceId: string; - inviterSequence: number | null; + inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号 registeredAt: Date; }, ) { @@ -32,10 +32,10 @@ export class UserAccountCreatedEvent extends DomainEvent { constructor( public readonly payload: { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 phoneNumber: string; initialDeviceId: string; - inviterSequence: number | null; + inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号 registeredAt: Date; }, ) { @@ -51,7 +51,7 @@ export class DeviceAddedEvent extends DomainEvent { constructor( public readonly payload: { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 deviceId: string; deviceName: string; }, @@ -177,7 +177,7 @@ export class MpcKeygenRequestedEvent extends DomainEvent { public readonly payload: { sessionId: string; userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 username: string; threshold: number; totalParties: number; diff --git a/backend/services/identity-service/src/domain/value-objects/account-sequence.vo.ts b/backend/services/identity-service/src/domain/value-objects/account-sequence.vo.ts index d6182b44..34048d45 100644 --- a/backend/services/identity-service/src/domain/value-objects/account-sequence.vo.ts +++ b/backend/services/identity-service/src/domain/value-objects/account-sequence.vo.ts @@ -1,19 +1,58 @@ import { DomainError } from '@/shared/exceptions/domain.exception'; +/** + * 账户序列号值对象 + * 格式: D + 年(2位) + 月(2位) + 日(2位) + 5位序号 + * 示例: D2512110008 -> 2025年12月11日的第8个注册用户 + */ export class AccountSequence { - constructor(public readonly value: number) { - if (value <= 0) throw new DomainError('账户序列号必须大于0'); + private static readonly PATTERN = /^D\d{11}$/; + + constructor(public readonly value: string) { + if (!AccountSequence.PATTERN.test(value)) { + throw new DomainError(`账户序列号格式无效: ${value},应为 D + 年月日(6位) + 序号(5位)`); + } } - static create(value: number): AccountSequence { + static create(value: string): AccountSequence { return new AccountSequence(value); } - static next(current: AccountSequence): AccountSequence { - return new AccountSequence(current.value + 1); + /** + * 根据日期和当日序号生成新的账户序列号 + * @param date 日期 + * @param dailySequence 当日序号 (0-99999) + */ + static generate(date: Date, dailySequence: number): AccountSequence { + if (dailySequence < 0 || dailySequence > 99999) { + throw new DomainError(`当日序号超出范围: ${dailySequence},应为 0-99999`); + } + const year = String(date.getFullYear()).slice(-2); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const seq = String(dailySequence).padStart(5, '0'); + return new AccountSequence(`D${year}${month}${day}${seq}`); + } + + /** + * 从序列号中提取日期字符串 (YYMMDD) + */ + get dateString(): string { + return this.value.slice(1, 7); + } + + /** + * 从序列号中提取当日序号 + */ + get dailySequence(): number { + return parseInt(this.value.slice(7), 10); } equals(other: AccountSequence): boolean { return this.value === other.value; } + + toString(): string { + return this.value; + } } diff --git a/backend/services/identity-service/src/domain/value-objects/index.ts b/backend/services/identity-service/src/domain/value-objects/index.ts index 7e24fe89..5b6b2c42 100644 --- a/backend/services/identity-service/src/domain/value-objects/index.ts +++ b/backend/services/identity-service/src/domain/value-objects/index.ts @@ -1,284 +1,269 @@ -import { DomainError } from '@/shared/exceptions/domain.exception'; -import { createHash, createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto'; -import * as bip39 from '@scure/bip39'; -import { wordlist } from '@scure/bip39/wordlists/english'; - -// ============ UserId ============ -export class UserId { - constructor(public readonly value: bigint) { - // 允许 0 作为临时值(表示未持久化的新账户) - if (value === null || value === undefined) { - throw new DomainError('UserId不能为空'); - } - } - - static create(value: bigint | string | number): UserId { - if (typeof value === 'string') { - return new UserId(BigInt(value)); - } - if (typeof value === 'number') { - return new UserId(BigInt(value)); - } - return new UserId(value); - } - - equals(other: UserId): boolean { - return this.value === other.value; - } - - toString(): string { - return this.value.toString(); - } -} - -// ============ AccountSequence ============ -export class AccountSequence { - constructor(public readonly value: number) { - if (value <= 0) throw new DomainError('账户序列号必须大于0'); - } - - static create(value: number): AccountSequence { - return new AccountSequence(value); - } - - static next(current: AccountSequence): AccountSequence { - return new AccountSequence(current.value + 1); - } - - equals(other: AccountSequence): boolean { - return this.value === other.value; - } -} - -// ============ PhoneNumber ============ -export class PhoneNumber { - constructor(public readonly value: string) { - if (!/^1[3-9]\d{9}$/.test(value)) { - throw new DomainError('手机号格式错误'); - } - } - - static create(value: string): PhoneNumber { - return new PhoneNumber(value); - } - - equals(other: PhoneNumber): boolean { - return this.value === other.value; - } - - masked(): string { - return this.value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); - } -} - -// ============ ReferralCode ============ -export class ReferralCode { - constructor(public readonly value: string) { - if (!/^[A-Z0-9]{6}$/.test(value)) { - throw new DomainError('推荐码格式错误'); - } - } - - static generate(): ReferralCode { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let code = ''; - for (let i = 0; i < 6; i++) { - code += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return new ReferralCode(code); - } - - static create(value: string): ReferralCode { - return new ReferralCode(value.toUpperCase()); - } - - equals(other: ReferralCode): boolean { - return this.value === other.value; - } -} - -// ============ Mnemonic ============ -export class Mnemonic { - constructor(public readonly value: string) { - if (!bip39.validateMnemonic(value, wordlist)) { - throw new DomainError('助记词格式错误'); - } - } - - static generate(): Mnemonic { - const mnemonic = bip39.generateMnemonic(wordlist, 128); - return new Mnemonic(mnemonic); - } - - static create(value: string): Mnemonic { - return new Mnemonic(value); - } - - toSeed(): Uint8Array { - return bip39.mnemonicToSeedSync(this.value); - } - - getWords(): string[] { - return this.value.split(' '); - } - - equals(other: Mnemonic): boolean { - return this.value === other.value; - } -} - -// ============ DeviceInfo ============ -// deviceInfo: 完整的设备信息 JSON,100% 保持前端传递的原样 -export class DeviceInfo { - private _lastActiveAt: Date; - private _deviceInfo: Record; - - constructor( - public readonly deviceId: string, - public readonly deviceName: string, - public readonly addedAt: Date, - lastActiveAt: Date, - deviceInfo?: Record, - ) { - this._lastActiveAt = lastActiveAt; - this._deviceInfo = deviceInfo || {}; - } - - get lastActiveAt(): Date { - return this._lastActiveAt; - } - - // 100% 保持原样的完整设备信息 JSON - get deviceInfo(): Record { - return this._deviceInfo; - } - - // 便捷访问器 - get platform(): string | undefined { - return this._deviceInfo.platform as string | undefined; - } - - get deviceModel(): string | undefined { - return (this._deviceInfo.model || this._deviceInfo.deviceModel) as string | undefined; - } - - get osVersion(): string | undefined { - return this._deviceInfo.osVersion as string | undefined; - } - - get appVersion(): string | undefined { - return this._deviceInfo.appVersion as string | undefined; - } - - updateActivity(): void { - this._lastActiveAt = new Date(); - } - - updateDeviceInfo(info: Record): void { - this._deviceInfo = { ...this._deviceInfo, ...info }; - } -} - -// ============ ChainType ============ -export enum ChainType { - KAVA = 'KAVA', - DST = 'DST', - BSC = 'BSC', -} - -export const CHAIN_CONFIG = { - [ChainType.KAVA]: { prefix: 'kava', derivationPath: "m/44'/459'/0'/0/0" }, - [ChainType.DST]: { prefix: 'dst', derivationPath: "m/44'/118'/0'/0/0" }, - [ChainType.BSC]: { prefix: '0x', derivationPath: "m/44'/60'/0'/0/0" }, -}; - -// ============ KYCInfo ============ -export class KYCInfo { - constructor( - public readonly realName: string, - public readonly idCardNumber: string, - public readonly idCardFrontUrl: string, - public readonly idCardBackUrl: string, - ) { - if (!realName || realName.length < 2) { - throw new DomainError('真实姓名不合法'); - } - if (!/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[0-9Xx]$/.test(idCardNumber)) { - throw new DomainError('身份证号格式错误'); - } - } - - static create(params: { realName: string; idCardNumber: string; idCardFrontUrl: string; idCardBackUrl: string }): KYCInfo { - return new KYCInfo(params.realName, params.idCardNumber, params.idCardFrontUrl, params.idCardBackUrl); - } - - maskedIdCardNumber(): string { - return this.idCardNumber.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2'); - } -} - -// ============ Enums ============ -export enum KYCStatus { - NOT_VERIFIED = 'NOT_VERIFIED', - PENDING = 'PENDING', - VERIFIED = 'VERIFIED', - REJECTED = 'REJECTED', -} - -export enum AccountStatus { - ACTIVE = 'ACTIVE', - FROZEN = 'FROZEN', - DEACTIVATED = 'DEACTIVATED', -} - -export enum AddressStatus { - ACTIVE = 'ACTIVE', - DISABLED = 'DISABLED', -} - -// ============ AddressId ============ -export class AddressId { - constructor(public readonly value: string) {} - - static generate(): AddressId { - return new AddressId(crypto.randomUUID()); - } - - static create(value: string): AddressId { - return new AddressId(value); - } -} - -// ============ MnemonicEncryption ============ -export class MnemonicEncryption { - static encrypt(mnemonic: string, key: string): string { - const derivedKey = this.deriveKey(key); - const iv = randomBytes(16); - const cipher = createCipheriv('aes-256-gcm', derivedKey, iv); - - let encrypted = cipher.update(mnemonic, 'utf8', 'hex'); - encrypted += cipher.final('hex'); - const authTag = cipher.getAuthTag(); - - return JSON.stringify({ - encrypted, - authTag: authTag.toString('hex'), - iv: iv.toString('hex'), - }); - } - - static decrypt(encryptedData: string, key: string): string { - const { encrypted, authTag, iv } = JSON.parse(encryptedData); - const derivedKey = this.deriveKey(key); - const decipher = createDecipheriv('aes-256-gcm', derivedKey, Buffer.from(iv, 'hex')); - decipher.setAuthTag(Buffer.from(authTag, 'hex')); - - let decrypted = decipher.update(encrypted, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - return decrypted; - } - - private static deriveKey(password: string): Buffer { - return scryptSync(password, 'rwa-wallet-salt', 32); - } -} +import { DomainError } from '@/shared/exceptions/domain.exception'; +import { createHash, createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto'; +import * as bip39 from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; + +// ============ UserId ============ +export class UserId { + constructor(public readonly value: bigint) { + // 允许 0 作为临时值(表示未持久化的新账户) + if (value === null || value === undefined) { + throw new DomainError('UserId不能为空'); + } + } + + static create(value: bigint | string | number): UserId { + if (typeof value === 'string') { + return new UserId(BigInt(value)); + } + if (typeof value === 'number') { + return new UserId(BigInt(value)); + } + return new UserId(value); + } + + equals(other: UserId): boolean { + return this.value === other.value; + } + + toString(): string { + return this.value.toString(); + } +} + +// ============ AccountSequence ============ +// 导出新格式的账户序列号 (D + YYMMDD + 5位序号) +export { AccountSequence } from './account-sequence.vo'; + +// ============ PhoneNumber ============ +export class PhoneNumber { + constructor(public readonly value: string) { + if (!/^1[3-9]\d{9}$/.test(value)) { + throw new DomainError('手机号格式错误'); + } + } + + static create(value: string): PhoneNumber { + return new PhoneNumber(value); + } + + equals(other: PhoneNumber): boolean { + return this.value === other.value; + } + + masked(): string { + return this.value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); + } +} + +// ============ ReferralCode ============ +export class ReferralCode { + constructor(public readonly value: string) { + if (!/^[A-Z0-9]{6}$/.test(value)) { + throw new DomainError('推荐码格式错误'); + } + } + + static generate(): ReferralCode { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let code = ''; + for (let i = 0; i < 6; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return new ReferralCode(code); + } + + static create(value: string): ReferralCode { + return new ReferralCode(value.toUpperCase()); + } + + equals(other: ReferralCode): boolean { + return this.value === other.value; + } +} + +// ============ Mnemonic ============ +export class Mnemonic { + constructor(public readonly value: string) { + if (!bip39.validateMnemonic(value, wordlist)) { + throw new DomainError('助记词格式错误'); + } + } + + static generate(): Mnemonic { + const mnemonic = bip39.generateMnemonic(wordlist, 128); + return new Mnemonic(mnemonic); + } + + static create(value: string): Mnemonic { + return new Mnemonic(value); + } + + toSeed(): Uint8Array { + return bip39.mnemonicToSeedSync(this.value); + } + + getWords(): string[] { + return this.value.split(' '); + } + + equals(other: Mnemonic): boolean { + return this.value === other.value; + } +} + +// ============ DeviceInfo ============ +// deviceInfo: 完整的设备信息 JSON,100% 保持前端传递的原样 +export class DeviceInfo { + private _lastActiveAt: Date; + private _deviceInfo: Record; + + constructor( + public readonly deviceId: string, + public readonly deviceName: string, + public readonly addedAt: Date, + lastActiveAt: Date, + deviceInfo?: Record, + ) { + this._lastActiveAt = lastActiveAt; + this._deviceInfo = deviceInfo || {}; + } + + get lastActiveAt(): Date { + return this._lastActiveAt; + } + + // 100% 保持原样的完整设备信息 JSON + get deviceInfo(): Record { + return this._deviceInfo; + } + + // 便捷访问器 + get platform(): string | undefined { + return this._deviceInfo.platform as string | undefined; + } + + get deviceModel(): string | undefined { + return (this._deviceInfo.model || this._deviceInfo.deviceModel) as string | undefined; + } + + get osVersion(): string | undefined { + return this._deviceInfo.osVersion as string | undefined; + } + + get appVersion(): string | undefined { + return this._deviceInfo.appVersion as string | undefined; + } + + updateActivity(): void { + this._lastActiveAt = new Date(); + } + + updateDeviceInfo(info: Record): void { + this._deviceInfo = { ...this._deviceInfo, ...info }; + } +} + +// ============ ChainType ============ +export enum ChainType { + KAVA = 'KAVA', + DST = 'DST', + BSC = 'BSC', +} + +export const CHAIN_CONFIG = { + [ChainType.KAVA]: { prefix: 'kava', derivationPath: "m/44'/459'/0'/0/0" }, + [ChainType.DST]: { prefix: 'dst', derivationPath: "m/44'/118'/0'/0/0" }, + [ChainType.BSC]: { prefix: '0x', derivationPath: "m/44'/60'/0'/0/0" }, +}; + +// ============ KYCInfo ============ +export class KYCInfo { + constructor( + public readonly realName: string, + public readonly idCardNumber: string, + public readonly idCardFrontUrl: string, + public readonly idCardBackUrl: string, + ) { + if (!realName || realName.length < 2) { + throw new DomainError('真实姓名不合法'); + } + if (!/^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[0-9Xx]$/.test(idCardNumber)) { + throw new DomainError('身份证号格式错误'); + } + } + + static create(params: { realName: string; idCardNumber: string; idCardFrontUrl: string; idCardBackUrl: string }): KYCInfo { + return new KYCInfo(params.realName, params.idCardNumber, params.idCardFrontUrl, params.idCardBackUrl); + } + + maskedIdCardNumber(): string { + return this.idCardNumber.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2'); + } +} + +// ============ Enums ============ +export enum KYCStatus { + NOT_VERIFIED = 'NOT_VERIFIED', + PENDING = 'PENDING', + VERIFIED = 'VERIFIED', + REJECTED = 'REJECTED', +} + +export enum AccountStatus { + ACTIVE = 'ACTIVE', + FROZEN = 'FROZEN', + DEACTIVATED = 'DEACTIVATED', +} + +export enum AddressStatus { + ACTIVE = 'ACTIVE', + DISABLED = 'DISABLED', +} + +// ============ AddressId ============ +export class AddressId { + constructor(public readonly value: string) {} + + static generate(): AddressId { + return new AddressId(crypto.randomUUID()); + } + + static create(value: string): AddressId { + return new AddressId(value); + } +} + +// ============ MnemonicEncryption ============ +export class MnemonicEncryption { + static encrypt(mnemonic: string, key: string): string { + const derivedKey = this.deriveKey(key); + const iv = randomBytes(16); + const cipher = createCipheriv('aes-256-gcm', derivedKey, iv); + + let encrypted = cipher.update(mnemonic, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + const authTag = cipher.getAuthTag(); + + return JSON.stringify({ + encrypted, + authTag: authTag.toString('hex'), + iv: iv.toString('hex'), + }); + } + + static decrypt(encryptedData: string, key: string): string { + const { encrypted, authTag, iv } = JSON.parse(encryptedData); + const derivedKey = this.deriveKey(key); + const decipher = createDecipheriv('aes-256-gcm', derivedKey, Buffer.from(iv, 'hex')); + decipher.setAuthTag(Buffer.from(authTag, 'hex')); + + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + return decrypted; + } + + private static deriveKey(password: string): Buffer { + return scryptSync(password, 'rwa-wallet-salt', 32); + } +} diff --git a/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts b/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts index 26eb7a54..070852e8 100644 --- a/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts +++ b/backend/services/identity-service/src/infrastructure/external/blockchain/blockchain-client.service.ts @@ -26,7 +26,7 @@ export interface VerifyMnemonicResult { } export interface VerifyMnemonicByAccountParams { - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 mnemonic: string; } @@ -144,7 +144,7 @@ export class BlockchainClientService { /** * 标记助记词已备份 */ - async markMnemonicBackedUp(accountSequence: number): Promise { + async markMnemonicBackedUp(accountSequence: string): Promise { this.logger.log(`Marking mnemonic as backed up for account ${accountSequence}`); try { diff --git a/backend/services/identity-service/src/infrastructure/kafka/mpc-event-consumer.service.ts b/backend/services/identity-service/src/infrastructure/kafka/mpc-event-consumer.service.ts index 39cabc97..91055f97 100644 --- a/backend/services/identity-service/src/infrastructure/kafka/mpc-event-consumer.service.ts +++ b/backend/services/identity-service/src/infrastructure/kafka/mpc-event-consumer.service.ts @@ -32,7 +32,7 @@ export interface KeygenCompletedPayload { threshold: string; extraPayload?: { userId: string; - accountSequence: number; // 8位账户序列号 + accountSequence: string; // 格式: D + YYMMDD + 5位序号 username: string; delegateShare?: { partyId: string; diff --git a/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts b/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts index d82500ce..bdcd1c32 100644 --- a/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts +++ b/backend/services/identity-service/src/infrastructure/persistence/entities/user-account.entity.ts @@ -1,56 +1,56 @@ -// Prisma Entity Types - 用于Mapper转换 -export interface UserAccountEntity { - userId: bigint; - accountSequence: bigint; - phoneNumber: string | null; - nickname: string; - avatarUrl: string | null; - inviterSequence: bigint | null; - referralCode: string; - kycStatus: string; - realName: string | null; - idCardNumber: string | null; - idCardFrontUrl: string | null; - idCardBackUrl: string | null; - kycVerifiedAt: Date | null; - status: string; - registeredAt: Date; - lastLoginAt: Date | null; - updatedAt: Date; - devices?: UserDeviceEntity[]; - walletAddresses?: WalletAddressEntity[]; -} - -export interface UserDeviceEntity { - id: bigint; - userId: bigint; - deviceId: string; - deviceName: string | null; - deviceInfo: Record | null; // 完整的设备信息 JSON - // Hardware Info (冗余字段,便于查询) - platform: string | null; - deviceModel: string | null; - osVersion: string | null; - appVersion: string | null; - screenWidth: number | null; - screenHeight: number | null; - locale: string | null; - timezone: string | null; - // Timestamps - addedAt: Date; - lastActiveAt: Date; -} - -export interface WalletAddressEntity { - addressId: bigint; - userId: bigint; - chainType: string; - address: string; - publicKey: string; - addressDigest: string; - mpcSignatureR: string; - mpcSignatureS: string; - mpcSignatureV: number; - status: string; - boundAt: Date; -} +// Prisma Entity Types - 用于Mapper转换 +export interface UserAccountEntity { + userId: bigint; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 + phoneNumber: string | null; + nickname: string; + avatarUrl: string | null; + inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号 + referralCode: string; + kycStatus: string; + realName: string | null; + idCardNumber: string | null; + idCardFrontUrl: string | null; + idCardBackUrl: string | null; + kycVerifiedAt: Date | null; + status: string; + registeredAt: Date; + lastLoginAt: Date | null; + updatedAt: Date; + devices?: UserDeviceEntity[]; + walletAddresses?: WalletAddressEntity[]; +} + +export interface UserDeviceEntity { + id: bigint; + userId: bigint; + deviceId: string; + deviceName: string | null; + deviceInfo: Record | null; // 完整的设备信息 JSON + // Hardware Info (冗余字段,便于查询) + platform: string | null; + deviceModel: string | null; + osVersion: string | null; + appVersion: string | null; + screenWidth: number | null; + screenHeight: number | null; + locale: string | null; + timezone: string | null; + // Timestamps + addedAt: Date; + lastActiveAt: Date; +} + +export interface WalletAddressEntity { + addressId: bigint; + userId: bigint; + chainType: string; + address: string; + publicKey: string; + addressDigest: string; + mpcSignatureR: string; + mpcSignatureS: string; + mpcSignatureV: number; + status: string; + boundAt: Date; +} diff --git a/backend/services/identity-service/src/infrastructure/persistence/mappers/user-account.mapper.ts b/backend/services/identity-service/src/infrastructure/persistence/mappers/user-account.mapper.ts index 6a90345b..af76cbc4 100644 --- a/backend/services/identity-service/src/infrastructure/persistence/mappers/user-account.mapper.ts +++ b/backend/services/identity-service/src/infrastructure/persistence/mappers/user-account.mapper.ts @@ -1,64 +1,64 @@ -import { Injectable } from '@nestjs/common'; -import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate'; -import { WalletAddress } from '@/domain/entities/wallet-address.entity'; -import { DeviceInfo, KYCInfo, KYCStatus, AccountStatus, ChainType, AddressStatus } from '@/domain/value-objects'; -import { UserAccountEntity } from '../entities/user-account.entity'; -import { toMpcSignatureString } from '../entities/wallet-address.entity'; - -@Injectable() -export class UserAccountMapper { - toDomain(entity: UserAccountEntity): UserAccount { - const devices = (entity.devices || []).map((d) => { - // 直接使用完整的 deviceInfo JSON,100% 保持原样 - return new DeviceInfo( - d.deviceId, - d.deviceName || '未命名设备', - d.addedAt, - d.lastActiveAt, - d.deviceInfo || undefined, - ); - }); - - const wallets = (entity.walletAddresses || []).map((w) => - WalletAddress.reconstruct({ - addressId: w.addressId.toString(), - userId: w.userId.toString(), - chainType: w.chainType as ChainType, - address: w.address, - publicKey: w.publicKey, - addressDigest: w.addressDigest, - mpcSignature: toMpcSignatureString(w), // 64 bytes hex (r + s) - status: w.status as AddressStatus, - boundAt: w.boundAt, - }), - ); - - const kycInfo = - entity.realName && entity.idCardNumber - ? KYCInfo.create({ - realName: entity.realName, - idCardNumber: entity.idCardNumber, - idCardFrontUrl: entity.idCardFrontUrl || '', - idCardBackUrl: entity.idCardBackUrl || '', - }) - : null; - - return UserAccount.reconstruct({ - userId: entity.userId.toString(), - accountSequence: Number(entity.accountSequence), - devices, - phoneNumber: entity.phoneNumber, - nickname: entity.nickname, - avatarUrl: entity.avatarUrl, - inviterSequence: entity.inviterSequence ? Number(entity.inviterSequence) : null, - referralCode: entity.referralCode, - walletAddresses: wallets, - kycInfo, - kycStatus: entity.kycStatus as KYCStatus, - status: entity.status as AccountStatus, - registeredAt: entity.registeredAt, - lastLoginAt: entity.lastLoginAt, - updatedAt: entity.updatedAt, - }); - } -} +import { Injectable } from '@nestjs/common'; +import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate'; +import { WalletAddress } from '@/domain/entities/wallet-address.entity'; +import { DeviceInfo, KYCInfo, KYCStatus, AccountStatus, ChainType, AddressStatus } from '@/domain/value-objects'; +import { UserAccountEntity } from '../entities/user-account.entity'; +import { toMpcSignatureString } from '../entities/wallet-address.entity'; + +@Injectable() +export class UserAccountMapper { + toDomain(entity: UserAccountEntity): UserAccount { + const devices = (entity.devices || []).map((d) => { + // 直接使用完整的 deviceInfo JSON,100% 保持原样 + return new DeviceInfo( + d.deviceId, + d.deviceName || '未命名设备', + d.addedAt, + d.lastActiveAt, + d.deviceInfo || undefined, + ); + }); + + const wallets = (entity.walletAddresses || []).map((w) => + WalletAddress.reconstruct({ + addressId: w.addressId.toString(), + userId: w.userId.toString(), + chainType: w.chainType as ChainType, + address: w.address, + publicKey: w.publicKey, + addressDigest: w.addressDigest, + mpcSignature: toMpcSignatureString(w), // 64 bytes hex (r + s) + status: w.status as AddressStatus, + boundAt: w.boundAt, + }), + ); + + const kycInfo = + entity.realName && entity.idCardNumber + ? KYCInfo.create({ + realName: entity.realName, + idCardNumber: entity.idCardNumber, + idCardFrontUrl: entity.idCardFrontUrl || '', + idCardBackUrl: entity.idCardBackUrl || '', + }) + : null; + + return UserAccount.reconstruct({ + userId: entity.userId.toString(), + accountSequence: entity.accountSequence, // 现在是字符串类型 + devices, + phoneNumber: entity.phoneNumber, + nickname: entity.nickname, + avatarUrl: entity.avatarUrl, + inviterSequence: entity.inviterSequence, // 现在是字符串类型 + referralCode: entity.referralCode, + walletAddresses: wallets, + kycInfo, + kycStatus: entity.kycStatus as KYCStatus, + status: entity.status as AccountStatus, + registeredAt: entity.registeredAt, + lastLoginAt: entity.lastLoginAt, + updatedAt: entity.updatedAt, + }); + } +} 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 34578861..30f50d79 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 @@ -26,11 +26,11 @@ export class UserAccountRepositoryImpl implements UserAccountRepository { // 新账户,让数据库自动生成userId const created = await tx.userAccount.create({ data: { - accountSequence: BigInt(account.accountSequence.value), + accountSequence: account.accountSequence.value, phoneNumber: account.phoneNumber?.value || null, nickname: account.nickname, avatarUrl: account.avatarUrl, - inviterSequence: account.inviterSequence ? BigInt(account.inviterSequence.value) : null, + inviterSequence: account.inviterSequence?.value || null, referralCode: account.referralCode.value, kycStatus: account.kycStatus, realName: account.kycInfo?.realName || null, @@ -125,7 +125,7 @@ export class UserAccountRepositoryImpl implements UserAccountRepository { async findByAccountSequence(sequence: AccountSequence): Promise { const data = await this.prisma.userAccount.findUnique({ - where: { accountSequence: BigInt(sequence.value) }, + where: { accountSequence: sequence.value }, include: { devices: true, walletAddresses: true }, }); return data ? this.toDomain(data) : null; @@ -163,18 +163,38 @@ export class UserAccountRepositoryImpl implements UserAccountRepository { async getMaxAccountSequence(): Promise { const result = await this.prisma.userAccount.aggregate({ _max: { accountSequence: true } }); - return result._max.accountSequence ? AccountSequence.create(Number(result._max.accountSequence)) : null; + return result._max.accountSequence ? AccountSequence.create(result._max.accountSequence) : null; } async getNextAccountSequence(): Promise { + const now = new Date(); + const year = String(now.getFullYear()).slice(-2); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const dateKey = `${year}${month}${day}`; + const result = await this.prisma.$transaction(async (tx) => { - const updated = await tx.accountSequenceGenerator.update({ - where: { id: 1 }, - data: { currentSequence: { increment: 1 } }, + // 尝试更新当日记录,如果不存在则创建 + const existing = await tx.accountSequenceGenerator.findUnique({ + where: { dateKey }, }); - return updated.currentSequence; + + if (existing) { + const updated = await tx.accountSequenceGenerator.update({ + where: { dateKey }, + data: { currentSequence: { increment: 1 } }, + }); + return updated.currentSequence; + } else { + // 当日第一个用户,创建新记录 + const created = await tx.accountSequenceGenerator.create({ + data: { dateKey, currentSequence: 0 }, + }); + return created.currentSequence; + } }); - return AccountSequence.create(Number(result)); + + return AccountSequence.generate(now, result); } async findUsers( @@ -247,12 +267,12 @@ export class UserAccountRepositoryImpl implements UserAccountRepository { return UserAccount.reconstruct({ userId: data.userId.toString(), - accountSequence: Number(data.accountSequence), + accountSequence: data.accountSequence, devices, phoneNumber: data.phoneNumber, nickname: data.nickname, avatarUrl: data.avatarUrl, - inviterSequence: data.inviterSequence ? Number(data.inviterSequence) : null, + inviterSequence: data.inviterSequence || null, referralCode: data.referralCode, walletAddresses: wallets, kycInfo, @@ -268,7 +288,7 @@ export class UserAccountRepositoryImpl implements UserAccountRepository { async findByInviterSequence(inviterSequence: AccountSequence): Promise { const data = await this.prisma.userAccount.findMany({ - where: { inviterSequence: BigInt(inviterSequence.value) }, + where: { inviterSequence: inviterSequence.value }, include: { devices: true, walletAddresses: true }, orderBy: { registeredAt: 'desc' }, }); diff --git a/backend/services/identity-service/src/shared/guards/jwt-auth.guard.ts b/backend/services/identity-service/src/shared/guards/jwt-auth.guard.ts index f8c323e8..1bc401aa 100644 --- a/backend/services/identity-service/src/shared/guards/jwt-auth.guard.ts +++ b/backend/services/identity-service/src/shared/guards/jwt-auth.guard.ts @@ -5,14 +5,14 @@ import { UnauthorizedException } from '@/shared/exceptions/domain.exception'; export interface JwtPayload { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 deviceId: string; type: 'access' | 'refresh'; } export interface CurrentUserData { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 deviceId: string; } diff --git a/backend/services/identity-service/src/shared/strategies/jwt.strategy.ts b/backend/services/identity-service/src/shared/strategies/jwt.strategy.ts index 3beaba2c..0cc86444 100644 --- a/backend/services/identity-service/src/shared/strategies/jwt.strategy.ts +++ b/backend/services/identity-service/src/shared/strategies/jwt.strategy.ts @@ -5,7 +5,7 @@ import { ConfigService } from '@nestjs/config'; export interface JwtPayload { userId: string; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 deviceId: string; type: 'access' | 'refresh'; iat: number; diff --git a/backend/services/identity-service/src/shared/utils/random-identity.util.ts b/backend/services/identity-service/src/shared/utils/random-identity.util.ts index d26a58a3..1a37dff4 100644 --- a/backend/services/identity-service/src/shared/utils/random-identity.util.ts +++ b/backend/services/identity-service/src/shared/utils/random-identity.util.ts @@ -3,7 +3,7 @@ */ // 生成用户名: 榴莲女皇x号 -export function generateUsername(accountSequence: number): string { +export function generateUsername(accountSequence: string): string { return `榴莲女皇${accountSequence}号`; } @@ -132,9 +132,9 @@ export function generateRandomAvatarSvg(): string { /** * 生成用户身份 - * @param accountSequence 用户序列号 + * @param accountSequence 用户序列号 (格式: D + YYMMDD + 5位序号) */ -export function generateIdentity(accountSequence: number): { username: string; avatarSvg: string } { +export function generateIdentity(accountSequence: string): { username: string; avatarSvg: string } { return { username: generateUsername(accountSequence), avatarSvg: generateRandomAvatarSvg(), diff --git a/backend/services/mpc-service/src/application/event-handlers/keygen-requested.handler.ts b/backend/services/mpc-service/src/application/event-handlers/keygen-requested.handler.ts index c0a68b60..851850f8 100644 --- a/backend/services/mpc-service/src/application/event-handlers/keygen-requested.handler.ts +++ b/backend/services/mpc-service/src/application/event-handlers/keygen-requested.handler.ts @@ -81,7 +81,7 @@ export class KeygenRequestedHandler implements OnModuleInit { try { const deriveResult = await this.blockchainClient.deriveAddresses({ userId, - accountSequence, // 8位账户序列号,用于关联恢复助记词 + accountSequence, // 账户序列号,格式: D + YYMMDD + 5位序号,如 D2512110008 publicKey: result.publicKey, }); derivedAddresses = deriveResult.addresses; @@ -131,7 +131,7 @@ export class KeygenRequestedHandler implements OnModuleInit { // Add extra payload for identity-service (completedEvent as any).extraPayload = { userId, - accountSequence, // 8位账户序列号,用于关联恢复助记词 + accountSequence, // 账户序列号,格式: D + YYMMDD + 5位序号,如 D2512110008 username, delegateShare: result.delegateShare, derivedAddresses, // BSC, KAVA, DST addresses diff --git a/backend/services/mpc-service/src/infrastructure/external/backup/backup-client.service.ts b/backend/services/mpc-service/src/infrastructure/external/backup/backup-client.service.ts index b9d563ea..fe73c3cc 100644 --- a/backend/services/mpc-service/src/infrastructure/external/backup/backup-client.service.ts +++ b/backend/services/mpc-service/src/infrastructure/external/backup/backup-client.service.ts @@ -13,7 +13,7 @@ import * as jwt from 'jsonwebtoken'; export interface StoreBackupShareParams { userId: string; - accountSequence: number; + accountSequence: string; username: string; publicKey: string; partyId: string; diff --git a/backend/services/mpc-service/src/infrastructure/external/blockchain/blockchain-client.service.ts b/backend/services/mpc-service/src/infrastructure/external/blockchain/blockchain-client.service.ts index 8ab49270..6c29c7e4 100644 --- a/backend/services/mpc-service/src/infrastructure/external/blockchain/blockchain-client.service.ts +++ b/backend/services/mpc-service/src/infrastructure/external/blockchain/blockchain-client.service.ts @@ -11,7 +11,7 @@ import { firstValueFrom } from 'rxjs'; export interface DeriveAddressParams { userId: string; - accountSequence: number; // 8位账户序列号,用于关联恢复助记词 + accountSequence: string; // 账户序列号,格式: D + YYMMDD + 5位序号,如 D2512110008 publicKey: string; } diff --git a/backend/services/mpc-service/src/infrastructure/messaging/kafka/event-consumer.service.ts b/backend/services/mpc-service/src/infrastructure/messaging/kafka/event-consumer.service.ts index 81bc6453..e0ec71a4 100644 --- a/backend/services/mpc-service/src/infrastructure/messaging/kafka/event-consumer.service.ts +++ b/backend/services/mpc-service/src/infrastructure/messaging/kafka/event-consumer.service.ts @@ -18,7 +18,7 @@ export const MPC_CONSUME_TOPICS = { export interface KeygenRequestedPayload { sessionId: string; userId: string; - accountSequence: number; // 8位账户序列号,用于关联恢复助记词 + accountSequence: string; // 账户序列号,格式: D + YYMMDD + 5位序号,如 D2512110008 username: string; threshold: number; totalParties: number; 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 4a3bf550..667ae8a1 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; accountSequence: number }; + user: { id: string; accountSequence: string }; } @ApiTags('认种订单') 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 9adac864..658a8b7e 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,7 +10,7 @@ import * as jwt from 'jsonwebtoken'; export interface JwtPayload { sub: string; userId: string; - accountSequence: number; + accountSequence: string; iat: number; exp: number; } 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 b573c3aa..552329bf 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,7 +173,7 @@ export class PlantingApplicationService { async payOrder( orderNo: string, userId: bigint, - accountSequence?: number, + accountSequence?: string, ): Promise<{ orderNo: string; status: string; @@ -207,20 +207,8 @@ export class PlantingApplicationService { ); } - // 3. 获取推荐链上下文 (先获取,确保服务可用) - const referralContext = await this.referralService.getReferralContext( - accountSequence!, - selection.provinceCode, - selection.cityCode, - ); - this.logger.log(`Referral context fetched: ${JSON.stringify(referralContext)}`); - - // 4. 预计算资金分配 (纯内存计算,无副作用) - const allocations = this.fundAllocationService.calculateAllocations( - order, - referralContext, - ); - this.logger.log(`Fund allocations calculated: ${allocations.length} targets`); + // 注意:资金分配已移至 reward-service,由其调用 authorization-service 进行考核后分配 + // planting-service 只负责:冻结 → 扣款 → 发事件 // ==================== 冻结阶段 ==================== // 5. 冻结用户资金(幂等,可回滚) @@ -228,6 +216,7 @@ export class PlantingApplicationService { try { await this.walletService.freezeForPlanting({ userId: userId.toString(), + accountSequence: accountSequence, amount: order.totalAmount, orderId: order.orderNo, }); @@ -243,9 +232,9 @@ export class PlantingApplicationService { // ==================== 执行阶段 ==================== try { - // 6. 标记已支付并分配资金 (内存操作) + // 6. 标记已支付 (内存操作) + // 注意:资金分配已移至 reward-service order.markAsPaid(); - order.allocateFunds(allocations); // 7. 使用事务保存本地数据库的所有变更 + Outbox事件 // 这确保了订单状态、用户持仓、批次数据、以及事件发布的原子性 @@ -291,17 +280,18 @@ export class PlantingApplicationService { // ==================== 确认阶段 ==================== // 9. 确认扣款(从冻结金额中正式扣除) + // 钱会进入"待分配"状态,由 reward-service 通过事件触发后执行真正的分配 await this.walletService.confirmPlantingDeduction({ userId: userId.toString(), + accountSequence: accountSequence, orderId: order.orderNo, }); this.logger.log(`Wallet deduction confirmed for order ${order.orderNo}`); - // 10. 调用钱包服务执行资金分配 (外部调用,在事务外) - await this.walletService.allocateFunds({ - orderId: order.orderNo, - allocations: allocations.map((a) => a.toDTO()), - }); + // 注意:资金分配已移至 reward-service + // reward-service 收到 planting.order.paid 事件后,会: + // 1. 调用 authorization-service 获取考核后的分配方案 + // 2. 调用 wallet-service 执行真正的资金分配 this.logger.log(`Order paid successfully: ${order.orderNo}`); @@ -311,7 +301,7 @@ export class PlantingApplicationService { return { orderNo: order.orderNo, status: order.status, - allocations: allocations.map((a) => a.toDTO()), + allocations: [], // 分配由 reward-service 执行 }; } catch (error) { // 执行阶段出错,需要解冻资金 @@ -325,6 +315,7 @@ export class PlantingApplicationService { try { await this.walletService.unfreezeForPlanting({ userId: userId.toString(), + accountSequence: accountSequence, orderId: order.orderNo, }); this.logger.log(`Wallet unfrozen (rollback) for order ${order.orderNo}`); 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 8af78d05..1a10b913 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,7 +30,7 @@ export class ReferralServiceClient { * 获取用户的推荐链和权限上级信息 */ async getReferralContext( - accountSequence: number, + accountSequence: string, provinceCode: string, cityCode: string, ): Promise { diff --git a/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts b/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts index 2cf3867a..7e073350 100644 --- a/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts +++ b/backend/services/planting-service/src/infrastructure/external/wallet-service.client.ts @@ -24,17 +24,20 @@ export interface WalletBalance { export interface FreezeForPlantingRequest { userId: string; + accountSequence?: string; // 跨服务关联标识(优先使用) amount: number; orderId: string; } export interface ConfirmPlantingDeductionRequest { userId: string; + accountSequence?: string; // 跨服务关联标识(优先使用) orderId: string; } export interface UnfreezeForPlantingRequest { userId: string; + accountSequence?: string; // 跨服务关联标识(优先使用) orderId: string; } diff --git a/backend/services/referral-service/prisma/schema.prisma b/backend/services/referral-service/prisma/schema.prisma index 40ffc1f5..47f1dfc5 100644 --- a/backend/services/referral-service/prisma/schema.prisma +++ b/backend/services/referral-service/prisma/schema.prisma @@ -14,7 +14,7 @@ datasource db { model ReferralRelationship { id BigInt @id @default(autoincrement()) @map("relationship_id") userId BigInt @unique @map("user_id") - accountSequence Int @unique @map("account_sequence") // 8位账户序列号,用于跨服务关联 + accountSequence String @unique @map("account_sequence") @db.VarChar(12) // 格式: D + YYMMDD + 5位序号 // 推荐人信息 referrerId BigInt? @map("referrer_id") // 直接推荐人 (null = 无推荐人/根节点) @@ -113,7 +113,7 @@ model DirectReferral { id BigInt @id @default(autoincrement()) @map("direct_referral_id") referrerId BigInt @map("referrer_id") // 推荐人ID referralId BigInt @map("referral_id") // 被推荐人ID - referralSequence BigInt @map("referral_sequence") // 被推荐人序列号 + referralSequence String @map("referral_sequence") @db.VarChar(12) // 被推荐人序列号 (格式: D + YYMMDD + 5位序号) // 被推荐人信息快照 (冗余存储,避免跨服务查询) referralNickname String? @map("referral_nickname") @db.VarChar(100) diff --git a/backend/services/referral-service/src/api/controllers/internal-referral-chain.controller.ts b/backend/services/referral-service/src/api/controllers/internal-referral-chain.controller.ts index a561dc9a..f139f19e 100644 --- a/backend/services/referral-service/src/api/controllers/internal-referral-chain.controller.ts +++ b/backend/services/referral-service/src/api/controllers/internal-referral-chain.controller.ts @@ -31,7 +31,7 @@ export class InternalReferralChainController { schema: { type: 'object', properties: { - accountSequence: { type: 'number' }, + accountSequence: { type: 'string', description: '格式: D + YYMMDD + 5位序号' }, userId: { type: 'string' }, ancestorPath: { type: 'array', @@ -45,13 +45,13 @@ export class InternalReferralChainController { async getReferralChain(@Param('accountSequence') accountSequence: string) { this.logger.debug(`[INTERNAL] getReferralChain: accountSequence=${accountSequence}`); - const relationship = await this.referralRepo.findByAccountSequence(Number(accountSequence)); + const relationship = await this.referralRepo.findByAccountSequence(accountSequence); if (!relationship) { this.logger.debug(`[INTERNAL] No referral found for accountSequence: ${accountSequence}`); // 返回空的祖先链而不是抛出错误 return { - accountSequence: Number(accountSequence), + accountSequence: accountSequence, userId: null, ancestorPath: [], referrerId: null, @@ -80,10 +80,10 @@ export class InternalReferralChainController { description: '批量推荐链数据', }) async getBatchReferralChains(@Param('accountSequences') accountSequences: string) { - const sequences = accountSequences.split(',').map((s) => Number(s.trim())); + const sequences = accountSequences.split(',').map((s) => s.trim()); this.logger.debug(`[INTERNAL] getBatchReferralChains: ${sequences.length} accounts`); - const results: Record = {}; + const results: Record = {}; for (const seq of sequences) { const relationship = await this.referralRepo.findByAccountSequence(seq); @@ -118,10 +118,10 @@ export class InternalReferralChainController { schema: { type: 'object', properties: { - accountSequence: { type: 'number' }, + accountSequence: { type: 'string', description: '格式: D + YYMMDD + 5位序号' }, teamMembers: { type: 'array', - items: { type: 'number' }, + items: { type: 'string' }, description: '团队成员accountSequence列表(直接和间接下级)', }, }, @@ -130,11 +130,11 @@ export class InternalReferralChainController { async getTeamMembers(@Param('accountSequence') accountSequence: string) { this.logger.debug(`[INTERNAL] getTeamMembers: accountSequence=${accountSequence}`); - const relationship = await this.referralRepo.findByAccountSequence(Number(accountSequence)); + const relationship = await this.referralRepo.findByAccountSequence(accountSequence); if (!relationship) { return { - accountSequence: Number(accountSequence), + accountSequence: accountSequence, teamMembers: [], }; } @@ -143,7 +143,7 @@ export class InternalReferralChainController { const directReferrals = await this.referralRepo.findDirectReferrals(relationship.userId); // 递归获取所有下级成员的accountSequence - const teamMembers: number[] = []; + const teamMembers: string[] = []; const queue = [...directReferrals]; while (queue.length > 0) { @@ -156,7 +156,7 @@ export class InternalReferralChainController { } return { - accountSequence: Number(accountSequence), + accountSequence: accountSequence, teamMembers, }; } diff --git a/backend/services/referral-service/src/api/controllers/internal-team-statistics.controller.ts b/backend/services/referral-service/src/api/controllers/internal-team-statistics.controller.ts index e3c9b35a..8bc89b5d 100644 --- a/backend/services/referral-service/src/api/controllers/internal-team-statistics.controller.ts +++ b/backend/services/referral-service/src/api/controllers/internal-team-statistics.controller.ts @@ -52,7 +52,7 @@ export class InternalTeamStatisticsController { return { userId: stats.userId.toString(), accountSequence: '0', // userId 查询时无法获取 accountSequence - totalTeamPlantingCount: stats.teamPlantingCount, // 团队总认种(含自己) + totalTeamPlantingCount: stats.teamPlantingCount, // 团队总认种(不含自己,只有下级) selfPlantingCount: stats.personalPlantingCount, // 自己的认种数 provinceCityDistribution: distribution.toJson(), }; @@ -85,7 +85,7 @@ export class InternalTeamStatisticsController { try { // 需要先通过 accountSequence 查找 userId // 这里需要扩展 repository 方法 - const stats = await this.findByAccountSequence(BigInt(accountSequence)); + const stats = await this.findByAccountSequence(accountSequence); if (!stats) { this.logger.debug(`[INTERNAL] No stats found for accountSequence: ${accountSequence}`); @@ -97,7 +97,7 @@ export class InternalTeamStatisticsController { return { userId: stats.userId.toString(), accountSequence: accountSequence, - totalTeamPlantingCount: stats.teamPlantingCount, // 团队总认种(含自己) + totalTeamPlantingCount: stats.teamPlantingCount, // 团队总认种(不含自己,只有下级) selfPlantingCount: stats.personalPlantingCount, // 自己的认种数 provinceCityDistribution: distribution.toJson(), }; @@ -111,10 +111,17 @@ export class InternalTeamStatisticsController { * 通过 accountSequence 查找团队统计 * 需要先查询 referral_relationships 获取 userId,再查询 team_statistics */ - private async findByAccountSequence(accountSequence: bigint) { + private async findByAccountSequence(accountSequence: string) { // 使用 repository 的 findByUserId,但这里需要 accountSequence 到 userId 的映射 // 由于当前架构 accountSequence 和 userId 不一定相等,需要通过 referral_relationships 表查询 - // 暂时尝试用 accountSequence 作为 userId 查询 - return this.teamStatsRepo.findByUserId(accountSequence); + // 暂时尝试将 accountSequence 字符串转换为 BigInt 作为 userId 查询 + // 注意:这里的实现取决于业务逻辑,可能需要通过 referral_relationships 表查询 + try { + // 尝试从 accountSequence 中提取数字部分或直接使用 + // 这是临时方案,实际应该通过 referral_relationships 查询 + return this.teamStatsRepo.findByUserId(BigInt(accountSequence.replace(/\D/g, ''))); + } catch { + return null; + } } } 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 4789da30..4d1cf04e 100644 --- a/backend/services/referral-service/src/api/controllers/referral.controller.ts +++ b/backend/services/referral-service/src/api/controllers/referral.controller.ts @@ -56,7 +56,7 @@ export class ReferralController { @ApiOperation({ summary: '获取当前用户推荐信息' }) @ApiResponse({ status: 200, type: ReferralInfoResponseDto }) async getMyReferralInfo(@CurrentUser('userId') userId: bigint): Promise { - const query = new GetUserReferralInfoQuery(Number(userId)); + const query = new GetUserReferralInfoQuery(userId.toString()); // 转换为字符串 return this.referralService.getUserReferralInfo(query); } @@ -171,7 +171,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(Number(userId)); + const query = new GetUserReferralInfoQuery(userId); // userId 已经是字符串 return this.referralService.getUserReferralInfo(query); } } @@ -199,16 +199,14 @@ export class InternalReferralController { @Query('provinceCode') provinceCode: string, @Query('cityCode') cityCode: string, ) { - const accountSeqNum = Number(accountSequence); - // 1. 获取用户的推荐链 - const query = new GetUserReferralInfoQuery(accountSeqNum); + const query = new GetUserReferralInfoQuery(accountSequence); // accountSequence 现在是字符串 const referralInfo = await this.referralService.getUserReferralInfo(query); // 2. 并行查询授权信息(省/市/社区) // 使用 fallback 机制:如果 authorization-service 不可用,返回 null const authorizations = await this.authorizationClient.findAllNearestAuthorizations( - accountSeqNum, + accountSequence, // accountSequence 现在是字符串 provinceCode, cityCode, ); @@ -227,9 +225,9 @@ export class InternalReferralController { accountSequence, referralChain: referralInfo.referrerId ? [referralInfo.referrerId] : [], referrerId: referralInfo.referrerId, - nearestProvinceAuth: authorizations.nearestProvinceAuth?.toString() ?? null, - nearestCityAuth: authorizations.nearestCityAuth?.toString() ?? null, - nearestCommunity: authorizations.nearestCommunity?.toString() ?? null, + nearestProvinceAuth: authorizations.nearestProvinceAuth ?? null, // 已经是字符串,不需要 toString() + nearestCityAuth: authorizations.nearestCityAuth ?? null, // 已经是字符串,不需要 toString() + nearestCommunity: authorizations.nearestCommunity ?? null, // 已经是字符串,不需要 toString() }; } } diff --git a/backend/services/referral-service/src/api/dto/referral.dto.ts b/backend/services/referral-service/src/api/dto/referral.dto.ts index 329be2b4..50fa717e 100644 --- a/backend/services/referral-service/src/api/dto/referral.dto.ts +++ b/backend/services/referral-service/src/api/dto/referral.dto.ts @@ -14,9 +14,9 @@ export class CreateReferralDto { @IsString() userId: string; - @ApiProperty({ description: '账户序列号 (8位)', example: 10000001 }) - @IsInt() - accountSequence: number; + @ApiProperty({ description: '账户序列号 (新格式: D + YYMMDD + 5位序号)', example: 'D2512110008' }) + @IsString() + accountSequence: string; // 格式: D + YYMMDD + 5位序号 @ApiPropertyOptional({ description: '推荐码', example: 'RWA123ABC' }) @IsOptional() @@ -24,10 +24,10 @@ export class CreateReferralDto { @Length(6, 20) referrerCode?: string; - @ApiPropertyOptional({ description: '邀请人账户序列号', example: 10000001 }) + @ApiPropertyOptional({ description: '邀请人账户序列号 (新格式: D + YYMMDD + 5位序号)', example: 'D2512110007' }) @IsOptional() - @IsInt() - inviterAccountSequence?: number; + @IsString() + inviterAccountSequence?: string; } export class GetDirectReferralsDto { @@ -84,8 +84,8 @@ export class DirectReferralResponseDto { @ApiProperty({ description: '用户ID' }) userId: string; - @ApiProperty({ description: '账户序列号 (8位)' }) - accountSequence: number; + @ApiProperty({ description: '账户序列号 (新格式: D + YYMMDD + 5位序号)' }) + accountSequence: string; // 格式: D + YYMMDD + 5位序号 @ApiProperty({ description: '推荐码' }) referralCode: string; diff --git a/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts b/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts index 8eb76dcf..6f610e8c 100644 --- a/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts +++ b/backend/services/referral-service/src/application/commands/create-referral-relationship.command.ts @@ -1,8 +1,8 @@ export class CreateReferralRelationshipCommand { constructor( public readonly userId: bigint, - public readonly accountSequence: number, + public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号 public readonly referrerCode: string | null = null, - public readonly inviterAccountSequence: number | null = null, + public readonly inviterAccountSequence: string | null = null, // 格式: D + YYMMDD + 5位序号 ) {} } diff --git a/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts b/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts index 2fc86286..ce7a6f12 100644 --- a/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts +++ b/backend/services/referral-service/src/application/event-handlers/user-registered.handler.ts @@ -8,8 +8,8 @@ import { CreateReferralRelationshipCommand } from '../commands'; */ interface UserAccountCreatedPayload { userId: string; - accountSequence: number; - inviterSequence: number | null; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 + inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号 registeredAt: string; // UserAccountCreated 有 phoneNumber, UserAccountAutoCreated 没有 phoneNumber?: string; @@ -66,11 +66,13 @@ export class UserRegisteredHandler implements OnModuleInit { // 使用 accountSequence 作为 userId,因为 identity-service 的 userId 是内部自增ID, // 在事件发布时可能还是临时值 0,而 accountSequence 是全局唯一的业务标识 + // 注意:userId 仍然需要是 bigint,这里我们需要从 accountSequence 字符串中提取数值部分或使用其他方式 + // 暂时保持原有逻辑,但 accountSequence 本身现在是字符串类型 const command = new CreateReferralRelationshipCommand( - BigInt(payload.accountSequence), // 使用 accountSequence 作为 userId - payload.accountSequence, + BigInt(payload.userId), // 使用 userId + payload.accountSequence, // 现在是字符串格式 null, // referrerCode - 不通过推荐码查找 - payload.inviterSequence, // 通过 accountSequence 查找推荐人 + payload.inviterSequence, // 通过 accountSequence 查找推荐人,现在是字符串格式 ); const result = await this.referralService.createReferralRelationship(command); diff --git a/backend/services/referral-service/src/application/queries/get-direct-referrals.query.ts b/backend/services/referral-service/src/application/queries/get-direct-referrals.query.ts index 7854fd0e..d7c54ec4 100644 --- a/backend/services/referral-service/src/application/queries/get-direct-referrals.query.ts +++ b/backend/services/referral-service/src/application/queries/get-direct-referrals.query.ts @@ -8,7 +8,7 @@ export class GetDirectReferralsQuery { export interface DirectReferralResult { userId: string; - accountSequence: number; // 8位账户序列号,显示用 + accountSequence: string; // 格式: D + YYMMDD + 5位序号 referralCode: string; personalPlantingCount: number; // 个人认种量 teamPlantingCount: number; // 团队认种量 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 bfb9af83..486e16b4 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 accountSequence: number) {} + constructor(public readonly accountSequence: string) {} // 格式: D + YYMMDD + 5位序号 } export interface UserReferralInfoResult { diff --git a/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts b/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts index b256963d..73e2b316 100644 --- a/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts +++ b/backend/services/referral-service/src/domain/aggregates/referral-relationship/referral-relationship.aggregate.ts @@ -4,7 +4,7 @@ import { DomainEvent, ReferralRelationshipCreatedEvent } from '../../events'; export interface ReferralRelationshipProps { id: bigint; userId: bigint; - accountSequence: number; // 8位账户序列号,用于跨服务关联 + accountSequence: string; // 格式: D + YYMMDD + 5位序号 referrerId: bigint | null; referralCode: string; referralChain: bigint[]; @@ -26,7 +26,7 @@ export class ReferralRelationship { private constructor( private readonly _id: bigint, private readonly _userId: UserId, - private readonly _accountSequence: number, + private readonly _accountSequence: string, // 格式: D + YYMMDD + 5位序号 private readonly _referrerId: UserId | null, private readonly _referralCode: ReferralCode, private readonly _referralChain: ReferralChain, @@ -41,7 +41,7 @@ export class ReferralRelationship { get userId(): bigint { return this._userId.value; } - get accountSequence(): number { + get accountSequence(): string { return this._accountSequence; } get referrerId(): bigint | null { @@ -68,7 +68,7 @@ export class ReferralRelationship { */ static create( userId: bigint, - accountSequence: number, + accountSequence: string, // 格式: D + YYMMDD + 5位序号 referrerId: bigint | null, parentReferralChain: bigint[] = [], ): ReferralRelationship { diff --git a/backend/services/referral-service/src/domain/aggregates/team-statistics/team-statistics.aggregate.ts b/backend/services/referral-service/src/domain/aggregates/team-statistics/team-statistics.aggregate.ts index fba11b87..21699a6a 100644 --- a/backend/services/referral-service/src/domain/aggregates/team-statistics/team-statistics.aggregate.ts +++ b/backend/services/referral-service/src/domain/aggregates/team-statistics/team-statistics.aggregate.ts @@ -191,16 +191,16 @@ export class TeamStatistics { /** * 增加个人认种量 + * 注意:个人认种不计入团队认种(teamPlantingCount 只统计下级的认种) */ addPersonalPlanting(count: number, provinceCode: string, cityCode: string): void { this._personalPlantingCount += count; - this._teamPlantingCount += count; + // 不更新 _teamPlantingCount,因为团队认种不包含自己 this._provinceCityDistribution = this._provinceCityDistribution.add( provinceCode, cityCode, count, ); - this.recalculateLeaderboardScore(); this._lastCalculatedAt = new Date(); this._updatedAt = new Date(); diff --git a/backend/services/referral-service/src/domain/repositories/referral-relationship.repository.interface.ts b/backend/services/referral-service/src/domain/repositories/referral-relationship.repository.interface.ts index 02de568f..359dc5b0 100644 --- a/backend/services/referral-service/src/domain/repositories/referral-relationship.repository.interface.ts +++ b/backend/services/referral-service/src/domain/repositories/referral-relationship.repository.interface.ts @@ -16,8 +16,9 @@ export interface IReferralRelationshipRepository { /** * 根据账户序列号查找 (用于跨服务关联) + * @param accountSequence 格式: D + YYMMDD + 5位序号 */ - findByAccountSequence(accountSequence: number): Promise; + findByAccountSequence(accountSequence: string): Promise; /** * 根据推荐码查找 diff --git a/backend/services/referral-service/src/infrastructure/external/authorization-service.client.ts b/backend/services/referral-service/src/infrastructure/external/authorization-service.client.ts index 1dfd54e0..0d815821 100644 --- a/backend/services/referral-service/src/infrastructure/external/authorization-service.client.ts +++ b/backend/services/referral-service/src/infrastructure/external/authorization-service.client.ts @@ -12,7 +12,7 @@ export interface AuthorizationServiceResponse { } export interface NearestAuthorizationResult { - accountSequence: number | null; + accountSequence: string | null; // 格式: D + YYMMDD + 5位序号 } /** @@ -37,10 +37,10 @@ export class AuthorizationServiceClient { /** * 查找用户推荐链中最近的社区授权用户 - * @param accountSequence 用户的 accountSequence + * @param accountSequence 用户的 accountSequence (格式: D + YYMMDD + 5位序号) * @returns 最近社区授权用户的 accountSequence,如果没有则返回 null */ - async findNearestCommunity(accountSequence: number): Promise { + async findNearestCommunity(accountSequence: string): Promise { try { const response = await firstValueFrom( this.httpService @@ -74,14 +74,14 @@ export class AuthorizationServiceClient { /** * 查找用户推荐链中最近的省公司授权用户(匹配指定省份) - * @param accountSequence 用户的 accountSequence + * @param accountSequence 用户的 accountSequence (格式: D + YYMMDD + 5位序号) * @param provinceCode 省份代码 * @returns 最近省公司授权用户的 accountSequence,如果没有则返回 null */ async findNearestProvince( - accountSequence: number, + accountSequence: string, provinceCode: string, - ): Promise { + ): Promise { try { const response = await firstValueFrom( this.httpService @@ -115,14 +115,14 @@ export class AuthorizationServiceClient { /** * 查找用户推荐链中最近的市公司授权用户(匹配指定城市) - * @param accountSequence 用户的 accountSequence + * @param accountSequence 用户的 accountSequence (格式: D + YYMMDD + 5位序号) * @param cityCode 城市代码 * @returns 最近市公司授权用户的 accountSequence,如果没有则返回 null */ async findNearestCity( - accountSequence: number, + accountSequence: string, cityCode: string, - ): Promise { + ): Promise { try { const response = await firstValueFrom( this.httpService @@ -159,13 +159,13 @@ export class AuthorizationServiceClient { * 优化性能:同时发起三个请求 */ async findAllNearestAuthorizations( - accountSequence: number, + accountSequence: string, // 格式: D + YYMMDD + 5位序号 provinceCode: string, cityCode: string, ): Promise<{ - nearestCommunity: number | null; - nearestProvinceAuth: number | null; - nearestCityAuth: number | null; + nearestCommunity: string | null; + nearestProvinceAuth: string | null; + nearestCityAuth: string | null; }> { const [nearestCommunity, nearestProvinceAuth, nearestCityAuth] = await Promise.all([ diff --git a/backend/services/referral-service/src/infrastructure/repositories/referral-relationship.repository.ts b/backend/services/referral-service/src/infrastructure/repositories/referral-relationship.repository.ts index 6b32f48a..b1a355f0 100644 --- a/backend/services/referral-service/src/infrastructure/repositories/referral-relationship.repository.ts +++ b/backend/services/referral-service/src/infrastructure/repositories/referral-relationship.repository.ts @@ -46,7 +46,7 @@ export class ReferralRelationshipRepository implements IReferralRelationshipRepo return ReferralRelationship.reconstitute(this.mapToProps(record)); } - async findByAccountSequence(accountSequence: number): Promise { + async findByAccountSequence(accountSequence: string): Promise { const record = await this.prisma.referralRelationship.findUnique({ where: { accountSequence }, }); @@ -100,7 +100,7 @@ export class ReferralRelationshipRepository implements IReferralRelationshipRepo private mapToProps(record: { id: bigint; userId: bigint; - accountSequence: number; + accountSequence: string; // 格式: D + YYMMDD + 5位序号 referrerId: bigint | null; myReferralCode: string; ancestorPath: bigint[]; diff --git a/backend/services/referral-service/src/infrastructure/repositories/team-statistics.repository.ts b/backend/services/referral-service/src/infrastructure/repositories/team-statistics.repository.ts index 6e683b5d..8327ab9d 100644 --- a/backend/services/referral-service/src/infrastructure/repositories/team-statistics.repository.ts +++ b/backend/services/referral-service/src/infrastructure/repositories/team-statistics.repository.ts @@ -52,13 +52,13 @@ export class TeamStatisticsRepository implements ITeamStatisticsRepository { where: { userId: { in: referralIds } }, select: { userId: true, accountSequence: true }, }); - const sequenceMap = new Map(); + const sequenceMap = new Map(); for (const rel of referralRelations) { sequenceMap.set(rel.userId, rel.accountSequence); } for (const dr of data.directReferrals) { - const accountSequence = sequenceMap.get(dr.referralId) ?? Number(dr.referralId); + const accountSequence = sequenceMap.get(dr.referralId) ?? dr.referralId.toString(); await tx.directReferral.upsert({ where: { uk_referrer_referral: { @@ -73,7 +73,7 @@ export class TeamStatisticsRepository implements ITeamStatisticsRepository { create: { referrerId: data.userId, referralId: dr.referralId, - referralSequence: BigInt(accountSequence), + referralSequence: accountSequence, // 现在是字符串类型,不需要 BigInt 转换 teamPlantingCount: dr.teamCount, }, }); @@ -210,7 +210,7 @@ export class TeamStatisticsRepository implements ITeamStatisticsRepository { create: { referrerId: update.userId, referralId: update.fromDirectReferralId, - referralSequence: update.fromDirectReferralId, + referralSequence: update.fromDirectReferralId.toString(), // 转换为字符串 teamPlantingCount: update.countDelta, }, }); diff --git a/backend/services/reward-service/prisma/schema.prisma b/backend/services/reward-service/prisma/schema.prisma index 3ec3573e..198b7a4f 100644 --- a/backend/services/reward-service/prisma/schema.prisma +++ b/backend/services/reward-service/prisma/schema.prisma @@ -14,7 +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") // 账户序列号 + accountSequence String @map("account_sequence") @db.VarChar(20) // 账户序列号 // === 奖励来源 === sourceOrderNo String @map("source_order_no") @db.VarChar(50) // 来源认种订单号(字符串格式如PLT1765391584505Q0Q6QD) @@ -58,7 +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") // 账户序列号 + accountSequence String @unique @map("account_sequence") @db.VarChar(20) // 账户序列号 // === 待领取收益 (24h倒计时) === pendingUsdt Decimal @default(0) @map("pending_usdt") @db.Decimal(20, 8) @@ -124,7 +124,7 @@ model RightDefinition { model SettlementRecord { id BigInt @id @default(autoincrement()) @map("settlement_id") userId BigInt @map("user_id") - accountSequence BigInt @map("account_sequence") // 账户序列号 + accountSequence String @map("account_sequence") @db.VarChar(20) // 账户序列号 // === 结算金额 === usdtAmount Decimal @map("usdt_amount") @db.Decimal(20, 8) 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 b200a903..9874ada1 100644 --- a/backend/services/reward-service/src/api/controllers/reward.controller.ts +++ b/backend/services/reward-service/src/api/controllers/reward.controller.ts @@ -18,7 +18,7 @@ export class RewardController { @ApiOperation({ summary: '获取我的收益汇总' }) @ApiResponse({ status: 200, description: '成功', type: RewardSummaryDto }) async getSummary(@Request() req): Promise { - const accountSequence = BigInt(req.user.accountSequence); + const accountSequence = req.user.accountSequence; const summary = await this.rewardService.getRewardSummary(accountSequence); return { @@ -43,7 +43,7 @@ export class RewardController { @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number = 1, @Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number = 20, ) { - const accountSequence = BigInt(req.user.accountSequence); + const accountSequence = req.user.accountSequence; const filters: any = {}; if (status) filters.status = status; if (rightType) filters.rightType = rightType; @@ -55,7 +55,7 @@ export class RewardController { @ApiOperation({ summary: '获取待领取奖励(含倒计时)' }) @ApiResponse({ status: 200, description: '成功' }) async getPending(@Request() req) { - const accountSequence = BigInt(req.user.accountSequence); + const accountSequence = 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 9f0adce5..0599ab50 100644 --- a/backend/services/reward-service/src/api/controllers/settlement.controller.ts +++ b/backend/services/reward-service/src/api/controllers/settlement.controller.ts @@ -20,7 +20,7 @@ export class SettlementController { @Request() req, @Body() dto: SettleRewardsDto, ): Promise { - const accountSequence = BigInt(req.user.accountSequence); + const accountSequence = req.user.accountSequence; return this.rewardService.settleRewards({ accountSequence, 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 411335b0..85a45b14 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 @@ -36,6 +36,11 @@ export class RewardApplicationService { /** * 分配奖励 (响应认种订单支付成功事件) + * + * 核心职责: + * 1. 计算考核后的奖励分配方案(调用 authorization-service) + * 2. 调用 wallet-service 执行真正的资金分配 + * 3. 保存奖励流水记录 */ async distributeRewards(params: { sourceOrderNo: string; // 订单号是字符串格式如 PLT1765391584505Q0Q6QD @@ -46,13 +51,38 @@ export class RewardApplicationService { }): Promise { this.logger.log(`Distributing rewards for order ${params.sourceOrderNo}`); - // 1. 计算所有奖励 + // 1. 计算所有奖励(包含考核逻辑,调用 authorization-service) const rewards = await this.rewardCalculationService.calculateRewards(params); - // 2. 保存奖励流水 + // 2. 调用 wallet-service 执行真正的资金分配 + const allocations = rewards.map(reward => ({ + targetType: reward.rewardSource.rightType, + amount: reward.usdtAmount.amount, + targetAccountId: `USER:${reward.userId}`, + metadata: { + rightType: reward.rewardSource.rightType, + sourceOrderNo: params.sourceOrderNo, + sourceUserId: params.sourceUserId.toString(), + memo: reward.memo, + }, + })); + + const allocateResult = await this.walletService.allocateFunds({ + orderId: params.sourceOrderNo, + allocations, + }); + + if (!allocateResult.success) { + this.logger.error(`Failed to allocate funds for order ${params.sourceOrderNo}: ${allocateResult.error}`); + throw new Error(`资金分配失败: ${allocateResult.error}`); + } + + this.logger.log(`Wallet allocation completed for order ${params.sourceOrderNo}`); + + // 3. 保存奖励流水 await this.rewardLedgerEntryRepository.saveAll(rewards); - // 3. 更新各用户的汇总数据并收集需要同步到 wallet-service 的汇总 + // 4. 更新各用户的汇总数据 const userIds = [...new Set(rewards.map(r => r.userId))]; const updatedSummaries: RewardSummary[] = []; @@ -77,10 +107,6 @@ export class RewardApplicationService { updatedSummaries.push(summary); } - // 4. 写入 Outbox 发送到 wallet-service 同步汇总数据 - // [已屏蔽] 前端直接从 reward-service 查询,不再同步到 wallet-service - // await this.publishSummaryUpdatesToOutbox(updatedSummaries); - // 5. 发布领域事件 for (const reward of rewards) { await this.eventPublisher.publishAll(reward.domainEvents); @@ -101,7 +127,7 @@ export class RewardApplicationService { const pendingRewards = await this.rewardLedgerEntryRepository.findPendingByUserId(userId); // 使用 userId 作为 accountSequence (系统账户场景) - const summary = await this.rewardSummaryRepository.getOrCreate(userId, userId); + const summary = await this.rewardSummaryRepository.getOrCreate(userId, userId.toString()); let claimedCount = 0; let totalUsdtClaimed = 0; @@ -138,7 +164,7 @@ export class RewardApplicationService { * 结算可结算收益 */ async settleRewards(params: { - accountSequence: bigint; + accountSequence: string; settleCurrency: string; // BNB/OG/USDT/DST }): Promise<{ success: boolean; @@ -248,7 +274,7 @@ export class RewardApplicationService { for (const [userId, rewards] of userRewardsMap) { const userIdBigInt = BigInt(userId); // 使用 userId 作为 accountSequence (过期任务中无法获取真实 accountSequence) - const summary = await this.rewardSummaryRepository.getOrCreate(userIdBigInt, userIdBigInt); + const summary = await this.rewardSummaryRepository.getOrCreate(userIdBigInt, userId); for (const reward of rewards) { await this.rewardLedgerEntryRepository.save(reward); @@ -263,7 +289,7 @@ export class RewardApplicationService { // 将过期奖励转入总部社区 if (expiredRewards.length > 0) { - const hqSummary = await this.rewardSummaryRepository.getOrCreate(HEADQUARTERS_COMMUNITY_USER_ID, HEADQUARTERS_COMMUNITY_USER_ID); + const hqSummary = await this.rewardSummaryRepository.getOrCreate(HEADQUARTERS_COMMUNITY_USER_ID, HEADQUARTERS_COMMUNITY_USER_ID.toString()); const totalHqUsdt = expiredRewards.reduce((sum, r) => sum + r.usdtAmount.amount, 0); const totalHqHashpower = expiredRewards.reduce((sum, r) => sum + r.hashpowerAmount.value, 0); hqSummary.addSettleable(Money.USDT(totalHqUsdt), Hashpower.create(totalHqHashpower)); @@ -288,7 +314,7 @@ export class RewardApplicationService { /** * 获取用户奖励汇总 */ - async getRewardSummary(accountSequence: bigint) { + async getRewardSummary(accountSequence: string) { const summary = await this.rewardSummaryRepository.findByAccountSequence(accountSequence); if (!summary) { @@ -322,7 +348,7 @@ export class RewardApplicationService { * 获取用户奖励明细 */ async getRewardDetails( - accountSequence: bigint, + accountSequence: string, filters?: { status?: RewardStatus; rightType?: RightType; @@ -360,7 +386,7 @@ export class RewardApplicationService { /** * 获取待领取奖励(含倒计时) */ - async getPendingRewards(accountSequence: bigint) { + async getPendingRewards(accountSequence: string) { const rewards = await this.rewardLedgerEntryRepository.findPendingByAccountSequence(accountSequence); return rewards.map(r => ({ @@ -384,11 +410,11 @@ export class RewardApplicationService { const outboxEvents: OutboxEventData[] = summaries.map(summary => ({ eventType: 'reward.summary.updated', topic: 'reward.summary.updated', - key: summary.accountSequence.toString(), - aggregateId: summary.accountSequence.toString(), + key: summary.accountSequence, + aggregateId: summary.accountSequence, aggregateType: 'RewardSummary', payload: { - accountSequence: summary.accountSequence.toString(), + accountSequence: summary.accountSequence, userId: summary.userId.toString(), pendingUsdt: summary.pendingUsdt.amount, pendingHashpower: summary.pendingHashpower.value, 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 35728c28..586f2afd 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,7 +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 _accountSequence: string; private readonly _rewardSource: RewardSource; private readonly _usdtAmount: Money; private readonly _hashpowerAmount: Hashpower; @@ -36,7 +36,7 @@ export class RewardLedgerEntry { private constructor( userId: bigint, - accountSequence: bigint, + accountSequence: string, rewardSource: RewardSource, usdtAmount: Money, hashpowerAmount: Hashpower, @@ -62,7 +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 accountSequence(): string { return this._accountSequence; } get rewardSource(): RewardSource { return this._rewardSource; } get usdtAmount(): Money { return this._usdtAmount; } get hashpowerAmount(): Hashpower { return this._hashpowerAmount; } @@ -88,7 +88,7 @@ export class RewardLedgerEntry { */ static createPending(params: { userId: bigint; - accountSequence: bigint; + accountSequence: string; rewardSource: RewardSource; usdtAmount: Money; hashpowerAmount: Hashpower; @@ -130,7 +130,7 @@ export class RewardLedgerEntry { */ static createSettleable(params: { userId: bigint; - accountSequence: bigint; + accountSequence: string; rewardSource: RewardSource; usdtAmount: Money; hashpowerAmount: Hashpower; @@ -260,7 +260,7 @@ export class RewardLedgerEntry { static reconstitute(data: { id: bigint; userId: bigint; - accountSequence: bigint; + accountSequence: string; rewardSource: RewardSource; usdtAmount: number; hashpowerAmount: number; diff --git a/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.spec.ts b/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.spec.ts index ac8dbb46..772a9c85 100644 --- a/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.spec.ts +++ b/backend/services/reward-service/src/domain/aggregates/reward-ledger-entry/reward-ledger-entry.spec.ts @@ -13,7 +13,7 @@ describe('RewardLedgerEntry', () => { it('should create a pending reward with 24h expiration', () => { const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -32,7 +32,7 @@ describe('RewardLedgerEntry', () => { const before = Date.now(); const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -51,7 +51,7 @@ describe('RewardLedgerEntry', () => { it('should create a settleable reward without expiration', () => { const entry = RewardLedgerEntry.createSettleable({ userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.create(5), @@ -69,7 +69,7 @@ describe('RewardLedgerEntry', () => { it('should transition pending to settleable', () => { const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -88,7 +88,7 @@ describe('RewardLedgerEntry', () => { it('should throw error when not pending', () => { const entry = RewardLedgerEntry.createSettleable({ userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -102,7 +102,7 @@ describe('RewardLedgerEntry', () => { it('should transition pending to expired', () => { const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -120,7 +120,7 @@ describe('RewardLedgerEntry', () => { it('should throw error when not pending', () => { const entry = RewardLedgerEntry.createSettleable({ userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -134,7 +134,7 @@ describe('RewardLedgerEntry', () => { it('should transition settleable to settled', () => { const entry = RewardLedgerEntry.createSettleable({ userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -152,7 +152,7 @@ describe('RewardLedgerEntry', () => { it('should throw error when not settleable', () => { const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -166,7 +166,7 @@ describe('RewardLedgerEntry', () => { it('should return remaining time for pending rewards', () => { const entry = RewardLedgerEntry.createPending({ userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -182,7 +182,7 @@ describe('RewardLedgerEntry', () => { it('should return 0 for settleable rewards', () => { const entry = RewardLedgerEntry.createSettleable({ userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: Money.USDT(500), hashpowerAmount: Hashpower.zero(), @@ -197,7 +197,7 @@ describe('RewardLedgerEntry', () => { const data = { id: BigInt(1), userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', rewardSource: createRewardSource(), usdtAmount: 500, hashpowerAmount: 5, 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 e0a8dc4e..86069e1a 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,7 +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 readonly _accountSequence: string; // 待领取收益 private _pendingUsdt: Money; @@ -30,7 +30,7 @@ export class RewardSummary { private _lastUpdateAt: Date; private readonly _createdAt: Date; - private constructor(userId: bigint, accountSequence: bigint) { + private constructor(userId: bigint, accountSequence: string) { this._userId = userId; this._accountSequence = accountSequence; this._pendingUsdt = Money.zero(); @@ -49,7 +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 accountSequence(): string { return this._accountSequence; } get pendingUsdt(): Money { return this._pendingUsdt; } get pendingHashpower(): Hashpower { return this._pendingHashpower; } get pendingExpireAt(): Date | null { return this._pendingExpireAt; } @@ -64,7 +64,7 @@ export class RewardSummary { // ============ 工厂方法 ============ - static create(userId: bigint, accountSequence: bigint): RewardSummary { + static create(userId: bigint, accountSequence: string): RewardSummary { return new RewardSummary(userId, accountSequence); } @@ -143,7 +143,7 @@ export class RewardSummary { static reconstitute(data: { id: bigint; userId: bigint; - accountSequence: bigint; + accountSequence: string; pendingUsdt: number; pendingHashpower: number; pendingExpireAt: Date | null; diff --git a/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.spec.ts b/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.spec.ts index c6cd6719..dc1b6c9e 100644 --- a/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.spec.ts +++ b/backend/services/reward-service/src/domain/aggregates/reward-summary/reward-summary.spec.ts @@ -5,10 +5,10 @@ import { Hashpower } from '../../value-objects/hashpower.vo'; describe('RewardSummary', () => { describe('create', () => { it('should create a new summary with zero values', () => { - const summary = RewardSummary.create(BigInt(100), BigInt(100)); + const summary = RewardSummary.create(BigInt(100), 'D251211000'); expect(summary.userId).toBe(BigInt(100)); - expect(summary.accountSequence).toBe(BigInt(100)); + expect(summary.accountSequence).toBe('D251211000'); expect(summary.pendingUsdt.amount).toBe(0); expect(summary.settleableUsdt.amount).toBe(0); expect(summary.settledTotalUsdt.amount).toBe(0); @@ -18,7 +18,7 @@ describe('RewardSummary', () => { describe('addPending', () => { it('should add pending rewards and update expire time', () => { - const summary = RewardSummary.create(BigInt(100), BigInt(100)); + const summary = RewardSummary.create(BigInt(100), 'D251211000'); const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000); summary.addPending(Money.USDT(500), Hashpower.create(5), expireAt); @@ -29,7 +29,7 @@ describe('RewardSummary', () => { }); it('should keep earliest expire time', () => { - const summary = RewardSummary.create(BigInt(100), BigInt(100)); + const summary = RewardSummary.create(BigInt(100), 'D251211000'); const earlyExpire = new Date(Date.now() + 12 * 60 * 60 * 1000); const lateExpire = new Date(Date.now() + 24 * 60 * 60 * 1000); @@ -43,7 +43,7 @@ describe('RewardSummary', () => { describe('movePendingToSettleable', () => { it('should move amounts from pending to settleable', () => { - const summary = RewardSummary.create(BigInt(100), BigInt(100)); + const summary = RewardSummary.create(BigInt(100), 'D251211000'); const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000); summary.addPending(Money.USDT(500), Hashpower.create(5), expireAt); @@ -57,7 +57,7 @@ describe('RewardSummary', () => { }); it('should partially move pending amounts', () => { - const summary = RewardSummary.create(BigInt(100), BigInt(100)); + const summary = RewardSummary.create(BigInt(100), 'D251211000'); const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000); summary.addPending(Money.USDT(500), Hashpower.create(5), expireAt); @@ -72,7 +72,7 @@ describe('RewardSummary', () => { describe('movePendingToExpired', () => { it('should move amounts from pending to expired', () => { - const summary = RewardSummary.create(BigInt(100), BigInt(100)); + const summary = RewardSummary.create(BigInt(100), 'D251211000'); const expireAt = new Date(Date.now() + 24 * 60 * 60 * 1000); summary.addPending(Money.USDT(500), Hashpower.create(5), expireAt); @@ -86,7 +86,7 @@ describe('RewardSummary', () => { describe('addSettleable', () => { it('should add directly to settleable', () => { - const summary = RewardSummary.create(BigInt(100), BigInt(100)); + const summary = RewardSummary.create(BigInt(100), 'D251211000'); summary.addSettleable(Money.USDT(1000), Hashpower.create(10)); @@ -97,7 +97,7 @@ describe('RewardSummary', () => { describe('settle', () => { it('should move settleable to settled total', () => { - const summary = RewardSummary.create(BigInt(100), BigInt(100)); + const summary = RewardSummary.create(BigInt(100), 'D251211000'); summary.addSettleable(Money.USDT(1000), Hashpower.create(10)); summary.settle(Money.USDT(1000), Hashpower.create(10)); @@ -108,7 +108,7 @@ describe('RewardSummary', () => { }); it('should accumulate settled totals', () => { - const summary = RewardSummary.create(BigInt(100), BigInt(100)); + const summary = RewardSummary.create(BigInt(100), 'D251211000'); summary.addSettleable(Money.USDT(1000), Hashpower.create(10)); summary.settle(Money.USDT(500), Hashpower.create(5)); @@ -125,7 +125,7 @@ describe('RewardSummary', () => { const data = { id: BigInt(1), userId: BigInt(100), - accountSequence: BigInt(100), + accountSequence: 'D251211000', pendingUsdt: 500, pendingHashpower: 5, pendingExpireAt: new Date(), 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 523b6272..4c9146fb 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 @@ -17,7 +17,7 @@ export interface IRewardLedgerEntryRepository { pagination?: { page: number; pageSize: number }, ): Promise; findByAccountSequence( - accountSequence: bigint, + accountSequence: string, filters?: { status?: RewardStatus; rightType?: RightType; @@ -27,13 +27,13 @@ export interface IRewardLedgerEntryRepository { pagination?: { page: number; pageSize: number }, ): Promise; findPendingByUserId(userId: bigint): Promise; - findPendingByAccountSequence(accountSequence: bigint): Promise; + findPendingByAccountSequence(accountSequence: string): Promise; findSettleableByUserId(userId: bigint): Promise; - findSettleableByAccountSequence(accountSequence: bigint): Promise; + findSettleableByAccountSequence(accountSequence: string): Promise; findExpiredPending(beforeDate: Date): Promise; findBySourceOrderNo(sourceOrderNo: string): Promise; countByUserId(userId: bigint, status?: RewardStatus): Promise; - countByAccountSequence(accountSequence: bigint, status?: RewardStatus): Promise; + countByAccountSequence(accountSequence: string, 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 810d1a29..48530dbe 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,9 +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, accountSequence: bigint): Promise; - getOrCreateByAccountSequence(accountSequence: bigint): Promise; + findByAccountSequence(accountSequence: string): Promise; + getOrCreate(userId: bigint, accountSequence: string): Promise; + getOrCreateByAccountSequence(accountSequence: string): Promise; findByUserIds(userIds: bigint[]): Promise>; findTopSettleableUsers(limit: number): Promise; } diff --git a/backend/services/reward-service/src/domain/services/reward-calculation.service.ts b/backend/services/reward-service/src/domain/services/reward-calculation.service.ts index b0154b82..19a0fb3c 100644 --- a/backend/services/reward-service/src/domain/services/reward-calculation.service.ts +++ b/backend/services/reward-service/src/domain/services/reward-calculation.service.ts @@ -1,657 +1,657 @@ -import { Injectable, Inject, Logger } from '@nestjs/common'; -import { RewardLedgerEntry } from '../aggregates/reward-ledger-entry/reward-ledger-entry.aggregate'; -import { RewardSource } from '../value-objects/reward-source.vo'; -import { RightType, RIGHT_AMOUNTS } from '../value-objects/right-type.enum'; -import { Money } from '../value-objects/money.vo'; -import { Hashpower } from '../value-objects/hashpower.vo'; - -// 外部服务接口 (防腐层) -export interface IReferralServiceClient { - getReferralChain(userId: bigint): Promise<{ - ancestors: Array<{ userId: bigint; hasPlanted: boolean }>; - }>; -} - -export interface RewardDistribution { - distributions: Array<{ - accountSequence: number; - treeCount: number; - reason: string; - }>; -} - -export interface AreaRewardDistribution { - distributions: Array<{ - accountSequence: number; - treeCount: number; - reason: string; - isSystemAccount: boolean; - }>; -} - -export interface IAuthorizationServiceClient { - findNearestAuthorizedProvince(userId: bigint, provinceCode: string): Promise; - findNearestAuthorizedCity(userId: bigint, cityCode: string): Promise; - findNearestCommunity(userId: bigint): Promise; - getCommunityRewardDistribution(userId: bigint, treeCount: number): Promise; - getProvinceTeamRewardDistribution(userId: bigint, provinceCode: string, treeCount: number): Promise; - getProvinceAreaRewardDistribution(provinceCode: string, treeCount: number): Promise; - getCityTeamRewardDistribution(userId: bigint, cityCode: string, treeCount: number): Promise; - getCityAreaRewardDistribution(cityCode: string, treeCount: number): Promise; -} - -export const REFERRAL_SERVICE_CLIENT = Symbol('IReferralServiceClient'); -export const AUTHORIZATION_SERVICE_CLIENT = Symbol('IAuthorizationServiceClient'); - -// ============================================ -// 系统账户ID配置 -// ============================================ - -// 总部社区账户ID -const HEADQUARTERS_COMMUNITY_USER_ID = BigInt(1); - -// 成本费账户ID (需要在系统初始化时创建) -const COST_FEE_ACCOUNT_ID = BigInt(2); - -// 运营费账户ID (需要在系统初始化时创建) -const OPERATION_FEE_ACCOUNT_ID = BigInt(3); - -// RWAD 1号底池账户ID (需要在系统初始化时创建) -const RWAD_POOL_ACCOUNT_ID = BigInt(4); - -@Injectable() -export class RewardCalculationService { - private readonly logger = new Logger(RewardCalculationService.name); - - constructor( - @Inject(REFERRAL_SERVICE_CLIENT) - private readonly referralService: IReferralServiceClient, - @Inject(AUTHORIZATION_SERVICE_CLIENT) - private readonly authorizationService: IAuthorizationServiceClient, - ) {} - - /** - * 计算认种订单产生的所有奖励 - * 总计 2199 USDT = 400 + 300 + 9 + 800 + 500 + 15 + 20 + 35 + 40 + 80 - */ - async calculateRewards(params: { - sourceOrderNo: string; // 订单号是字符串格式 - sourceUserId: bigint; - treeCount: number; - provinceCode: string; - cityCode: string; - }): Promise { - this.logger.log( - `[calculateRewards] START orderNo=${params.sourceOrderNo}, userId=${params.sourceUserId}, ` + - `treeCount=${params.treeCount}, province=${params.provinceCode}, city=${params.cityCode}`, - ); - - const rewards: RewardLedgerEntry[] = []; - - // ============================================ - // 系统费用类 (709 USDT) - // ============================================ - - // 1. 成本费 (400 USDT) - const costFeeReward = this.calculateCostFee( - params.sourceOrderNo, - params.sourceUserId, - params.treeCount, - ); - rewards.push(costFeeReward); - - // 2. 运营费 (300 USDT) - const operationFeeReward = this.calculateOperationFee( - params.sourceOrderNo, - params.sourceUserId, - params.treeCount, - ); - rewards.push(operationFeeReward); - - // 3. 总部社区基础费 (9 USDT) - const headquartersBaseFeeReward = this.calculateHeadquartersBaseFee( - params.sourceOrderNo, - params.sourceUserId, - params.treeCount, - ); - rewards.push(headquartersBaseFeeReward); - - // 4. RWAD底池注入 (800 USDT) - const rwadPoolReward = this.calculateRwadPoolInjection( - params.sourceOrderNo, - params.sourceUserId, - params.treeCount, - ); - rewards.push(rwadPoolReward); - - // ============================================ - // 用户权益类 (690 USDT + 算力) - // ============================================ - - // 5. 分享权益 (500 USDT) - const shareRewards = await this.calculateShareRights( - params.sourceOrderNo, - params.sourceUserId, - params.treeCount, - ); - rewards.push(...shareRewards); - - // 6. 省团队权益 (20 USDT) - 可能返回多条记录(考核分配) - const provinceTeamRewards = await this.calculateProvinceTeamRight( - params.sourceOrderNo, - params.sourceUserId, - params.provinceCode, - params.treeCount, - ); - rewards.push(...provinceTeamRewards); - - // 7. 省区域权益 (15 USDT + 1%算力) - 可能返回多条记录(考核分配) - const provinceAreaRewards = await this.calculateProvinceAreaRight( - params.sourceOrderNo, - params.sourceUserId, - params.provinceCode, - params.treeCount, - ); - rewards.push(...provinceAreaRewards); - - // 8. 市团队权益 (40 USDT) - 可能返回多条记录(考核分配) - const cityTeamRewards = await this.calculateCityTeamRight( - params.sourceOrderNo, - params.sourceUserId, - params.cityCode, - params.treeCount, - ); - rewards.push(...cityTeamRewards); - - // 9. 市区域权益 (35 USDT + 2%算力) - 可能返回多条记录(考核分配) - const cityAreaRewards = await this.calculateCityAreaRight( - params.sourceOrderNo, - params.sourceUserId, - params.cityCode, - params.treeCount, - ); - rewards.push(...cityAreaRewards); - - // 10. 社区权益 (80 USDT) - 可能返回多条记录(考核分配) - const communityRewards = await this.calculateCommunityRight( - params.sourceOrderNo, - params.sourceUserId, - params.treeCount, - ); - rewards.push(...communityRewards); - - this.logger.log( - `[calculateRewards] DONE orderNo=${params.sourceOrderNo}, totalRewards=${rewards.length}`, - ); - - return rewards; - } - - // ============================================ - // 系统费用计算方法 - // ============================================ - - /** - * 计算成本费 (400 USDT) - * 分配至指定成本账户 - */ - private calculateCostFee( - sourceOrderNo: string, - sourceUserId: bigint, - treeCount: number, - ): RewardLedgerEntry { - const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.COST_FEE]; - const usdtAmount = Money.USDT(usdt * treeCount); - const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); - - const rewardSource = RewardSource.create( - RightType.COST_FEE, - sourceOrderNo, - sourceUserId, - ); - - return RewardLedgerEntry.createSettleable({ - userId: COST_FEE_ACCOUNT_ID, - accountSequence: COST_FEE_ACCOUNT_ID, - rewardSource, - usdtAmount, - hashpowerAmount: hashpower, - memo: `成本费:来自用户${sourceUserId}的认种,${treeCount}棵树`, - }); - } - - /** - * 计算运营费 (300 USDT) - * 分配至指定运营账户 - */ - private calculateOperationFee( - sourceOrderNo: string, - sourceUserId: bigint, - treeCount: number, - ): RewardLedgerEntry { - const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.OPERATION_FEE]; - const usdtAmount = Money.USDT(usdt * treeCount); - const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); - - const rewardSource = RewardSource.create( - RightType.OPERATION_FEE, - sourceOrderNo, - sourceUserId, - ); - - return RewardLedgerEntry.createSettleable({ - userId: OPERATION_FEE_ACCOUNT_ID, - accountSequence: OPERATION_FEE_ACCOUNT_ID, - rewardSource, - usdtAmount, - hashpowerAmount: hashpower, - memo: `运营费:来自用户${sourceUserId}的认种,${treeCount}棵树`, - }); - } - - /** - * 计算总部社区基础费 (9 USDT) - * 分配至总部社区账户 - */ - private calculateHeadquartersBaseFee( - sourceOrderNo: string, - sourceUserId: bigint, - treeCount: number, - ): RewardLedgerEntry { - const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.HEADQUARTERS_BASE_FEE]; - const usdtAmount = Money.USDT(usdt * treeCount); - const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); - - const rewardSource = RewardSource.create( - RightType.HEADQUARTERS_BASE_FEE, - sourceOrderNo, - sourceUserId, - ); - - return RewardLedgerEntry.createSettleable({ - userId: HEADQUARTERS_COMMUNITY_USER_ID, - accountSequence: HEADQUARTERS_COMMUNITY_USER_ID, - rewardSource, - usdtAmount, - hashpowerAmount: hashpower, - memo: `总部社区基础费:来自用户${sourceUserId}的认种,${treeCount}棵树`, - }); - } - - /** - * 计算RWAD底池注入 (800 USDT) - * 注入RWAD 1号底池 - */ - private calculateRwadPoolInjection( - sourceOrderNo: string, - sourceUserId: bigint, - treeCount: number, - ): RewardLedgerEntry { - const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.RWAD_POOL_INJECTION]; - const usdtAmount = Money.USDT(usdt * treeCount); - const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); - - const rewardSource = RewardSource.create( - RightType.RWAD_POOL_INJECTION, - sourceOrderNo, - sourceUserId, - ); - - return RewardLedgerEntry.createSettleable({ - userId: RWAD_POOL_ACCOUNT_ID, - accountSequence: RWAD_POOL_ACCOUNT_ID, - rewardSource, - usdtAmount, - hashpowerAmount: hashpower, - memo: `RWAD底池注入:来自用户${sourceUserId}的认种,${treeCount}棵树,800U注入1号底池`, - }); - } - - // ============================================ - // 用户权益计算方法 - // ============================================ - - /** - * 计算分享权益 (500 USDT) - */ - private async calculateShareRights( - sourceOrderNo: string, - sourceUserId: bigint, - treeCount: number, - ): Promise { - this.logger.debug(`[calculateShareRights] userId=${sourceUserId}, treeCount=${treeCount}`); - - const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.SHARE_RIGHT]; - const usdtAmount = Money.USDT(usdt * treeCount); - const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); - - const rewardSource = RewardSource.create( - RightType.SHARE_RIGHT, - sourceOrderNo, - sourceUserId, - ); - - // 获取推荐链 - const referralChain = await this.referralService.getReferralChain(sourceUserId); - - if (referralChain.ancestors.length > 0) { - const directReferrer = referralChain.ancestors[0]; - - if (directReferrer.hasPlanted) { - // 推荐人已认种,直接可结算 - this.logger.debug( - `[calculateShareRights] referrer=${directReferrer.userId} hasPlanted=true -> SETTLEABLE`, - ); - return [RewardLedgerEntry.createSettleable({ - userId: directReferrer.userId, - accountSequence: directReferrer.userId, - rewardSource, - usdtAmount, - hashpowerAmount: hashpower, - memo: `分享权益:来自用户${sourceUserId}的认种`, - })]; - } else { - // 推荐人未认种,进入待领取(24h倒计时) - this.logger.debug( - `[calculateShareRights] referrer=${directReferrer.userId} hasPlanted=false -> PENDING (24h)`, - ); - return [RewardLedgerEntry.createPending({ - userId: directReferrer.userId, - accountSequence: directReferrer.userId, - rewardSource, - usdtAmount, - hashpowerAmount: hashpower, - memo: `分享权益:来自用户${sourceUserId}的认种(24h内认种可领取)`, - })]; - } - } else { - // 无推荐人,进总部社区 - this.logger.debug(`[calculateShareRights] no referrer -> HEADQUARTERS`); - return [RewardLedgerEntry.createSettleable({ - userId: HEADQUARTERS_COMMUNITY_USER_ID, - accountSequence: HEADQUARTERS_COMMUNITY_USER_ID, - rewardSource, - usdtAmount, - hashpowerAmount: hashpower, - memo: '分享权益:无推荐人,进总部社区', - })]; - } - } - - /** - * 计算省团队权益 (20 USDT) - * 根据考核规则(500棵初审),可能返回多条分配记录 - */ - private async calculateProvinceTeamRight( - sourceOrderNo: string, - sourceUserId: bigint, - provinceCode: string, - treeCount: number, - ): Promise { - this.logger.debug( - `[calculateProvinceTeamRight] userId=${sourceUserId}, province=${provinceCode}, treeCount=${treeCount}`, - ); - - const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.PROVINCE_TEAM_RIGHT]; - - // 调用 authorization-service 获取省团队权益分配方案 - const distribution = await this.authorizationService.getProvinceTeamRewardDistribution( - sourceUserId, - provinceCode, - treeCount, - ); - - this.logger.debug( - `[calculateProvinceTeamRight] distribution: ${JSON.stringify(distribution.distributions)}`, - ); - - const rewards: RewardLedgerEntry[] = []; - - // 根据分配方案创建奖励记录 - for (const item of distribution.distributions) { - const itemUsdtAmount = Money.USDT(usdt * item.treeCount); - const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent); - - const rewardSource = RewardSource.create( - RightType.PROVINCE_TEAM_RIGHT, - sourceOrderNo, - sourceUserId, - ); - - rewards.push( - RewardLedgerEntry.createSettleable({ - userId: BigInt(item.accountSequence), - accountSequence: BigInt(item.accountSequence), - rewardSource, - usdtAmount: itemUsdtAmount, - hashpowerAmount: itemHashpower, - memo: `省团队权益(${provinceCode}):${item.reason}`, - }), - ); - } - - return rewards; - } - - /** - * 计算省区域权益 (15 USDT + 1%算力) - * 根据考核规则(50000棵),可能返回多条分配记录 - * - 未达标:进系统省账户 - * - 已达标:进正式省公司账户 - */ - private async calculateProvinceAreaRight( - sourceOrderNo: string, - sourceUserId: bigint, - provinceCode: string, - treeCount: number, - ): Promise { - this.logger.debug( - `[calculateProvinceAreaRight] province=${provinceCode}, treeCount=${treeCount}`, - ); - - const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.PROVINCE_AREA_RIGHT]; - - // 调用 authorization-service 获取省区域权益分配方案 - const distribution = await this.authorizationService.getProvinceAreaRewardDistribution( - provinceCode, - treeCount, - ); - - this.logger.debug( - `[calculateProvinceAreaRight] distribution: ${JSON.stringify(distribution.distributions)}`, - ); - - const rewards: RewardLedgerEntry[] = []; - - // 根据分配方案创建奖励记录 - for (const item of distribution.distributions) { - const itemUsdtAmount = Money.USDT(usdt * item.treeCount); - const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent); - - const rewardSource = RewardSource.create( - RightType.PROVINCE_AREA_RIGHT, - sourceOrderNo, - sourceUserId, - ); - - rewards.push( - RewardLedgerEntry.createSettleable({ - userId: BigInt(item.accountSequence), - accountSequence: BigInt(item.accountSequence), - rewardSource, - usdtAmount: itemUsdtAmount, - hashpowerAmount: itemHashpower, - memo: `省区域权益(${provinceCode}):${item.reason}`, - }), - ); - } - - return rewards; - } - - /** - * 计算市团队权益 (40 USDT) - * 根据考核规则(100棵初审),可能返回多条分配记录 - */ - private async calculateCityTeamRight( - sourceOrderNo: string, - sourceUserId: bigint, - cityCode: string, - treeCount: number, - ): Promise { - this.logger.debug( - `[calculateCityTeamRight] userId=${sourceUserId}, city=${cityCode}, treeCount=${treeCount}`, - ); - - const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.CITY_TEAM_RIGHT]; - - // 调用 authorization-service 获取市团队权益分配方案 - const distribution = await this.authorizationService.getCityTeamRewardDistribution( - sourceUserId, - cityCode, - treeCount, - ); - - this.logger.debug( - `[calculateCityTeamRight] distribution: ${JSON.stringify(distribution.distributions)}`, - ); - - const rewards: RewardLedgerEntry[] = []; - - // 根据分配方案创建奖励记录 - for (const item of distribution.distributions) { - const itemUsdtAmount = Money.USDT(usdt * item.treeCount); - const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent); - - const rewardSource = RewardSource.create( - RightType.CITY_TEAM_RIGHT, - sourceOrderNo, - sourceUserId, - ); - - rewards.push( - RewardLedgerEntry.createSettleable({ - userId: BigInt(item.accountSequence), - accountSequence: BigInt(item.accountSequence), - rewardSource, - usdtAmount: itemUsdtAmount, - hashpowerAmount: itemHashpower, - memo: `市团队权益(${cityCode}):${item.reason}`, - }), - ); - } - - return rewards; - } - - /** - * 计算市区域权益 (35 USDT + 2%算力) - * 根据考核规则(10000棵),可能返回多条分配记录 - * - 未达标:进系统市账户 - * - 已达标:进正式市公司账户 - */ - private async calculateCityAreaRight( - sourceOrderNo: string, - sourceUserId: bigint, - cityCode: string, - treeCount: number, - ): Promise { - this.logger.debug( - `[calculateCityAreaRight] city=${cityCode}, treeCount=${treeCount}`, - ); - - const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.CITY_AREA_RIGHT]; - - // 调用 authorization-service 获取市区域权益分配方案 - const distribution = await this.authorizationService.getCityAreaRewardDistribution( - cityCode, - treeCount, - ); - - this.logger.debug( - `[calculateCityAreaRight] distribution: ${JSON.stringify(distribution.distributions)}`, - ); - - const rewards: RewardLedgerEntry[] = []; - - // 根据分配方案创建奖励记录 - for (const item of distribution.distributions) { - const itemUsdtAmount = Money.USDT(usdt * item.treeCount); - const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent); - - const rewardSource = RewardSource.create( - RightType.CITY_AREA_RIGHT, - sourceOrderNo, - sourceUserId, - ); - - rewards.push( - RewardLedgerEntry.createSettleable({ - userId: BigInt(item.accountSequence), - accountSequence: BigInt(item.accountSequence), - rewardSource, - usdtAmount: itemUsdtAmount, - hashpowerAmount: itemHashpower, - memo: `市区域权益(${cityCode}):${item.reason}`, - }), - ); - } - - return rewards; - } - - /** - * 计算社区权益 (80 USDT) - * 根据考核规则,可能返回多条分配记录: - * - 权益已激活:全部给该社区 - * - 权益未激活:考核前的部分给上级社区/总部,考核后的部分给该社区 - */ - private async calculateCommunityRight( - sourceOrderNo: string, - sourceUserId: bigint, - treeCount: number, - ): Promise { - this.logger.debug( - `[calculateCommunityRight] userId=${sourceUserId}, treeCount=${treeCount}`, - ); - - const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.COMMUNITY_RIGHT]; - - // 调用 authorization-service 获取社区权益分配方案 - const distribution = await this.authorizationService.getCommunityRewardDistribution( - sourceUserId, - treeCount, - ); - - this.logger.debug( - `[calculateCommunityRight] distribution: ${JSON.stringify(distribution.distributions)}`, - ); - - const rewards: RewardLedgerEntry[] = []; - - // 根据分配方案创建奖励记录 - for (const item of distribution.distributions) { - const itemUsdtAmount = Money.USDT(usdt * item.treeCount); - const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent); - - const rewardSource = RewardSource.create( - RightType.COMMUNITY_RIGHT, - sourceOrderNo, - sourceUserId, - ); - - rewards.push( - RewardLedgerEntry.createSettleable({ - userId: BigInt(item.accountSequence), - accountSequence: BigInt(item.accountSequence), - rewardSource, - usdtAmount: itemUsdtAmount, - hashpowerAmount: itemHashpower, - memo: `社区权益:${item.reason}`, - }), - ); - } - - return rewards; - } -} +import { Injectable, Inject, Logger } from '@nestjs/common'; +import { RewardLedgerEntry } from '../aggregates/reward-ledger-entry/reward-ledger-entry.aggregate'; +import { RewardSource } from '../value-objects/reward-source.vo'; +import { RightType, RIGHT_AMOUNTS } from '../value-objects/right-type.enum'; +import { Money } from '../value-objects/money.vo'; +import { Hashpower } from '../value-objects/hashpower.vo'; + +// 外部服务接口 (防腐层) +export interface IReferralServiceClient { + getReferralChain(userId: bigint): Promise<{ + ancestors: Array<{ userId: bigint; hasPlanted: boolean }>; + }>; +} + +export interface RewardDistribution { + distributions: Array<{ + accountSequence: string; + treeCount: number; + reason: string; + }>; +} + +export interface AreaRewardDistribution { + distributions: Array<{ + accountSequence: string; + treeCount: number; + reason: string; + isSystemAccount: boolean; + }>; +} + +export interface IAuthorizationServiceClient { + findNearestAuthorizedProvince(userId: bigint, provinceCode: string): Promise; + findNearestAuthorizedCity(userId: bigint, cityCode: string): Promise; + findNearestCommunity(userId: bigint): Promise; + getCommunityRewardDistribution(userId: bigint, treeCount: number): Promise; + getProvinceTeamRewardDistribution(userId: bigint, provinceCode: string, treeCount: number): Promise; + getProvinceAreaRewardDistribution(provinceCode: string, treeCount: number): Promise; + getCityTeamRewardDistribution(userId: bigint, cityCode: string, treeCount: number): Promise; + getCityAreaRewardDistribution(cityCode: string, treeCount: number): Promise; +} + +export const REFERRAL_SERVICE_CLIENT = Symbol('IReferralServiceClient'); +export const AUTHORIZATION_SERVICE_CLIENT = Symbol('IAuthorizationServiceClient'); + +// ============================================ +// 系统账户ID配置 +// ============================================ + +// 总部社区账户ID +const HEADQUARTERS_COMMUNITY_USER_ID = BigInt(1); + +// 成本费账户ID (需要在系统初始化时创建) +const COST_FEE_ACCOUNT_ID = BigInt(2); + +// 运营费账户ID (需要在系统初始化时创建) +const OPERATION_FEE_ACCOUNT_ID = BigInt(3); + +// RWAD 1号底池账户ID (需要在系统初始化时创建) +const RWAD_POOL_ACCOUNT_ID = BigInt(4); + +@Injectable() +export class RewardCalculationService { + private readonly logger = new Logger(RewardCalculationService.name); + + constructor( + @Inject(REFERRAL_SERVICE_CLIENT) + private readonly referralService: IReferralServiceClient, + @Inject(AUTHORIZATION_SERVICE_CLIENT) + private readonly authorizationService: IAuthorizationServiceClient, + ) {} + + /** + * 计算认种订单产生的所有奖励 + * 总计 2199 USDT = 400 + 300 + 9 + 800 + 500 + 15 + 20 + 35 + 40 + 80 + */ + async calculateRewards(params: { + sourceOrderNo: string; // 订单号是字符串格式 + sourceUserId: bigint; + treeCount: number; + provinceCode: string; + cityCode: string; + }): Promise { + this.logger.log( + `[calculateRewards] START orderNo=${params.sourceOrderNo}, userId=${params.sourceUserId}, ` + + `treeCount=${params.treeCount}, province=${params.provinceCode}, city=${params.cityCode}`, + ); + + const rewards: RewardLedgerEntry[] = []; + + // ============================================ + // 系统费用类 (709 USDT) + // ============================================ + + // 1. 成本费 (400 USDT) + const costFeeReward = this.calculateCostFee( + params.sourceOrderNo, + params.sourceUserId, + params.treeCount, + ); + rewards.push(costFeeReward); + + // 2. 运营费 (300 USDT) + const operationFeeReward = this.calculateOperationFee( + params.sourceOrderNo, + params.sourceUserId, + params.treeCount, + ); + rewards.push(operationFeeReward); + + // 3. 总部社区基础费 (9 USDT) + const headquartersBaseFeeReward = this.calculateHeadquartersBaseFee( + params.sourceOrderNo, + params.sourceUserId, + params.treeCount, + ); + rewards.push(headquartersBaseFeeReward); + + // 4. RWAD底池注入 (800 USDT) + const rwadPoolReward = this.calculateRwadPoolInjection( + params.sourceOrderNo, + params.sourceUserId, + params.treeCount, + ); + rewards.push(rwadPoolReward); + + // ============================================ + // 用户权益类 (690 USDT + 算力) + // ============================================ + + // 5. 分享权益 (500 USDT) + const shareRewards = await this.calculateShareRights( + params.sourceOrderNo, + params.sourceUserId, + params.treeCount, + ); + rewards.push(...shareRewards); + + // 6. 省团队权益 (20 USDT) - 可能返回多条记录(考核分配) + const provinceTeamRewards = await this.calculateProvinceTeamRight( + params.sourceOrderNo, + params.sourceUserId, + params.provinceCode, + params.treeCount, + ); + rewards.push(...provinceTeamRewards); + + // 7. 省区域权益 (15 USDT + 1%算力) - 可能返回多条记录(考核分配) + const provinceAreaRewards = await this.calculateProvinceAreaRight( + params.sourceOrderNo, + params.sourceUserId, + params.provinceCode, + params.treeCount, + ); + rewards.push(...provinceAreaRewards); + + // 8. 市团队权益 (40 USDT) - 可能返回多条记录(考核分配) + const cityTeamRewards = await this.calculateCityTeamRight( + params.sourceOrderNo, + params.sourceUserId, + params.cityCode, + params.treeCount, + ); + rewards.push(...cityTeamRewards); + + // 9. 市区域权益 (35 USDT + 2%算力) - 可能返回多条记录(考核分配) + const cityAreaRewards = await this.calculateCityAreaRight( + params.sourceOrderNo, + params.sourceUserId, + params.cityCode, + params.treeCount, + ); + rewards.push(...cityAreaRewards); + + // 10. 社区权益 (80 USDT) - 可能返回多条记录(考核分配) + const communityRewards = await this.calculateCommunityRight( + params.sourceOrderNo, + params.sourceUserId, + params.treeCount, + ); + rewards.push(...communityRewards); + + this.logger.log( + `[calculateRewards] DONE orderNo=${params.sourceOrderNo}, totalRewards=${rewards.length}`, + ); + + return rewards; + } + + // ============================================ + // 系统费用计算方法 + // ============================================ + + /** + * 计算成本费 (400 USDT) + * 分配至指定成本账户 + */ + private calculateCostFee( + sourceOrderNo: string, + sourceUserId: bigint, + treeCount: number, + ): RewardLedgerEntry { + const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.COST_FEE]; + const usdtAmount = Money.USDT(usdt * treeCount); + const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); + + const rewardSource = RewardSource.create( + RightType.COST_FEE, + sourceOrderNo, + sourceUserId, + ); + + return RewardLedgerEntry.createSettleable({ + userId: COST_FEE_ACCOUNT_ID, + accountSequence: 'S0000000002', + rewardSource, + usdtAmount, + hashpowerAmount: hashpower, + memo: `成本费:来自用户${sourceUserId}的认种,${treeCount}棵树`, + }); + } + + /** + * 计算运营费 (300 USDT) + * 分配至指定运营账户 + */ + private calculateOperationFee( + sourceOrderNo: string, + sourceUserId: bigint, + treeCount: number, + ): RewardLedgerEntry { + const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.OPERATION_FEE]; + const usdtAmount = Money.USDT(usdt * treeCount); + const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); + + const rewardSource = RewardSource.create( + RightType.OPERATION_FEE, + sourceOrderNo, + sourceUserId, + ); + + return RewardLedgerEntry.createSettleable({ + userId: OPERATION_FEE_ACCOUNT_ID, + accountSequence: 'S0000000003', + rewardSource, + usdtAmount, + hashpowerAmount: hashpower, + memo: `运营费:来自用户${sourceUserId}的认种,${treeCount}棵树`, + }); + } + + /** + * 计算总部社区基础费 (9 USDT) + * 分配至总部社区账户 + */ + private calculateHeadquartersBaseFee( + sourceOrderNo: string, + sourceUserId: bigint, + treeCount: number, + ): RewardLedgerEntry { + const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.HEADQUARTERS_BASE_FEE]; + const usdtAmount = Money.USDT(usdt * treeCount); + const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); + + const rewardSource = RewardSource.create( + RightType.HEADQUARTERS_BASE_FEE, + sourceOrderNo, + sourceUserId, + ); + + return RewardLedgerEntry.createSettleable({ + userId: HEADQUARTERS_COMMUNITY_USER_ID, + accountSequence: 'S0000000001', + rewardSource, + usdtAmount, + hashpowerAmount: hashpower, + memo: `总部社区基础费:来自用户${sourceUserId}的认种,${treeCount}棵树`, + }); + } + + /** + * 计算RWAD底池注入 (800 USDT) + * 注入RWAD 1号底池 + */ + private calculateRwadPoolInjection( + sourceOrderNo: string, + sourceUserId: bigint, + treeCount: number, + ): RewardLedgerEntry { + const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.RWAD_POOL_INJECTION]; + const usdtAmount = Money.USDT(usdt * treeCount); + const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); + + const rewardSource = RewardSource.create( + RightType.RWAD_POOL_INJECTION, + sourceOrderNo, + sourceUserId, + ); + + return RewardLedgerEntry.createSettleable({ + userId: RWAD_POOL_ACCOUNT_ID, + accountSequence: 'S0000000004', + rewardSource, + usdtAmount, + hashpowerAmount: hashpower, + memo: `RWAD底池注入:来自用户${sourceUserId}的认种,${treeCount}棵树,800U注入1号底池`, + }); + } + + // ============================================ + // 用户权益计算方法 + // ============================================ + + /** + * 计算分享权益 (500 USDT) + */ + private async calculateShareRights( + sourceOrderNo: string, + sourceUserId: bigint, + treeCount: number, + ): Promise { + this.logger.debug(`[calculateShareRights] userId=${sourceUserId}, treeCount=${treeCount}`); + + const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.SHARE_RIGHT]; + const usdtAmount = Money.USDT(usdt * treeCount); + const hashpower = Hashpower.fromTreeCount(treeCount, hashpowerPercent); + + const rewardSource = RewardSource.create( + RightType.SHARE_RIGHT, + sourceOrderNo, + sourceUserId, + ); + + // 获取推荐链 + const referralChain = await this.referralService.getReferralChain(sourceUserId); + + if (referralChain.ancestors.length > 0) { + const directReferrer = referralChain.ancestors[0]; + + if (directReferrer.hasPlanted) { + // 推荐人已认种,直接可结算 + this.logger.debug( + `[calculateShareRights] referrer=${directReferrer.userId} hasPlanted=true -> SETTLEABLE`, + ); + return [RewardLedgerEntry.createSettleable({ + userId: directReferrer.userId, + accountSequence: directReferrer.userId.toString(), + rewardSource, + usdtAmount, + hashpowerAmount: hashpower, + memo: `分享权益:来自用户${sourceUserId}的认种`, + })]; + } else { + // 推荐人未认种,进入待领取(24h倒计时) + this.logger.debug( + `[calculateShareRights] referrer=${directReferrer.userId} hasPlanted=false -> PENDING (24h)`, + ); + return [RewardLedgerEntry.createPending({ + userId: directReferrer.userId, + accountSequence: directReferrer.userId.toString(), + rewardSource, + usdtAmount, + hashpowerAmount: hashpower, + memo: `分享权益:来自用户${sourceUserId}的认种(24h内认种可领取)`, + })]; + } + } else { + // 无推荐人,进总部社区 + this.logger.debug(`[calculateShareRights] no referrer -> HEADQUARTERS`); + return [RewardLedgerEntry.createSettleable({ + userId: HEADQUARTERS_COMMUNITY_USER_ID, + accountSequence: 'S0000000001', + rewardSource, + usdtAmount, + hashpowerAmount: hashpower, + memo: '分享权益:无推荐人,进总部社区', + })]; + } + } + + /** + * 计算省团队权益 (20 USDT) + * 根据考核规则(500棵初审),可能返回多条分配记录 + */ + private async calculateProvinceTeamRight( + sourceOrderNo: string, + sourceUserId: bigint, + provinceCode: string, + treeCount: number, + ): Promise { + this.logger.debug( + `[calculateProvinceTeamRight] userId=${sourceUserId}, province=${provinceCode}, treeCount=${treeCount}`, + ); + + const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.PROVINCE_TEAM_RIGHT]; + + // 调用 authorization-service 获取省团队权益分配方案 + const distribution = await this.authorizationService.getProvinceTeamRewardDistribution( + sourceUserId, + provinceCode, + treeCount, + ); + + this.logger.debug( + `[calculateProvinceTeamRight] distribution: ${JSON.stringify(distribution.distributions)}`, + ); + + const rewards: RewardLedgerEntry[] = []; + + // 根据分配方案创建奖励记录 + for (const item of distribution.distributions) { + const itemUsdtAmount = Money.USDT(usdt * item.treeCount); + const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent); + + const rewardSource = RewardSource.create( + RightType.PROVINCE_TEAM_RIGHT, + sourceOrderNo, + sourceUserId, + ); + + rewards.push( + RewardLedgerEntry.createSettleable({ + userId: BigInt(item.accountSequence), + accountSequence: item.accountSequence, + rewardSource, + usdtAmount: itemUsdtAmount, + hashpowerAmount: itemHashpower, + memo: `省团队权益(${provinceCode}):${item.reason}`, + }), + ); + } + + return rewards; + } + + /** + * 计算省区域权益 (15 USDT + 1%算力) + * 根据考核规则(50000棵),可能返回多条分配记录 + * - 未达标:进系统省账户 + * - 已达标:进正式省公司账户 + */ + private async calculateProvinceAreaRight( + sourceOrderNo: string, + sourceUserId: bigint, + provinceCode: string, + treeCount: number, + ): Promise { + this.logger.debug( + `[calculateProvinceAreaRight] province=${provinceCode}, treeCount=${treeCount}`, + ); + + const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.PROVINCE_AREA_RIGHT]; + + // 调用 authorization-service 获取省区域权益分配方案 + const distribution = await this.authorizationService.getProvinceAreaRewardDistribution( + provinceCode, + treeCount, + ); + + this.logger.debug( + `[calculateProvinceAreaRight] distribution: ${JSON.stringify(distribution.distributions)}`, + ); + + const rewards: RewardLedgerEntry[] = []; + + // 根据分配方案创建奖励记录 + for (const item of distribution.distributions) { + const itemUsdtAmount = Money.USDT(usdt * item.treeCount); + const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent); + + const rewardSource = RewardSource.create( + RightType.PROVINCE_AREA_RIGHT, + sourceOrderNo, + sourceUserId, + ); + + rewards.push( + RewardLedgerEntry.createSettleable({ + userId: BigInt(item.accountSequence), + accountSequence: item.accountSequence, + rewardSource, + usdtAmount: itemUsdtAmount, + hashpowerAmount: itemHashpower, + memo: `省区域权益(${provinceCode}):${item.reason}`, + }), + ); + } + + return rewards; + } + + /** + * 计算市团队权益 (40 USDT) + * 根据考核规则(100棵初审),可能返回多条分配记录 + */ + private async calculateCityTeamRight( + sourceOrderNo: string, + sourceUserId: bigint, + cityCode: string, + treeCount: number, + ): Promise { + this.logger.debug( + `[calculateCityTeamRight] userId=${sourceUserId}, city=${cityCode}, treeCount=${treeCount}`, + ); + + const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.CITY_TEAM_RIGHT]; + + // 调用 authorization-service 获取市团队权益分配方案 + const distribution = await this.authorizationService.getCityTeamRewardDistribution( + sourceUserId, + cityCode, + treeCount, + ); + + this.logger.debug( + `[calculateCityTeamRight] distribution: ${JSON.stringify(distribution.distributions)}`, + ); + + const rewards: RewardLedgerEntry[] = []; + + // 根据分配方案创建奖励记录 + for (const item of distribution.distributions) { + const itemUsdtAmount = Money.USDT(usdt * item.treeCount); + const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent); + + const rewardSource = RewardSource.create( + RightType.CITY_TEAM_RIGHT, + sourceOrderNo, + sourceUserId, + ); + + rewards.push( + RewardLedgerEntry.createSettleable({ + userId: BigInt(item.accountSequence), + accountSequence: item.accountSequence, + rewardSource, + usdtAmount: itemUsdtAmount, + hashpowerAmount: itemHashpower, + memo: `市团队权益(${cityCode}):${item.reason}`, + }), + ); + } + + return rewards; + } + + /** + * 计算市区域权益 (35 USDT + 2%算力) + * 根据考核规则(10000棵),可能返回多条分配记录 + * - 未达标:进系统市账户 + * - 已达标:进正式市公司账户 + */ + private async calculateCityAreaRight( + sourceOrderNo: string, + sourceUserId: bigint, + cityCode: string, + treeCount: number, + ): Promise { + this.logger.debug( + `[calculateCityAreaRight] city=${cityCode}, treeCount=${treeCount}`, + ); + + const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.CITY_AREA_RIGHT]; + + // 调用 authorization-service 获取市区域权益分配方案 + const distribution = await this.authorizationService.getCityAreaRewardDistribution( + cityCode, + treeCount, + ); + + this.logger.debug( + `[calculateCityAreaRight] distribution: ${JSON.stringify(distribution.distributions)}`, + ); + + const rewards: RewardLedgerEntry[] = []; + + // 根据分配方案创建奖励记录 + for (const item of distribution.distributions) { + const itemUsdtAmount = Money.USDT(usdt * item.treeCount); + const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent); + + const rewardSource = RewardSource.create( + RightType.CITY_AREA_RIGHT, + sourceOrderNo, + sourceUserId, + ); + + rewards.push( + RewardLedgerEntry.createSettleable({ + userId: BigInt(item.accountSequence), + accountSequence: item.accountSequence, + rewardSource, + usdtAmount: itemUsdtAmount, + hashpowerAmount: itemHashpower, + memo: `市区域权益(${cityCode}):${item.reason}`, + }), + ); + } + + return rewards; + } + + /** + * 计算社区权益 (80 USDT) + * 根据考核规则,可能返回多条分配记录: + * - 权益已激活:全部给该社区 + * - 权益未激活:考核前的部分给上级社区/总部,考核后的部分给该社区 + */ + private async calculateCommunityRight( + sourceOrderNo: string, + sourceUserId: bigint, + treeCount: number, + ): Promise { + this.logger.debug( + `[calculateCommunityRight] userId=${sourceUserId}, treeCount=${treeCount}`, + ); + + const { usdt, hashpowerPercent } = RIGHT_AMOUNTS[RightType.COMMUNITY_RIGHT]; + + // 调用 authorization-service 获取社区权益分配方案 + const distribution = await this.authorizationService.getCommunityRewardDistribution( + sourceUserId, + treeCount, + ); + + this.logger.debug( + `[calculateCommunityRight] distribution: ${JSON.stringify(distribution.distributions)}`, + ); + + const rewards: RewardLedgerEntry[] = []; + + // 根据分配方案创建奖励记录 + for (const item of distribution.distributions) { + const itemUsdtAmount = Money.USDT(usdt * item.treeCount); + const itemHashpower = Hashpower.fromTreeCount(item.treeCount, hashpowerPercent); + + const rewardSource = RewardSource.create( + RightType.COMMUNITY_RIGHT, + sourceOrderNo, + sourceUserId, + ); + + rewards.push( + RewardLedgerEntry.createSettleable({ + userId: BigInt(item.accountSequence), + accountSequence: item.accountSequence, + rewardSource, + usdtAmount: itemUsdtAmount, + hashpowerAmount: itemHashpower, + memo: `社区权益:${item.reason}`, + }), + ); + } + + return rewards; + } +} diff --git a/backend/services/reward-service/src/infrastructure/external/authorization-service/authorization-service.client.ts b/backend/services/reward-service/src/infrastructure/external/authorization-service/authorization-service.client.ts index 9b3b0cbc..eb030feb 100644 --- a/backend/services/reward-service/src/infrastructure/external/authorization-service/authorization-service.client.ts +++ b/backend/services/reward-service/src/infrastructure/external/authorization-service/authorization-service.client.ts @@ -1,315 +1,315 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { IAuthorizationServiceClient, RewardDistribution, AreaRewardDistribution } from '../../../domain/services/reward-calculation.service'; - -// authorization-service 返回格式(经过 TransformInterceptor 包装) -interface AuthorizationServiceResponse { - success: boolean; - data: T; - timestamp: string; -} - -interface NearestAuthorizationResult { - accountSequence: number | null; -} - -interface RewardDistributionResult { - distributions: Array<{ - accountSequence: number; - treeCount: number; - reason: string; - }>; -} - -interface AreaRewardDistributionResult { - distributions: Array<{ - accountSequence: number; - treeCount: number; - reason: string; - isSystemAccount: boolean; - }>; -} - -@Injectable() -export class AuthorizationServiceClient implements IAuthorizationServiceClient { - private readonly logger = new Logger(AuthorizationServiceClient.name); - private readonly baseUrl: string; - - constructor(private readonly configService: ConfigService) { - this.baseUrl = this.configService.get('AUTHORIZATION_SERVICE_URL', 'http://localhost:3006'); - } - - async findNearestAuthorizedProvince(userId: bigint, provinceCode: string): Promise { - try { - const response = await fetch( - `${this.baseUrl}/api/v1/authorization/nearest-province?accountSequence=${userId}&provinceCode=${provinceCode}`, - ); - - if (!response.ok) { - this.logger.warn(`No authorized province found for user ${userId}, province ${provinceCode}`); - return null; - } - - // authorization-service 返回格式: { success, data: { accountSequence }, timestamp } - const result: AuthorizationServiceResponse = await response.json(); - const accountSeq = result.data?.accountSequence; - return accountSeq ? BigInt(accountSeq) : null; - } catch (error) { - this.logger.error(`Error finding nearest authorized province:`, error); - return null; - } - } - - async findNearestAuthorizedCity(userId: bigint, cityCode: string): Promise { - try { - const response = await fetch( - `${this.baseUrl}/api/v1/authorization/nearest-city?accountSequence=${userId}&cityCode=${cityCode}`, - ); - - if (!response.ok) { - this.logger.warn(`No authorized city found for user ${userId}, city ${cityCode}`); - return null; - } - - // authorization-service 返回格式: { success, data: { accountSequence }, timestamp } - const result: AuthorizationServiceResponse = await response.json(); - const accountSeq = result.data?.accountSequence; - return accountSeq ? BigInt(accountSeq) : null; - } catch (error) { - this.logger.error(`Error finding nearest authorized city:`, error); - return null; - } - } - - async findNearestCommunity(userId: bigint): Promise { - try { - const response = await fetch( - `${this.baseUrl}/api/v1/authorization/nearest-community?accountSequence=${userId}`, - ); - - if (!response.ok) { - this.logger.warn(`No community found for user ${userId}`); - return null; - } - - // authorization-service 返回格式: { success, data: { accountSequence }, timestamp } - const result: AuthorizationServiceResponse = await response.json(); - const accountSeq = result.data?.accountSequence; - this.logger.debug(`findNearestCommunity for userId=${userId}: result=${accountSeq}`); - return accountSeq ? BigInt(accountSeq) : null; - } catch (error) { - this.logger.error(`Error finding nearest community:`, error); - return null; - } - } - - async getCommunityRewardDistribution(userId: bigint, treeCount: number): Promise { - const HEADQUARTERS_ACCOUNT_SEQUENCE = 1; - const defaultDistribution: RewardDistribution = { - distributions: [ - { - accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, - treeCount, - reason: '服务调用失败,默认进总部社区', - }, - ], - }; - - try { - const response = await fetch( - `${this.baseUrl}/api/v1/authorization/community-reward-distribution?accountSequence=${userId}&treeCount=${treeCount}`, - ); - - if (!response.ok) { - this.logger.warn(`Failed to get community reward distribution for user ${userId}, treeCount ${treeCount}`); - return defaultDistribution; - } - - // authorization-service 返回格式: { success, data: { distributions }, timestamp } - const result: AuthorizationServiceResponse = await response.json(); - - if (!result.data?.distributions || result.data.distributions.length === 0) { - this.logger.warn(`Empty distributions returned for user ${userId}`); - return defaultDistribution; - } - - this.logger.debug( - `getCommunityRewardDistribution for userId=${userId}, treeCount=${treeCount}: ` + - `distributions=${JSON.stringify(result.data.distributions)}`, - ); - - return result.data; - } catch (error) { - this.logger.error(`Error getting community reward distribution:`, error); - return defaultDistribution; - } - } - - async getProvinceTeamRewardDistribution(userId: bigint, provinceCode: string, treeCount: number): Promise { - const HEADQUARTERS_ACCOUNT_SEQUENCE = 1; - const defaultDistribution: RewardDistribution = { - distributions: [ - { - accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, - treeCount, - reason: '服务调用失败,默认进总部社区', - }, - ], - }; - - try { - const response = await fetch( - `${this.baseUrl}/api/v1/authorization/province-team-reward-distribution?accountSequence=${userId}&provinceCode=${provinceCode}&treeCount=${treeCount}`, - ); - - if (!response.ok) { - this.logger.warn(`Failed to get province team reward distribution for user ${userId}, province ${provinceCode}`); - return defaultDistribution; - } - - const result: AuthorizationServiceResponse = await response.json(); - - if (!result.data?.distributions || result.data.distributions.length === 0) { - this.logger.warn(`Empty province team distributions returned for user ${userId}`); - return defaultDistribution; - } - - this.logger.debug( - `getProvinceTeamRewardDistribution for userId=${userId}, provinceCode=${provinceCode}, treeCount=${treeCount}: ` + - `distributions=${JSON.stringify(result.data.distributions)}`, - ); - - return result.data; - } catch (error) { - this.logger.error(`Error getting province team reward distribution:`, error); - return defaultDistribution; - } - } - - async getProvinceAreaRewardDistribution(provinceCode: string, treeCount: number): Promise { - // 系统省账户ID: 9 + provinceCode (6位) - const systemProvinceAccountId = Number(`9${provinceCode.padStart(6, '0')}`); - const defaultDistribution: AreaRewardDistribution = { - distributions: [ - { - accountSequence: systemProvinceAccountId, - treeCount, - reason: '服务调用失败,默认进系统省账户', - isSystemAccount: true, - }, - ], - }; - - try { - const response = await fetch( - `${this.baseUrl}/api/v1/authorization/province-area-reward-distribution?provinceCode=${provinceCode}&treeCount=${treeCount}`, - ); - - if (!response.ok) { - this.logger.warn(`Failed to get province area reward distribution for province ${provinceCode}`); - return defaultDistribution; - } - - const result: AuthorizationServiceResponse = await response.json(); - - if (!result.data?.distributions || result.data.distributions.length === 0) { - this.logger.warn(`Empty province area distributions returned for province ${provinceCode}`); - return defaultDistribution; - } - - this.logger.debug( - `getProvinceAreaRewardDistribution for provinceCode=${provinceCode}, treeCount=${treeCount}: ` + - `distributions=${JSON.stringify(result.data.distributions)}`, - ); - - return result.data; - } catch (error) { - this.logger.error(`Error getting province area reward distribution:`, error); - return defaultDistribution; - } - } - - async getCityTeamRewardDistribution(userId: bigint, cityCode: string, treeCount: number): Promise { - const HEADQUARTERS_ACCOUNT_SEQUENCE = 1; - const defaultDistribution: RewardDistribution = { - distributions: [ - { - accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, - treeCount, - reason: '服务调用失败,默认进总部社区', - }, - ], - }; - - try { - const response = await fetch( - `${this.baseUrl}/api/v1/authorization/city-team-reward-distribution?accountSequence=${userId}&cityCode=${cityCode}&treeCount=${treeCount}`, - ); - - if (!response.ok) { - this.logger.warn(`Failed to get city team reward distribution for user ${userId}, city ${cityCode}`); - return defaultDistribution; - } - - const result: AuthorizationServiceResponse = await response.json(); - - if (!result.data?.distributions || result.data.distributions.length === 0) { - this.logger.warn(`Empty city team distributions returned for user ${userId}`); - return defaultDistribution; - } - - this.logger.debug( - `getCityTeamRewardDistribution for userId=${userId}, cityCode=${cityCode}, treeCount=${treeCount}: ` + - `distributions=${JSON.stringify(result.data.distributions)}`, - ); - - return result.data; - } catch (error) { - this.logger.error(`Error getting city team reward distribution:`, error); - return defaultDistribution; - } - } - - async getCityAreaRewardDistribution(cityCode: string, treeCount: number): Promise { - // 系统市账户ID: 8 + cityCode (6位) - const systemCityAccountId = Number(`8${cityCode.padStart(6, '0')}`); - const defaultDistribution: AreaRewardDistribution = { - distributions: [ - { - accountSequence: systemCityAccountId, - treeCount, - reason: '服务调用失败,默认进系统市账户', - isSystemAccount: true, - }, - ], - }; - - try { - const response = await fetch( - `${this.baseUrl}/api/v1/authorization/city-area-reward-distribution?cityCode=${cityCode}&treeCount=${treeCount}`, - ); - - if (!response.ok) { - this.logger.warn(`Failed to get city area reward distribution for city ${cityCode}`); - return defaultDistribution; - } - - const result: AuthorizationServiceResponse = await response.json(); - - if (!result.data?.distributions || result.data.distributions.length === 0) { - this.logger.warn(`Empty city area distributions returned for city ${cityCode}`); - return defaultDistribution; - } - - this.logger.debug( - `getCityAreaRewardDistribution for cityCode=${cityCode}, treeCount=${treeCount}: ` + - `distributions=${JSON.stringify(result.data.distributions)}`, - ); - - return result.data; - } catch (error) { - this.logger.error(`Error getting city area reward distribution:`, error); - return defaultDistribution; - } - } -} +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { IAuthorizationServiceClient, RewardDistribution, AreaRewardDistribution } from '../../../domain/services/reward-calculation.service'; + +// authorization-service 返回格式(经过 TransformInterceptor 包装) +interface AuthorizationServiceResponse { + success: boolean; + data: T; + timestamp: string; +} + +interface NearestAuthorizationResult { + accountSequence: string | null; +} + +interface RewardDistributionResult { + distributions: Array<{ + accountSequence: string; + treeCount: number; + reason: string; + }>; +} + +interface AreaRewardDistributionResult { + distributions: Array<{ + accountSequence: string; + treeCount: number; + reason: string; + isSystemAccount: boolean; + }>; +} + +@Injectable() +export class AuthorizationServiceClient implements IAuthorizationServiceClient { + private readonly logger = new Logger(AuthorizationServiceClient.name); + private readonly baseUrl: string; + + constructor(private readonly configService: ConfigService) { + this.baseUrl = this.configService.get('AUTHORIZATION_SERVICE_URL', 'http://localhost:3006'); + } + + async findNearestAuthorizedProvince(userId: bigint, provinceCode: string): Promise { + try { + const response = await fetch( + `${this.baseUrl}/api/v1/authorization/nearest-province?accountSequence=${userId}&provinceCode=${provinceCode}`, + ); + + if (!response.ok) { + this.logger.warn(`No authorized province found for user ${userId}, province ${provinceCode}`); + return null; + } + + // authorization-service 返回格式: { success, data: { accountSequence }, timestamp } + const result: AuthorizationServiceResponse = await response.json(); + const accountSeq = result.data?.accountSequence; + return accountSeq ? BigInt(accountSeq) : null; + } catch (error) { + this.logger.error(`Error finding nearest authorized province:`, error); + return null; + } + } + + async findNearestAuthorizedCity(userId: bigint, cityCode: string): Promise { + try { + const response = await fetch( + `${this.baseUrl}/api/v1/authorization/nearest-city?accountSequence=${userId}&cityCode=${cityCode}`, + ); + + if (!response.ok) { + this.logger.warn(`No authorized city found for user ${userId}, city ${cityCode}`); + return null; + } + + // authorization-service 返回格式: { success, data: { accountSequence }, timestamp } + const result: AuthorizationServiceResponse = await response.json(); + const accountSeq = result.data?.accountSequence; + return accountSeq ? BigInt(accountSeq) : null; + } catch (error) { + this.logger.error(`Error finding nearest authorized city:`, error); + return null; + } + } + + async findNearestCommunity(userId: bigint): Promise { + try { + const response = await fetch( + `${this.baseUrl}/api/v1/authorization/nearest-community?accountSequence=${userId}`, + ); + + if (!response.ok) { + this.logger.warn(`No community found for user ${userId}`); + return null; + } + + // authorization-service 返回格式: { success, data: { accountSequence }, timestamp } + const result: AuthorizationServiceResponse = await response.json(); + const accountSeq = result.data?.accountSequence; + this.logger.debug(`findNearestCommunity for userId=${userId}: result=${accountSeq}`); + return accountSeq ? BigInt(accountSeq) : null; + } catch (error) { + this.logger.error(`Error finding nearest community:`, error); + return null; + } + } + + async getCommunityRewardDistribution(userId: bigint, treeCount: number): Promise { + const HEADQUARTERS_ACCOUNT_SEQUENCE = 'S0000000001'; + const defaultDistribution: RewardDistribution = { + distributions: [ + { + accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, + treeCount, + reason: '服务调用失败,默认进总部社区', + }, + ], + }; + + try { + const response = await fetch( + `${this.baseUrl}/api/v1/authorization/community-reward-distribution?accountSequence=${userId}&treeCount=${treeCount}`, + ); + + if (!response.ok) { + this.logger.warn(`Failed to get community reward distribution for user ${userId}, treeCount ${treeCount}`); + return defaultDistribution; + } + + // authorization-service 返回格式: { success, data: { distributions }, timestamp } + const result: AuthorizationServiceResponse = await response.json(); + + if (!result.data?.distributions || result.data.distributions.length === 0) { + this.logger.warn(`Empty distributions returned for user ${userId}`); + return defaultDistribution; + } + + this.logger.debug( + `getCommunityRewardDistribution for userId=${userId}, treeCount=${treeCount}: ` + + `distributions=${JSON.stringify(result.data.distributions)}`, + ); + + return result.data; + } catch (error) { + this.logger.error(`Error getting community reward distribution:`, error); + return defaultDistribution; + } + } + + async getProvinceTeamRewardDistribution(userId: bigint, provinceCode: string, treeCount: number): Promise { + const HEADQUARTERS_ACCOUNT_SEQUENCE = 'S0000000001'; + const defaultDistribution: RewardDistribution = { + distributions: [ + { + accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, + treeCount, + reason: '服务调用失败,默认进总部社区', + }, + ], + }; + + try { + const response = await fetch( + `${this.baseUrl}/api/v1/authorization/province-team-reward-distribution?accountSequence=${userId}&provinceCode=${provinceCode}&treeCount=${treeCount}`, + ); + + if (!response.ok) { + this.logger.warn(`Failed to get province team reward distribution for user ${userId}, province ${provinceCode}`); + return defaultDistribution; + } + + const result: AuthorizationServiceResponse = await response.json(); + + if (!result.data?.distributions || result.data.distributions.length === 0) { + this.logger.warn(`Empty province team distributions returned for user ${userId}`); + return defaultDistribution; + } + + this.logger.debug( + `getProvinceTeamRewardDistribution for userId=${userId}, provinceCode=${provinceCode}, treeCount=${treeCount}: ` + + `distributions=${JSON.stringify(result.data.distributions)}`, + ); + + return result.data; + } catch (error) { + this.logger.error(`Error getting province team reward distribution:`, error); + return defaultDistribution; + } + } + + async getProvinceAreaRewardDistribution(provinceCode: string, treeCount: number): Promise { + // 系统省账户ID: 9 + provinceCode (6位) + const systemProvinceAccountId = `9${provinceCode.padStart(6, '0')}`; + const defaultDistribution: AreaRewardDistribution = { + distributions: [ + { + accountSequence: systemProvinceAccountId, + treeCount, + reason: '服务调用失败,默认进系统省账户', + isSystemAccount: true, + }, + ], + }; + + try { + const response = await fetch( + `${this.baseUrl}/api/v1/authorization/province-area-reward-distribution?provinceCode=${provinceCode}&treeCount=${treeCount}`, + ); + + if (!response.ok) { + this.logger.warn(`Failed to get province area reward distribution for province ${provinceCode}`); + return defaultDistribution; + } + + const result: AuthorizationServiceResponse = await response.json(); + + if (!result.data?.distributions || result.data.distributions.length === 0) { + this.logger.warn(`Empty province area distributions returned for province ${provinceCode}`); + return defaultDistribution; + } + + this.logger.debug( + `getProvinceAreaRewardDistribution for provinceCode=${provinceCode}, treeCount=${treeCount}: ` + + `distributions=${JSON.stringify(result.data.distributions)}`, + ); + + return result.data; + } catch (error) { + this.logger.error(`Error getting province area reward distribution:`, error); + return defaultDistribution; + } + } + + async getCityTeamRewardDistribution(userId: bigint, cityCode: string, treeCount: number): Promise { + const HEADQUARTERS_ACCOUNT_SEQUENCE = 'S0000000001'; + const defaultDistribution: RewardDistribution = { + distributions: [ + { + accountSequence: HEADQUARTERS_ACCOUNT_SEQUENCE, + treeCount, + reason: '服务调用失败,默认进总部社区', + }, + ], + }; + + try { + const response = await fetch( + `${this.baseUrl}/api/v1/authorization/city-team-reward-distribution?accountSequence=${userId}&cityCode=${cityCode}&treeCount=${treeCount}`, + ); + + if (!response.ok) { + this.logger.warn(`Failed to get city team reward distribution for user ${userId}, city ${cityCode}`); + return defaultDistribution; + } + + const result: AuthorizationServiceResponse = await response.json(); + + if (!result.data?.distributions || result.data.distributions.length === 0) { + this.logger.warn(`Empty city team distributions returned for user ${userId}`); + return defaultDistribution; + } + + this.logger.debug( + `getCityTeamRewardDistribution for userId=${userId}, cityCode=${cityCode}, treeCount=${treeCount}: ` + + `distributions=${JSON.stringify(result.data.distributions)}`, + ); + + return result.data; + } catch (error) { + this.logger.error(`Error getting city team reward distribution:`, error); + return defaultDistribution; + } + } + + async getCityAreaRewardDistribution(cityCode: string, treeCount: number): Promise { + // 系统市账户ID: 8 + cityCode (6位) + const systemCityAccountId = `8${cityCode.padStart(6, '0')}`; + const defaultDistribution: AreaRewardDistribution = { + distributions: [ + { + accountSequence: systemCityAccountId, + treeCount, + reason: '服务调用失败,默认进系统市账户', + isSystemAccount: true, + }, + ], + }; + + try { + const response = await fetch( + `${this.baseUrl}/api/v1/authorization/city-area-reward-distribution?cityCode=${cityCode}&treeCount=${treeCount}`, + ); + + if (!response.ok) { + this.logger.warn(`Failed to get city area reward distribution for city ${cityCode}`); + return defaultDistribution; + } + + const result: AuthorizationServiceResponse = await response.json(); + + if (!result.data?.distributions || result.data.distributions.length === 0) { + this.logger.warn(`Empty city area distributions returned for city ${cityCode}`); + return defaultDistribution; + } + + this.logger.debug( + `getCityAreaRewardDistribution for cityCode=${cityCode}, treeCount=${treeCount}: ` + + `distributions=${JSON.stringify(result.data.distributions)}`, + ); + + return result.data; + } catch (error) { + this.logger.error(`Error getting city area reward distribution:`, error); + return defaultDistribution; + } + } +} diff --git a/backend/services/reward-service/src/infrastructure/external/wallet-service/wallet-service.client.ts b/backend/services/reward-service/src/infrastructure/external/wallet-service/wallet-service.client.ts index 0be427a8..ef60d9a6 100644 --- a/backend/services/reward-service/src/infrastructure/external/wallet-service/wallet-service.client.ts +++ b/backend/services/reward-service/src/infrastructure/external/wallet-service/wallet-service.client.ts @@ -9,6 +9,23 @@ export interface SwapResult { error?: string; } +export interface FundAllocationItem { + targetType: string; + amount: number; + targetAccountId: string; + metadata?: Record; +} + +export interface AllocateFundsRequest { + orderId: string; + allocations: FundAllocationItem[]; +} + +export interface AllocateFundsResult { + success: boolean; + error?: string; +} + @Injectable() export class WalletServiceClient { private readonly logger = new Logger(WalletServiceClient.name); @@ -77,4 +94,40 @@ export class WalletServiceClient { return null; } } + + /** + * 执行资金分配 + * 将认种订单的资金分配到各个目标账户 + */ + async allocateFunds(request: AllocateFundsRequest): Promise { + try { + this.logger.log(`Allocating funds for order ${request.orderId}, ${request.allocations.length} targets`); + + const response = await fetch(`${this.baseUrl}/api/v1/wallets/allocate-funds`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + this.logger.error(`Failed to allocate funds for order ${request.orderId}:`, errorData); + return { + success: false, + error: errorData.message || `Allocation failed with status ${response.status}`, + }; + } + + this.logger.log(`Successfully allocated funds for order ${request.orderId}`); + return { success: true }; + } catch (error) { + this.logger.error(`Error allocating funds for order ${request.orderId}:`, error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + } } 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 124dec5b..298809fb 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 @@ -155,7 +155,7 @@ export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryReposi } async findByAccountSequence( - accountSequence: bigint, + accountSequence: string, filters?: { status?: RewardStatus; rightType?: RightType; @@ -195,7 +195,7 @@ export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryReposi return rawList.map(RewardLedgerEntryMapper.toDomain); } - async findPendingByAccountSequence(accountSequence: bigint): Promise { + async findPendingByAccountSequence(accountSequence: string): Promise { const rawList = await this.prisma.rewardLedgerEntry.findMany({ where: { accountSequence, @@ -207,7 +207,7 @@ export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryReposi return rawList.map(RewardLedgerEntryMapper.toDomain); } - async findSettleableByAccountSequence(accountSequence: bigint): Promise { + async findSettleableByAccountSequence(accountSequence: string): Promise { const rawList = await this.prisma.rewardLedgerEntry.findMany({ where: { accountSequence, @@ -219,7 +219,7 @@ export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryReposi return rawList.map(RewardLedgerEntryMapper.toDomain); } - async countByAccountSequence(accountSequence: bigint, status?: RewardStatus): Promise { + async countByAccountSequence(accountSequence: string, status?: RewardStatus): Promise { const where: any = { accountSequence }; if (status) { where.rewardStatus = status; 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 236b1cdb..cf5e7c5a 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 @@ -53,7 +53,7 @@ export class RewardSummaryRepositoryImpl implements IRewardSummaryRepository { return raw ? RewardSummaryMapper.toDomain(raw) : null; } - async getOrCreate(userId: bigint, accountSequence: bigint): Promise { + async getOrCreate(userId: bigint, accountSequence: string): Promise { const existing = await this.findByUserId(userId); if (existing) { return existing; @@ -90,14 +90,14 @@ export class RewardSummaryRepositoryImpl implements IRewardSummaryRepository { return rawList.map(RewardSummaryMapper.toDomain); } - async findByAccountSequence(accountSequence: bigint): Promise { + async findByAccountSequence(accountSequence: string): Promise { const raw = await this.prisma.rewardSummary.findUnique({ where: { accountSequence }, }); return raw ? RewardSummaryMapper.toDomain(raw) : null; } - async getOrCreateByAccountSequence(accountSequence: bigint): Promise { + async getOrCreateByAccountSequence(accountSequence: string): Promise { const existing = await this.findByAccountSequence(accountSequence); if (existing) { return existing; diff --git a/backend/services/wallet-service/prisma/schema.prisma b/backend/services/wallet-service/prisma/schema.prisma index 76bd24bf..646bbccf 100644 --- a/backend/services/wallet-service/prisma/schema.prisma +++ b/backend/services/wallet-service/prisma/schema.prisma @@ -12,7 +12,7 @@ datasource db { // ============================================ model WalletAccount { id BigInt @id @default(autoincrement()) @map("wallet_id") - accountSequence BigInt @unique @map("account_sequence") // 跨服务关联标识 (全局唯一业务ID) + accountSequence String @unique @map("account_sequence") @db.VarChar(20) // 跨服务关联标识 (全局唯一业务ID) userId BigInt @unique @map("user_id") // 保留兼容 // USDT 余额 @@ -73,7 +73,7 @@ model WalletAccount { // ============================================ model LedgerEntry { id BigInt @id @default(autoincrement()) @map("entry_id") - accountSequence BigInt @map("account_sequence") // 跨服务关联标识 + accountSequence String @map("account_sequence") @db.VarChar(20) // 跨服务关联标识 userId BigInt @map("user_id") // 保留兼容 // 流水类型 @@ -113,7 +113,7 @@ model LedgerEntry { // ============================================ model DepositOrder { id BigInt @id @default(autoincrement()) @map("order_id") - accountSequence BigInt @map("account_sequence") // 跨服务关联标识 + accountSequence String @map("account_sequence") @db.VarChar(20) // 跨服务关联标识 userId BigInt @map("user_id") // 保留兼容 // 充值信息 @@ -169,7 +169,7 @@ model SettlementOrder { model WithdrawalOrder { id BigInt @id @default(autoincrement()) @map("order_id") orderNo String @unique @map("order_no") @db.VarChar(50) - accountSequence BigInt @map("account_sequence") // 跨服务关联标识 + accountSequence String @map("account_sequence") @db.VarChar(20) // 跨服务关联标识 userId BigInt @map("user_id") // 提现信息 diff --git a/backend/services/wallet-service/src/api/controllers/wallet.controller.ts b/backend/services/wallet-service/src/api/controllers/wallet.controller.ts index 211aebe0..089b06dd 100644 --- a/backend/services/wallet-service/src/api/controllers/wallet.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/wallet.controller.ts @@ -20,7 +20,7 @@ export class WalletController { @ApiResponse({ status: 200, type: WalletResponseDTO }) async getMyWallet(@CurrentUser() user: CurrentUserPayload): Promise { const query = new GetMyWalletQuery( - user.accountSequence.toString(), + user.accountSequence, user.userId, ); return this.walletService.getMyWallet(query); 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 e973c676..5b22c4af 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 @@ -95,7 +95,7 @@ export class WalletApplicationService { throw new DuplicateTransactionError(command.txHash); } - const accountSequence = BigInt(command.accountSequence); + const accountSequence = command.accountSequence; const userId = BigInt(command.userId); const amount = Money.USDT(command.amount); @@ -154,7 +154,7 @@ export class WalletApplicationService { } // 优先按 accountSequence 查找,如果未找到则按 userId 查找 - let wallet = await this.walletRepo.findByAccountSequence(userId); + let wallet = await this.walletRepo.findByAccountSequence(command.userId); if (!wallet) { wallet = await this.walletRepo.findByUserId(userId); } @@ -214,7 +214,7 @@ export class WalletApplicationService { } // 优先按 accountSequence 查找,如果未找到则按 userId 查找 - let wallet = await this.walletRepo.findByAccountSequence(userId); + let wallet = await this.walletRepo.findByAccountSequence(command.userId); if (!wallet) { wallet = await this.walletRepo.findByUserId(userId); } @@ -298,7 +298,7 @@ export class WalletApplicationService { const frozenAmount = Money.USDT(Math.abs(freezeEntry.amount.value)); // 优先按 accountSequence 查找,如果未找到则按 userId 查找 - let wallet = await this.walletRepo.findByAccountSequence(userId); + let wallet = await this.walletRepo.findByAccountSequence(command.userId); if (!wallet) { wallet = await this.walletRepo.findByUserId(userId); } @@ -376,7 +376,7 @@ export class WalletApplicationService { const frozenAmount = Money.USDT(Math.abs(freezeEntry.amount.value)); // 优先按 accountSequence 查找,如果未找到则按 userId 查找 - let wallet = await this.walletRepo.findByAccountSequence(userId); + let wallet = await this.walletRepo.findByAccountSequence(command.userId); if (!wallet) { wallet = await this.walletRepo.findByUserId(userId); } @@ -410,7 +410,7 @@ export class WalletApplicationService { const userId = BigInt(command.userId); // 优先按 accountSequence 查找,如果未找到则按 userId 查找 - let wallet = await this.walletRepo.findByAccountSequence(userId); + let wallet = await this.walletRepo.findByAccountSequence(command.userId); if (!wallet) { wallet = await this.walletRepo.findByUserId(userId); } @@ -460,7 +460,7 @@ export class WalletApplicationService { const userId = BigInt(command.userId); // 优先按 accountSequence 查找,如果未找到则按 userId 查找 - let wallet = await this.walletRepo.findByAccountSequence(userId); + let wallet = await this.walletRepo.findByAccountSequence(command.userId); if (!wallet) { wallet = await this.walletRepo.findByUserId(userId); } @@ -507,7 +507,7 @@ export class WalletApplicationService { const usdtAmount = Money.USDT(command.usdtAmount); // 优先按 accountSequence 查找,如果未找到则按 userId 查找 - let wallet = await this.walletRepo.findByAccountSequence(userId); + let wallet = await this.walletRepo.findByAccountSequence(command.userId); if (!wallet) { wallet = await this.walletRepo.findByUserId(userId); } @@ -615,7 +615,7 @@ export class WalletApplicationService { const userId = BigInt(allocation.targetId); // 优先按 accountSequence 查找,如果未找到则按 userId 查找 - let wallet = await this.walletRepo.findByAccountSequence(userId); + let wallet = await this.walletRepo.findByAccountSequence(allocation.targetId); if (!wallet) { wallet = await this.walletRepo.findByUserId(userId); } @@ -727,7 +727,7 @@ export class WalletApplicationService { } // 优先按 accountSequence 查找,如果未找到则按 userId 查找 - let wallet = await this.walletRepo.findByAccountSequence(userId); + let wallet = await this.walletRepo.findByAccountSequence(command.userId); if (!wallet) { wallet = await this.walletRepo.findByUserId(userId); } @@ -775,7 +775,7 @@ export class WalletApplicationService { // 发布事件通知 blockchain-service const event = new WithdrawalRequestedEvent({ orderNo: savedOrder.orderNo, - accountSequence: wallet.accountSequence.toString(), + accountSequence: wallet.accountSequence, userId: userId.toString(), walletId: wallet.walletId.toString(), amount: command.amount.toString(), @@ -919,7 +919,7 @@ export class WalletApplicationService { // =============== Queries =============== async getMyWallet(query: GetMyWalletQuery): Promise { - const accountSequence = BigInt(query.accountSequence); + const accountSequence = query.accountSequence; const userId = BigInt(query.userId); // Try to get from cache first diff --git a/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.spec.ts b/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.spec.ts index 7b9212ad..bfdde3a9 100644 --- a/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.spec.ts +++ b/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.spec.ts @@ -15,7 +15,7 @@ describe('DepositOrder Aggregate', () => { describe('create', () => { it('should create a new deposit order', () => { const order = DepositOrder.create({ - accountSequence: BigInt(1), + accountSequence: 'D2512110001', userId: UserId.create(1), chainType: ChainType.KAVA, amount: Money.USDT(100), @@ -36,7 +36,7 @@ describe('DepositOrder Aggregate', () => { describe('confirm', () => { it('should confirm a pending deposit', () => { const order = DepositOrder.create({ - accountSequence: BigInt(1), + accountSequence: 'D2512110001', userId: UserId.create(1), chainType: ChainType.BSC, amount: Money.USDT(50), @@ -53,7 +53,7 @@ describe('DepositOrder Aggregate', () => { it('should throw error when confirming non-pending deposit', () => { const order = DepositOrder.create({ - accountSequence: BigInt(1), + accountSequence: 'D2512110001', userId: UserId.create(1), chainType: ChainType.KAVA, amount: Money.USDT(100), @@ -68,7 +68,7 @@ describe('DepositOrder Aggregate', () => { describe('fail', () => { it('should mark pending deposit as failed', () => { const order = DepositOrder.create({ - accountSequence: BigInt(1), + accountSequence: 'D2512110001', userId: UserId.create(1), chainType: ChainType.KAVA, amount: Money.USDT(100), @@ -82,7 +82,7 @@ describe('DepositOrder Aggregate', () => { it('should throw error when failing non-pending deposit', () => { const order = DepositOrder.create({ - accountSequence: BigInt(1), + accountSequence: 'D2512110001', userId: UserId.create(1), chainType: ChainType.KAVA, amount: Money.USDT(100), @@ -98,7 +98,7 @@ describe('DepositOrder Aggregate', () => { it('should reconstruct from database record', () => { const order = DepositOrder.reconstruct({ id: BigInt(1), - accountSequence: BigInt(100), + accountSequence: 'D2512110100', userId: BigInt(100), chainType: 'KAVA', amount: new Decimal(200), diff --git a/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.ts b/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.ts index e199e965..b05b766d 100644 --- a/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.ts +++ b/backend/services/wallet-service/src/domain/aggregates/deposit-order.aggregate.ts @@ -4,7 +4,7 @@ import { DomainError } from '@/shared/exceptions/domain.exception'; export class DepositOrder { private readonly _id: bigint; - private readonly _accountSequence: bigint; // 跨服务关联标识 (全局唯一业务ID) + private readonly _accountSequence: string; // 跨服务关联标识 (全局唯一业务ID) private readonly _userId: UserId; // 保留兼容 private readonly _chainType: ChainType; private readonly _amount: Money; @@ -15,7 +15,7 @@ export class DepositOrder { private constructor( id: bigint, - accountSequence: bigint, + accountSequence: string, userId: UserId, chainType: ChainType, amount: Money, @@ -37,7 +37,7 @@ export class DepositOrder { // Getters get id(): bigint { return this._id; } - get accountSequence(): bigint { return this._accountSequence; } + get accountSequence(): string { return this._accountSequence; } get userId(): UserId { return this._userId; } get chainType(): ChainType { return this._chainType; } get amount(): Money { return this._amount; } @@ -49,7 +49,7 @@ export class DepositOrder { get isConfirmed(): boolean { return this._status === DepositStatus.CONFIRMED; } static create(params: { - accountSequence: bigint; + accountSequence: string; userId: UserId; chainType: ChainType; amount: Money; @@ -70,7 +70,7 @@ export class DepositOrder { static reconstruct(params: { id: bigint; - accountSequence: bigint; + accountSequence: string; userId: bigint; chainType: string; amount: Decimal; diff --git a/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.spec.ts b/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.spec.ts index 45c5630b..b24cb754 100644 --- a/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.spec.ts +++ b/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.spec.ts @@ -15,7 +15,7 @@ describe('LedgerEntry Aggregate', () => { describe('create', () => { it('should create a new ledger entry', () => { const entry = LedgerEntry.create({ - accountSequence: BigInt(1), + accountSequence: 'D2512110001', userId: UserId.create(1), entryType: LedgerEntryType.DEPOSIT_KAVA, amount: Money.USDT(100), @@ -34,7 +34,7 @@ describe('LedgerEntry Aggregate', () => { it('should create entry with optional fields as null', () => { const entry = LedgerEntry.create({ - accountSequence: BigInt(1), + accountSequence: 'D2512110001', userId: UserId.create(1), entryType: LedgerEntryType.PLANT_PAYMENT, amount: Money.signed(-50, 'USDT'), @@ -50,7 +50,7 @@ describe('LedgerEntry Aggregate', () => { it('should create entry with payload json', () => { const payload = { key: 'value', number: 123 }; const entry = LedgerEntry.create({ - accountSequence: BigInt(1), + accountSequence: 'D2512110001', userId: UserId.create(1), entryType: LedgerEntryType.REWARD_PENDING, amount: Money.USDT(10), @@ -65,7 +65,7 @@ describe('LedgerEntry Aggregate', () => { it('should reconstruct ledger entry from database record', () => { const entry = LedgerEntry.reconstruct({ id: BigInt(1), - accountSequence: BigInt(100), + accountSequence: 'D2512110100', userId: BigInt(100), entryType: 'DEPOSIT_KAVA', amount: new Decimal(50), diff --git a/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.ts b/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.ts index e59c74b2..f4fa3222 100644 --- a/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.ts +++ b/backend/services/wallet-service/src/domain/aggregates/ledger-entry.aggregate.ts @@ -3,7 +3,7 @@ import { UserId, AssetType, LedgerEntryType, Money } from '@/domain/value-object export class LedgerEntry { private readonly _id: bigint; - private readonly _accountSequence: bigint; // 跨服务关联标识 (全局唯一业务ID) + private readonly _accountSequence: string; // 跨服务关联标识 (全局唯一业务ID) private readonly _userId: UserId; // 保留兼容 private readonly _entryType: LedgerEntryType; private readonly _amount: Money; @@ -16,7 +16,7 @@ export class LedgerEntry { private constructor( id: bigint, - accountSequence: bigint, + accountSequence: string, userId: UserId, entryType: LedgerEntryType, amount: Money, @@ -42,7 +42,7 @@ export class LedgerEntry { // Getters get id(): bigint { return this._id; } - get accountSequence(): bigint { return this._accountSequence; } + get accountSequence(): string { return this._accountSequence; } get userId(): UserId { return this._userId; } get entryType(): LedgerEntryType { return this._entryType; } get amount(): Money { return this._amount; } @@ -55,7 +55,7 @@ export class LedgerEntry { get createdAt(): Date { return this._createdAt; } static create(params: { - accountSequence: bigint; + accountSequence: string; userId: UserId; entryType: LedgerEntryType; amount: Money; @@ -82,7 +82,7 @@ export class LedgerEntry { static reconstruct(params: { id: bigint; - accountSequence: bigint; + accountSequence: string; userId: bigint; entryType: string; amount: Decimal; diff --git a/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts b/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts index 779ec8f1..69d47170 100644 --- a/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts +++ b/backend/services/wallet-service/src/domain/aggregates/wallet-account.aggregate.ts @@ -33,7 +33,7 @@ export interface WalletRewards { export class WalletAccount { private readonly _walletId: WalletId; - private readonly _accountSequence: bigint; // 跨服务关联标识 (全局唯一业务ID) + private readonly _accountSequence: string; // 跨服务关联标识 (全局唯一业务ID) private readonly _userId: UserId; // 保留兼容 private _balances: WalletBalances; private _hashpower: Hashpower; @@ -45,7 +45,7 @@ export class WalletAccount { private constructor( walletId: WalletId, - accountSequence: bigint, + accountSequence: string, userId: UserId, balances: WalletBalances, hashpower: Hashpower, @@ -67,7 +67,7 @@ export class WalletAccount { // Getters get walletId(): WalletId { return this._walletId; } - get accountSequence(): bigint { return this._accountSequence; } + get accountSequence(): string { return this._accountSequence; } get userId(): UserId { return this._userId; } get balances(): WalletBalances { return this._balances; } get hashpower(): Hashpower { return this._hashpower; } @@ -78,7 +78,7 @@ export class WalletAccount { get isActive(): boolean { return this._status === WalletStatus.ACTIVE; } get domainEvents(): DomainEvent[] { return [...this._domainEvents]; } - static createNew(accountSequence: bigint, userId: UserId): WalletAccount { + static createNew(accountSequence: string, userId: UserId): WalletAccount { const now = new Date(); return new WalletAccount( WalletId.create(0), // Will be set by database @@ -111,7 +111,7 @@ export class WalletAccount { static reconstruct(params: { walletId: bigint; - accountSequence: bigint; + accountSequence: string; userId: bigint; usdtAvailable: Decimal; usdtFrozen: Decimal; diff --git a/backend/services/wallet-service/src/domain/aggregates/withdrawal-order.aggregate.ts b/backend/services/wallet-service/src/domain/aggregates/withdrawal-order.aggregate.ts index 0769b7c8..b7b294f6 100644 --- a/backend/services/wallet-service/src/domain/aggregates/withdrawal-order.aggregate.ts +++ b/backend/services/wallet-service/src/domain/aggregates/withdrawal-order.aggregate.ts @@ -17,7 +17,7 @@ import { DomainError } from '@/shared/exceptions/domain.exception'; export class WithdrawalOrder { private readonly _id: bigint; private readonly _orderNo: string; - private readonly _accountSequence: bigint; + private readonly _accountSequence: string; private readonly _userId: UserId; private readonly _amount: Money; private readonly _fee: Money; // 手续费 @@ -34,7 +34,7 @@ export class WithdrawalOrder { private constructor( id: bigint, orderNo: string, - accountSequence: bigint, + accountSequence: string, userId: UserId, amount: Money, fee: Money, @@ -68,7 +68,7 @@ export class WithdrawalOrder { // Getters get id(): bigint { return this._id; } get orderNo(): string { return this._orderNo; } - get accountSequence(): bigint { return this._accountSequence; } + get accountSequence(): string { return this._accountSequence; } get userId(): UserId { return this._userId; } get amount(): Money { return this._amount; } get fee(): Money { return this._fee; } @@ -108,7 +108,7 @@ export class WithdrawalOrder { * 创建提现订单 */ static create(params: { - accountSequence: bigint; + accountSequence: string; userId: UserId; amount: Money; fee: Money; @@ -160,7 +160,7 @@ export class WithdrawalOrder { static reconstruct(params: { id: bigint; orderNo: string; - accountSequence: bigint; + accountSequence: string; userId: bigint; amount: Decimal; fee: Decimal; diff --git a/backend/services/wallet-service/src/domain/repositories/deposit-order.repository.interface.ts b/backend/services/wallet-service/src/domain/repositories/deposit-order.repository.interface.ts index 83fdc22f..5d6b5eb7 100644 --- a/backend/services/wallet-service/src/domain/repositories/deposit-order.repository.interface.ts +++ b/backend/services/wallet-service/src/domain/repositories/deposit-order.repository.interface.ts @@ -6,7 +6,7 @@ export interface IDepositOrderRepository { findById(orderId: bigint): Promise; findByTxHash(txHash: string): Promise; findByUserId(userId: bigint, status?: DepositStatus): Promise; - findByAccountSequence(accountSequence: bigint, status?: DepositStatus): Promise; + findByAccountSequence(accountSequence: string, status?: DepositStatus): Promise; existsByTxHash(txHash: string): Promise; } diff --git a/backend/services/wallet-service/src/domain/repositories/ledger-entry.repository.interface.ts b/backend/services/wallet-service/src/domain/repositories/ledger-entry.repository.interface.ts index ae98f792..2eaed8e1 100644 --- a/backend/services/wallet-service/src/domain/repositories/ledger-entry.repository.interface.ts +++ b/backend/services/wallet-service/src/domain/repositories/ledger-entry.repository.interface.ts @@ -25,7 +25,7 @@ export interface ILedgerEntryRepository { save(entry: LedgerEntry): Promise; saveAll(entries: LedgerEntry[]): Promise; findByUserId(userId: bigint, filters?: LedgerFilters, pagination?: Pagination): Promise>; - findByAccountSequence(accountSequence: bigint, filters?: LedgerFilters, pagination?: Pagination): Promise>; + findByAccountSequence(accountSequence: string, filters?: LedgerFilters, pagination?: Pagination): Promise>; findByRefOrderId(refOrderId: string): Promise; findByRefTxHash(refTxHash: string): Promise; } diff --git a/backend/services/wallet-service/src/domain/repositories/wallet-account.repository.interface.ts b/backend/services/wallet-service/src/domain/repositories/wallet-account.repository.interface.ts index 048d6c64..73e30902 100644 --- a/backend/services/wallet-service/src/domain/repositories/wallet-account.repository.interface.ts +++ b/backend/services/wallet-service/src/domain/repositories/wallet-account.repository.interface.ts @@ -4,8 +4,8 @@ export interface IWalletAccountRepository { save(wallet: WalletAccount): Promise; findById(walletId: bigint): Promise; findByUserId(userId: bigint): Promise; - findByAccountSequence(accountSequence: bigint): Promise; - getOrCreate(accountSequence: bigint, userId: bigint): Promise; + findByAccountSequence(accountSequence: string): Promise; + getOrCreate(accountSequence: string, userId: bigint): Promise; findByUserIds(userIds: bigint[]): Promise>; } diff --git a/backend/services/wallet-service/src/infrastructure/kafka/reward-event-consumer.controller.ts b/backend/services/wallet-service/src/infrastructure/kafka/reward-event-consumer.controller.ts index 7eea09aa..e350272e 100644 --- a/backend/services/wallet-service/src/infrastructure/kafka/reward-event-consumer.controller.ts +++ b/backend/services/wallet-service/src/infrastructure/kafka/reward-event-consumer.controller.ts @@ -89,7 +89,7 @@ export class RewardEventConsumerController { // 2. 更新 wallet_accounts 表的 rewards 数据 // 注意:payload 字段直接展开在消息顶层 - const accountSequence = BigInt(message.accountSequence); + const accountSequence = message.accountSequence; await this.prisma.$transaction(async (tx) => { // 更新钱包账户的奖励数据 diff --git a/backend/services/wallet-service/src/infrastructure/persistence/repositories/deposit-order.repository.impl.ts b/backend/services/wallet-service/src/infrastructure/persistence/repositories/deposit-order.repository.impl.ts index 708233fd..4c35e541 100644 --- a/backend/services/wallet-service/src/infrastructure/persistence/repositories/deposit-order.repository.impl.ts +++ b/backend/services/wallet-service/src/infrastructure/persistence/repositories/deposit-order.repository.impl.ts @@ -66,7 +66,7 @@ export class DepositOrderRepositoryImpl implements IDepositOrderRepository { return count > 0; } - async findByAccountSequence(accountSequence: bigint, status?: DepositStatus): Promise { + async findByAccountSequence(accountSequence: string, status?: DepositStatus): Promise { const where: Record = { accountSequence }; if (status) { where.status = status; @@ -81,7 +81,7 @@ export class DepositOrderRepositoryImpl implements IDepositOrderRepository { private toDomain(record: { id: bigint; - accountSequence: bigint; + accountSequence: string; userId: bigint; chainType: string; amount: Decimal; diff --git a/backend/services/wallet-service/src/infrastructure/persistence/repositories/ledger-entry.repository.impl.ts b/backend/services/wallet-service/src/infrastructure/persistence/repositories/ledger-entry.repository.impl.ts index 1a43fc5e..95ca967e 100644 --- a/backend/services/wallet-service/src/infrastructure/persistence/repositories/ledger-entry.repository.impl.ts +++ b/backend/services/wallet-service/src/infrastructure/persistence/repositories/ledger-entry.repository.impl.ts @@ -109,7 +109,7 @@ export class LedgerEntryRepositoryImpl implements ILedgerEntryRepository { } async findByAccountSequence( - accountSequence: bigint, + accountSequence: string, filters?: LedgerFilters, pagination?: Pagination, ): Promise> { @@ -156,7 +156,7 @@ export class LedgerEntryRepositoryImpl implements ILedgerEntryRepository { private toDomain(record: { id: bigint; - accountSequence: bigint; + accountSequence: string; userId: bigint; entryType: string; amount: Decimal; diff --git a/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts b/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts index 60dd3184..56245fa0 100644 --- a/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts +++ b/backend/services/wallet-service/src/infrastructure/persistence/repositories/wallet-account.repository.impl.ts @@ -64,14 +64,14 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository { return record ? this.toDomain(record) : null; } - async findByAccountSequence(accountSequence: bigint): Promise { + async findByAccountSequence(accountSequence: string): Promise { const record = await this.prisma.walletAccount.findUnique({ where: { accountSequence }, }); return record ? this.toDomain(record) : null; } - async getOrCreate(accountSequence: bigint, userId: bigint): Promise { + async getOrCreate(accountSequence: string, userId: bigint): Promise { const existing = await this.findByAccountSequence(accountSequence); if (existing) { return existing; @@ -95,7 +95,7 @@ export class WalletAccountRepositoryImpl implements IWalletAccountRepository { private toDomain(record: { id: bigint; - accountSequence: bigint; + accountSequence: string; userId: bigint; usdtAvailable: Decimal; usdtFrozen: Decimal; diff --git a/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts b/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts index 94b9ba1c..a5401662 100644 --- a/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts +++ b/backend/services/wallet-service/src/infrastructure/persistence/repositories/withdrawal-order.repository.impl.ts @@ -92,7 +92,7 @@ export class WithdrawalOrderRepositoryImpl implements IWithdrawalOrderRepository private toDomain(record: { id: bigint; orderNo: string; - accountSequence: bigint; + accountSequence: string; userId: bigint; amount: Decimal; fee: Decimal; diff --git a/backend/services/wallet-service/src/shared/decorators/current-user.decorator.ts b/backend/services/wallet-service/src/shared/decorators/current-user.decorator.ts index d7f2eb45..68cd3352 100644 --- a/backend/services/wallet-service/src/shared/decorators/current-user.decorator.ts +++ b/backend/services/wallet-service/src/shared/decorators/current-user.decorator.ts @@ -2,7 +2,7 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common'; export interface CurrentUserPayload { userId: string; - accountSequence: number; + accountSequence: string; } export const CurrentUser = createParamDecorator( diff --git a/backend/services/wallet-service/src/shared/strategies/jwt.strategy.ts b/backend/services/wallet-service/src/shared/strategies/jwt.strategy.ts index a19d75d7..e7e697f2 100644 --- a/backend/services/wallet-service/src/shared/strategies/jwt.strategy.ts +++ b/backend/services/wallet-service/src/shared/strategies/jwt.strategy.ts @@ -6,7 +6,7 @@ import { ConfigService } from '@nestjs/config'; // JWT payload 格式与 identity-service 生成的 token 一致 interface JwtPayload { userId: string; - accountSequence: number; + accountSequence: string; deviceId: string; type: 'access' | 'refresh'; iat: number; diff --git a/frontend/mobile-app/lib/core/services/account_service.dart b/frontend/mobile-app/lib/core/services/account_service.dart index bd2cec48..49584044 100644 --- a/frontend/mobile-app/lib/core/services/account_service.dart +++ b/frontend/mobile-app/lib/core/services/account_service.dart @@ -79,7 +79,7 @@ class CreateAccountRequest { /// 创建账号响应 (新版 - 不含钱包信息) class CreateAccountResponse { - final int userSerialNum; // 用户序列号 + final String userSerialNum; // 用户序列号(新格式: D + YYMMDD + 5位序号) final String referralCode; // 推荐码 final String username; // 随机用户名 final String avatarSvg; // 随机 SVG 头像 @@ -98,7 +98,7 @@ class CreateAccountResponse { factory CreateAccountResponse.fromJson(Map json) { debugPrint('[AccountService] 解析 CreateAccountResponse: ${json.keys.toList()}'); return CreateAccountResponse( - userSerialNum: json['userSerialNum'] as int, + userSerialNum: json['userSerialNum']?.toString() ?? '', referralCode: json['referralCode'] as String, username: json['username'] as String, avatarSvg: json['avatarSvg'] as String, @@ -180,7 +180,7 @@ String _maskAddress(String? address) { /// 恢复账户响应 class RecoverAccountResponse { final String userId; - final int userSerialNum; // accountSequence + final String userSerialNum; // accountSequence(新格式: D + YYMMDD + 5位序号) final String username; // nickname final String? avatarSvg; // avatarUrl final String referralCode; @@ -206,7 +206,7 @@ class RecoverAccountResponse { } return RecoverAccountResponse( userId: json['userId'] as String, - userSerialNum: json['accountSequence'] as int, + userSerialNum: json['accountSequence']?.toString() ?? '', username: json['nickname'] as String, avatarSvg: avatarUrl, referralCode: json['referralCode'] as String, @@ -223,13 +223,13 @@ class RecoverAccountResponse { /// 当前用户信息响应 (GET /me) class MeResponse { final String userId; - final int accountSequence; + final String accountSequence; // 账户序列号(新格式: D + YYMMDD + 5位序号) final String? phoneNumber; final String nickname; final String? avatarUrl; final String referralCode; final String referralLink; - final int? inviterSequence; // 推荐人序列号 + final String? inviterSequence; // 推荐人序列号(新格式: D + YYMMDD + 5位序号) final List walletAddresses; final String kycStatus; final String status; @@ -253,13 +253,13 @@ class MeResponse { factory MeResponse.fromJson(Map json) { return MeResponse( userId: json['userId'] as String, - accountSequence: json['accountSequence'] as int, + accountSequence: json['accountSequence']?.toString() ?? '', phoneNumber: json['phoneNumber'] as String?, nickname: json['nickname'] as String, avatarUrl: json['avatarUrl'] as String?, referralCode: json['referralCode'] as String, referralLink: json['referralLink'] as String, - inviterSequence: json['inviterSequence'] as int?, + inviterSequence: json['inviterSequence']?.toString(), walletAddresses: (json['walletAddresses'] as List?) ?.map((e) => WalletAddressInfo.fromJson(e as Map)) .toList() ?? @@ -441,7 +441,7 @@ class AccountService { /// 获取钱包信息 (包含助记词) /// /// 通过 JWT token 获取当前用户的钱包状态 - Future getWalletInfo(int userSerialNum) async { + Future getWalletInfo(String userSerialNum) async { debugPrint('$_tag getWalletInfo() - 开始获取钱包信息'); debugPrint('$_tag getWalletInfo() - userSerialNum: $userSerialNum'); @@ -509,7 +509,7 @@ class AccountService { if (result.inviterSequence != null) { await _secureStorage.write( key: StorageKeys.inviterSequence, - value: result.inviterSequence.toString(), + value: result.inviterSequence!, ); debugPrint('$_tag getMe() - 保存 inviterSequence: ${result.inviterSequence}'); } @@ -526,9 +526,9 @@ class AccountService { } /// 获取推荐人序列号 (从本地存储) - Future getInviterSequence() async { + Future getInviterSequence() async { final value = await _secureStorage.read(key: StorageKeys.inviterSequence); - return value != null ? int.tryParse(value) : null; + return value; } /// 保存账号数据 @@ -542,7 +542,7 @@ class AccountService { debugPrint('$_tag _saveAccountData() - 保存 userSerialNum: ${response.userSerialNum}'); await _secureStorage.write( key: StorageKeys.userSerialNum, - value: response.userSerialNum.toString(), + value: response.userSerialNum, ); debugPrint('$_tag _saveAccountData() - 保存 referralCode: ${response.referralCode}'); @@ -661,11 +661,9 @@ class AccountService { } /// 获取用户序列号 - Future getUserSerialNum() async { + Future getUserSerialNum() async { debugPrint('$_tag getUserSerialNum() - 获取用户序列号'); - final serialNum = - await _secureStorage.read(key: StorageKeys.userSerialNum); - final result = serialNum != null ? int.tryParse(serialNum) : null; + final result = await _secureStorage.read(key: StorageKeys.userSerialNum); debugPrint('$_tag getUserSerialNum() - 结果: $result'); return result; } @@ -778,9 +776,9 @@ class AccountService { /// 通过助记词恢复账户 /// /// 使用序列号和 12 个助记词恢复已有账户 - /// [accountSequence] - 用户序列号 + /// [accountSequence] - 用户序列号(新格式: D + YYMMDD + 5位序号) /// [mnemonic] - 12 个助记词,用空格分隔 - Future recoverByMnemonic(int accountSequence, String mnemonic) async { + Future recoverByMnemonic(String accountSequence, String mnemonic) async { debugPrint('$_tag recoverByMnemonic() - 开始恢复账户'); debugPrint('$_tag recoverByMnemonic() - 序列号: $accountSequence'); @@ -873,7 +871,7 @@ class AccountService { debugPrint('$_tag _saveRecoverAccountData() - 保存 userSerialNum: ${response.userSerialNum}'); await _secureStorage.write( key: StorageKeys.userSerialNum, - value: response.userSerialNum.toString(), + value: response.userSerialNum, ); debugPrint('$_tag _saveRecoverAccountData() - 保存 referralCode: ${response.referralCode}'); diff --git a/frontend/mobile-app/lib/core/services/authorization_service.dart b/frontend/mobile-app/lib/core/services/authorization_service.dart index 8e74accf..427da9a0 100644 --- a/frontend/mobile-app/lib/core/services/authorization_service.dart +++ b/frontend/mobile-app/lib/core/services/authorization_service.dart @@ -252,7 +252,7 @@ class UserAuthorizationSummary { /// 社区简要信息 class CommunityInfo { final String authorizationId; - final int accountSequence; + final String accountSequence; // 账户序列号(新格式: D + YYMMDD + 5位序号) final String communityName; final String? userId; final bool isHeadquarters; @@ -268,7 +268,7 @@ class CommunityInfo { factory CommunityInfo.fromJson(Map json) { return CommunityInfo( authorizationId: json['authorizationId'] ?? '', - accountSequence: json['accountSequence'] ?? 0, + accountSequence: json['accountSequence']?.toString() ?? '', communityName: json['communityName'] ?? '', userId: json['userId'], isHeadquarters: json['isHeadquarters'] ?? false, diff --git a/frontend/mobile-app/lib/core/services/referral_service.dart b/frontend/mobile-app/lib/core/services/referral_service.dart index 84287c70..f5bb5c77 100644 --- a/frontend/mobile-app/lib/core/services/referral_service.dart +++ b/frontend/mobile-app/lib/core/services/referral_service.dart @@ -52,7 +52,7 @@ class ReferralInfoResponse { /// 直推成员信息 class DirectReferralInfo { final String userId; - final int accountSequence; // 8位账户序列号,用于显示 + final String accountSequence; // 账户序列号(新格式: D + YYMMDD + 5位序号),用于显示 final String referralCode; final int personalPlantingCount; // 个人认种量 final int teamPlantingCount; // 团队认种量 @@ -70,7 +70,7 @@ class DirectReferralInfo { factory DirectReferralInfo.fromJson(Map json) { return DirectReferralInfo( userId: json['userId']?.toString() ?? '', - accountSequence: json['accountSequence'] ?? 0, + accountSequence: json['accountSequence']?.toString() ?? '', referralCode: json['referralCode'] ?? '', personalPlantingCount: json['personalPlantingCount'] ?? 0, teamPlantingCount: json['teamPlantingCount'] ?? 0, @@ -145,7 +145,7 @@ class ReferralLinkResponse { /// 用户信息响应 (含推荐链接) class MeResponse { final String userId; - final int accountSequence; + final String accountSequence; final String? phoneNumber; final String nickname; final String? avatarUrl; @@ -173,7 +173,7 @@ class MeResponse { factory MeResponse.fromJson(Map json) { return MeResponse( userId: json['userId'] ?? '', - accountSequence: json['accountSequence'] ?? 0, + accountSequence: json['accountSequence']?.toString() ?? '', phoneNumber: json['phoneNumber'], nickname: json['nickname'] ?? '', avatarUrl: json['avatarUrl'], diff --git a/frontend/mobile-app/lib/features/auth/presentation/pages/backup_mnemonic_page.dart b/frontend/mobile-app/lib/features/auth/presentation/pages/backup_mnemonic_page.dart index b8e6178c..64d27445 100644 --- a/frontend/mobile-app/lib/features/auth/presentation/pages/backup_mnemonic_page.dart +++ b/frontend/mobile-app/lib/features/auth/presentation/pages/backup_mnemonic_page.dart @@ -15,7 +15,7 @@ import '../../../../core/services/account_service.dart'; /// 进入时调用 API 获取钱包信息和助记词 class BackupMnemonicPage extends ConsumerStatefulWidget { /// 用户序列号 - final int userSerialNum; + final String userSerialNum; /// 推荐码 final String? referralCode; @@ -1155,7 +1155,7 @@ ${DateTime.now().toString()} _buildAddressItemWithData( iconWidget: _buildSequenceIcon(), label: '序列号', - address: widget.userSerialNum.toString(), + address: widget.userSerialNum, showBorder: false, isPlaceholder: false, ), @@ -1319,7 +1319,7 @@ ${DateTime.now().toString()} _buildAddressItem( iconWidget: _buildSequenceIcon(), label: '序列号', - address: widget.userSerialNum.toString(), + address: widget.userSerialNum, showBorder: false, ), ], diff --git a/frontend/mobile-app/lib/features/auth/presentation/pages/import_mnemonic_page.dart b/frontend/mobile-app/lib/features/auth/presentation/pages/import_mnemonic_page.dart index 9a84da38..c0682630 100644 --- a/frontend/mobile-app/lib/features/auth/presentation/pages/import_mnemonic_page.dart +++ b/frontend/mobile-app/lib/features/auth/presentation/pages/import_mnemonic_page.dart @@ -56,10 +56,10 @@ class _ImportMnemonicPageState extends ConsumerState { } /// 获取序列号 - int? get _serialNum { + String? get _serialNum { final text = _serialNumController.text.trim(); if (text.isEmpty) return null; - return int.tryParse(text); + return text; } /// 检查是否所有单词都已填写 @@ -341,16 +341,13 @@ class _ImportMnemonicPageState extends ConsumerState { disabledBorder: InputBorder.none, contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12), filled: false, - hintText: '请输入账户序列号', + hintText: '请输入账户序列号(如 D2512110008)', hintStyle: TextStyle( fontSize: 16, color: Color(0x668B5A2B), ), ), - keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - ], + keyboardType: TextInputType.text, onChanged: (_) { if (_errorMessage != null) { setState(() { diff --git a/frontend/mobile-app/lib/features/auth/presentation/pages/onboarding_page.dart b/frontend/mobile-app/lib/features/auth/presentation/pages/onboarding_page.dart index 9d2d53a9..808d7a4b 100644 --- a/frontend/mobile-app/lib/features/auth/presentation/pages/onboarding_page.dart +++ b/frontend/mobile-app/lib/features/auth/presentation/pages/onboarding_page.dart @@ -26,7 +26,7 @@ class _OnboardingPageState extends ConsumerState { // 是否正在加载状态 bool _isLoading = true; // 已创建的账号数据 - int? _userSerialNum; + String? _userSerialNum; String? _username; String? _avatarSvg; String? _referralCode; diff --git a/frontend/mobile-app/lib/features/auth/presentation/pages/verify_mnemonic_page.dart b/frontend/mobile-app/lib/features/auth/presentation/pages/verify_mnemonic_page.dart index b868f8ca..1482592c 100644 --- a/frontend/mobile-app/lib/features/auth/presentation/pages/verify_mnemonic_page.dart +++ b/frontend/mobile-app/lib/features/auth/presentation/pages/verify_mnemonic_page.dart @@ -18,7 +18,7 @@ class VerifyMnemonicPage extends ConsumerStatefulWidget { /// BSC 钱包地址 final String bscAddress; /// 用户序列号 - final int userSerialNum; + final String userSerialNum; /// 推荐码 final String? referralCode; diff --git a/frontend/mobile-app/lib/features/auth/presentation/pages/wallet_created_page.dart b/frontend/mobile-app/lib/features/auth/presentation/pages/wallet_created_page.dart index 1831a3f7..b92c721f 100644 --- a/frontend/mobile-app/lib/features/auth/presentation/pages/wallet_created_page.dart +++ b/frontend/mobile-app/lib/features/auth/presentation/pages/wallet_created_page.dart @@ -15,7 +15,7 @@ class WalletCreatedPage extends ConsumerWidget { /// BSC 钱包地址 final String bscAddress; /// 用户序列号 - final int userSerialNum; + final String userSerialNum; /// 推荐码 final String? referralCode; @@ -199,7 +199,7 @@ class WalletCreatedPage extends ConsumerWidget { context: context, iconWidget: _buildKeyIcon(), label: '序列号', - value: userSerialNum.toString(), + value: userSerialNum, isAddress: false, showDivider: true, ), diff --git a/frontend/mobile-app/lib/features/auth/presentation/providers/auth_provider.dart b/frontend/mobile-app/lib/features/auth/presentation/providers/auth_provider.dart index 434a99f9..7106d0b4 100644 --- a/frontend/mobile-app/lib/features/auth/presentation/providers/auth_provider.dart +++ b/frontend/mobile-app/lib/features/auth/presentation/providers/auth_provider.dart @@ -22,7 +22,7 @@ class AuthState { // 新增:钱包是否已就绪 final bool isWalletReady; // 新增:用户序列号(用于跳转到备份助记词页面) - final int? userSerialNum; + final String? userSerialNum; // 新增:推荐码 final String? referralCode; @@ -48,7 +48,7 @@ class AuthState { String? errorMessage, bool? isAccountCreated, bool? isWalletReady, - int? userSerialNum, + String? userSerialNum, String? referralCode, }) { return AuthState( @@ -92,8 +92,7 @@ class AuthNotifier extends StateNotifier { final isMnemonicBackedUp = isMnemonicBackedUpStr == 'true'; // 获取用户序列号和推荐码(用于跳转到备份页面) - final userSerialNumStr = await _secureStorage.read(key: StorageKeys.userSerialNum); - final userSerialNum = userSerialNumStr != null ? int.tryParse(userSerialNumStr) : null; + final userSerialNum = await _secureStorage.read(key: StorageKeys.userSerialNum); final referralCode = await _secureStorage.read(key: StorageKeys.referralCode); // 旧版兼容:检查旧的 walletAddress 字段 diff --git a/frontend/mobile-app/lib/routes/app_router.dart b/frontend/mobile-app/lib/routes/app_router.dart index 1c2e1f7c..38e7dbee 100644 --- a/frontend/mobile-app/lib/routes/app_router.dart +++ b/frontend/mobile-app/lib/routes/app_router.dart @@ -32,7 +32,7 @@ final _shellNavigatorKey = GlobalKey(); /// 备份助记词页面参数 (新版 - 只需要用户序列号) class BackupMnemonicParams { - final int userSerialNum; + final String userSerialNum; final String? referralCode; BackupMnemonicParams({ @@ -47,7 +47,7 @@ class VerifyMnemonicParams { final String kavaAddress; final String dstAddress; final String bscAddress; - final int userSerialNum; + final String userSerialNum; final String? referralCode; VerifyMnemonicParams({ @@ -65,7 +65,7 @@ class WalletCreatedParams { final String kavaAddress; final String dstAddress; final String bscAddress; - final int userSerialNum; + final String userSerialNum; final String? referralCode; WalletCreatedParams({