refactor!: 重构账户序列号格式 (BREAKING CHANGE)

将 accountSequence 从数字类型改为字符串类型,新格式为:
- 普通用户: D + YYMMDD + 5位序号 (例: D2512120001)
- 系统账户: S + 10位序号 (例: S0000000001)

主要变更:
- identity-service: AccountSequence 值对象改为字符串类型
- identity-service: 序列号生成器改为按日期重置计数
- 所有服务: Prisma schema 字段类型从 BigInt/Int 改为 String
- 所有服务: DTO、Command、Event 中的类型定义更新
- Flutter 前端: 相关数据模型类型更新

涉及服务:
- identity-service (核心变更)
- referral-service
- authorization-service
- wallet-service
- reward-service
- blockchain-service
- backup-service
- planting-service
- mpc-service
- admin-service
- mobile-app (Flutter)

注意: 此为破坏性变更,需要清空数据库并重新运行 migration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-12 09:11:18 -08:00
parent 8148d1d127
commit 4be9c1fb82
163 changed files with 5050 additions and 4917 deletions

View File

@ -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 = ''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 = ''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(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": [], "deny": [],
"ask": [] "ask": []

View File

@ -13,8 +13,8 @@ datasource db {
// ============ 授权角色表 ============ // ============ 授权角色表 ============
model AuthorizationRole { model AuthorizationRole {
id String @id @default(uuid()) id String @id @default(uuid())
userId BigInt @map("user_id") userId String @map("user_id")
accountSequence BigInt @map("account_sequence") accountSequence String @map("account_sequence")
roleType RoleType @map("role_type") roleType RoleType @map("role_type")
regionCode String @map("region_code") regionCode String @map("region_code")
regionName String @map("region_name") regionName String @map("region_name")
@ -23,9 +23,9 @@ model AuthorizationRole {
// 授权信息 // 授权信息
authorizedAt DateTime? @map("authorized_at") authorizedAt DateTime? @map("authorized_at")
authorizedBy BigInt? @map("authorized_by") authorizedBy String? @map("authorized_by")
revokedAt DateTime? @map("revoked_at") revokedAt DateTime? @map("revoked_at")
revokedBy BigInt? @map("revoked_by") revokedBy String? @map("revoked_by")
revokeReason String? @map("revoke_reason") revokeReason String? @map("revoke_reason")
// 考核配置 // 考核配置
@ -65,8 +65,8 @@ model AuthorizationRole {
model MonthlyAssessment { model MonthlyAssessment {
id String @id @default(uuid()) id String @id @default(uuid())
authorizationId String @map("authorization_id") authorizationId String @map("authorization_id")
userId BigInt @map("user_id") userId String @map("user_id")
accountSequence BigInt @map("account_sequence") accountSequence String @map("account_sequence")
roleType RoleType @map("role_type") roleType RoleType @map("role_type")
regionCode String @map("region_code") regionCode String @map("region_code")
@ -101,7 +101,7 @@ model MonthlyAssessment {
// 豁免 // 豁免
isBypassed Boolean @default(false) @map("is_bypassed") isBypassed Boolean @default(false) @map("is_bypassed")
bypassedBy BigInt? @map("bypassed_by") bypassedBy String? @map("bypassed_by")
bypassedAt DateTime? @map("bypassed_at") bypassedAt DateTime? @map("bypassed_at")
// 时间戳 // 时间戳
@ -125,22 +125,22 @@ model MonthlyAssessment {
model MonthlyBypass { model MonthlyBypass {
id String @id @default(uuid()) id String @id @default(uuid())
authorizationId String @map("authorization_id") authorizationId String @map("authorization_id")
userId BigInt @map("user_id") userId String @map("user_id")
accountSequence BigInt @map("account_sequence") accountSequence String @map("account_sequence")
roleType RoleType @map("role_type") roleType RoleType @map("role_type")
bypassMonth String @map("bypass_month") // YYYY-MM bypassMonth String @map("bypass_month") // YYYY-MM
// 授权信息 // 授权信息
grantedBy BigInt @map("granted_by") grantedBy String @map("granted_by")
grantedAt DateTime @map("granted_at") grantedAt DateTime @map("granted_at")
reason String? reason String?
// 审批信息(三人授权) // 审批信息(三人授权)
approver1Id BigInt @map("approver1_id") approver1Id String @map("approver1_id")
approver1At DateTime @map("approver1_at") approver1At DateTime @map("approver1_at")
approver2Id BigInt? @map("approver2_id") approver2Id String? @map("approver2_id")
approver2At DateTime? @map("approver2_at") approver2At DateTime? @map("approver2_at")
approver3Id BigInt? @map("approver3_id") approver3Id String? @map("approver3_id")
approver3At DateTime? @map("approver3_at") approver3At DateTime? @map("approver3_at")
approvalStatus ApprovalStatus @default(PENDING) @map("approval_status") approvalStatus ApprovalStatus @default(PENDING) @map("approval_status")
@ -281,8 +281,8 @@ model RegionHeatMap {
// ============ 火柴人排名视图数据表 ============ // ============ 火柴人排名视图数据表 ============
model StickmanRanking { model StickmanRanking {
id String @id @default(uuid()) id String @id @default(uuid())
userId BigInt @map("user_id") userId String @map("user_id")
accountSequence BigInt @map("account_sequence") accountSequence String @map("account_sequence")
authorizationId String @map("authorization_id") authorizationId String @map("authorization_id")
roleType RoleType @map("role_type") roleType RoleType @map("role_type")
regionCode String @map("region_code") regionCode String @map("region_code")

View File

@ -1,133 +1,133 @@
import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common' import { Controller, Post, Body, UseGuards, HttpCode, HttpStatus } from '@nestjs/common'
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger' import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'
import { AuthorizationApplicationService } from '@/application/services' import { AuthorizationApplicationService } from '@/application/services'
import { import {
GrantCommunityCommand, GrantCommunityCommand,
GrantProvinceCompanyCommand, GrantProvinceCompanyCommand,
GrantCityCompanyCommand, GrantCityCompanyCommand,
GrantAuthProvinceCompanyCommand, GrantAuthProvinceCompanyCommand,
GrantAuthCityCompanyCommand, GrantAuthCityCompanyCommand,
} from '@/application/commands' } from '@/application/commands'
import { import {
GrantCommunityDto, GrantCommunityDto,
GrantProvinceCompanyDto, GrantProvinceCompanyDto,
GrantCityCompanyDto, GrantCityCompanyDto,
GrantAuthProvinceCompanyDto, GrantAuthProvinceCompanyDto,
GrantAuthCityCompanyDto, GrantAuthCityCompanyDto,
} from '@/api/dto/request' } from '@/api/dto/request'
import { CurrentUser } from '@/shared/decorators' import { CurrentUser } from '@/shared/decorators'
import { JwtAuthGuard } from '@/shared/guards' import { JwtAuthGuard } from '@/shared/guards'
@ApiTags('Admin Authorization') @ApiTags('Admin Authorization')
@Controller('admin/authorizations') @Controller('admin/authorizations')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
export class AdminAuthorizationController { export class AdminAuthorizationController {
constructor(private readonly applicationService: AuthorizationApplicationService) {} constructor(private readonly applicationService: AuthorizationApplicationService) {}
@Post('community') @Post('community')
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: '授权社区(管理员)' }) @ApiOperation({ summary: '授权社区(管理员)' })
@ApiResponse({ status: 201, description: '授权成功' }) @ApiResponse({ status: 201, description: '授权成功' })
async grantCommunity( async grantCommunity(
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
@Body() dto: GrantCommunityDto, @Body() dto: GrantCommunityDto,
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const command = new GrantCommunityCommand( const command = new GrantCommunityCommand(
dto.userId, dto.userId,
dto.accountSequence, dto.accountSequence,
dto.communityName, dto.communityName,
user.userId, user.userId,
user.accountSequence, user.accountSequence,
dto.skipAssessment ?? false, dto.skipAssessment ?? false,
) )
await this.applicationService.grantCommunity(command) await this.applicationService.grantCommunity(command)
return { message: '社区授权成功' } return { message: '社区授权成功' }
} }
@Post('province-company') @Post('province-company')
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: '授权正式省公司(管理员)' }) @ApiOperation({ summary: '授权正式省公司(管理员)' })
@ApiResponse({ status: 201, description: '授权成功' }) @ApiResponse({ status: 201, description: '授权成功' })
async grantProvinceCompany( async grantProvinceCompany(
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
@Body() dto: GrantProvinceCompanyDto, @Body() dto: GrantProvinceCompanyDto,
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const command = new GrantProvinceCompanyCommand( const command = new GrantProvinceCompanyCommand(
dto.userId, dto.userId,
dto.accountSequence, dto.accountSequence,
dto.provinceCode, dto.provinceCode,
dto.provinceName, dto.provinceName,
user.userId, user.userId,
user.accountSequence, user.accountSequence,
dto.skipAssessment ?? false, dto.skipAssessment ?? false,
) )
await this.applicationService.grantProvinceCompany(command) await this.applicationService.grantProvinceCompany(command)
return { message: '正式省公司授权成功' } return { message: '正式省公司授权成功' }
} }
@Post('city-company') @Post('city-company')
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: '授权正式市公司(管理员)' }) @ApiOperation({ summary: '授权正式市公司(管理员)' })
@ApiResponse({ status: 201, description: '授权成功' }) @ApiResponse({ status: 201, description: '授权成功' })
async grantCityCompany( async grantCityCompany(
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
@Body() dto: GrantCityCompanyDto, @Body() dto: GrantCityCompanyDto,
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const command = new GrantCityCompanyCommand( const command = new GrantCityCompanyCommand(
dto.userId, dto.userId,
dto.accountSequence, dto.accountSequence,
dto.cityCode, dto.cityCode,
dto.cityName, dto.cityName,
user.userId, user.userId,
user.accountSequence, user.accountSequence,
dto.skipAssessment ?? false, dto.skipAssessment ?? false,
) )
await this.applicationService.grantCityCompany(command) await this.applicationService.grantCityCompany(command)
return { message: '正式市公司授权成功' } return { message: '正式市公司授权成功' }
} }
@Post('auth-province-company') @Post('auth-province-company')
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: '授权省团队(管理员)' }) @ApiOperation({ summary: '授权省团队(管理员)' })
@ApiResponse({ status: 201, description: '授权成功' }) @ApiResponse({ status: 201, description: '授权成功' })
@ApiResponse({ status: 400, description: '验证失败(如团队内已存在相同省份授权)' }) @ApiResponse({ status: 400, description: '验证失败(如团队内已存在相同省份授权)' })
async grantAuthProvinceCompany( async grantAuthProvinceCompany(
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
@Body() dto: GrantAuthProvinceCompanyDto, @Body() dto: GrantAuthProvinceCompanyDto,
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const command = new GrantAuthProvinceCompanyCommand( const command = new GrantAuthProvinceCompanyCommand(
dto.userId, dto.userId,
dto.accountSequence, dto.accountSequence,
dto.provinceCode, dto.provinceCode,
dto.provinceName, dto.provinceName,
user.userId, user.userId,
user.accountSequence, user.accountSequence,
dto.skipAssessment ?? false, dto.skipAssessment ?? false,
) )
await this.applicationService.grantAuthProvinceCompany(command) await this.applicationService.grantAuthProvinceCompany(command)
return { message: '省团队授权成功' } return { message: '省团队授权成功' }
} }
@Post('auth-city-company') @Post('auth-city-company')
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: '授权市团队(管理员)' }) @ApiOperation({ summary: '授权市团队(管理员)' })
@ApiResponse({ status: 201, description: '授权成功' }) @ApiResponse({ status: 201, description: '授权成功' })
@ApiResponse({ status: 400, description: '验证失败(如团队内已存在相同城市授权)' }) @ApiResponse({ status: 400, description: '验证失败(如团队内已存在相同城市授权)' })
async grantAuthCityCompany( async grantAuthCityCompany(
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
@Body() dto: GrantAuthCityCompanyDto, @Body() dto: GrantAuthCityCompanyDto,
): Promise<{ message: string }> { ): Promise<{ message: string }> {
const command = new GrantAuthCityCompanyCommand( const command = new GrantAuthCityCompanyCommand(
dto.userId, dto.userId,
dto.accountSequence, dto.accountSequence,
dto.cityCode, dto.cityCode,
dto.cityName, dto.cityName,
user.userId, user.userId,
user.accountSequence, user.accountSequence,
dto.skipAssessment ?? false, dto.skipAssessment ?? false,
) )
await this.applicationService.grantAuthCityCompany(command) await this.applicationService.grantAuthCityCompany(command)
return { message: '市团队授权成功' } return { message: '市团队授权成功' }
} }
} }

View File

@ -1,172 +1,172 @@
import { import {
Controller, Controller,
Get, Get,
Post, Post,
Delete, Delete,
Param, Param,
Body, Body,
Query, Query,
UseGuards, UseGuards,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
} from '@nestjs/common' } from '@nestjs/common'
import { import {
ApiTags, ApiTags,
ApiOperation, ApiOperation,
ApiResponse, ApiResponse,
ApiBearerAuth, ApiBearerAuth,
ApiParam, ApiParam,
ApiQuery, ApiQuery,
} from '@nestjs/swagger' } from '@nestjs/swagger'
import { AuthorizationApplicationService } from '@/application/services' import { AuthorizationApplicationService } from '@/application/services'
import { import {
ApplyCommunityAuthCommand, ApplyCommunityAuthCommand,
ApplyAuthProvinceCompanyCommand, ApplyAuthProvinceCompanyCommand,
ApplyAuthCityCompanyCommand, ApplyAuthCityCompanyCommand,
RevokeAuthorizationCommand, RevokeAuthorizationCommand,
GrantMonthlyBypassCommand, GrantMonthlyBypassCommand,
ExemptLocalPercentageCheckCommand, ExemptLocalPercentageCheckCommand,
} from '@/application/commands' } from '@/application/commands'
import { import {
ApplyCommunityAuthDto, ApplyCommunityAuthDto,
ApplyAuthProvinceDto, ApplyAuthProvinceDto,
ApplyAuthCityDto, ApplyAuthCityDto,
RevokeAuthorizationDto, RevokeAuthorizationDto,
GrantMonthlyBypassDto, GrantMonthlyBypassDto,
} from '@/api/dto/request' } from '@/api/dto/request'
import { import {
AuthorizationResponse, AuthorizationResponse,
ApplyAuthorizationResponse, ApplyAuthorizationResponse,
StickmanRankingResponse, StickmanRankingResponse,
CommunityHierarchyResponse, CommunityHierarchyResponse,
} from '@/api/dto/response' } from '@/api/dto/response'
import { CurrentUser } from '@/shared/decorators' import { CurrentUser } from '@/shared/decorators'
import { JwtAuthGuard } from '@/shared/guards' import { JwtAuthGuard } from '@/shared/guards'
import { RoleType } from '@/domain/enums' import { RoleType } from '@/domain/enums'
@ApiTags('Authorization') @ApiTags('Authorization')
@Controller('authorizations') @Controller('authorizations')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
export class AuthorizationController { export class AuthorizationController {
constructor(private readonly applicationService: AuthorizationApplicationService) {} constructor(private readonly applicationService: AuthorizationApplicationService) {}
@Post('community') @Post('community')
@ApiOperation({ summary: '申请社区授权' }) @ApiOperation({ summary: '申请社区授权' })
@ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) @ApiResponse({ status: 201, type: ApplyAuthorizationResponse })
async applyCommunityAuth( async applyCommunityAuth(
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
@Body() dto: ApplyCommunityAuthDto, @Body() dto: ApplyCommunityAuthDto,
): Promise<ApplyAuthorizationResponse> { ): Promise<ApplyAuthorizationResponse> {
const command = new ApplyCommunityAuthCommand(user.userId, user.accountSequence, dto.communityName) const command = new ApplyCommunityAuthCommand(user.userId, user.accountSequence, dto.communityName)
return await this.applicationService.applyCommunityAuth(command) return await this.applicationService.applyCommunityAuth(command)
} }
@Post('province') @Post('province')
@ApiOperation({ summary: '申请授权省公司' }) @ApiOperation({ summary: '申请授权省公司' })
@ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) @ApiResponse({ status: 201, type: ApplyAuthorizationResponse })
async applyAuthProvinceCompany( async applyAuthProvinceCompany(
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
@Body() dto: ApplyAuthProvinceDto, @Body() dto: ApplyAuthProvinceDto,
): Promise<ApplyAuthorizationResponse> { ): Promise<ApplyAuthorizationResponse> {
const command = new ApplyAuthProvinceCompanyCommand( const command = new ApplyAuthProvinceCompanyCommand(
user.userId, user.userId,
user.accountSequence, user.accountSequence,
dto.provinceCode, dto.provinceCode,
dto.provinceName, dto.provinceName,
) )
return await this.applicationService.applyAuthProvinceCompany(command) return await this.applicationService.applyAuthProvinceCompany(command)
} }
@Post('city') @Post('city')
@ApiOperation({ summary: '申请授权市公司' }) @ApiOperation({ summary: '申请授权市公司' })
@ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) @ApiResponse({ status: 201, type: ApplyAuthorizationResponse })
async applyAuthCityCompany( async applyAuthCityCompany(
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
@Body() dto: ApplyAuthCityDto, @Body() dto: ApplyAuthCityDto,
): Promise<ApplyAuthorizationResponse> { ): Promise<ApplyAuthorizationResponse> {
const command = new ApplyAuthCityCompanyCommand(user.userId, user.accountSequence, dto.cityCode, dto.cityName) const command = new ApplyAuthCityCompanyCommand(user.userId, user.accountSequence, dto.cityCode, dto.cityName)
return await this.applicationService.applyAuthCityCompany(command) return await this.applicationService.applyAuthCityCompany(command)
} }
@Get('my') @Get('my')
@ApiOperation({ summary: '获取我的授权列表' }) @ApiOperation({ summary: '获取我的授权列表' })
@ApiResponse({ status: 200, type: [AuthorizationResponse] }) @ApiResponse({ status: 200, type: [AuthorizationResponse] })
async getMyAuthorizations( async getMyAuthorizations(
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
): Promise<AuthorizationResponse[]> { ): Promise<AuthorizationResponse[]> {
return await this.applicationService.getUserAuthorizations(user.accountSequence) return await this.applicationService.getUserAuthorizations(user.accountSequence)
} }
@Get('my/community-hierarchy') @Get('my/community-hierarchy')
@ApiOperation({ summary: '获取我的社区层级(上级社区和下级社区)' }) @ApiOperation({ summary: '获取我的社区层级(上级社区和下级社区)' })
@ApiResponse({ status: 200, type: CommunityHierarchyResponse }) @ApiResponse({ status: 200, type: CommunityHierarchyResponse })
async getMyCommunityHierarchy( async getMyCommunityHierarchy(
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
): Promise<CommunityHierarchyResponse> { ): Promise<CommunityHierarchyResponse> {
return await this.applicationService.getCommunityHierarchy(user.accountSequence) return await this.applicationService.getCommunityHierarchy(user.accountSequence)
} }
@Get(':id') @Get(':id')
@ApiOperation({ summary: '获取授权详情' }) @ApiOperation({ summary: '获取授权详情' })
@ApiParam({ name: 'id', description: '授权ID' }) @ApiParam({ name: 'id', description: '授权ID' })
@ApiResponse({ status: 200, type: AuthorizationResponse }) @ApiResponse({ status: 200, type: AuthorizationResponse })
async getAuthorizationById(@Param('id') id: string): Promise<AuthorizationResponse | null> { async getAuthorizationById(@Param('id') id: string): Promise<AuthorizationResponse | null> {
return await this.applicationService.getAuthorizationById(id) return await this.applicationService.getAuthorizationById(id)
} }
@Get('ranking/stickman') @Get('ranking/stickman')
@ApiOperation({ summary: '获取火柴人排名' }) @ApiOperation({ summary: '获取火柴人排名' })
@ApiQuery({ name: 'month', description: '月份 (YYYY-MM)', example: '2024-01' }) @ApiQuery({ name: 'month', description: '月份 (YYYY-MM)', example: '2024-01' })
@ApiQuery({ name: 'roleType', description: '角色类型', enum: RoleType }) @ApiQuery({ name: 'roleType', description: '角色类型', enum: RoleType })
@ApiQuery({ name: 'regionCode', description: '区域代码' }) @ApiQuery({ name: 'regionCode', description: '区域代码' })
@ApiResponse({ status: 200, type: [StickmanRankingResponse] }) @ApiResponse({ status: 200, type: [StickmanRankingResponse] })
async getStickmanRanking( async getStickmanRanking(
@Query('month') month: string, @Query('month') month: string,
@Query('roleType') roleType: RoleType, @Query('roleType') roleType: RoleType,
@Query('regionCode') regionCode: string, @Query('regionCode') regionCode: string,
): Promise<StickmanRankingResponse[]> { ): Promise<StickmanRankingResponse[]> {
return await this.applicationService.getStickmanRanking(month, roleType, regionCode) return await this.applicationService.getStickmanRanking(month, roleType, regionCode)
} }
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: '撤销授权(管理员)' }) @ApiOperation({ summary: '撤销授权(管理员)' })
@ApiParam({ name: 'id', description: '授权ID' }) @ApiParam({ name: 'id', description: '授权ID' })
@ApiResponse({ status: 204 }) @ApiResponse({ status: 204 })
async revokeAuthorization( async revokeAuthorization(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
@Body() dto: RevokeAuthorizationDto, @Body() dto: RevokeAuthorizationDto,
): Promise<void> { ): Promise<void> {
const command = new RevokeAuthorizationCommand(id, user.accountSequence, dto.reason) const command = new RevokeAuthorizationCommand(id, user.accountSequence, dto.reason)
await this.applicationService.revokeAuthorization(command) await this.applicationService.revokeAuthorization(command)
} }
@Post(':id/bypass') @Post(':id/bypass')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: '授予单月豁免(管理员)' }) @ApiOperation({ summary: '授予单月豁免(管理员)' })
@ApiParam({ name: 'id', description: '授权ID' }) @ApiParam({ name: 'id', description: '授权ID' })
@ApiResponse({ status: 204 }) @ApiResponse({ status: 204 })
async grantMonthlyBypass( async grantMonthlyBypass(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
@Body() dto: GrantMonthlyBypassDto, @Body() dto: GrantMonthlyBypassDto,
): Promise<void> { ): Promise<void> {
const command = new GrantMonthlyBypassCommand(id, dto.month, user.accountSequence, dto.reason) const command = new GrantMonthlyBypassCommand(id, dto.month, user.accountSequence, dto.reason)
await this.applicationService.grantMonthlyBypass(command) await this.applicationService.grantMonthlyBypass(command)
} }
@Post(':id/exempt-percentage') @Post(':id/exempt-percentage')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: '豁免占比考核(管理员)' }) @ApiOperation({ summary: '豁免占比考核(管理员)' })
@ApiParam({ name: 'id', description: '授权ID' }) @ApiParam({ name: 'id', description: '授权ID' })
@ApiResponse({ status: 204 }) @ApiResponse({ status: 204 })
async exemptLocalPercentageCheck( async exemptLocalPercentageCheck(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: { userId: string; accountSequence: number }, @CurrentUser() user: { userId: string; accountSequence: string },
): Promise<void> { ): Promise<void> {
const command = new ExemptLocalPercentageCheckCommand(id, user.accountSequence) const command = new ExemptLocalPercentageCheckCommand(id, user.accountSequence)
await this.applicationService.exemptLocalPercentageCheck(command) await this.applicationService.exemptLocalPercentageCheck(command)
} }
} }

View File

@ -1,16 +1,16 @@
import { Controller, Get } from '@nestjs/common' import { Controller, Get } from '@nestjs/common'
import { ApiTags, ApiOperation } from '@nestjs/swagger' import { ApiTags, ApiOperation } from '@nestjs/swagger'
@ApiTags('Health') @ApiTags('Health')
@Controller('health') @Controller('health')
export class HealthController { export class HealthController {
@Get() @Get()
@ApiOperation({ summary: '健康检查' }) @ApiOperation({ summary: '健康检查' })
check() { check() {
return { return {
status: 'ok', status: 'ok',
service: 'authorization-service', service: 'authorization-service',
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
} }
} }
} }

View File

@ -26,21 +26,21 @@ export class InternalAuthorizationController {
schema: { schema: {
type: 'object', type: 'object',
properties: { properties: {
accountSequence: { type: 'number', nullable: true }, accountSequence: { type: 'string', nullable: true },
}, },
}, },
}) })
async findNearestCommunity( async findNearestCommunity(
@Query('accountSequence') accountSequence: string, @Query('accountSequence') accountSequence: string,
): Promise<{ accountSequence: number | null }> { ): Promise<{ accountSequence: string | null }> {
this.logger.debug(`[INTERNAL] findNearestCommunity: accountSequence=${accountSequence}`) this.logger.debug(`[INTERNAL] findNearestCommunity: accountSequence=${accountSequence}`)
const result = await this.applicationService.findNearestAuthorizedCommunity( const result = await this.applicationService.findNearestAuthorizedCommunity(
Number(accountSequence), accountSequence,
) )
return { return {
accountSequence: result ? Number(result) : null, accountSequence: result,
} }
} }
@ -58,25 +58,25 @@ export class InternalAuthorizationController {
schema: { schema: {
type: 'object', type: 'object',
properties: { properties: {
accountSequence: { type: 'number', nullable: true }, accountSequence: { type: 'string', nullable: true },
}, },
}, },
}) })
async findNearestProvince( async findNearestProvince(
@Query('accountSequence') accountSequence: string, @Query('accountSequence') accountSequence: string,
@Query('provinceCode') provinceCode: string, @Query('provinceCode') provinceCode: string,
): Promise<{ accountSequence: number | null }> { ): Promise<{ accountSequence: string | null }> {
this.logger.debug( this.logger.debug(
`[INTERNAL] findNearestProvince: accountSequence=${accountSequence}, provinceCode=${provinceCode}`, `[INTERNAL] findNearestProvince: accountSequence=${accountSequence}, provinceCode=${provinceCode}`,
) )
const result = await this.applicationService.findNearestAuthorizedProvince( const result = await this.applicationService.findNearestAuthorizedProvince(
Number(accountSequence), accountSequence,
provinceCode, provinceCode,
) )
return { return {
accountSequence: result ? Number(result) : null, accountSequence: result ? result : null,
} }
} }
@ -94,25 +94,25 @@ export class InternalAuthorizationController {
schema: { schema: {
type: 'object', type: 'object',
properties: { properties: {
accountSequence: { type: 'number', nullable: true }, accountSequence: { type: 'string', nullable: true },
}, },
}, },
}) })
async findNearestCity( async findNearestCity(
@Query('accountSequence') accountSequence: string, @Query('accountSequence') accountSequence: string,
@Query('cityCode') cityCode: string, @Query('cityCode') cityCode: string,
): Promise<{ accountSequence: number | null }> { ): Promise<{ accountSequence: string | null }> {
this.logger.debug( this.logger.debug(
`[INTERNAL] findNearestCity: accountSequence=${accountSequence}, cityCode=${cityCode}`, `[INTERNAL] findNearestCity: accountSequence=${accountSequence}, cityCode=${cityCode}`,
) )
const result = await this.applicationService.findNearestAuthorizedCity( const result = await this.applicationService.findNearestAuthorizedCity(
Number(accountSequence), accountSequence,
cityCode, cityCode,
) )
return { return {
accountSequence: result ? Number(result) : null, accountSequence: result ? result : null,
} }
} }
@ -136,8 +136,8 @@ export class InternalAuthorizationController {
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
accountSequence: { type: 'number', description: '接收者账号' }, accountSequence: { type: 'string', description: '接收者账号' },
treeCount: { type: 'number', description: '分配棵数' }, treeCount: { type: 'string', description: '分配棵数' },
reason: { type: 'string', description: '分配原因' }, reason: { type: 'string', description: '分配原因' },
}, },
}, },
@ -150,7 +150,7 @@ export class InternalAuthorizationController {
@Query('treeCount') treeCount: string, @Query('treeCount') treeCount: string,
): Promise<{ ): Promise<{
distributions: Array<{ distributions: Array<{
accountSequence: number accountSequence: string
treeCount: number treeCount: number
reason: string reason: string
}> }>
@ -160,7 +160,7 @@ export class InternalAuthorizationController {
) )
return this.applicationService.getCommunityRewardDistribution( return this.applicationService.getCommunityRewardDistribution(
Number(accountSequence), accountSequence,
Number(treeCount), Number(treeCount),
) )
} }
@ -179,7 +179,7 @@ export class InternalAuthorizationController {
@Query('treeCount') treeCount: string, @Query('treeCount') treeCount: string,
): Promise<{ ): Promise<{
distributions: Array<{ distributions: Array<{
accountSequence: number accountSequence: string
treeCount: number treeCount: number
reason: string reason: string
}> }>
@ -189,7 +189,7 @@ export class InternalAuthorizationController {
) )
return this.applicationService.getProvinceTeamRewardDistribution( return this.applicationService.getProvinceTeamRewardDistribution(
Number(accountSequence), accountSequence,
provinceCode, provinceCode,
Number(treeCount), Number(treeCount),
) )
@ -207,7 +207,7 @@ export class InternalAuthorizationController {
@Query('treeCount') treeCount: string, @Query('treeCount') treeCount: string,
): Promise<{ ): Promise<{
distributions: Array<{ distributions: Array<{
accountSequence: number accountSequence: string
treeCount: number treeCount: number
reason: string reason: string
isSystemAccount: boolean isSystemAccount: boolean
@ -237,7 +237,7 @@ export class InternalAuthorizationController {
@Query('treeCount') treeCount: string, @Query('treeCount') treeCount: string,
): Promise<{ ): Promise<{
distributions: Array<{ distributions: Array<{
accountSequence: number accountSequence: string
treeCount: number treeCount: number
reason: string reason: string
}> }>
@ -247,7 +247,7 @@ export class InternalAuthorizationController {
) )
return this.applicationService.getCityTeamRewardDistribution( return this.applicationService.getCityTeamRewardDistribution(
Number(accountSequence), accountSequence,
cityCode, cityCode,
Number(treeCount), Number(treeCount),
) )
@ -265,7 +265,7 @@ export class InternalAuthorizationController {
@Query('treeCount') treeCount: string, @Query('treeCount') treeCount: string,
): Promise<{ ): Promise<{
distributions: Array<{ distributions: Array<{
accountSequence: number accountSequence: string
treeCount: number treeCount: number
reason: string reason: string
isSystemAccount: boolean isSystemAccount: boolean

View File

@ -1,16 +1,16 @@
import { IsString, IsNotEmpty, MaxLength } from 'class-validator' import { IsString, IsNotEmpty, MaxLength } from 'class-validator'
import { ApiProperty } from '@nestjs/swagger' import { ApiProperty } from '@nestjs/swagger'
export class ApplyAuthCityDto { export class ApplyAuthCityDto {
@ApiProperty({ description: '城市代码', example: '430100' }) @ApiProperty({ description: '城市代码', example: '430100' })
@IsString() @IsString()
@IsNotEmpty({ message: '城市代码不能为空' }) @IsNotEmpty({ message: '城市代码不能为空' })
@MaxLength(20, { message: '城市代码最大20字符' }) @MaxLength(20, { message: '城市代码最大20字符' })
cityCode: string cityCode: string
@ApiProperty({ description: '城市名称', example: '长沙市' }) @ApiProperty({ description: '城市名称', example: '长沙市' })
@IsString() @IsString()
@IsNotEmpty({ message: '城市名称不能为空' }) @IsNotEmpty({ message: '城市名称不能为空' })
@MaxLength(50, { message: '城市名称最大50字符' }) @MaxLength(50, { message: '城市名称最大50字符' })
cityName: string cityName: string
} }

View File

@ -1,16 +1,16 @@
import { IsString, IsNotEmpty, MaxLength } from 'class-validator' import { IsString, IsNotEmpty, MaxLength } from 'class-validator'
import { ApiProperty } from '@nestjs/swagger' import { ApiProperty } from '@nestjs/swagger'
export class ApplyAuthProvinceDto { export class ApplyAuthProvinceDto {
@ApiProperty({ description: '省份代码', example: '430000' }) @ApiProperty({ description: '省份代码', example: '430000' })
@IsString() @IsString()
@IsNotEmpty({ message: '省份代码不能为空' }) @IsNotEmpty({ message: '省份代码不能为空' })
@MaxLength(20, { message: '省份代码最大20字符' }) @MaxLength(20, { message: '省份代码最大20字符' })
provinceCode: string provinceCode: string
@ApiProperty({ description: '省份名称', example: '湖南省' }) @ApiProperty({ description: '省份名称', example: '湖南省' })
@IsString() @IsString()
@IsNotEmpty({ message: '省份名称不能为空' }) @IsNotEmpty({ message: '省份名称不能为空' })
@MaxLength(50, { message: '省份名称最大50字符' }) @MaxLength(50, { message: '省份名称最大50字符' })
provinceName: string provinceName: string
} }

View File

@ -1,10 +1,10 @@
import { IsString, IsNotEmpty, MaxLength } from 'class-validator' import { IsString, IsNotEmpty, MaxLength } from 'class-validator'
import { ApiProperty } from '@nestjs/swagger' import { ApiProperty } from '@nestjs/swagger'
export class ApplyCommunityAuthDto { export class ApplyCommunityAuthDto {
@ApiProperty({ description: '社区名称', example: '量子社区' }) @ApiProperty({ description: '社区名称', example: '量子社区' })
@IsString() @IsString()
@IsNotEmpty({ message: '社区名称不能为空' }) @IsNotEmpty({ message: '社区名称不能为空' })
@MaxLength(50, { message: '社区名称最大50字符' }) @MaxLength(50, { message: '社区名称最大50字符' })
communityName: string communityName: string
} }

View File

@ -8,9 +8,9 @@ export class GrantAuthCityCompanyDto {
userId: string userId: string
@ApiProperty({ description: '账户序列号' }) @ApiProperty({ description: '账户序列号' })
@IsNumber() @IsString()
@IsNotEmpty({ message: '账户序列号不能为空' }) @IsNotEmpty({ message: '账户序列号不能为空' })
accountSequence: number accountSequence: string
@ApiProperty({ description: '城市代码', example: '430100' }) @ApiProperty({ description: '城市代码', example: '430100' })
@IsString() @IsString()

View File

@ -8,9 +8,9 @@ export class GrantAuthProvinceCompanyDto {
userId: string userId: string
@ApiProperty({ description: '账户序列号' }) @ApiProperty({ description: '账户序列号' })
@IsNumber() @IsString()
@IsNotEmpty({ message: '账户序列号不能为空' }) @IsNotEmpty({ message: '账户序列号不能为空' })
accountSequence: number accountSequence: string
@ApiProperty({ description: '省份代码', example: '430000' }) @ApiProperty({ description: '省份代码', example: '430000' })
@IsString() @IsString()

View File

@ -1,31 +1,31 @@
import { IsString, IsNotEmpty, MaxLength, IsNumber, IsBoolean, IsOptional } from 'class-validator' import { IsString, IsNotEmpty, MaxLength, IsNumber, IsBoolean, IsOptional } from 'class-validator'
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
export class GrantCityCompanyDto { export class GrantCityCompanyDto {
@ApiProperty({ description: '用户ID' }) @ApiProperty({ description: '用户ID' })
@IsString() @IsString()
@IsNotEmpty({ message: '用户ID不能为空' }) @IsNotEmpty({ message: '用户ID不能为空' })
userId: string userId: string
@ApiProperty({ description: '账户序列号' }) @ApiProperty({ description: '账户序列号' })
@IsNumber() @IsString()
@IsNotEmpty({ message: '账户序列号不能为空' }) @IsNotEmpty({ message: '账户序列号不能为空' })
accountSequence: number accountSequence: string
@ApiProperty({ description: '城市代码', example: '430100' }) @ApiProperty({ description: '城市代码', example: '430100' })
@IsString() @IsString()
@IsNotEmpty({ message: '城市代码不能为空' }) @IsNotEmpty({ message: '城市代码不能为空' })
@MaxLength(20, { message: '城市代码最大20字符' }) @MaxLength(20, { message: '城市代码最大20字符' })
cityCode: string cityCode: string
@ApiProperty({ description: '城市名称', example: '长沙市' }) @ApiProperty({ description: '城市名称', example: '长沙市' })
@IsString() @IsString()
@IsNotEmpty({ message: '城市名称不能为空' }) @IsNotEmpty({ message: '城市名称不能为空' })
@MaxLength(50, { message: '城市名称最大50字符' }) @MaxLength(50, { message: '城市名称最大50字符' })
cityName: string cityName: string
@ApiPropertyOptional({ description: '是否跳过考核直接激活权益', default: false }) @ApiPropertyOptional({ description: '是否跳过考核直接激活权益', default: false })
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
skipAssessment?: boolean skipAssessment?: boolean
} }

View File

@ -8,9 +8,9 @@ export class GrantCommunityDto {
userId: string userId: string
@ApiProperty({ description: '账户序列号' }) @ApiProperty({ description: '账户序列号' })
@IsNumber() @IsString()
@IsNotEmpty({ message: '账户序列号不能为空' }) @IsNotEmpty({ message: '账户序列号不能为空' })
accountSequence: number accountSequence: string
@ApiProperty({ description: '社区名称', example: '深圳社区' }) @ApiProperty({ description: '社区名称', example: '深圳社区' })
@IsString() @IsString()

View File

@ -1,16 +1,16 @@
import { IsString, IsNotEmpty, IsOptional, MaxLength, Matches } from 'class-validator' import { IsString, IsNotEmpty, IsOptional, MaxLength, Matches } from 'class-validator'
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
export class GrantMonthlyBypassDto { export class GrantMonthlyBypassDto {
@ApiProperty({ description: '豁免月份', example: '2024-01' }) @ApiProperty({ description: '豁免月份', example: '2024-01' })
@IsString() @IsString()
@IsNotEmpty({ message: '豁免月份不能为空' }) @IsNotEmpty({ message: '豁免月份不能为空' })
@Matches(/^\d{4}-\d{2}$/, { message: '月份格式应为YYYY-MM' }) @Matches(/^\d{4}-\d{2}$/, { message: '月份格式应为YYYY-MM' })
month: string month: string
@ApiPropertyOptional({ description: '豁免原因', example: '特殊情况' }) @ApiPropertyOptional({ description: '豁免原因', example: '特殊情况' })
@IsOptional() @IsOptional()
@IsString() @IsString()
@MaxLength(200, { message: '豁免原因最大200字符' }) @MaxLength(200, { message: '豁免原因最大200字符' })
reason?: string reason?: string
} }

View File

@ -1,31 +1,31 @@
import { IsString, IsNotEmpty, MaxLength, IsNumber, IsBoolean, IsOptional } from 'class-validator' import { IsString, IsNotEmpty, MaxLength, IsNumber, IsBoolean, IsOptional } from 'class-validator'
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
export class GrantProvinceCompanyDto { export class GrantProvinceCompanyDto {
@ApiProperty({ description: '用户ID' }) @ApiProperty({ description: '用户ID' })
@IsString() @IsString()
@IsNotEmpty({ message: '用户ID不能为空' }) @IsNotEmpty({ message: '用户ID不能为空' })
userId: string userId: string
@ApiProperty({ description: '账户序列号' }) @ApiProperty({ description: '账户序列号' })
@IsNumber() @IsString()
@IsNotEmpty({ message: '账户序列号不能为空' }) @IsNotEmpty({ message: '账户序列号不能为空' })
accountSequence: number accountSequence: string
@ApiProperty({ description: '省份代码', example: '430000' }) @ApiProperty({ description: '省份代码', example: '430000' })
@IsString() @IsString()
@IsNotEmpty({ message: '省份代码不能为空' }) @IsNotEmpty({ message: '省份代码不能为空' })
@MaxLength(20, { message: '省份代码最大20字符' }) @MaxLength(20, { message: '省份代码最大20字符' })
provinceCode: string provinceCode: string
@ApiProperty({ description: '省份名称', example: '湖南省' }) @ApiProperty({ description: '省份名称', example: '湖南省' })
@IsString() @IsString()
@IsNotEmpty({ message: '省份名称不能为空' }) @IsNotEmpty({ message: '省份名称不能为空' })
@MaxLength(50, { message: '省份名称最大50字符' }) @MaxLength(50, { message: '省份名称最大50字符' })
provinceName: string provinceName: string
@ApiPropertyOptional({ description: '是否跳过考核直接激活权益', default: false }) @ApiPropertyOptional({ description: '是否跳过考核直接激活权益', default: false })
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
skipAssessment?: boolean skipAssessment?: boolean
} }

View File

@ -1,10 +1,10 @@
import { IsString, IsNotEmpty, MaxLength } from 'class-validator' import { IsString, IsNotEmpty, MaxLength } from 'class-validator'
import { ApiProperty } from '@nestjs/swagger' import { ApiProperty } from '@nestjs/swagger'
export class RevokeAuthorizationDto { export class RevokeAuthorizationDto {
@ApiProperty({ description: '撤销原因', example: '违规操作' }) @ApiProperty({ description: '撤销原因', example: '违规操作' })
@IsString() @IsString()
@IsNotEmpty({ message: '撤销原因不能为空' }) @IsNotEmpty({ message: '撤销原因不能为空' })
@MaxLength(200, { message: '撤销原因最大200字符' }) @MaxLength(200, { message: '撤销原因最大200字符' })
reason: string reason: string
} }

View File

@ -1,122 +1,122 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger' import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { RoleType, AuthorizationStatus } from '@/domain/enums' import { RoleType, AuthorizationStatus } from '@/domain/enums'
export class AuthorizationResponse { export class AuthorizationResponse {
@ApiProperty({ description: '授权ID' }) @ApiProperty({ description: '授权ID' })
authorizationId: string authorizationId: string
@ApiProperty({ description: '用户ID' }) @ApiProperty({ description: '用户ID' })
userId: string userId: string
@ApiProperty({ description: '角色类型', enum: RoleType }) @ApiProperty({ description: '角色类型', enum: RoleType })
roleType: RoleType roleType: RoleType
@ApiProperty({ description: '区域代码' }) @ApiProperty({ description: '区域代码' })
regionCode: string regionCode: string
@ApiProperty({ description: '区域名称' }) @ApiProperty({ description: '区域名称' })
regionName: string regionName: string
@ApiProperty({ description: '授权状态', enum: AuthorizationStatus }) @ApiProperty({ description: '授权状态', enum: AuthorizationStatus })
status: AuthorizationStatus status: AuthorizationStatus
@ApiProperty({ description: '显示标题' }) @ApiProperty({ description: '显示标题' })
displayTitle: string displayTitle: string
@ApiProperty({ description: '权益是否激活' }) @ApiProperty({ description: '权益是否激活' })
benefitActive: boolean benefitActive: boolean
@ApiProperty({ description: '当前考核月份索引' }) @ApiProperty({ description: '当前考核月份索引' })
currentMonthIndex: number currentMonthIndex: number
@ApiProperty({ description: '本地占比要求' }) @ApiProperty({ description: '本地占比要求' })
requireLocalPercentage: number requireLocalPercentage: number
@ApiProperty({ description: '是否豁免占比考核' }) @ApiProperty({ description: '是否豁免占比考核' })
exemptFromPercentageCheck: boolean exemptFromPercentageCheck: boolean
@ApiProperty({ description: '初始考核目标社区10市100省500' }) @ApiProperty({ description: '初始考核目标社区10市100省500' })
initialTargetTreeCount: number initialTargetTreeCount: number
@ApiProperty({ description: '当前团队认种数量' }) @ApiProperty({ description: '当前团队认种数量' })
currentTreeCount: number currentTreeCount: number
@ApiProperty({ description: '月度考核目标' }) @ApiProperty({ description: '月度考核目标' })
monthlyTargetTreeCount: number monthlyTargetTreeCount: number
@ApiProperty({ description: '创建时间' }) @ApiProperty({ description: '创建时间' })
createdAt: Date createdAt: Date
@ApiProperty({ description: '更新时间' }) @ApiProperty({ description: '更新时间' })
updatedAt: Date updatedAt: Date
} }
export class ApplyAuthorizationResponse { export class ApplyAuthorizationResponse {
@ApiProperty({ description: '授权ID' }) @ApiProperty({ description: '授权ID' })
authorizationId: string authorizationId: string
@ApiProperty({ description: '授权状态' }) @ApiProperty({ description: '授权状态' })
status: string status: string
@ApiProperty({ description: '权益是否激活' }) @ApiProperty({ description: '权益是否激活' })
benefitActive: boolean benefitActive: boolean
@ApiPropertyOptional({ description: '显示标题' }) @ApiPropertyOptional({ description: '显示标题' })
displayTitle?: string displayTitle?: string
@ApiProperty({ description: '消息提示' }) @ApiProperty({ description: '消息提示' })
message: string message: string
@ApiProperty({ description: '当前认种数量' }) @ApiProperty({ description: '当前认种数量' })
currentTreeCount: number currentTreeCount: number
@ApiProperty({ description: '所需认种数量' }) @ApiProperty({ description: '所需认种数量' })
requiredTreeCount: number requiredTreeCount: number
} }
export class StickmanRankingResponse { export class StickmanRankingResponse {
@ApiProperty({ description: '用户ID' }) @ApiProperty({ description: '用户ID' })
userId: string userId: string
@ApiProperty({ description: '授权ID' }) @ApiProperty({ description: '授权ID' })
authorizationId: string authorizationId: string
@ApiProperty({ description: '角色类型', enum: RoleType }) @ApiProperty({ description: '角色类型', enum: RoleType })
roleType: RoleType roleType: RoleType
@ApiProperty({ description: '区域代码' }) @ApiProperty({ description: '区域代码' })
regionCode: string regionCode: string
@ApiPropertyOptional({ description: '昵称' }) @ApiPropertyOptional({ description: '昵称' })
nickname?: string nickname?: string
@ApiPropertyOptional({ description: '头像URL' }) @ApiPropertyOptional({ description: '头像URL' })
avatarUrl?: string avatarUrl?: string
@ApiProperty({ description: '排名' }) @ApiProperty({ description: '排名' })
ranking: number ranking: number
@ApiProperty({ description: '是否第一名' }) @ApiProperty({ description: '是否第一名' })
isFirstPlace: boolean isFirstPlace: boolean
@ApiProperty({ description: '累计完成数量' }) @ApiProperty({ description: '累计完成数量' })
cumulativeCompleted: number cumulativeCompleted: number
@ApiProperty({ description: '累计目标数量' }) @ApiProperty({ description: '累计目标数量' })
cumulativeTarget: number cumulativeTarget: number
@ApiProperty({ description: '最终目标数量' }) @ApiProperty({ description: '最终目标数量' })
finalTarget: number finalTarget: number
@ApiProperty({ description: '进度百分比' }) @ApiProperty({ description: '进度百分比' })
progressPercentage: number progressPercentage: number
@ApiProperty({ description: '超越比例' }) @ApiProperty({ description: '超越比例' })
exceedRatio: number exceedRatio: number
@ApiProperty({ description: '本月USDT收益' }) @ApiProperty({ description: '本月USDT收益' })
monthlyRewardUsdt: number monthlyRewardUsdt: number
@ApiProperty({ description: '本月RWAD收益' }) @ApiProperty({ description: '本月RWAD收益' })
monthlyRewardRwad: number monthlyRewardRwad: number
} }

View File

@ -8,7 +8,7 @@ export class CommunityInfo {
authorizationId: string authorizationId: string
@ApiProperty({ description: '账户序列号' }) @ApiProperty({ description: '账户序列号' })
accountSequence: number accountSequence: string
@ApiProperty({ description: '社区名称' }) @ApiProperty({ description: '社区名称' })
communityName: string communityName: string
@ -45,7 +45,7 @@ export class CommunityHierarchyResponse {
*/ */
export const HEADQUARTERS_COMMUNITY: CommunityInfo = { export const HEADQUARTERS_COMMUNITY: CommunityInfo = {
authorizationId: 'headquarters', authorizationId: 'headquarters',
accountSequence: 0, accountSequence: '',
communityName: '总部社区', communityName: '总部社区',
userId: undefined, userId: undefined,
isHeadquarters: true, isHeadquarters: true,

View File

@ -1,18 +1,18 @@
export class ApplyAuthCityCompanyCommand { export class ApplyAuthCityCompanyCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number, public readonly accountSequence: string,
public readonly cityCode: string, public readonly cityCode: string,
public readonly cityName: string, public readonly cityName: string,
) {} ) {}
} }
export interface ApplyAuthCityCompanyResult { export interface ApplyAuthCityCompanyResult {
authorizationId: string authorizationId: string
status: string status: string
benefitActive: boolean benefitActive: boolean
displayTitle: string displayTitle: string
message: string message: string
currentTreeCount: number currentTreeCount: number
requiredTreeCount: number requiredTreeCount: number
} }

View File

@ -1,18 +1,18 @@
export class ApplyAuthProvinceCompanyCommand { export class ApplyAuthProvinceCompanyCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number, public readonly accountSequence: string,
public readonly provinceCode: string, public readonly provinceCode: string,
public readonly provinceName: string, public readonly provinceName: string,
) {} ) {}
} }
export interface ApplyAuthProvinceCompanyResult { export interface ApplyAuthProvinceCompanyResult {
authorizationId: string authorizationId: string
status: string status: string
benefitActive: boolean benefitActive: boolean
displayTitle: string displayTitle: string
message: string message: string
currentTreeCount: number currentTreeCount: number
requiredTreeCount: number requiredTreeCount: number
} }

View File

@ -1,16 +1,16 @@
export class ApplyCommunityAuthCommand { export class ApplyCommunityAuthCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number, public readonly accountSequence: string,
public readonly communityName: string, public readonly communityName: string,
) {} ) {}
} }
export interface ApplyCommunityAuthResult { export interface ApplyCommunityAuthResult {
authorizationId: string authorizationId: string
status: string status: string
benefitActive: boolean benefitActive: boolean
message: string message: string
currentTreeCount: number currentTreeCount: number
requiredTreeCount: number requiredTreeCount: number
} }

View File

@ -1,6 +1,6 @@
export class ExemptLocalPercentageCheckCommand { export class ExemptLocalPercentageCheckCommand {
constructor( constructor(
public readonly authorizationId: string, public readonly authorizationId: string,
public readonly adminAccountSequence: number, public readonly adminAccountSequence: string,
) {} ) {}
} }

View File

@ -1,11 +1,11 @@
export class GrantAuthCityCompanyCommand { export class GrantAuthCityCompanyCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number, public readonly accountSequence: string,
public readonly cityCode: string, public readonly cityCode: string,
public readonly cityName: string, public readonly cityName: string,
public readonly adminId: string, public readonly adminId: string,
public readonly adminAccountSequence: number, public readonly adminAccountSequence: string,
public readonly skipAssessment: boolean = false, public readonly skipAssessment: boolean = false,
) {} ) {}
} }

View File

@ -1,11 +1,11 @@
export class GrantAuthProvinceCompanyCommand { export class GrantAuthProvinceCompanyCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number, public readonly accountSequence: string,
public readonly provinceCode: string, public readonly provinceCode: string,
public readonly provinceName: string, public readonly provinceName: string,
public readonly adminId: string, public readonly adminId: string,
public readonly adminAccountSequence: number, public readonly adminAccountSequence: string,
public readonly skipAssessment: boolean = false, public readonly skipAssessment: boolean = false,
) {} ) {}
} }

View File

@ -1,11 +1,11 @@
export class GrantCityCompanyCommand { export class GrantCityCompanyCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number, public readonly accountSequence: string,
public readonly cityCode: string, public readonly cityCode: string,
public readonly cityName: string, public readonly cityName: string,
public readonly adminId: string, public readonly adminId: string,
public readonly adminAccountSequence: number, public readonly adminAccountSequence: string,
public readonly skipAssessment: boolean = false, public readonly skipAssessment: boolean = false,
) {} ) {}
} }

View File

@ -1,10 +1,10 @@
export class GrantCommunityCommand { export class GrantCommunityCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number, public readonly accountSequence: string,
public readonly communityName: string, public readonly communityName: string,
public readonly adminId: string, public readonly adminId: string,
public readonly adminAccountSequence: number, public readonly adminAccountSequence: string,
public readonly skipAssessment: boolean = false, public readonly skipAssessment: boolean = false,
) {} ) {}
} }

View File

@ -1,8 +1,8 @@
export class GrantMonthlyBypassCommand { export class GrantMonthlyBypassCommand {
constructor( constructor(
public readonly authorizationId: string, public readonly authorizationId: string,
public readonly month: string, public readonly month: string,
public readonly adminAccountSequence: number, public readonly adminAccountSequence: string,
public readonly reason?: string, public readonly reason?: string,
) {} ) {}
} }

View File

@ -1,11 +1,11 @@
export class GrantProvinceCompanyCommand { export class GrantProvinceCompanyCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number, public readonly accountSequence: string,
public readonly provinceCode: string, public readonly provinceCode: string,
public readonly provinceName: string, public readonly provinceName: string,
public readonly adminId: string, public readonly adminId: string,
public readonly adminAccountSequence: number, public readonly adminAccountSequence: string,
public readonly skipAssessment: boolean = false, public readonly skipAssessment: boolean = false,
) {} ) {}
} }

View File

@ -1,7 +1,7 @@
export class RevokeAuthorizationCommand { export class RevokeAuthorizationCommand {
constructor( constructor(
public readonly authorizationId: string, public readonly authorizationId: string,
public readonly adminAccountSequence: number, public readonly adminAccountSequence: string,
public readonly reason: string, public readonly reason: string,
) {} ) {}
} }

View File

@ -1,60 +1,60 @@
import { RoleType, AuthorizationStatus } from '@/domain/enums' import { RoleType, AuthorizationStatus } from '@/domain/enums'
export interface AuthorizationDTO { export interface AuthorizationDTO {
authorizationId: string authorizationId: string
userId: string userId: string
roleType: RoleType roleType: RoleType
regionCode: string regionCode: string
regionName: string regionName: string
status: AuthorizationStatus status: AuthorizationStatus
displayTitle: string displayTitle: string
benefitActive: boolean benefitActive: boolean
currentMonthIndex: number currentMonthIndex: number
requireLocalPercentage: number requireLocalPercentage: number
exemptFromPercentageCheck: boolean exemptFromPercentageCheck: boolean
// 考核进度字段 // 考核进度字段
initialTargetTreeCount: number // 初始考核目标社区10市100省500 initialTargetTreeCount: number // 初始考核目标社区10市100省500
currentTreeCount: number // 当前团队认种数量 currentTreeCount: number // 当前团队认种数量
monthlyTargetTreeCount: number // 月度考核目标社区固定10 monthlyTargetTreeCount: number // 月度考核目标社区固定10
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
} }
export interface StickmanRankingDTO { export interface StickmanRankingDTO {
userId: string userId: string
authorizationId: string authorizationId: string
roleType: RoleType roleType: RoleType
regionCode: string regionCode: string
nickname?: string nickname?: string
avatarUrl?: string avatarUrl?: string
ranking: number ranking: number
isFirstPlace: boolean isFirstPlace: boolean
cumulativeCompleted: number cumulativeCompleted: number
cumulativeTarget: number cumulativeTarget: number
finalTarget: number finalTarget: number
progressPercentage: number progressPercentage: number
exceedRatio: number exceedRatio: number
monthlyRewardUsdt: number monthlyRewardUsdt: number
monthlyRewardRwad: number monthlyRewardRwad: number
} }
export interface MonthlyAssessmentDTO { export interface MonthlyAssessmentDTO {
assessmentId: string assessmentId: string
authorizationId: string authorizationId: string
userId: string userId: string
roleType: RoleType roleType: RoleType
regionCode: string regionCode: string
assessmentMonth: string assessmentMonth: string
monthIndex: number monthIndex: number
monthlyTarget: number monthlyTarget: number
cumulativeTarget: number cumulativeTarget: number
monthlyCompleted: number monthlyCompleted: number
cumulativeCompleted: number cumulativeCompleted: number
localPercentage: number localPercentage: number
localPercentagePass: boolean localPercentagePass: boolean
exceedRatio: number exceedRatio: number
result: string result: string
rankingInRegion: number | null rankingInRegion: number | null
isFirstPlace: boolean isFirstPlace: boolean
isBypassed: boolean isBypassed: boolean
} }

View File

@ -3,7 +3,7 @@
*/ */
export interface CommunityInfoDTO { export interface CommunityInfoDTO {
authorizationId: string authorizationId: string
accountSequence: number accountSequence: string
communityName: string communityName: string
userId?: string userId?: string
isHeadquarters: boolean isHeadquarters: boolean

View File

@ -1,193 +1,193 @@
import { AuthorizationRole } from './authorization-role.aggregate' import { AuthorizationRole } from './authorization-role.aggregate'
import { UserId, AdminUserId } from '@/domain/value-objects' import { UserId, AdminUserId } from '@/domain/value-objects'
import { RoleType, AuthorizationStatus, MonthlyTargetType } from '@/domain/enums' import { RoleType, AuthorizationStatus, MonthlyTargetType } from '@/domain/enums'
import { DomainError } from '@/shared/exceptions' import { DomainError } from '@/shared/exceptions'
describe('AuthorizationRole Aggregate', () => { describe('AuthorizationRole Aggregate', () => {
describe('createCommunityAuth', () => { describe('createCommunityAuth', () => {
it('should create community authorization', () => { it('should create community authorization', () => {
const auth = AuthorizationRole.createCommunityAuth({ const auth = AuthorizationRole.createCommunityAuth({
userId: UserId.create('user-1', BigInt(1)), userId: UserId.create('user-1', '1'),
communityName: '量子社区', communityName: '量子社区',
}) })
expect(auth.roleType).toBe(RoleType.COMMUNITY) expect(auth.roleType).toBe(RoleType.COMMUNITY)
expect(auth.status).toBe(AuthorizationStatus.PENDING) expect(auth.status).toBe(AuthorizationStatus.PENDING)
expect(auth.displayTitle).toBe('量子社区') expect(auth.displayTitle).toBe('量子社区')
expect(auth.benefitActive).toBe(false) expect(auth.benefitActive).toBe(false)
expect(auth.getInitialTarget()).toBe(10) expect(auth.getInitialTarget()).toBe(10)
expect(auth.domainEvents.length).toBe(1) expect(auth.domainEvents.length).toBe(1)
expect(auth.domainEvents[0].eventType).toBe('authorization.community.requested') expect(auth.domainEvents[0].eventType).toBe('authorization.community.requested')
}) })
}) })
describe('createAuthProvinceCompany', () => { describe('createAuthProvinceCompany', () => {
it('should create auth province company authorization', () => { it('should create auth province company authorization', () => {
const auth = AuthorizationRole.createAuthProvinceCompany({ const auth = AuthorizationRole.createAuthProvinceCompany({
userId: UserId.create('user-1', BigInt(1)), userId: UserId.create('user-1', '1'),
provinceCode: '430000', provinceCode: '430000',
provinceName: '湖南省', provinceName: '湖南省',
}) })
expect(auth.roleType).toBe(RoleType.AUTH_PROVINCE_COMPANY) expect(auth.roleType).toBe(RoleType.AUTH_PROVINCE_COMPANY)
expect(auth.status).toBe(AuthorizationStatus.PENDING) expect(auth.status).toBe(AuthorizationStatus.PENDING)
expect(auth.displayTitle).toBe('授权湖南省') expect(auth.displayTitle).toBe('授权湖南省')
expect(auth.benefitActive).toBe(false) expect(auth.benefitActive).toBe(false)
expect(auth.getInitialTarget()).toBe(500) expect(auth.getInitialTarget()).toBe(500)
expect(auth.requireLocalPercentage).toBe(5.0) expect(auth.requireLocalPercentage).toBe(5.0)
expect(auth.needsLadderAssessment()).toBe(true) expect(auth.needsLadderAssessment()).toBe(true)
}) })
}) })
describe('createAuthCityCompany', () => { describe('createAuthCityCompany', () => {
it('should create auth city company authorization', () => { it('should create auth city company authorization', () => {
const auth = AuthorizationRole.createAuthCityCompany({ const auth = AuthorizationRole.createAuthCityCompany({
userId: UserId.create('user-1', BigInt(1)), userId: UserId.create('user-1', '1'),
cityCode: '430100', cityCode: '430100',
cityName: '长沙市', cityName: '长沙市',
}) })
expect(auth.roleType).toBe(RoleType.AUTH_CITY_COMPANY) expect(auth.roleType).toBe(RoleType.AUTH_CITY_COMPANY)
expect(auth.status).toBe(AuthorizationStatus.PENDING) expect(auth.status).toBe(AuthorizationStatus.PENDING)
expect(auth.displayTitle).toBe('授权长沙市') expect(auth.displayTitle).toBe('授权长沙市')
expect(auth.benefitActive).toBe(false) expect(auth.benefitActive).toBe(false)
expect(auth.getInitialTarget()).toBe(100) expect(auth.getInitialTarget()).toBe(100)
}) })
}) })
describe('createProvinceCompany', () => { describe('createProvinceCompany', () => {
it('should create official province company with active benefits', () => { it('should create official province company with active benefits', () => {
const adminId = AdminUserId.create('admin-1', BigInt(101)) const adminId = AdminUserId.create('admin-1', '101')
const auth = AuthorizationRole.createProvinceCompany({ const auth = AuthorizationRole.createProvinceCompany({
userId: UserId.create('user-1', BigInt(1)), userId: UserId.create('user-1', '1'),
provinceCode: '430000', provinceCode: '430000',
provinceName: '湖南省', provinceName: '湖南省',
adminId, adminId,
}) })
expect(auth.roleType).toBe(RoleType.PROVINCE_COMPANY) expect(auth.roleType).toBe(RoleType.PROVINCE_COMPANY)
expect(auth.status).toBe(AuthorizationStatus.AUTHORIZED) expect(auth.status).toBe(AuthorizationStatus.AUTHORIZED)
expect(auth.displayTitle).toBe('湖南省') expect(auth.displayTitle).toBe('湖南省')
expect(auth.benefitActive).toBe(true) expect(auth.benefitActive).toBe(true)
expect(auth.getInitialTarget()).toBe(0) // No initial target for official company expect(auth.getInitialTarget()).toBe(0) // No initial target for official company
}) })
}) })
describe('activateBenefit', () => { describe('activateBenefit', () => {
it('should activate benefit and emit event', () => { it('should activate benefit and emit event', () => {
const auth = AuthorizationRole.createCommunityAuth({ const auth = AuthorizationRole.createCommunityAuth({
userId: UserId.create('user-1', BigInt(1)), userId: UserId.create('user-1', '1'),
communityName: '量子社区', communityName: '量子社区',
}) })
auth.clearDomainEvents() auth.clearDomainEvents()
auth.activateBenefit() auth.activateBenefit()
expect(auth.benefitActive).toBe(true) expect(auth.benefitActive).toBe(true)
expect(auth.status).toBe(AuthorizationStatus.AUTHORIZED) expect(auth.status).toBe(AuthorizationStatus.AUTHORIZED)
expect(auth.currentMonthIndex).toBe(1) expect(auth.currentMonthIndex).toBe(1)
expect(auth.domainEvents.length).toBe(1) expect(auth.domainEvents.length).toBe(1)
expect(auth.domainEvents[0].eventType).toBe('authorization.benefit.activated') expect(auth.domainEvents[0].eventType).toBe('authorization.benefit.activated')
}) })
it('should throw error if already active', () => { it('should throw error if already active', () => {
const auth = AuthorizationRole.createProvinceCompany({ const auth = AuthorizationRole.createProvinceCompany({
userId: UserId.create('user-1', BigInt(1)), userId: UserId.create('user-1', '1'),
provinceCode: '430000', provinceCode: '430000',
provinceName: '湖南省', provinceName: '湖南省',
adminId: AdminUserId.create('admin-1', BigInt(101)), adminId: AdminUserId.create('admin-1', '101'),
}) })
expect(() => auth.activateBenefit()).toThrow(DomainError) expect(() => auth.activateBenefit()).toThrow(DomainError)
}) })
}) })
describe('deactivateBenefit', () => { describe('deactivateBenefit', () => {
it('should deactivate benefit and reset month index', () => { it('should deactivate benefit and reset month index', () => {
const auth = AuthorizationRole.createProvinceCompany({ const auth = AuthorizationRole.createProvinceCompany({
userId: UserId.create('user-1', BigInt(1)), userId: UserId.create('user-1', '1'),
provinceCode: '430000', provinceCode: '430000',
provinceName: '湖南省', provinceName: '湖南省',
adminId: AdminUserId.create('admin-1', BigInt(101)), adminId: AdminUserId.create('admin-1', '101'),
}) })
auth.clearDomainEvents() auth.clearDomainEvents()
auth.deactivateBenefit('考核不达标') auth.deactivateBenefit('考核不达标')
expect(auth.benefitActive).toBe(false) expect(auth.benefitActive).toBe(false)
expect(auth.currentMonthIndex).toBe(0) expect(auth.currentMonthIndex).toBe(0)
expect(auth.domainEvents.length).toBe(1) expect(auth.domainEvents.length).toBe(1)
expect(auth.domainEvents[0].eventType).toBe('authorization.benefit.deactivated') expect(auth.domainEvents[0].eventType).toBe('authorization.benefit.deactivated')
}) })
}) })
describe('revoke', () => { describe('revoke', () => {
it('should revoke authorization', () => { it('should revoke authorization', () => {
const auth = AuthorizationRole.createProvinceCompany({ const auth = AuthorizationRole.createProvinceCompany({
userId: UserId.create('user-1', BigInt(1)), userId: UserId.create('user-1', '1'),
provinceCode: '430000', provinceCode: '430000',
provinceName: '湖南省', provinceName: '湖南省',
adminId: AdminUserId.create('admin-1', BigInt(101)), adminId: AdminUserId.create('admin-1', '101'),
}) })
auth.clearDomainEvents() auth.clearDomainEvents()
auth.revoke(AdminUserId.create('admin-2', BigInt(102)), '违规操作') auth.revoke(AdminUserId.create('admin-2', '102'), '违规操作')
expect(auth.status).toBe(AuthorizationStatus.REVOKED) expect(auth.status).toBe(AuthorizationStatus.REVOKED)
expect(auth.benefitActive).toBe(false) expect(auth.benefitActive).toBe(false)
expect(auth.revokeReason).toBe('违规操作') expect(auth.revokeReason).toBe('违规操作')
expect(auth.domainEvents.length).toBe(1) expect(auth.domainEvents.length).toBe(1)
expect(auth.domainEvents[0].eventType).toBe('authorization.role.revoked') expect(auth.domainEvents[0].eventType).toBe('authorization.role.revoked')
}) })
it('should throw error if already revoked', () => { it('should throw error if already revoked', () => {
const auth = AuthorizationRole.createProvinceCompany({ const auth = AuthorizationRole.createProvinceCompany({
userId: UserId.create('user-1', BigInt(1)), userId: UserId.create('user-1', '1'),
provinceCode: '430000', provinceCode: '430000',
provinceName: '湖南省', provinceName: '湖南省',
adminId: AdminUserId.create('admin-1', BigInt(101)), adminId: AdminUserId.create('admin-1', '101'),
}) })
auth.revoke(AdminUserId.create('admin-2', BigInt(102)), '违规操作') auth.revoke(AdminUserId.create('admin-2', '102'), '违规操作')
expect(() => auth.revoke(AdminUserId.create('admin-3', BigInt(103)), '再次撤销')).toThrow( expect(() => auth.revoke(AdminUserId.create('admin-3', '103'), '再次撤销')).toThrow(
DomainError, DomainError,
) )
}) })
}) })
describe('exemptLocalPercentageCheck', () => { describe('exemptLocalPercentageCheck', () => {
it('should exempt from percentage check', () => { it('should exempt from percentage check', () => {
const auth = AuthorizationRole.createAuthProvinceCompany({ const auth = AuthorizationRole.createAuthProvinceCompany({
userId: UserId.create('user-1', BigInt(1)), userId: UserId.create('user-1', '1'),
provinceCode: '430000', provinceCode: '430000',
provinceName: '湖南省', provinceName: '湖南省',
}) })
expect(auth.exemptFromPercentageCheck).toBe(false) expect(auth.exemptFromPercentageCheck).toBe(false)
expect(auth.needsLocalPercentageCheck()).toBe(true) expect(auth.needsLocalPercentageCheck()).toBe(true)
auth.exemptLocalPercentageCheck(AdminUserId.create('admin-1', BigInt(101))) auth.exemptLocalPercentageCheck(AdminUserId.create('admin-1', '101'))
expect(auth.exemptFromPercentageCheck).toBe(true) expect(auth.exemptFromPercentageCheck).toBe(true)
expect(auth.needsLocalPercentageCheck()).toBe(false) expect(auth.needsLocalPercentageCheck()).toBe(false)
}) })
}) })
describe('incrementMonthIndex', () => { describe('incrementMonthIndex', () => {
it('should increment month index', () => { it('should increment month index', () => {
const auth = AuthorizationRole.createCommunityAuth({ const auth = AuthorizationRole.createCommunityAuth({
userId: UserId.create('user-1', BigInt(1)), userId: UserId.create('user-1', '1'),
communityName: '量子社区', communityName: '量子社区',
}) })
auth.activateBenefit() auth.activateBenefit()
expect(auth.currentMonthIndex).toBe(1) expect(auth.currentMonthIndex).toBe(1)
auth.incrementMonthIndex() auth.incrementMonthIndex()
expect(auth.currentMonthIndex).toBe(2) expect(auth.currentMonthIndex).toBe(2)
auth.incrementMonthIndex() auth.incrementMonthIndex()
expect(auth.currentMonthIndex).toBe(3) expect(auth.currentMonthIndex).toBe(3)
}) })
}) })
}) })

View File

@ -1,86 +1,86 @@
import { AuthorizationRole } from '@/domain/aggregates' import { AuthorizationRole } from '@/domain/aggregates'
import { AuthorizationId, UserId, RegionCode } from '@/domain/value-objects' import { AuthorizationId, UserId, RegionCode } from '@/domain/value-objects'
import { RoleType, AuthorizationStatus } from '@/domain/enums' import { RoleType, AuthorizationStatus } from '@/domain/enums'
export const AUTHORIZATION_ROLE_REPOSITORY = Symbol('IAuthorizationRoleRepository') export const AUTHORIZATION_ROLE_REPOSITORY = Symbol('IAuthorizationRoleRepository')
export interface IAuthorizationRoleRepository { export interface IAuthorizationRoleRepository {
save(authorization: AuthorizationRole): Promise<void> save(authorization: AuthorizationRole): Promise<void>
findById(authorizationId: AuthorizationId): Promise<AuthorizationRole | null> findById(authorizationId: AuthorizationId): Promise<AuthorizationRole | null>
findByUserIdAndRoleType(userId: UserId, roleType: RoleType): Promise<AuthorizationRole | null> findByUserIdAndRoleType(userId: UserId, roleType: RoleType): Promise<AuthorizationRole | null>
findByAccountSequenceAndRoleType(accountSequence: bigint, roleType: RoleType): Promise<AuthorizationRole | null> findByAccountSequenceAndRoleType(accountSequence: string, roleType: RoleType): Promise<AuthorizationRole | null>
findByUserIdRoleTypeAndRegion( findByUserIdRoleTypeAndRegion(
userId: UserId, userId: UserId,
roleType: RoleType, roleType: RoleType,
regionCode: RegionCode, regionCode: RegionCode,
): Promise<AuthorizationRole | null> ): Promise<AuthorizationRole | null>
findByUserId(userId: UserId): Promise<AuthorizationRole[]> findByUserId(userId: UserId): Promise<AuthorizationRole[]>
findByAccountSequence(accountSequence: bigint): Promise<AuthorizationRole[]> findByAccountSequence(accountSequence: string): Promise<AuthorizationRole[]>
findActiveByRoleTypeAndRegion( findActiveByRoleTypeAndRegion(
roleType: RoleType, roleType: RoleType,
regionCode: RegionCode, regionCode: RegionCode,
): Promise<AuthorizationRole[]> ): Promise<AuthorizationRole[]>
findAllActive(roleType?: RoleType): Promise<AuthorizationRole[]> findAllActive(roleType?: RoleType): Promise<AuthorizationRole[]>
findPendingByUserId(userId: UserId): Promise<AuthorizationRole[]> findPendingByUserId(userId: UserId): Promise<AuthorizationRole[]>
findByStatus(status: AuthorizationStatus): Promise<AuthorizationRole[]> findByStatus(status: AuthorizationStatus): Promise<AuthorizationRole[]>
delete(authorizationId: AuthorizationId): Promise<void> delete(authorizationId: AuthorizationId): Promise<void>
/** /**
* accountSequence * accountSequence
*/ */
findActiveCommunityByAccountSequences(accountSequences: bigint[]): Promise<AuthorizationRole[]> findActiveCommunityByAccountSequences(accountSequences: string[]): Promise<AuthorizationRole[]>
/** /**
* accountSequence * accountSequence
*/ */
findActiveProvinceByAccountSequencesAndRegion( findActiveProvinceByAccountSequencesAndRegion(
accountSequences: bigint[], accountSequences: string[],
provinceCode: string, provinceCode: string,
): Promise<AuthorizationRole[]> ): Promise<AuthorizationRole[]>
/** /**
* accountSequence * accountSequence
*/ */
findActiveCityByAccountSequencesAndRegion( findActiveCityByAccountSequencesAndRegion(
accountSequences: bigint[], accountSequences: string[],
cityCode: string, cityCode: string,
): Promise<AuthorizationRole[]> ): Promise<AuthorizationRole[]>
/** /**
* accountSequence benefitActive=false * accountSequence benefitActive=false
* *
*/ */
findCommunityByAccountSequences(accountSequences: bigint[]): Promise<AuthorizationRole[]> findCommunityByAccountSequences(accountSequences: string[]): Promise<AuthorizationRole[]>
/** /**
* accountSequence benefitActive=false * accountSequence benefitActive=false
* *
* @deprecated 使 findAuthProvinceByAccountSequences * @deprecated 使 findAuthProvinceByAccountSequences
*/ */
findAuthProvinceByAccountSequencesAndRegion( findAuthProvinceByAccountSequencesAndRegion(
accountSequences: bigint[], accountSequences: string[],
provinceCode: string, provinceCode: string,
): Promise<AuthorizationRole[]> ): Promise<AuthorizationRole[]>
/** /**
* accountSequence benefitActive=false * accountSequence benefitActive=false
* *
* @deprecated 使 findAuthCityByAccountSequences * @deprecated 使 findAuthCityByAccountSequences
*/ */
findAuthCityByAccountSequencesAndRegion( findAuthCityByAccountSequencesAndRegion(
accountSequences: bigint[], accountSequences: string[],
cityCode: string, cityCode: string,
): Promise<AuthorizationRole[]> ): Promise<AuthorizationRole[]>
/** /**
* accountSequence () benefitActive=false * accountSequence () benefitActive=false
* *
*/ */
findAuthProvinceByAccountSequences(accountSequences: bigint[]): Promise<AuthorizationRole[]> findAuthProvinceByAccountSequences(accountSequences: string[]): Promise<AuthorizationRole[]>
/** /**
* accountSequence () benefitActive=false * accountSequence () benefitActive=false
* *
*/ */
findAuthCityByAccountSequences(accountSequences: bigint[]): Promise<AuthorizationRole[]> findAuthCityByAccountSequences(accountSequences: string[]): Promise<AuthorizationRole[]>
/** /**
* *
*/ */
findProvinceCompanyByRegion(provinceCode: string): Promise<AuthorizationRole | null> findProvinceCompanyByRegion(provinceCode: string): Promise<AuthorizationRole | null>
/** /**
* *
*/ */
findCityCompanyByRegion(cityCode: string): Promise<AuthorizationRole | null> findCityCompanyByRegion(cityCode: string): Promise<AuthorizationRole | null>
} }

View File

@ -6,7 +6,7 @@ import { IMonthlyAssessmentRepository, IAuthorizationRoleRepository } from '@/do
export interface TeamStatistics { export interface TeamStatistics {
userId: string userId: string
accountSequence: bigint accountSequence: string
totalTeamPlantingCount: number totalTeamPlantingCount: number
selfPlantingCount: number selfPlantingCount: number
/** 下级团队认种数(不包括自己)= totalTeamPlantingCount - selfPlantingCount */ /** 下级团队认种数(不包括自己)= totalTeamPlantingCount - selfPlantingCount */
@ -17,7 +17,7 @@ export interface TeamStatistics {
export interface ITeamStatisticsRepository { export interface ITeamStatisticsRepository {
findByUserId(userId: string): Promise<TeamStatistics | null> findByUserId(userId: string): Promise<TeamStatistics | null>
findByAccountSequence(accountSequence: bigint): Promise<TeamStatistics | null> findByAccountSequence(accountSequence: string): Promise<TeamStatistics | null>
} }
export class AssessmentCalculatorService { export class AssessmentCalculatorService {

View File

@ -3,7 +3,7 @@ import { DomainError } from '@/shared/exceptions'
export class UserId { export class UserId {
constructor( constructor(
public readonly value: string, public readonly value: string,
public readonly accountSequence: bigint, public readonly accountSequence: string,
) { ) {
if (!value) { if (!value) {
throw new DomainError('用户ID不能为空') throw new DomainError('用户ID不能为空')
@ -13,8 +13,8 @@ export class UserId {
} }
} }
static create(value: string, accountSequence: number | bigint): UserId { static create(value: string, accountSequence: string): UserId {
return new UserId(value, BigInt(accountSequence)) return new UserId(value, accountSequence)
} }
equals(other: UserId): boolean { equals(other: UserId): boolean {
@ -29,7 +29,7 @@ export class UserId {
export class AdminUserId { export class AdminUserId {
constructor( constructor(
public readonly value: string, public readonly value: string,
public readonly accountSequence: bigint, public readonly accountSequence: string,
) { ) {
if (!value) { if (!value) {
throw new DomainError('管理员ID不能为空') throw new DomainError('管理员ID不能为空')
@ -39,8 +39,8 @@ export class AdminUserId {
} }
} }
static create(value: string, accountSequence: number | bigint): AdminUserId { static create(value: string, accountSequence: string): AdminUserId {
return new AdminUserId(value, BigInt(accountSequence)) return new AdminUserId(value, accountSequence)
} }
equals(other: AdminUserId): boolean { equals(other: AdminUserId): boolean {

View File

@ -21,7 +21,7 @@ interface ReferralTeamStatsResponse {
* *
*/ */
interface ReferralChainResponse { interface ReferralChainResponse {
accountSequence: number; accountSequence: string;
userId: string | null; userId: string | null;
ancestorPath: string[]; ancestorPath: string[];
referrerId: string | null; referrerId: string | null;
@ -31,8 +31,8 @@ interface ReferralChainResponse {
* *
*/ */
interface TeamMembersResponse { interface TeamMembersResponse {
accountSequence: number; accountSequence: string;
teamMembers: number[]; teamMembers: string[];
} }
/** /**
@ -41,7 +41,7 @@ interface TeamMembersResponse {
class TeamStatisticsAdapter implements TeamStatistics { class TeamStatisticsAdapter implements TeamStatistics {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: bigint, public readonly accountSequence: string,
public readonly totalTeamPlantingCount: number, public readonly totalTeamPlantingCount: number,
public readonly selfPlantingCount: number, public readonly selfPlantingCount: number,
private readonly provinceCityDistribution: Record<string, Record<string, number>> | null, private readonly provinceCityDistribution: Record<string, Record<string, number>> | null,
@ -110,7 +110,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul
async findByUserId(userId: string): Promise<TeamStatistics | null> { async findByUserId(userId: string): Promise<TeamStatistics | null> {
if (!this.enabled) { if (!this.enabled) {
this.logger.debug('[DISABLED] Referral service integration is disabled'); this.logger.debug('[DISABLED] Referral service integration is disabled');
return this.createEmptyStats(userId, BigInt(0)); return this.createEmptyStats(userId, '0');
} }
try { try {
@ -122,7 +122,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul
if (!response.data) { if (!response.data) {
this.logger.debug(`[HTTP] No stats found for userId: ${userId}`); 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; const data = response.data;
@ -130,7 +130,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul
return new TeamStatisticsAdapter( return new TeamStatisticsAdapter(
data.userId, data.userId,
BigInt(data.accountSequence || 0), data.accountSequence || '0',
data.totalTeamPlantingCount, data.totalTeamPlantingCount,
data.selfPlantingCount || 0, data.selfPlantingCount || 0,
data.provinceCityDistribution, data.provinceCityDistribution,
@ -138,14 +138,14 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul
} catch (error) { } catch (error) {
this.logger.error(`[HTTP] Failed to get stats for userId ${userId}:`, 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 * accountSequence
*/ */
async findByAccountSequence(accountSequence: bigint): Promise<TeamStatistics | null> { async findByAccountSequence(accountSequence: string): Promise<TeamStatistics | null> {
if (!this.enabled) { if (!this.enabled) {
this.logger.debug('[DISABLED] Referral service integration is disabled'); this.logger.debug('[DISABLED] Referral service integration is disabled');
return this.createEmptyStats('', accountSequence); return this.createEmptyStats('', accountSequence);
@ -168,7 +168,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul
return new TeamStatisticsAdapter( return new TeamStatisticsAdapter(
data.userId, data.userId,
BigInt(data.accountSequence || accountSequence.toString()), data.accountSequence || accountSequence,
data.totalTeamPlantingCount, data.totalTeamPlantingCount,
data.selfPlantingCount || 0, data.selfPlantingCount || 0,
data.provinceCityDistribution, 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); return new TeamStatisticsAdapter(userId, accountSequence, 0, 0, null);
} }
@ -191,7 +191,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul
* *
* accountSequence * accountSequence
*/ */
async getReferralChain(accountSequence: bigint): Promise<number[]> { async getReferralChain(accountSequence: string): Promise<string[]> {
if (!this.enabled) { if (!this.enabled) {
this.logger.debug('[DISABLED] Referral service integration is disabled'); this.logger.debug('[DISABLED] Referral service integration is disabled');
return []; return [];
@ -208,9 +208,8 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul
return []; return [];
} }
// ancestorPath 存储的是 userId (bigint string),我们需要映射到 accountSequence // ancestorPath 存储的是 accountSequence string
// 由于 referral-service 中 userId = BigInt(accountSequence),可以直接转换 return response.data.ancestorPath;
return response.data.ancestorPath.map((id) => Number(id));
} catch (error) { } catch (error) {
this.logger.error(`[HTTP] Failed to get referral chain for accountSequence ${accountSequence}:`, error); this.logger.error(`[HTTP] Failed to get referral chain for accountSequence ${accountSequence}:`, error);
return []; return [];
@ -220,7 +219,7 @@ export class ReferralServiceClient implements ITeamStatisticsRepository, OnModul
/** /**
* accountSequence * accountSequence
*/ */
async getTeamMembers(accountSequence: bigint): Promise<number[]> { async getTeamMembers(accountSequence: string): Promise<string[]> {
if (!this.enabled) { if (!this.enabled) {
this.logger.debug('[DISABLED] Referral service integration is disabled'); this.logger.debug('[DISABLED] Referral service integration is disabled');
return []; return [];

View File

@ -76,7 +76,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
): Promise<AuthorizationRole | null> { ): Promise<AuthorizationRole | null> {
const record = await this.prisma.authorizationRole.findFirst({ const record = await this.prisma.authorizationRole.findFirst({
where: { where: {
userId: BigInt(userId.value), userId: userId.value,
roleType: roleType, roleType: roleType,
}, },
}) })
@ -90,7 +90,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
): Promise<AuthorizationRole | null> { ): Promise<AuthorizationRole | null> {
const record = await this.prisma.authorizationRole.findFirst({ const record = await this.prisma.authorizationRole.findFirst({
where: { where: {
userId: BigInt(userId.value), userId: userId.value,
roleType: roleType, roleType: roleType,
regionCode: regionCode.value, regionCode: regionCode.value,
}, },
@ -99,7 +99,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
} }
async findByAccountSequenceAndRoleType( async findByAccountSequenceAndRoleType(
accountSequence: bigint, accountSequence: string,
roleType: RoleType, roleType: RoleType,
): Promise<AuthorizationRole | null> { ): Promise<AuthorizationRole | null> {
const record = await this.prisma.authorizationRole.findFirst({ const record = await this.prisma.authorizationRole.findFirst({
@ -113,13 +113,13 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
async findByUserId(userId: UserId): Promise<AuthorizationRole[]> { async findByUserId(userId: UserId): Promise<AuthorizationRole[]> {
const records = await this.prisma.authorizationRole.findMany({ const records = await this.prisma.authorizationRole.findMany({
where: { userId: BigInt(userId.value) }, where: { userId: userId.value },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
}) })
return records.map((record) => this.toDomain(record)) return records.map((record) => this.toDomain(record))
} }
async findByAccountSequence(accountSequence: bigint): Promise<AuthorizationRole[]> { async findByAccountSequence(accountSequence: string): Promise<AuthorizationRole[]> {
const records = await this.prisma.authorizationRole.findMany({ const records = await this.prisma.authorizationRole.findMany({
where: { accountSequence: accountSequence }, where: { accountSequence: accountSequence },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' },
@ -156,7 +156,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
async findPendingByUserId(userId: UserId): Promise<AuthorizationRole[]> { async findPendingByUserId(userId: UserId): Promise<AuthorizationRole[]> {
const records = await this.prisma.authorizationRole.findMany({ const records = await this.prisma.authorizationRole.findMany({
where: { where: {
userId: BigInt(userId.value), userId: userId.value,
status: AuthorizationStatus.PENDING, status: AuthorizationStatus.PENDING,
}, },
}) })
@ -177,7 +177,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
} }
async findActiveCommunityByAccountSequences( async findActiveCommunityByAccountSequences(
accountSequences: bigint[], accountSequences: string[],
): Promise<AuthorizationRole[]> { ): Promise<AuthorizationRole[]> {
if (accountSequences.length === 0) { if (accountSequences.length === 0) {
return [] return []
@ -197,7 +197,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
} }
async findActiveProvinceByAccountSequencesAndRegion( async findActiveProvinceByAccountSequencesAndRegion(
accountSequences: bigint[], accountSequences: string[],
provinceCode: string, provinceCode: string,
): Promise<AuthorizationRole[]> { ): Promise<AuthorizationRole[]> {
if (accountSequences.length === 0) { if (accountSequences.length === 0) {
@ -218,7 +218,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
} }
async findActiveCityByAccountSequencesAndRegion( async findActiveCityByAccountSequencesAndRegion(
accountSequences: bigint[], accountSequences: string[],
cityCode: string, cityCode: string,
): Promise<AuthorizationRole[]> { ): Promise<AuthorizationRole[]> {
if (accountSequences.length === 0) { if (accountSequences.length === 0) {
@ -239,7 +239,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
} }
async findCommunityByAccountSequences( async findCommunityByAccountSequences(
accountSequences: bigint[], accountSequences: string[],
): Promise<AuthorizationRole[]> { ): Promise<AuthorizationRole[]> {
if (accountSequences.length === 0) { if (accountSequences.length === 0) {
return [] return []
@ -258,7 +258,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
} }
async findAuthProvinceByAccountSequencesAndRegion( async findAuthProvinceByAccountSequencesAndRegion(
accountSequences: bigint[], accountSequences: string[],
provinceCode: string, provinceCode: string,
): Promise<AuthorizationRole[]> { ): Promise<AuthorizationRole[]> {
if (accountSequences.length === 0) { if (accountSequences.length === 0) {
@ -279,7 +279,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
} }
async findAuthCityByAccountSequencesAndRegion( async findAuthCityByAccountSequencesAndRegion(
accountSequences: bigint[], accountSequences: string[],
cityCode: string, cityCode: string,
): Promise<AuthorizationRole[]> { ): Promise<AuthorizationRole[]> {
if (accountSequences.length === 0) { if (accountSequences.length === 0) {
@ -300,7 +300,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
} }
async findAuthProvinceByAccountSequences( async findAuthProvinceByAccountSequences(
accountSequences: bigint[], accountSequences: string[],
): Promise<AuthorizationRole[]> { ): Promise<AuthorizationRole[]> {
if (accountSequences.length === 0) { if (accountSequences.length === 0) {
return [] return []
@ -320,7 +320,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
} }
async findAuthCityByAccountSequences( async findAuthCityByAccountSequences(
accountSequences: bigint[], accountSequences: string[],
): Promise<AuthorizationRole[]> { ): Promise<AuthorizationRole[]> {
if (accountSequences.length === 0) { if (accountSequences.length === 0) {
return [] return []
@ -364,16 +364,16 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
private toDomain(record: any): AuthorizationRole { private toDomain(record: any): AuthorizationRole {
const props: AuthorizationRoleProps = { const props: AuthorizationRoleProps = {
authorizationId: AuthorizationId.create(record.id), 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, roleType: record.roleType as RoleType,
regionCode: RegionCode.create(record.regionCode), regionCode: RegionCode.create(record.regionCode),
regionName: record.regionName, regionName: record.regionName,
status: record.status as AuthorizationStatus, status: record.status as AuthorizationStatus,
displayTitle: record.displayTitle, displayTitle: record.displayTitle,
authorizedAt: record.authorizedAt, 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, 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, revokeReason: record.revokeReason,
assessmentConfig: new AssessmentConfig( assessmentConfig: new AssessmentConfig(
record.initialTargetTreeCount, record.initialTargetTreeCount,

View File

@ -148,7 +148,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi
async findByUserAndMonth(userId: UserId, month: Month): Promise<MonthlyAssessment[]> { async findByUserAndMonth(userId: UserId, month: Month): Promise<MonthlyAssessment[]> {
const records = await this.prisma.monthlyAssessment.findMany({ const records = await this.prisma.monthlyAssessment.findMany({
where: { where: {
userId: BigInt(userId.value), userId: userId.value,
assessmentMonth: month.value, assessmentMonth: month.value,
}, },
}) })
@ -214,7 +214,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi
const props: MonthlyAssessmentProps = { const props: MonthlyAssessmentProps = {
assessmentId: AssessmentId.create(record.id), assessmentId: AssessmentId.create(record.id),
authorizationId: AuthorizationId.create(record.authorizationId), 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, roleType: record.roleType as RoleType,
regionCode: RegionCode.create(record.regionCode), regionCode: RegionCode.create(record.regionCode),
assessmentMonth: Month.create(record.assessmentMonth), assessmentMonth: Month.create(record.assessmentMonth),
@ -233,7 +233,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi
rankingInRegion: record.rankingInRegion, rankingInRegion: record.rankingInRegion,
isFirstPlace: record.isFirstPlace, isFirstPlace: record.isFirstPlace,
isBypassed: record.isBypassed, 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, bypassedAt: record.bypassedAt,
assessedAt: record.assessedAt, assessedAt: record.assessedAt,
createdAt: record.createdAt, createdAt: record.createdAt,

View File

@ -1,15 +1,15 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common' import { createParamDecorator, ExecutionContext } from '@nestjs/common'
export interface CurrentUserData { export interface CurrentUserData {
userId: string userId: string
accountSequence?: number accountSequence?: string
walletAddress?: string walletAddress?: string
roles?: string[] roles?: string[]
} }
export const CurrentUser = createParamDecorator( export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext): CurrentUserData => { (data: unknown, ctx: ExecutionContext): CurrentUserData => {
const request = ctx.switchToHttp().getRequest() const request = ctx.switchToHttp().getRequest()
return request.user return request.user
}, },
) )

View File

@ -1,2 +1,2 @@
export * from './current-user.decorator' export * from './current-user.decorator'
export * from './public.decorator' export * from './public.decorator'

View File

@ -1,4 +1,4 @@
import { SetMetadata } from '@nestjs/common' import { SetMetadata } from '@nestjs/common'
export const IS_PUBLIC_KEY = 'isPublic' export const IS_PUBLIC_KEY = 'isPublic'
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true) export const Public = () => SetMetadata(IS_PUBLIC_KEY, true)

View File

@ -1,31 +1,31 @@
import { HttpException, HttpStatus } from '@nestjs/common' import { HttpException, HttpStatus } from '@nestjs/common'
export class ApplicationException extends HttpException { export class ApplicationException extends HttpException {
constructor(message: string, status: HttpStatus = HttpStatus.BAD_REQUEST) { constructor(message: string, status: HttpStatus = HttpStatus.BAD_REQUEST) {
super(message, status) super(message, status)
} }
} }
export class ApplicationError extends ApplicationException { export class ApplicationError extends ApplicationException {
constructor(message: string) { constructor(message: string) {
super(message, HttpStatus.BAD_REQUEST) super(message, HttpStatus.BAD_REQUEST)
} }
} }
export class NotFoundError extends ApplicationException { export class NotFoundError extends ApplicationException {
constructor(message: string) { constructor(message: string) {
super(message, HttpStatus.NOT_FOUND) super(message, HttpStatus.NOT_FOUND)
} }
} }
export class UnauthorizedError extends ApplicationException { export class UnauthorizedError extends ApplicationException {
constructor(message: string) { constructor(message: string) {
super(message, HttpStatus.UNAUTHORIZED) super(message, HttpStatus.UNAUTHORIZED)
} }
} }
export class ForbiddenError extends ApplicationException { export class ForbiddenError extends ApplicationException {
constructor(message: string) { constructor(message: string) {
super(message, HttpStatus.FORBIDDEN) super(message, HttpStatus.FORBIDDEN)
} }
} }

View File

@ -1,13 +1,13 @@
export class DomainException extends Error { export class DomainException extends Error {
constructor(message: string) { constructor(message: string) {
super(message) super(message)
this.name = 'DomainException' this.name = 'DomainException'
} }
} }
export class DomainError extends DomainException { export class DomainError extends DomainException {
constructor(message: string) { constructor(message: string) {
super(message) super(message)
this.name = 'DomainError' this.name = 'DomainError'
} }
} }

View File

@ -1,2 +1,2 @@
export * from './domain.exception' export * from './domain.exception'
export * from './application.exception' export * from './application.exception'

View File

@ -1,63 +1,63 @@
import { import {
ExceptionFilter, ExceptionFilter,
Catch, Catch,
ArgumentsHost, ArgumentsHost,
HttpException, HttpException,
HttpStatus, HttpStatus,
Logger, Logger,
} from '@nestjs/common' } from '@nestjs/common'
import { Request, Response } from 'express' import { Request, Response } from 'express'
import { DomainException } from '@/shared/exceptions' import { DomainException } from '@/shared/exceptions'
@Catch() @Catch()
export class GlobalExceptionFilter implements ExceptionFilter { export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name) private readonly logger = new Logger(GlobalExceptionFilter.name)
catch(exception: unknown, host: ArgumentsHost) { catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp() const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>() const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>() const request = ctx.getRequest<Request>()
let status: number let status: number
let message: string let message: string
let error: string let error: string
if (exception instanceof HttpException) { if (exception instanceof HttpException) {
status = exception.getStatus() status = exception.getStatus()
const exceptionResponse = exception.getResponse() const exceptionResponse = exception.getResponse()
message = message =
typeof exceptionResponse === 'string' typeof exceptionResponse === 'string'
? exceptionResponse ? exceptionResponse
: (exceptionResponse as any).message || exception.message : (exceptionResponse as any).message || exception.message
error = exception.name error = exception.name
} else if (exception instanceof DomainException) { } else if (exception instanceof DomainException) {
status = HttpStatus.BAD_REQUEST status = HttpStatus.BAD_REQUEST
message = exception.message message = exception.message
error = 'DomainError' error = 'DomainError'
} else if (exception instanceof Error) { } else if (exception instanceof Error) {
status = HttpStatus.INTERNAL_SERVER_ERROR status = HttpStatus.INTERNAL_SERVER_ERROR
message = 'Internal server error' message = 'Internal server error'
error = exception.name error = exception.name
this.logger.error( this.logger.error(
`Unhandled exception: ${exception.message}`, `Unhandled exception: ${exception.message}`,
exception.stack, exception.stack,
) )
} else { } else {
status = HttpStatus.INTERNAL_SERVER_ERROR status = HttpStatus.INTERNAL_SERVER_ERROR
message = 'Internal server error' message = 'Internal server error'
error = 'UnknownError' error = 'UnknownError'
} }
const responseBody = { const responseBody = {
statusCode: status, statusCode: status,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
path: request.url, path: request.url,
method: request.method, method: request.method,
error, error,
message: Array.isArray(message) ? message : [message], message: Array.isArray(message) ? message : [message],
} }
response.status(status).json(responseBody) response.status(status).json(responseBody)
} }
} }

View File

@ -1 +1 @@
export * from './global-exception.filter' export * from './global-exception.filter'

View File

@ -1 +1 @@
export * from './jwt-auth.guard' export * from './jwt-auth.guard'

View File

@ -1,31 +1,31 @@
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common' import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport' import { AuthGuard } from '@nestjs/passport'
import { Reflector } from '@nestjs/core' import { Reflector } from '@nestjs/core'
import { IS_PUBLIC_KEY } from '@/shared/decorators' import { IS_PUBLIC_KEY } from '@/shared/decorators'
@Injectable() @Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') { export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) { constructor(private reflector: Reflector) {
super() super()
} }
canActivate(context: ExecutionContext) { canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [ const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(), context.getHandler(),
context.getClass(), context.getClass(),
]) ])
if (isPublic) { if (isPublic) {
return true return true
} }
return super.canActivate(context) return super.canActivate(context)
} }
handleRequest(err: any, user: any, info: any) { handleRequest(err: any, user: any, info: any) {
if (err || !user) { if (err || !user) {
throw err || new UnauthorizedException('未授权访问') throw err || new UnauthorizedException('未授权访问')
} }
return user return user
} }
} }

View File

@ -1 +1 @@
export * from './transform.interceptor' export * from './transform.interceptor'

View File

@ -1,27 +1,27 @@
import { import {
Injectable, Injectable,
NestInterceptor, NestInterceptor,
ExecutionContext, ExecutionContext,
CallHandler, CallHandler,
} from '@nestjs/common' } from '@nestjs/common'
import { Observable } from 'rxjs' import { Observable } from 'rxjs'
import { map } from 'rxjs/operators' import { map } from 'rxjs/operators'
export interface ApiResponse<T> { export interface ApiResponse<T> {
success: boolean success: boolean
data: T data: T
timestamp: string timestamp: string
} }
@Injectable() @Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> { export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> { intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
return next.handle().pipe( return next.handle().pipe(
map((data) => ({ map((data) => ({
success: true, success: true,
data, data,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
})), })),
) )
} }
} }

View File

@ -1 +1 @@
export * from './jwt.strategy' export * from './jwt.strategy'

View File

@ -1,41 +1,41 @@
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport' import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt' import { ExtractJwt, Strategy } from 'passport-jwt'
import { ConfigService } from '@nestjs/config' import { ConfigService } from '@nestjs/config'
export interface JwtPayload { export interface JwtPayload {
// Identity-service uses 'userId' field // Identity-service uses 'userId' field
userId: string userId: string
accountSequence?: number accountSequence?: string
deviceId?: string deviceId?: string
type?: string type?: string
// Legacy support for 'sub' field // Legacy support for 'sub' field
sub?: string sub?: string
walletAddress?: string walletAddress?: string
roles?: string[] roles?: string[]
iat?: number iat?: number
exp?: number exp?: number
} }
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) { export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) { constructor(private configService: ConfigService) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'), secretOrKey: configService.get<string>('JWT_SECRET'),
}) })
} }
async validate(payload: JwtPayload) { async validate(payload: JwtPayload) {
// Support both 'userId' (from identity-service) and 'sub' (legacy) // Support both 'userId' (from identity-service) and 'sub' (legacy)
const userId = payload.userId || payload.sub const userId = payload.userId || payload.sub
return { return {
userId, userId,
accountSequence: payload.accountSequence, accountSequence: payload.accountSequence,
deviceId: payload.deviceId, deviceId: payload.deviceId,
walletAddress: payload.walletAddress, walletAddress: payload.walletAddress,
roles: payload.roles, roles: payload.roles,
} }
} }
} }

View File

@ -14,7 +14,7 @@ model BackupShare {
// 用户标识 (来自 identity-service) // 用户标识 (来自 identity-service)
userId BigInt @unique @map("user_id") userId BigInt @unique @map("user_id")
accountSequence BigInt @unique @map("account_sequence") accountSequence String @unique @map("account_sequence") // 格式: D + YYMMDD + 5位序号
// MPC 密钥信息 // MPC 密钥信息
publicKey String @unique @map("public_key") @db.VarChar(130) publicKey String @unique @map("public_key") @db.VarChar(130)

View File

@ -14,9 +14,8 @@ export class StoreShareDto {
userId: string; userId: string;
@IsNotEmpty() @IsNotEmpty()
@IsNumber() @IsString()
@Min(1) accountSequence: string;
accountSequence: number;
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()

View File

@ -1,7 +1,7 @@
export class StoreBackupShareCommand { export class StoreBackupShareCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number, public readonly accountSequence: string,
public readonly publicKey: string, public readonly publicKey: string,
public readonly encryptedShareData: string, public readonly encryptedShareData: string,
public readonly sourceService: string, public readonly sourceService: string,

View File

@ -53,7 +53,7 @@ export class StoreBackupShareHandler {
// Create domain entity // Create domain entity
const share = BackupShare.create({ const share = BackupShare.create({
userId, userId,
accountSequence: BigInt(command.accountSequence), accountSequence: command.accountSequence,
publicKey: command.publicKey, publicKey: command.publicKey,
encryptedShareData: encrypted, encryptedShareData: encrypted,
encryptionKeyId: keyId, encryptionKeyId: keyId,

View File

@ -9,7 +9,7 @@ export enum BackupShareStatus {
export interface BackupShareProps { export interface BackupShareProps {
shareId: bigint | null; shareId: bigint | null;
userId: bigint; userId: bigint;
accountSequence: bigint; accountSequence: string;
publicKey: string; publicKey: string;
partyIndex: number; partyIndex: number;
threshold: number; threshold: number;
@ -27,7 +27,7 @@ export interface BackupShareProps {
export class BackupShare { export class BackupShare {
private _shareId: bigint | null; private _shareId: bigint | null;
private readonly _userId: bigint; private readonly _userId: bigint;
private readonly _accountSequence: bigint; private readonly _accountSequence: string;
private readonly _publicKey: string; private readonly _publicKey: string;
private readonly _partyIndex: number; private readonly _partyIndex: number;
private readonly _threshold: number; private readonly _threshold: number;
@ -61,7 +61,7 @@ export class BackupShare {
static create(params: { static create(params: {
userId: bigint; userId: bigint;
accountSequence: bigint; accountSequence: string;
publicKey: string; publicKey: string;
encryptedShareData: string; encryptedShareData: string;
encryptionKeyId: string; encryptionKeyId: string;
@ -131,7 +131,7 @@ export class BackupShare {
get userId(): bigint { get userId(): bigint {
return this._userId; return this._userId;
} }
get accountSequence(): bigint { get accountSequence(): string {
return this._accountSequence; return this._accountSequence;
} }
get publicKey(): string { get publicKey(): string {

View File

@ -11,6 +11,6 @@ export interface BackupShareRepository {
userId: bigint, userId: bigint,
publicKey: string, publicKey: string,
): Promise<BackupShare | null>; ): Promise<BackupShare | null>;
findByAccountSequence(accountSequence: bigint): Promise<BackupShare | null>; findByAccountSequence(accountSequence: string): Promise<BackupShare | null>;
delete(shareId: bigint): Promise<void>; delete(shareId: bigint): Promise<void>;
} }

View File

@ -86,7 +86,7 @@ export class BackupShareRepositoryImpl implements BackupShareRepository {
} }
async findByAccountSequence( async findByAccountSequence(
accountSequence: bigint, accountSequence: string,
): Promise<BackupShare | null> { ): Promise<BackupShare | null> {
const record = await this.prisma.backupShare.findUnique({ const record = await this.prisma.backupShare.findUnique({
where: { accountSequence }, where: { accountSequence },

View File

@ -15,7 +15,7 @@ datasource db {
// 存储需要监听充值的地址(用户地址和系统账户地址) // 存储需要监听充值的地址(用户地址和系统账户地址)
// ============================================ // ============================================
model MonitoredAddress { 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 chainType String @map("chain_type") @db.VarChar(20) // KAVA, BSC
address String @db.VarChar(42) // 0x地址 address String @db.VarChar(42) // 0x地址
@ -24,7 +24,7 @@ model MonitoredAddress {
addressType String @default("USER") @map("address_type") @db.VarChar(20) addressType String @default("USER") @map("address_type") @db.VarChar(20)
// 用户地址关联 (addressType = USER 时使用) // 用户地址关联 (addressType = USER 时使用)
accountSequence BigInt? @map("account_sequence") // 跨服务关联标识 accountSequence String? @map("account_sequence") @db.VarChar(20) // 跨服务关联标识 (格式: D + YYMMDD + 5位序号)
userId BigInt? @map("user_id") // 保留兼容 userId BigInt? @map("user_id") // 保留兼容
// 系统账户关联 (addressType = SYSTEM 时使用) // 系统账户关联 (addressType = SYSTEM 时使用)
@ -74,11 +74,11 @@ model DepositTransaction {
status String @default("DETECTED") @db.VarChar(20) // DETECTED, CONFIRMING, CONFIRMED, NOTIFIED status String @default("DETECTED") @db.VarChar(20) // DETECTED, CONFIRMING, CONFIRMED, NOTIFIED
// 关联 - 使用 accountSequence 作为跨服务主键 // 关联 - 使用 accountSequence 作为跨服务主键
addressId BigInt @map("address_id") addressId BigInt @map("address_id")
addressType String @default("USER") @map("address_type") @db.VarChar(20) // USER 或 SYSTEM 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") // 保留兼容 userId BigInt? @map("user_id") // 保留兼容
// 系统账户关联(当 addressType = SYSTEM 时) // 系统账户关联(当 addressType = SYSTEM 时)
@ -174,26 +174,26 @@ model TransactionRequest {
// 与账户序列号关联,用于账户恢复验证 // 与账户序列号关联,用于账户恢复验证
// ============================================ // ============================================
model RecoveryMnemonic { model RecoveryMnemonic {
id BigInt @id @default(autoincrement()) id BigInt @id @default(autoincrement())
accountSequence Int @map("account_sequence") // 8位账户序列号 accountSequence String @map("account_sequence") @db.VarChar(20) // 账户序列号 (格式: D + YYMMDD + 5位序号)
publicKey String @map("public_key") @db.VarChar(130) // 关联的钱包公钥 publicKey String @map("public_key") @db.VarChar(130) // 关联的钱包公钥
// 助记词存储 (加密) // 助记词存储 (加密)
encryptedMnemonic String @map("encrypted_mnemonic") @db.Text // AES加密的助记词 encryptedMnemonic String @map("encrypted_mnemonic") @db.Text // AES加密的助记词
mnemonicHash String @map("mnemonic_hash") @db.VarChar(64) // SHA256哈希用于验证 mnemonicHash String @map("mnemonic_hash") @db.VarChar(64) // SHA256哈希用于验证
// 状态管理 // 状态管理
status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, REPLACED status String @default("ACTIVE") @db.VarChar(20) // ACTIVE, REVOKED, REPLACED
isBackedUp Boolean @default(false) @map("is_backed_up") // 用户是否已备份 isBackedUp Boolean @default(false) @map("is_backed_up") // 用户是否已备份
// 挂失/更换相关 // 挂失/更换相关
revokedAt DateTime? @map("revoked_at") revokedAt DateTime? @map("revoked_at")
revokedReason String? @map("revoked_reason") @db.VarChar(200) revokedReason String? @map("revoked_reason") @db.VarChar(200)
replacedById BigInt? @map("replaced_by_id") // 被哪个新助记词替代 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([accountSequence], name: "idx_recovery_account")
@@index([publicKey], name: "idx_recovery_public_key") @@index([publicKey], name: "idx_recovery_public_key")
@@index([status], name: "idx_recovery_status") @@index([status], name: "idx_recovery_status")
@ -217,15 +217,15 @@ model OutboxEvent {
status String @default("PENDING") @db.VarChar(20) status String @default("PENDING") @db.VarChar(20)
// 重试信息 // 重试信息
retryCount Int @default(0) @map("retry_count") retryCount Int @default(0) @map("retry_count")
maxRetries Int @default(10) @map("max_retries") maxRetries Int @default(10) @map("max_retries")
lastError String? @map("last_error") @db.Text lastError String? @map("last_error") @db.Text
nextRetryAt DateTime? @map("next_retry_at") nextRetryAt DateTime? @map("next_retry_at")
// 时间戳 // 时间戳
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
sentAt DateTime? @map("sent_at") sentAt DateTime? @map("sent_at")
ackedAt DateTime? @map("acked_at") ackedAt DateTime? @map("acked_at")
@@index([status, nextRetryAt], name: "idx_outbox_pending") @@index([status, nextRetryAt], name: "idx_outbox_pending")
@@index([aggregateType, aggregateId], name: "idx_outbox_aggregate") @@index([aggregateType, aggregateId], name: "idx_outbox_aggregate")

View File

@ -1,4 +1,4 @@
import { IsString, IsNumberString, IsInt } from 'class-validator'; import { IsString, IsNumberString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class DeriveAddressDto { export class DeriveAddressDto {
@ -6,9 +6,9 @@ export class DeriveAddressDto {
@IsNumberString() @IsNumberString()
userId: string; userId: string;
@ApiProperty({ description: '账户序列号 (8位数字)', example: 10000001 }) @ApiProperty({ description: '账户序列号 (格式: D + YYMMDD + 5位序号)', example: 'D2512110008' })
@IsInt() @IsString()
accountSequence: number; accountSequence: string;
@ApiProperty({ @ApiProperty({
description: '压缩公钥 (33 bytes, 0x02/0x03 开头)', description: '压缩公钥 (33 bytes, 0x02/0x03 开头)',

View File

@ -1,8 +1,8 @@
import { IsInt } from 'class-validator'; import { IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class MarkMnemonicBackupDto { export class MarkMnemonicBackupDto {
@ApiProperty({ description: '账户序列号 (8位数字)', example: 10000001 }) @ApiProperty({ description: '账户序列号 (格式: D + YYMMDD + 5位序号)', example: 'D2512110008' })
@IsInt() @IsString()
accountSequence: number; accountSequence: string;
} }

View File

@ -1,13 +1,13 @@
import { IsString, IsInt } from 'class-validator'; import { IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
export class VerifyMnemonicHashDto { export class VerifyMnemonicHashDto {
@ApiProperty({ @ApiProperty({
description: '账户序列号 (8位数字)', description: '账户序列号 (格式: D + YYMMDD + 5位序号)',
example: 10000001, example: 'D2512110008',
}) })
@IsInt() @IsString()
accountSequence: number; accountSequence: string;
@ApiProperty({ @ApiProperty({
description: '助记词 (12个单词空格分隔)', description: '助记词 (12个单词空格分隔)',

View File

@ -58,7 +58,7 @@ export class MpcKeygenCompletedHandler implements OnModuleInit {
const result = await this.addressDerivationService.deriveAndRegister({ const result = await this.addressDerivationService.deriveAndRegister({
userId: BigInt(userId), userId: BigInt(userId),
accountSequence: Number(accountSequence), accountSequence: accountSequence,
publicKey, publicKey,
}); });

View File

@ -18,13 +18,13 @@ import { ChainTypeEnum } from '@/domain/enums';
export interface DeriveAddressParams { export interface DeriveAddressParams {
userId: bigint; userId: bigint;
accountSequence: number; accountSequence: string;
publicKey: string; publicKey: string;
} }
export interface DeriveAddressResult { export interface DeriveAddressResult {
userId: bigint; userId: bigint;
accountSequence: number; accountSequence: string;
publicKey: string; publicKey: string;
addresses: DerivedAddress[]; addresses: DerivedAddress[];
} }
@ -146,7 +146,7 @@ export class AddressDerivationService {
*/ */
private async registerEvmAddressForMonitoring( private async registerEvmAddressForMonitoring(
userId: bigint, userId: bigint,
accountSequence: number, accountSequence: string,
derived: DerivedAddress, derived: DerivedAddress,
): Promise<void> { ): Promise<void> {
const chainType = ChainType.fromEnum(derived.chainType); const chainType = ChainType.fromEnum(derived.chainType);
@ -159,7 +159,7 @@ export class AddressDerivationService {
const monitored = MonitoredAddress.create({ const monitored = MonitoredAddress.create({
chainType, chainType,
address, address,
accountSequence: BigInt(accountSequence), accountSequence,
userId, userId,
}); });

View File

@ -60,7 +60,7 @@ export class DepositRepairService {
id: d.id?.toString() ?? '', id: d.id?.toString() ?? '',
txHash: d.txHash.toString(), txHash: d.txHash.toString(),
userId: d.userId.toString(), userId: d.userId.toString(),
accountSequence: d.accountSequence.toString(), accountSequence: d.accountSequence,
amount: d.amount.toFixed(6), amount: d.amount.toFixed(6),
confirmedAt: d.createdAt?.toISOString() ?? '', confirmedAt: d.createdAt?.toISOString() ?? '',
})), })),
@ -99,7 +99,7 @@ export class DepositRepairService {
amount: deposit.amount.raw.toString(), amount: deposit.amount.raw.toString(),
amountFormatted: deposit.amount.toFixed(8), amountFormatted: deposit.amount.toFixed(8),
confirmations: deposit.confirmations, confirmations: deposit.confirmations,
accountSequence: deposit.accountSequence.toString(), accountSequence: deposit.accountSequence,
userId: deposit.userId.toString(), userId: deposit.userId.toString(),
}); });

View File

@ -3,7 +3,7 @@ import { PrismaService } from '@/infrastructure/persistence/prisma/prisma.servic
import { RecoveryMnemonicAdapter } from '@/infrastructure/blockchain/recovery-mnemonic.adapter'; import { RecoveryMnemonicAdapter } from '@/infrastructure/blockchain/recovery-mnemonic.adapter';
export interface VerifyMnemonicByAccountParams { export interface VerifyMnemonicByAccountParams {
accountSequence: number; accountSequence: string;
mnemonic: string; mnemonic: string;
} }
@ -64,7 +64,7 @@ export class MnemonicVerificationService {
* *
*/ */
async saveRecoveryMnemonic(params: { async saveRecoveryMnemonic(params: {
accountSequence: number; accountSequence: string;
publicKey: string; publicKey: string;
encryptedMnemonic: string; encryptedMnemonic: string;
mnemonicHash: string; mnemonicHash: string;
@ -88,7 +88,7 @@ export class MnemonicVerificationService {
/** /**
* *
*/ */
async markAsBackedUp(accountSequence: number): Promise<void> { async markAsBackedUp(accountSequence: string): Promise<void> {
await this.prisma.recoveryMnemonic.updateMany({ await this.prisma.recoveryMnemonic.updateMany({
where: { where: {
accountSequence, accountSequence,

View File

@ -17,7 +17,7 @@ export interface DepositTransactionProps {
confirmations: number; confirmations: number;
status: DepositStatus; status: DepositStatus;
addressId: bigint; addressId: bigint;
accountSequence: bigint; // 跨服务关联标识 accountSequence: string; // 跨服务关联标识 (格式: D + YYMMDD + 5位序号)
userId: bigint; // 保留兼容 userId: bigint; // 保留兼容
notifiedAt?: Date; notifiedAt?: Date;
notifyAttempts: number; notifyAttempts: number;
@ -74,7 +74,7 @@ export class DepositTransaction extends AggregateRoot<bigint> {
get addressId(): bigint { get addressId(): bigint {
return this.props.addressId; return this.props.addressId;
} }
get accountSequence(): bigint { get accountSequence(): string {
return this.props.accountSequence; return this.props.accountSequence;
} }
get userId(): bigint { get userId(): bigint {
@ -117,7 +117,7 @@ export class DepositTransaction extends AggregateRoot<bigint> {
blockTimestamp: Date; blockTimestamp: Date;
logIndex: number; logIndex: number;
addressId: bigint; addressId: bigint;
accountSequence: bigint; accountSequence: string;
userId: bigint; userId: bigint;
}): DepositTransaction { }): DepositTransaction {
const deposit = new DepositTransaction({ const deposit = new DepositTransaction({
@ -139,7 +139,7 @@ export class DepositTransaction extends AggregateRoot<bigint> {
amountFormatted: params.amount.toFixed(8), amountFormatted: params.amount.toFixed(8),
blockNumber: params.blockNumber.toString(), blockNumber: params.blockNumber.toString(),
blockTimestamp: params.blockTimestamp.toISOString(), blockTimestamp: params.blockTimestamp.toISOString(),
accountSequence: params.accountSequence.toString(), accountSequence: params.accountSequence,
userId: params.userId.toString(), userId: params.userId.toString(),
}), }),
); );
@ -188,7 +188,7 @@ export class DepositTransaction extends AggregateRoot<bigint> {
amount: this.props.amount.raw.toString(), amount: this.props.amount.raw.toString(),
amountFormatted: this.props.amount.toFixed(8), amountFormatted: this.props.amount.toFixed(8),
confirmations: this.props.confirmations, confirmations: this.props.confirmations,
accountSequence: this.props.accountSequence.toString(), accountSequence: this.props.accountSequence,
userId: this.props.userId.toString(), userId: this.props.userId.toString(),
}), }),
); );

View File

@ -5,7 +5,7 @@ export interface MonitoredAddressProps {
id?: bigint; id?: bigint;
chainType: ChainType; chainType: ChainType;
address: EvmAddress; address: EvmAddress;
accountSequence: bigint; // 跨服务关联标识 (全局唯一业务ID) accountSequence: string; // 跨服务关联标识 (格式: D + YYMMDD + 5位序号)
userId: bigint; // 保留兼容 userId: bigint; // 保留兼容
isActive: boolean; isActive: boolean;
createdAt?: Date; createdAt?: Date;
@ -30,7 +30,7 @@ export class MonitoredAddress extends AggregateRoot<bigint> {
get address(): EvmAddress { get address(): EvmAddress {
return this.props.address; return this.props.address;
} }
get accountSequence(): bigint { get accountSequence(): string {
return this.props.accountSequence; return this.props.accountSequence;
} }
get userId(): bigint { get userId(): bigint {
@ -52,7 +52,7 @@ export class MonitoredAddress extends AggregateRoot<bigint> {
static create(params: { static create(params: {
chainType: ChainType; chainType: ChainType;
address: EvmAddress; address: EvmAddress;
accountSequence: bigint; accountSequence: string;
userId: bigint; userId: bigint;
}): MonitoredAddress { }): MonitoredAddress {
return new MonitoredAddress({ return new MonitoredAddress({

View File

@ -2,7 +2,7 @@ import { DomainEvent } from './domain-event.base';
export interface WalletAddressCreatedPayload { export interface WalletAddressCreatedPayload {
userId: string; userId: string;
accountSequence: number; // 8位账户序列号 accountSequence: string; // 账户序列号 (格式: D + YYMMDD + 5位序号)
publicKey: string; publicKey: string;
addresses: { addresses: {
chainType: string; chainType: string;

View File

@ -23,7 +23,7 @@ export interface KeygenCompletedPayload {
threshold: string; threshold: string;
extraPayload?: { extraPayload?: {
userId: string; userId: string;
accountSequence: number; // 8位账户序列号用于关联恢复助记词 accountSequence: string; // 账户序列号 (格式: D + YYMMDD + 5位序号)
username: string; username: string;
delegateShare?: { delegateShare?: {
partyId: string; partyId: string;

View File

@ -5,7 +5,7 @@ import { ConfigService } from '@nestjs/config';
interface JwtPayload { interface JwtPayload {
userId: string; userId: string;
accountSequence: number; accountSequence: string;
deviceId: string; deviceId: string;
type: 'access' | 'refresh'; type: 'access' | 'refresh';
iat: number; iat: number;

View File

@ -9,13 +9,13 @@ datasource db {
model UserAccount { model UserAccount {
userId BigInt @id @default(autoincrement()) @map("user_id") 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) phoneNumber String? @unique @map("phone_number") @db.VarChar(20)
nickname String @db.VarChar(100) nickname String @db.VarChar(100)
avatarUrl String? @map("avatar_url") @db.Text 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) referralCode String @unique @map("referral_code") @db.VarChar(10)
kycStatus String @default("NOT_VERIFIED") @map("kyc_status") @db.VarChar(20) kycStatus String @default("NOT_VERIFIED") @map("kyc_status") @db.VarChar(20)
@ -102,10 +102,11 @@ model WalletAddress {
} }
model AccountSequenceGenerator { model AccountSequenceGenerator {
id Int @id @default(1) id Int @id @default(autoincrement())
currentSequence BigInt @default(0) @map("current_sequence") 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") updatedAt DateTime @updatedAt @map("updated_at")
@@map("account_sequence_generator") @@map("account_sequence_generator")
} }

View File

@ -1,91 +1,91 @@
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
// ============================================ // ============================================
// 系统账户定义 // 系统账户定义
// ============================================ // 系统账户使用特殊序列号格式: S + 00000 + 序号 (S0000000001 ~ S0000000004)
const SYSTEM_ACCOUNTS = [ // ============================================
{ const SYSTEM_ACCOUNTS = [
userId: BigInt(1), {
accountSequence: BigInt(1), userId: BigInt(1),
nickname: '总部社区', accountSequence: 'S0000000001', // 总部社区
referralCode: 'HQ000001', nickname: '总部社区',
provinceCode: '000000', referralCode: 'HQ000001',
cityCode: '000000', status: 'SYSTEM',
status: 'SYSTEM', },
}, {
{ userId: BigInt(2),
userId: BigInt(2), accountSequence: 'S0000000002', // 成本费账户
accountSequence: BigInt(2), nickname: '成本费账户',
nickname: '成本费账户', referralCode: 'COST0002',
referralCode: 'COST0002', status: 'SYSTEM',
provinceCode: '000000', },
cityCode: '000000', {
status: 'SYSTEM', userId: BigInt(3),
}, accountSequence: 'S0000000003', // 运营费账户
{ nickname: '运营费账户',
userId: BigInt(3), referralCode: 'OPER0003',
accountSequence: BigInt(3), status: 'SYSTEM',
nickname: '运营费账户', },
referralCode: 'OPER0003', {
provinceCode: '000000', userId: BigInt(4),
cityCode: '000000', accountSequence: 'S0000000004', // RWAD底池账户
status: 'SYSTEM', nickname: 'RWAD底池账户',
}, referralCode: 'POOL0004',
{ status: 'SYSTEM',
userId: BigInt(4), },
accountSequence: BigInt(4), ];
nickname: 'RWAD底池账户',
referralCode: 'POOL0004', async function main() {
provinceCode: '000000', console.log('Seeding database...');
cityCode: '000000',
status: 'SYSTEM', // 清理现有数据
}, await prisma.deadLetterEvent.deleteMany();
]; await prisma.smsCode.deleteMany();
await prisma.userEvent.deleteMany();
async function main() { await prisma.deviceToken.deleteMany();
console.log('Seeding database...'); await prisma.walletAddress.deleteMany();
await prisma.userDevice.deleteMany();
// 清理现有数据 await prisma.userAccount.deleteMany();
await prisma.deadLetterEvent.deleteMany();
await prisma.smsCode.deleteMany(); // 初始化账户序列号生成器 (新格式: D + YYMMDD + 5位序号)
await prisma.userEvent.deleteMany(); await prisma.accountSequenceGenerator.deleteMany();
await prisma.deviceToken.deleteMany(); const today = new Date();
await prisma.walletAddress.deleteMany(); const year = String(today.getFullYear()).slice(-2);
await prisma.userDevice.deleteMany(); const month = String(today.getMonth() + 1).padStart(2, '0');
await prisma.userAccount.deleteMany(); const day = String(today.getDate()).padStart(2, '0');
const dateKey = `${year}${month}${day}`;
// 初始化账户序列号生成器 (从100000开始系统账户使用1-99)
await prisma.accountSequenceGenerator.deleteMany(); await prisma.accountSequenceGenerator.create({
await prisma.accountSequenceGenerator.create({ data: {
data: { id: 1,
id: 1, dateKey: dateKey,
currentSequence: BigInt(100000), // 普通用户从100000开始 currentSequence: 0,
}, },
}); });
// 创建系统账户 // 创建系统账户
console.log('Creating system accounts...'); console.log('Creating system accounts...');
for (const account of SYSTEM_ACCOUNTS) { for (const account of SYSTEM_ACCOUNTS) {
await prisma.userAccount.upsert({ await prisma.userAccount.upsert({
where: { userId: account.userId }, where: { userId: account.userId },
update: account, update: account,
create: account, create: account,
}); });
console.log(` - Created system account: ${account.nickname} (userId=${account.userId})`); console.log(` - Created system account: ${account.nickname} (accountSequence=${account.accountSequence})`);
} }
console.log('Database seeded successfully!'); console.log('Database seeded successfully!');
console.log('- Initialized account sequence generator starting at 100000'); console.log(`- Initialized account sequence generator for date ${dateKey}`);
console.log(`- Created ${SYSTEM_ACCOUNTS.length} system accounts (userId 1-4)`); console.log(`- Created ${SYSTEM_ACCOUNTS.length} system accounts (S0000000001-S0000000004)`);
} }
main() main()
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
process.exit(1); process.exit(1);
}) })
.finally(async () => { .finally(async () => {
await prisma.$disconnect(); await prisma.$disconnect();
}); });

View File

@ -112,8 +112,8 @@ export class RemoveDeviceDto {
// Response DTOs // Response DTOs
export class AutoCreateAccountResponseDto { export class AutoCreateAccountResponseDto {
@ApiProperty({ example: 100001, description: '用户序列号 (唯一标识)' }) @ApiProperty({ example: 'D2512110001', description: '用户序列号 (格式: D + YYMMDD + 5位序号)' })
userSerialNum: number; userSerialNum: string;
@ApiProperty({ example: 'ABC123', description: '推荐码' }) @ApiProperty({ example: 'ABC123', description: '推荐码' })
referralCode: string; referralCode: string;
@ -135,8 +135,8 @@ export class RecoverAccountResponseDto {
@ApiProperty() @ApiProperty()
userId: string; userId: string;
@ApiProperty() @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' })
accountSequence: number; accountSequence: string;
@ApiProperty() @ApiProperty()
nickname: string; nickname: string;
@ -188,8 +188,8 @@ export class LoginResponseDto {
@ApiProperty() @ApiProperty()
userId: string; userId: string;
@ApiProperty() @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' })
accountSequence: number; accountSequence: string;
@ApiProperty() @ApiProperty()
accessToken: string; accessToken: string;
@ -216,8 +216,8 @@ export class MeResponseDto {
@ApiProperty() @ApiProperty()
userId: string; userId: string;
@ApiProperty({ description: '账户序列号' }) @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' })
accountSequence: number; accountSequence: string;
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
phoneNumber: string | null; phoneNumber: string | null;
@ -234,8 +234,8 @@ export class MeResponseDto {
@ApiProperty({ description: '完整推荐链接' }) @ApiProperty({ description: '完整推荐链接' })
referralLink: string; referralLink: string;
@ApiProperty({ description: '推荐人序列号', nullable: true }) @ApiProperty({ example: 'D2512110001', description: '推荐人序列号', nullable: true })
inviterSequence: number | null; inviterSequence: string | null;
@ApiProperty({ description: '钱包地址列表' }) @ApiProperty({ description: '钱包地址列表' })
walletAddresses: Array<{ chainType: string; address: string }>; walletAddresses: Array<{ chainType: string; address: string }>;
@ -259,7 +259,7 @@ export class ReferralValidationResponseDto {
@ApiPropertyOptional({ description: '邀请人信息' }) @ApiPropertyOptional({ description: '邀请人信息' })
inviterInfo?: { inviterInfo?: {
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
nickname: string; nickname: string;
avatarUrl: string | null; avatarUrl: string | null;
}; };
@ -292,8 +292,8 @@ export class ReferralLinkResponseDto {
} }
export class InviteRecordDto { export class InviteRecordDto {
@ApiProperty() @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' })
accountSequence: number; accountSequence: string;
@ApiProperty() @ApiProperty()
nickname: string; nickname: string;

View File

@ -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'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class RecoverByMnemonicDto { export class RecoverByMnemonicDto {
@ApiProperty({ example: 10001 }) @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' })
@IsNumber() @IsString()
accountSequence: number; @Matches(/^D\d{11}$/, { message: '账户序列号格式错误,应为 D + 年月日(6位) + 序号(5位)' })
accountSequence: string;
@ApiProperty({ example: 'abandon ability able about above absent absorb abstract absurd abuse access accident' }) @ApiProperty({ example: 'abandon ability able about above absent absorb abstract absurd abuse access accident' })
@IsString() @IsString()

View File

@ -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'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class RecoverByPhoneDto { export class RecoverByPhoneDto {
@ApiProperty({ example: 10001 }) @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' })
@IsNumber() @IsString()
accountSequence: number; @Matches(/^D\d{11}$/, { message: '账户序列号格式错误,应为 D + 年月日(6位) + 序号(5位)' })
accountSequence: string;
@ApiProperty({ example: '13800138000' }) @ApiProperty({ example: '13800138000' })
@IsString() @IsString()

View File

@ -20,8 +20,8 @@ export class UserProfileDto {
@ApiProperty() @ApiProperty()
userId: string; userId: string;
@ApiProperty() @ApiProperty({ example: 'D2512110001', description: '账户序列号 (格式: D + YYMMDD + 5位序号)' })
accountSequence: number; accountSequence: string;
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
phoneNumber: string | null; phoneNumber: string | null;

View File

@ -18,7 +18,7 @@ export class AutoCreateAccountCommand {
export class RecoverByMnemonicCommand { export class RecoverByMnemonicCommand {
constructor( constructor(
public readonly accountSequence: number, public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号
public readonly mnemonic: string, public readonly mnemonic: string,
public readonly newDeviceId: string, public readonly newDeviceId: string,
public readonly deviceName?: string, public readonly deviceName?: string,
@ -27,7 +27,7 @@ export class RecoverByMnemonicCommand {
export class RecoverByPhoneCommand { export class RecoverByPhoneCommand {
constructor( constructor(
public readonly accountSequence: number, public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号
public readonly phoneNumber: string, public readonly phoneNumber: string,
public readonly smsCode: string, public readonly smsCode: string,
public readonly newDeviceId: string, public readonly newDeviceId: string,
@ -150,7 +150,7 @@ export class GenerateReferralLinkCommand {
} }
export class GetWalletStatusQuery { export class GetWalletStatusQuery {
constructor(public readonly userSerialNum: number) {} constructor(public readonly userSerialNum: string) {} // 格式: D + YYMMDD + 5位序号
} }
export class MarkMnemonicBackedUpCommand { export class MarkMnemonicBackedUpCommand {
@ -173,7 +173,7 @@ export interface WalletStatusResult {
errorMessage?: string; // 失败原因 (failed 状态时返回) errorMessage?: string; // 失败原因 (failed 状态时返回)
} }
export interface AutoCreateAccountResult { export interface AutoCreateAccountResult {
userSerialNum: number; // 用户序列号 userSerialNum: string; // 用户序列号 (格式: D + YYMMDD + 5位序号)
referralCode: string; // 推荐码 referralCode: string; // 推荐码
username: string; // 随机用户名 username: string; // 随机用户名
avatarSvg: string; // 随机SVG头像 avatarSvg: string; // 随机SVG头像
@ -183,7 +183,7 @@ export interface AutoCreateAccountResult {
export interface RecoverAccountResult { export interface RecoverAccountResult {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
nickname: string; nickname: string;
avatarUrl: string | null; avatarUrl: string | null;
referralCode: string; referralCode: string;
@ -193,14 +193,14 @@ export interface RecoverAccountResult {
export interface AutoLoginResult { export interface AutoLoginResult {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;
} }
export interface RegisterResult { export interface RegisterResult {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
referralCode: string; referralCode: string;
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;
@ -208,14 +208,14 @@ export interface RegisterResult {
export interface LoginResult { export interface LoginResult {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;
} }
export interface UserProfileDTO { export interface UserProfileDTO {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
phoneNumber: string | null; phoneNumber: string | null;
nickname: string; nickname: string;
avatarUrl: string | null; avatarUrl: string | null;
@ -238,7 +238,7 @@ export interface DeviceDTO {
export interface UserBriefDTO { export interface UserBriefDTO {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
nickname: string; nickname: string;
avatarUrl: string | null; avatarUrl: string | null;
} }
@ -247,7 +247,7 @@ export interface ReferralCodeValidationResult {
valid: boolean; valid: boolean;
referralCode?: string; referralCode?: string;
inviterInfo?: { inviterInfo?: {
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
nickname: string; nickname: string;
avatarUrl: string | null; avatarUrl: string | null;
}; };
@ -273,7 +273,7 @@ export interface ReferralStatsResult {
thisWeekInvites: number; // 本周邀请 thisWeekInvites: number; // 本周邀请
thisMonthInvites: number; // 本月邀请 thisMonthInvites: number; // 本月邀请
recentInvites: Array<{ // 最近邀请记录 recentInvites: Array<{ // 最近邀请记录
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
nickname: string; nickname: string;
avatarUrl: string | null; avatarUrl: string | null;
registeredAt: Date; registeredAt: Date;
@ -283,13 +283,13 @@ export interface ReferralStatsResult {
export interface MeResult { export interface MeResult {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
phoneNumber: string | null; phoneNumber: string | null;
nickname: string; nickname: string;
avatarUrl: string | null; avatarUrl: string | null;
referralCode: string; referralCode: string;
referralLink: string; // 完整推荐链接 referralLink: string; // 完整推荐链接
inviterSequence: number | null; // 推荐人序列号 inviterSequence: string | null; // 推荐人序列号 (格式: D + YYMMDD + 5位序号)
walletAddresses: Array<{ chainType: string; address: string }>; walletAddresses: Array<{ chainType: string; address: string }>;
kycStatus: string; kycStatus: string;
status: string; status: string;

View File

@ -1,6 +1,6 @@
export class RecoverByMnemonicCommand { export class RecoverByMnemonicCommand {
constructor( constructor(
public readonly accountSequence: number, public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号
public readonly mnemonic: string, public readonly mnemonic: string,
public readonly newDeviceId: string, public readonly newDeviceId: string,
public readonly deviceName?: string, public readonly deviceName?: string,

View File

@ -1,6 +1,6 @@
export class RecoverByPhoneCommand { export class RecoverByPhoneCommand {
constructor( constructor(
public readonly accountSequence: number, public readonly accountSequence: string, // 格式: D + YYMMDD + 5位序号
public readonly phoneNumber: string, public readonly phoneNumber: string,
public readonly smsCode: string, public readonly smsCode: string,
public readonly newDeviceId: string, public readonly newDeviceId: string,

View File

@ -7,7 +7,7 @@ import { ApplicationError } from '@/shared/exceptions/domain.exception';
export interface TokenPayload { export interface TokenPayload {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
deviceId: string; deviceId: string;
type: 'access' | 'refresh'; type: 'access' | 'refresh';
} }
@ -22,7 +22,7 @@ export class TokenService {
async generateTokenPair(payload: { async generateTokenPair(payload: {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
deviceId: string; deviceId: string;
}): Promise<{ accessToken: string; refreshToken: string }> { }): Promise<{ accessToken: string; refreshToken: string }> {
const accessToken = this.jwtService.sign( const accessToken = this.jwtService.sign(
@ -51,7 +51,7 @@ export class TokenService {
async verifyRefreshToken(token: string): Promise<{ async verifyRefreshToken(token: string): Promise<{
userId: string; userId: string;
accountSequence: number; accountSequence: string;
deviceId: string; deviceId: string;
}> { }> {
try { try {

View File

@ -147,9 +147,9 @@ export class UserAccount {
} }
static reconstruct(params: { static reconstruct(params: {
userId: string; accountSequence: number; devices: DeviceInfo[]; userId: string; accountSequence: string; devices: DeviceInfo[];
phoneNumber: string | null; nickname: string; avatarUrl: string | null; phoneNumber: string | null; nickname: string; avatarUrl: string | null;
inviterSequence: number | null; referralCode: string; inviterSequence: string | null; referralCode: string;
walletAddresses: WalletAddress[]; kycInfo: KYCInfo | null; walletAddresses: WalletAddress[]; kycInfo: KYCInfo | null;
kycStatus: KYCStatus; status: AccountStatus; kycStatus: KYCStatus; status: AccountStatus;
registeredAt: Date; lastLoginAt: Date | null; updatedAt: Date; registeredAt: Date; lastLoginAt: Date | null; updatedAt: Date;

View File

@ -14,9 +14,9 @@ export class UserAccountAutoCreatedEvent extends DomainEvent {
constructor( constructor(
public readonly payload: { public readonly payload: {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
initialDeviceId: string; initialDeviceId: string;
inviterSequence: number | null; inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号
registeredAt: Date; registeredAt: Date;
}, },
) { ) {
@ -32,10 +32,10 @@ export class UserAccountCreatedEvent extends DomainEvent {
constructor( constructor(
public readonly payload: { public readonly payload: {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
phoneNumber: string; phoneNumber: string;
initialDeviceId: string; initialDeviceId: string;
inviterSequence: number | null; inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号
registeredAt: Date; registeredAt: Date;
}, },
) { ) {
@ -51,7 +51,7 @@ export class DeviceAddedEvent extends DomainEvent {
constructor( constructor(
public readonly payload: { public readonly payload: {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
deviceId: string; deviceId: string;
deviceName: string; deviceName: string;
}, },
@ -177,7 +177,7 @@ export class MpcKeygenRequestedEvent extends DomainEvent {
public readonly payload: { public readonly payload: {
sessionId: string; sessionId: string;
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
username: string; username: string;
threshold: number; threshold: number;
totalParties: number; totalParties: number;

View File

@ -1,19 +1,58 @@
import { DomainError } from '@/shared/exceptions/domain.exception'; import { DomainError } from '@/shared/exceptions/domain.exception';
/**
*
* 格式: D + (2) + (2) + (2) + 5
* 示例: D2512110008 -> 202512118
*/
export class AccountSequence { export class AccountSequence {
constructor(public readonly value: number) { private static readonly PATTERN = /^D\d{11}$/;
if (value <= 0) throw new DomainError('账户序列号必须大于0');
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); 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 { equals(other: AccountSequence): boolean {
return this.value === other.value; return this.value === other.value;
} }
toString(): string {
return this.value;
}
} }

View File

@ -1,284 +1,269 @@
import { DomainError } from '@/shared/exceptions/domain.exception'; import { DomainError } from '@/shared/exceptions/domain.exception';
import { createHash, createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto'; import { createHash, createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
import * as bip39 from '@scure/bip39'; import * as bip39 from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english'; import { wordlist } from '@scure/bip39/wordlists/english';
// ============ UserId ============ // ============ UserId ============
export class UserId { export class UserId {
constructor(public readonly value: bigint) { constructor(public readonly value: bigint) {
// 允许 0 作为临时值(表示未持久化的新账户) // 允许 0 作为临时值(表示未持久化的新账户)
if (value === null || value === undefined) { if (value === null || value === undefined) {
throw new DomainError('UserId不能为空'); throw new DomainError('UserId不能为空');
} }
} }
static create(value: bigint | string | number): UserId { static create(value: bigint | string | number): UserId {
if (typeof value === 'string') { if (typeof value === 'string') {
return new UserId(BigInt(value)); return new UserId(BigInt(value));
} }
if (typeof value === 'number') { if (typeof value === 'number') {
return new UserId(BigInt(value)); return new UserId(BigInt(value));
} }
return new UserId(value); return new UserId(value);
} }
equals(other: UserId): boolean { equals(other: UserId): boolean {
return this.value === other.value; return this.value === other.value;
} }
toString(): string { toString(): string {
return this.value.toString(); return this.value.toString();
} }
} }
// ============ AccountSequence ============ // ============ AccountSequence ============
export class AccountSequence { // 导出新格式的账户序列号 (D + YYMMDD + 5位序号)
constructor(public readonly value: number) { export { AccountSequence } from './account-sequence.vo';
if (value <= 0) throw new DomainError('账户序列号必须大于0');
} // ============ PhoneNumber ============
export class PhoneNumber {
static create(value: number): AccountSequence { constructor(public readonly value: string) {
return new AccountSequence(value); if (!/^1[3-9]\d{9}$/.test(value)) {
} throw new DomainError('手机号格式错误');
}
static next(current: AccountSequence): AccountSequence { }
return new AccountSequence(current.value + 1);
} static create(value: string): PhoneNumber {
return new PhoneNumber(value);
equals(other: AccountSequence): boolean { }
return this.value === other.value;
} equals(other: PhoneNumber): boolean {
} return this.value === other.value;
}
// ============ PhoneNumber ============
export class PhoneNumber { masked(): string {
constructor(public readonly value: string) { return this.value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
if (!/^1[3-9]\d{9}$/.test(value)) { }
throw new DomainError('手机号格式错误'); }
}
} // ============ ReferralCode ============
export class ReferralCode {
static create(value: string): PhoneNumber { constructor(public readonly value: string) {
return new PhoneNumber(value); if (!/^[A-Z0-9]{6}$/.test(value)) {
} throw new DomainError('推荐码格式错误');
}
equals(other: PhoneNumber): boolean { }
return this.value === other.value;
} static generate(): ReferralCode {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
masked(): string { let code = '';
return this.value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); for (let i = 0; i < 6; i++) {
} code += chars.charAt(Math.floor(Math.random() * chars.length));
} }
return new ReferralCode(code);
// ============ ReferralCode ============ }
export class ReferralCode {
constructor(public readonly value: string) { static create(value: string): ReferralCode {
if (!/^[A-Z0-9]{6}$/.test(value)) { return new ReferralCode(value.toUpperCase());
throw new DomainError('推荐码格式错误'); }
}
} equals(other: ReferralCode): boolean {
return this.value === other.value;
static generate(): ReferralCode { }
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; }
let code = '';
for (let i = 0; i < 6; i++) { // ============ Mnemonic ============
code += chars.charAt(Math.floor(Math.random() * chars.length)); export class Mnemonic {
} constructor(public readonly value: string) {
return new ReferralCode(code); if (!bip39.validateMnemonic(value, wordlist)) {
} throw new DomainError('助记词格式错误');
}
static create(value: string): ReferralCode { }
return new ReferralCode(value.toUpperCase());
} static generate(): Mnemonic {
const mnemonic = bip39.generateMnemonic(wordlist, 128);
equals(other: ReferralCode): boolean { return new Mnemonic(mnemonic);
return this.value === other.value; }
}
} static create(value: string): Mnemonic {
return new Mnemonic(value);
// ============ Mnemonic ============ }
export class Mnemonic {
constructor(public readonly value: string) { toSeed(): Uint8Array {
if (!bip39.validateMnemonic(value, wordlist)) { return bip39.mnemonicToSeedSync(this.value);
throw new DomainError('助记词格式错误'); }
}
} getWords(): string[] {
return this.value.split(' ');
static generate(): Mnemonic { }
const mnemonic = bip39.generateMnemonic(wordlist, 128);
return new Mnemonic(mnemonic); equals(other: Mnemonic): boolean {
} return this.value === other.value;
}
static create(value: string): Mnemonic { }
return new Mnemonic(value);
} // ============ DeviceInfo ============
// deviceInfo: 完整的设备信息 JSON100% 保持前端传递的原样
toSeed(): Uint8Array { export class DeviceInfo {
return bip39.mnemonicToSeedSync(this.value); private _lastActiveAt: Date;
} private _deviceInfo: Record<string, unknown>;
getWords(): string[] { constructor(
return this.value.split(' '); public readonly deviceId: string,
} public readonly deviceName: string,
public readonly addedAt: Date,
equals(other: Mnemonic): boolean { lastActiveAt: Date,
return this.value === other.value; deviceInfo?: Record<string, unknown>,
} ) {
} this._lastActiveAt = lastActiveAt;
this._deviceInfo = deviceInfo || {};
// ============ DeviceInfo ============ }
// deviceInfo: 完整的设备信息 JSON100% 保持前端传递的原样
export class DeviceInfo { get lastActiveAt(): Date {
private _lastActiveAt: Date; return this._lastActiveAt;
private _deviceInfo: Record<string, unknown>; }
constructor( // 100% 保持原样的完整设备信息 JSON
public readonly deviceId: string, get deviceInfo(): Record<string, unknown> {
public readonly deviceName: string, return this._deviceInfo;
public readonly addedAt: Date, }
lastActiveAt: Date,
deviceInfo?: Record<string, unknown>, // 便捷访问器
) { get platform(): string | undefined {
this._lastActiveAt = lastActiveAt; return this._deviceInfo.platform as string | undefined;
this._deviceInfo = deviceInfo || {}; }
}
get deviceModel(): string | undefined {
get lastActiveAt(): Date { return (this._deviceInfo.model || this._deviceInfo.deviceModel) as string | undefined;
return this._lastActiveAt; }
}
get osVersion(): string | undefined {
// 100% 保持原样的完整设备信息 JSON return this._deviceInfo.osVersion as string | undefined;
get deviceInfo(): Record<string, unknown> { }
return this._deviceInfo;
} get appVersion(): string | undefined {
return this._deviceInfo.appVersion as string | undefined;
// 便捷访问器 }
get platform(): string | undefined {
return this._deviceInfo.platform as string | undefined; updateActivity(): void {
} this._lastActiveAt = new Date();
}
get deviceModel(): string | undefined {
return (this._deviceInfo.model || this._deviceInfo.deviceModel) as string | undefined; updateDeviceInfo(info: Record<string, unknown>): void {
} this._deviceInfo = { ...this._deviceInfo, ...info };
}
get osVersion(): string | undefined { }
return this._deviceInfo.osVersion as string | undefined;
} // ============ ChainType ============
export enum ChainType {
get appVersion(): string | undefined { KAVA = 'KAVA',
return this._deviceInfo.appVersion as string | undefined; DST = 'DST',
} BSC = 'BSC',
}
updateActivity(): void {
this._lastActiveAt = new Date(); 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" },
updateDeviceInfo(info: Record<string, unknown>): void { [ChainType.BSC]: { prefix: '0x', derivationPath: "m/44'/60'/0'/0/0" },
this._deviceInfo = { ...this._deviceInfo, ...info }; };
}
} // ============ KYCInfo ============
export class KYCInfo {
// ============ ChainType ============ constructor(
export enum ChainType { public readonly realName: string,
KAVA = 'KAVA', public readonly idCardNumber: string,
DST = 'DST', public readonly idCardFrontUrl: string,
BSC = 'BSC', public readonly idCardBackUrl: string,
} ) {
if (!realName || realName.length < 2) {
export const CHAIN_CONFIG = { throw new DomainError('真实姓名不合法');
[ChainType.KAVA]: { prefix: 'kava', derivationPath: "m/44'/459'/0'/0/0" }, }
[ChainType.DST]: { prefix: 'dst', derivationPath: "m/44'/118'/0'/0/0" }, 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)) {
[ChainType.BSC]: { prefix: '0x', derivationPath: "m/44'/60'/0'/0/0" }, throw new DomainError('身份证号格式错误');
}; }
}
// ============ KYCInfo ============
export class KYCInfo { static create(params: { realName: string; idCardNumber: string; idCardFrontUrl: string; idCardBackUrl: string }): KYCInfo {
constructor( return new KYCInfo(params.realName, params.idCardNumber, params.idCardFrontUrl, params.idCardBackUrl);
public readonly realName: string, }
public readonly idCardNumber: string,
public readonly idCardFrontUrl: string, maskedIdCardNumber(): string {
public readonly idCardBackUrl: string, return this.idCardNumber.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2');
) { }
if (!realName || realName.length < 2) { }
throw new DomainError('真实姓名不合法');
} // ============ Enums ============
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)) { export enum KYCStatus {
throw new DomainError('身份证号格式错误'); NOT_VERIFIED = 'NOT_VERIFIED',
} PENDING = 'PENDING',
} VERIFIED = 'VERIFIED',
REJECTED = 'REJECTED',
static create(params: { realName: string; idCardNumber: string; idCardFrontUrl: string; idCardBackUrl: string }): KYCInfo { }
return new KYCInfo(params.realName, params.idCardNumber, params.idCardFrontUrl, params.idCardBackUrl);
} export enum AccountStatus {
ACTIVE = 'ACTIVE',
maskedIdCardNumber(): string { FROZEN = 'FROZEN',
return this.idCardNumber.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2'); DEACTIVATED = 'DEACTIVATED',
} }
}
export enum AddressStatus {
// ============ Enums ============ ACTIVE = 'ACTIVE',
export enum KYCStatus { DISABLED = 'DISABLED',
NOT_VERIFIED = 'NOT_VERIFIED', }
PENDING = 'PENDING',
VERIFIED = 'VERIFIED', // ============ AddressId ============
REJECTED = 'REJECTED', export class AddressId {
} constructor(public readonly value: string) {}
export enum AccountStatus { static generate(): AddressId {
ACTIVE = 'ACTIVE', return new AddressId(crypto.randomUUID());
FROZEN = 'FROZEN', }
DEACTIVATED = 'DEACTIVATED',
} static create(value: string): AddressId {
return new AddressId(value);
export enum AddressStatus { }
ACTIVE = 'ACTIVE', }
DISABLED = 'DISABLED',
} // ============ MnemonicEncryption ============
export class MnemonicEncryption {
// ============ AddressId ============ static encrypt(mnemonic: string, key: string): string {
export class AddressId { const derivedKey = this.deriveKey(key);
constructor(public readonly value: string) {} const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', derivedKey, iv);
static generate(): AddressId {
return new AddressId(crypto.randomUUID()); let encrypted = cipher.update(mnemonic, 'utf8', 'hex');
} encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
static create(value: string): AddressId {
return new AddressId(value); return JSON.stringify({
} encrypted,
} authTag: authTag.toString('hex'),
iv: iv.toString('hex'),
// ============ MnemonicEncryption ============ });
export class MnemonicEncryption { }
static encrypt(mnemonic: string, key: string): string {
const derivedKey = this.deriveKey(key); static decrypt(encryptedData: string, key: string): string {
const iv = randomBytes(16); const { encrypted, authTag, iv } = JSON.parse(encryptedData);
const cipher = createCipheriv('aes-256-gcm', derivedKey, iv); const derivedKey = this.deriveKey(key);
const decipher = createDecipheriv('aes-256-gcm', derivedKey, Buffer.from(iv, 'hex'));
let encrypted = cipher.update(mnemonic, 'utf8', 'hex'); decipher.setAuthTag(Buffer.from(authTag, 'hex'));
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag(); let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.stringify({ return decrypted;
encrypted, }
authTag: authTag.toString('hex'),
iv: iv.toString('hex'), private static deriveKey(password: string): Buffer {
}); return scryptSync(password, 'rwa-wallet-salt', 32);
} }
}
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);
}
}

View File

@ -26,7 +26,7 @@ export interface VerifyMnemonicResult {
} }
export interface VerifyMnemonicByAccountParams { export interface VerifyMnemonicByAccountParams {
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
mnemonic: string; mnemonic: string;
} }
@ -144,7 +144,7 @@ export class BlockchainClientService {
/** /**
* *
*/ */
async markMnemonicBackedUp(accountSequence: number): Promise<void> { async markMnemonicBackedUp(accountSequence: string): Promise<void> {
this.logger.log(`Marking mnemonic as backed up for account ${accountSequence}`); this.logger.log(`Marking mnemonic as backed up for account ${accountSequence}`);
try { try {

View File

@ -32,7 +32,7 @@ export interface KeygenCompletedPayload {
threshold: string; threshold: string;
extraPayload?: { extraPayload?: {
userId: string; userId: string;
accountSequence: number; // 8位账户序列 accountSequence: string; // 格式: D + YYMMDD + 5位序
username: string; username: string;
delegateShare?: { delegateShare?: {
partyId: string; partyId: string;

View File

@ -1,56 +1,56 @@
// Prisma Entity Types - 用于Mapper转换 // Prisma Entity Types - 用于Mapper转换
export interface UserAccountEntity { export interface UserAccountEntity {
userId: bigint; userId: bigint;
accountSequence: bigint; accountSequence: string; // 格式: D + YYMMDD + 5位序号
phoneNumber: string | null; phoneNumber: string | null;
nickname: string; nickname: string;
avatarUrl: string | null; avatarUrl: string | null;
inviterSequence: bigint | null; inviterSequence: string | null; // 格式: D + YYMMDD + 5位序号
referralCode: string; referralCode: string;
kycStatus: string; kycStatus: string;
realName: string | null; realName: string | null;
idCardNumber: string | null; idCardNumber: string | null;
idCardFrontUrl: string | null; idCardFrontUrl: string | null;
idCardBackUrl: string | null; idCardBackUrl: string | null;
kycVerifiedAt: Date | null; kycVerifiedAt: Date | null;
status: string; status: string;
registeredAt: Date; registeredAt: Date;
lastLoginAt: Date | null; lastLoginAt: Date | null;
updatedAt: Date; updatedAt: Date;
devices?: UserDeviceEntity[]; devices?: UserDeviceEntity[];
walletAddresses?: WalletAddressEntity[]; walletAddresses?: WalletAddressEntity[];
} }
export interface UserDeviceEntity { export interface UserDeviceEntity {
id: bigint; id: bigint;
userId: bigint; userId: bigint;
deviceId: string; deviceId: string;
deviceName: string | null; deviceName: string | null;
deviceInfo: Record<string, unknown> | null; // 完整的设备信息 JSON deviceInfo: Record<string, unknown> | null; // 完整的设备信息 JSON
// Hardware Info (冗余字段,便于查询) // Hardware Info (冗余字段,便于查询)
platform: string | null; platform: string | null;
deviceModel: string | null; deviceModel: string | null;
osVersion: string | null; osVersion: string | null;
appVersion: string | null; appVersion: string | null;
screenWidth: number | null; screenWidth: number | null;
screenHeight: number | null; screenHeight: number | null;
locale: string | null; locale: string | null;
timezone: string | null; timezone: string | null;
// Timestamps // Timestamps
addedAt: Date; addedAt: Date;
lastActiveAt: Date; lastActiveAt: Date;
} }
export interface WalletAddressEntity { export interface WalletAddressEntity {
addressId: bigint; addressId: bigint;
userId: bigint; userId: bigint;
chainType: string; chainType: string;
address: string; address: string;
publicKey: string; publicKey: string;
addressDigest: string; addressDigest: string;
mpcSignatureR: string; mpcSignatureR: string;
mpcSignatureS: string; mpcSignatureS: string;
mpcSignatureV: number; mpcSignatureV: number;
status: string; status: string;
boundAt: Date; boundAt: Date;
} }

View File

@ -1,64 +1,64 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate'; import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
import { WalletAddress } from '@/domain/entities/wallet-address.entity'; import { WalletAddress } from '@/domain/entities/wallet-address.entity';
import { DeviceInfo, KYCInfo, KYCStatus, AccountStatus, ChainType, AddressStatus } from '@/domain/value-objects'; import { DeviceInfo, KYCInfo, KYCStatus, AccountStatus, ChainType, AddressStatus } from '@/domain/value-objects';
import { UserAccountEntity } from '../entities/user-account.entity'; import { UserAccountEntity } from '../entities/user-account.entity';
import { toMpcSignatureString } from '../entities/wallet-address.entity'; import { toMpcSignatureString } from '../entities/wallet-address.entity';
@Injectable() @Injectable()
export class UserAccountMapper { export class UserAccountMapper {
toDomain(entity: UserAccountEntity): UserAccount { toDomain(entity: UserAccountEntity): UserAccount {
const devices = (entity.devices || []).map((d) => { const devices = (entity.devices || []).map((d) => {
// 直接使用完整的 deviceInfo JSON100% 保持原样 // 直接使用完整的 deviceInfo JSON100% 保持原样
return new DeviceInfo( return new DeviceInfo(
d.deviceId, d.deviceId,
d.deviceName || '未命名设备', d.deviceName || '未命名设备',
d.addedAt, d.addedAt,
d.lastActiveAt, d.lastActiveAt,
d.deviceInfo || undefined, d.deviceInfo || undefined,
); );
}); });
const wallets = (entity.walletAddresses || []).map((w) => const wallets = (entity.walletAddresses || []).map((w) =>
WalletAddress.reconstruct({ WalletAddress.reconstruct({
addressId: w.addressId.toString(), addressId: w.addressId.toString(),
userId: w.userId.toString(), userId: w.userId.toString(),
chainType: w.chainType as ChainType, chainType: w.chainType as ChainType,
address: w.address, address: w.address,
publicKey: w.publicKey, publicKey: w.publicKey,
addressDigest: w.addressDigest, addressDigest: w.addressDigest,
mpcSignature: toMpcSignatureString(w), // 64 bytes hex (r + s) mpcSignature: toMpcSignatureString(w), // 64 bytes hex (r + s)
status: w.status as AddressStatus, status: w.status as AddressStatus,
boundAt: w.boundAt, boundAt: w.boundAt,
}), }),
); );
const kycInfo = const kycInfo =
entity.realName && entity.idCardNumber entity.realName && entity.idCardNumber
? KYCInfo.create({ ? KYCInfo.create({
realName: entity.realName, realName: entity.realName,
idCardNumber: entity.idCardNumber, idCardNumber: entity.idCardNumber,
idCardFrontUrl: entity.idCardFrontUrl || '', idCardFrontUrl: entity.idCardFrontUrl || '',
idCardBackUrl: entity.idCardBackUrl || '', idCardBackUrl: entity.idCardBackUrl || '',
}) })
: null; : null;
return UserAccount.reconstruct({ return UserAccount.reconstruct({
userId: entity.userId.toString(), userId: entity.userId.toString(),
accountSequence: Number(entity.accountSequence), accountSequence: entity.accountSequence, // 现在是字符串类型
devices, devices,
phoneNumber: entity.phoneNumber, phoneNumber: entity.phoneNumber,
nickname: entity.nickname, nickname: entity.nickname,
avatarUrl: entity.avatarUrl, avatarUrl: entity.avatarUrl,
inviterSequence: entity.inviterSequence ? Number(entity.inviterSequence) : null, inviterSequence: entity.inviterSequence, // 现在是字符串类型
referralCode: entity.referralCode, referralCode: entity.referralCode,
walletAddresses: wallets, walletAddresses: wallets,
kycInfo, kycInfo,
kycStatus: entity.kycStatus as KYCStatus, kycStatus: entity.kycStatus as KYCStatus,
status: entity.status as AccountStatus, status: entity.status as AccountStatus,
registeredAt: entity.registeredAt, registeredAt: entity.registeredAt,
lastLoginAt: entity.lastLoginAt, lastLoginAt: entity.lastLoginAt,
updatedAt: entity.updatedAt, updatedAt: entity.updatedAt,
}); });
} }
} }

View File

@ -26,11 +26,11 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
// 新账户让数据库自动生成userId // 新账户让数据库自动生成userId
const created = await tx.userAccount.create({ const created = await tx.userAccount.create({
data: { data: {
accountSequence: BigInt(account.accountSequence.value), accountSequence: account.accountSequence.value,
phoneNumber: account.phoneNumber?.value || null, phoneNumber: account.phoneNumber?.value || null,
nickname: account.nickname, nickname: account.nickname,
avatarUrl: account.avatarUrl, avatarUrl: account.avatarUrl,
inviterSequence: account.inviterSequence ? BigInt(account.inviterSequence.value) : null, inviterSequence: account.inviterSequence?.value || null,
referralCode: account.referralCode.value, referralCode: account.referralCode.value,
kycStatus: account.kycStatus, kycStatus: account.kycStatus,
realName: account.kycInfo?.realName || null, realName: account.kycInfo?.realName || null,
@ -125,7 +125,7 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
async findByAccountSequence(sequence: AccountSequence): Promise<UserAccount | null> { async findByAccountSequence(sequence: AccountSequence): Promise<UserAccount | null> {
const data = await this.prisma.userAccount.findUnique({ const data = await this.prisma.userAccount.findUnique({
where: { accountSequence: BigInt(sequence.value) }, where: { accountSequence: sequence.value },
include: { devices: true, walletAddresses: true }, include: { devices: true, walletAddresses: true },
}); });
return data ? this.toDomain(data) : null; return data ? this.toDomain(data) : null;
@ -163,18 +163,38 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
async getMaxAccountSequence(): Promise<AccountSequence | null> { async getMaxAccountSequence(): Promise<AccountSequence | null> {
const result = await this.prisma.userAccount.aggregate({ _max: { accountSequence: true } }); 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<AccountSequence> { async getNextAccountSequence(): Promise<AccountSequence> {
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 result = await this.prisma.$transaction(async (tx) => {
const updated = await tx.accountSequenceGenerator.update({ // 尝试更新当日记录,如果不存在则创建
where: { id: 1 }, const existing = await tx.accountSequenceGenerator.findUnique({
data: { currentSequence: { increment: 1 } }, 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( async findUsers(
@ -247,12 +267,12 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
return UserAccount.reconstruct({ return UserAccount.reconstruct({
userId: data.userId.toString(), userId: data.userId.toString(),
accountSequence: Number(data.accountSequence), accountSequence: data.accountSequence,
devices, devices,
phoneNumber: data.phoneNumber, phoneNumber: data.phoneNumber,
nickname: data.nickname, nickname: data.nickname,
avatarUrl: data.avatarUrl, avatarUrl: data.avatarUrl,
inviterSequence: data.inviterSequence ? Number(data.inviterSequence) : null, inviterSequence: data.inviterSequence || null,
referralCode: data.referralCode, referralCode: data.referralCode,
walletAddresses: wallets, walletAddresses: wallets,
kycInfo, kycInfo,
@ -268,7 +288,7 @@ export class UserAccountRepositoryImpl implements UserAccountRepository {
async findByInviterSequence(inviterSequence: AccountSequence): Promise<UserAccount[]> { async findByInviterSequence(inviterSequence: AccountSequence): Promise<UserAccount[]> {
const data = await this.prisma.userAccount.findMany({ const data = await this.prisma.userAccount.findMany({
where: { inviterSequence: BigInt(inviterSequence.value) }, where: { inviterSequence: inviterSequence.value },
include: { devices: true, walletAddresses: true }, include: { devices: true, walletAddresses: true },
orderBy: { registeredAt: 'desc' }, orderBy: { registeredAt: 'desc' },
}); });

View File

@ -5,14 +5,14 @@ import { UnauthorizedException } from '@/shared/exceptions/domain.exception';
export interface JwtPayload { export interface JwtPayload {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
deviceId: string; deviceId: string;
type: 'access' | 'refresh'; type: 'access' | 'refresh';
} }
export interface CurrentUserData { export interface CurrentUserData {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
deviceId: string; deviceId: string;
} }

View File

@ -5,7 +5,7 @@ import { ConfigService } from '@nestjs/config';
export interface JwtPayload { export interface JwtPayload {
userId: string; userId: string;
accountSequence: number; accountSequence: string; // 格式: D + YYMMDD + 5位序号
deviceId: string; deviceId: string;
type: 'access' | 'refresh'; type: 'access' | 'refresh';
iat: number; iat: number;

View File

@ -3,7 +3,7 @@
*/ */
// 生成用户名: 榴莲女皇x号 // 生成用户名: 榴莲女皇x号
export function generateUsername(accountSequence: number): string { export function generateUsername(accountSequence: string): string {
return `榴莲女皇${accountSequence}`; 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 { return {
username: generateUsername(accountSequence), username: generateUsername(accountSequence),
avatarSvg: generateRandomAvatarSvg(), avatarSvg: generateRandomAvatarSvg(),

View File

@ -81,7 +81,7 @@ export class KeygenRequestedHandler implements OnModuleInit {
try { try {
const deriveResult = await this.blockchainClient.deriveAddresses({ const deriveResult = await this.blockchainClient.deriveAddresses({
userId, userId,
accountSequence, // 8位账户序列号用于关联恢复助记词 accountSequence, // 账户序列号,格式: D + YYMMDD + 5位序号如 D2512110008
publicKey: result.publicKey, publicKey: result.publicKey,
}); });
derivedAddresses = deriveResult.addresses; derivedAddresses = deriveResult.addresses;
@ -131,7 +131,7 @@ export class KeygenRequestedHandler implements OnModuleInit {
// Add extra payload for identity-service // Add extra payload for identity-service
(completedEvent as any).extraPayload = { (completedEvent as any).extraPayload = {
userId, userId,
accountSequence, // 8位账户序列号用于关联恢复助记词 accountSequence, // 账户序列号,格式: D + YYMMDD + 5位序号如 D2512110008
username, username,
delegateShare: result.delegateShare, delegateShare: result.delegateShare,
derivedAddresses, // BSC, KAVA, DST addresses derivedAddresses, // BSC, KAVA, DST addresses

View File

@ -13,7 +13,7 @@ import * as jwt from 'jsonwebtoken';
export interface StoreBackupShareParams { export interface StoreBackupShareParams {
userId: string; userId: string;
accountSequence: number; accountSequence: string;
username: string; username: string;
publicKey: string; publicKey: string;
partyId: string; partyId: string;

View File

@ -11,7 +11,7 @@ import { firstValueFrom } from 'rxjs';
export interface DeriveAddressParams { export interface DeriveAddressParams {
userId: string; userId: string;
accountSequence: number; // 8位账户序列号用于关联恢复助记词 accountSequence: string; // 账户序列号,格式: D + YYMMDD + 5位序号如 D2512110008
publicKey: string; publicKey: string;
} }

View File

@ -18,7 +18,7 @@ export const MPC_CONSUME_TOPICS = {
export interface KeygenRequestedPayload { export interface KeygenRequestedPayload {
sessionId: string; sessionId: string;
userId: string; userId: string;
accountSequence: number; // 8位账户序列号用于关联恢复助记词 accountSequence: string; // 账户序列号,格式: D + YYMMDD + 5位序号如 D2512110008
username: string; username: string;
threshold: number; threshold: number;
totalParties: number; totalParties: number;

View File

@ -32,7 +32,7 @@ import {
import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { JwtAuthGuard } from '../guards/jwt-auth.guard';
interface AuthenticatedRequest { interface AuthenticatedRequest {
user: { id: string; accountSequence: number }; user: { id: string; accountSequence: string };
} }
@ApiTags('认种订单') @ApiTags('认种订单')

Some files were not shown because too many files have changed in this diff Show More