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 = ''0x6a664488d000e094baa8a055961921bf495c1152'';\n\nasync function transfer() {\n const provider = new ethers.JsonRpcProvider(KAVA_TESTNET_RPC);\n const wallet = new ethers.Wallet(privateKey, provider);\n \n const abi = [''function transfer(address to, uint256 amount) returns (bool)'', ''function balanceOf(address) view returns (uint256)''];\n const contract = new ethers.Contract(USDT_CONTRACT, abi, wallet);\n \n // 880,000 USDT = 880000 * 1e6 (6 decimals)\n const amount = BigInt(880000) * BigInt(1000000);\n \n console.log(''Transferring 880,000 USDT to'', TO_ADDRESS);\n const tx = await contract.transfer(TO_ADDRESS, amount, { gasLimit: 100000 });\n console.log(''TX Hash:'', tx.hash);\n await tx.wait();\n \n const newBalance = await contract.balanceOf(TO_ADDRESS);\n console.log(''New balance:'', Number(newBalance) / 1e6, ''USDT'');\n}\n\ntransfer().catch(e => console.error(''Error:'', e.message));\n\")",
"Bash(node -e \"\nconst { ethers } = require(''ethers'');\n\nconst KAVA_TESTNET_RPC = ''https://evm.testnet.kava.io'';\nconst privateKey = ''0xd42a6e6021ebd884f3f179d3793a32e97b9f1001db6ff44441ec455d748b9aa6'';\nconst USDT_CONTRACT = ''0xc12f6A4A7Fd0965085B044A67a39CcA2ff7fe0dF'';\nconst TO_ADDRESS = ''0x53fd262ef1a707b80f87581cc64e09800fdbd690'';\n\nasync function transfer() {\n const provider = new ethers.JsonRpcProvider(KAVA_TESTNET_RPC);\n const wallet = new ethers.Wallet(privateKey, provider);\n \n const abi = [''function transfer(address to, uint256 amount) returns (bool)'', ''function balanceOf(address) view returns (uint256)''];\n const contract = new ethers.Contract(USDT_CONTRACT, abi, wallet);\n \n // 360,000 USDT = 360000 * 1e6 (6 decimals)\n const amount = BigInt(360000) * BigInt(1000000);\n \n console.log(''Transferring 360,000 USDT to'', TO_ADDRESS);\n const tx = await contract.transfer(TO_ADDRESS, amount, { gasLimit: 100000 });\n console.log(''TX Hash:'', tx.hash);\n await tx.wait();\n \n const newBalance = await contract.balanceOf(TO_ADDRESS);\n console.log(''New balance:'', Number(newBalance) / 1e6, ''USDT'');\n}\n\ntransfer().catch(e => console.error(''Error:'', e.message));\n\")",
"Bash(docker exec:*)"
"Bash(docker exec:*)",
"Bash(node -e:*)",
"Bash(dir /s /b c:UsersdongDesktoprwadurianbackendservicesreward-servicesrc*.ts)",
"Bash(git tag:*)",
"Bash(dir:*)",
"Bash(grep:*)",
"Bash(npx prisma format)",
"Bash(DATABASE_URL=\"postgresql://dummy:dummy@localhost:5432/dummy\" npx prisma generate:*)",
"Bash(npx prisma generate)",
"Bash(for file in grant-*.dto.ts)",
"Bash(do sed -i '/@ApiProperty.*账户序列号/,/accountSequence:/ s/@IsNumber()/@IsString()/' \"$file\")",
"Bash(done)",
"Bash(git diff:*)",
"Bash(npm install:*)"
],
"deny": [],
"ask": []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -1,27 +1,27 @@
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
export interface ApiResponse<T> {
success: boolean
data: T
timestamp: string
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
timestamp: new Date().toISOString(),
})),
)
}
}
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
export interface ApiResponse<T> {
success: boolean
data: T
timestamp: string
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
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 { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { ConfigService } from '@nestjs/config'
export interface JwtPayload {
// Identity-service uses 'userId' field
userId: string
accountSequence?: number
deviceId?: string
type?: string
// Legacy support for 'sub' field
sub?: string
walletAddress?: string
roles?: string[]
iat?: number
exp?: number
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
})
}
async validate(payload: JwtPayload) {
// Support both 'userId' (from identity-service) and 'sub' (legacy)
const userId = payload.userId || payload.sub
return {
userId,
accountSequence: payload.accountSequence,
deviceId: payload.deviceId,
walletAddress: payload.walletAddress,
roles: payload.roles,
}
}
}
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { ConfigService } from '@nestjs/config'
export interface JwtPayload {
// Identity-service uses 'userId' field
userId: string
accountSequence?: string
deviceId?: string
type?: string
// Legacy support for 'sub' field
sub?: string
walletAddress?: string
roles?: string[]
iat?: number
exp?: number
}
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
})
}
async validate(payload: JwtPayload) {
// Support both 'userId' (from identity-service) and 'sub' (legacy)
const userId = payload.userId || payload.sub
return {
userId,
accountSequence: payload.accountSequence,
deviceId: payload.deviceId,
walletAddress: payload.walletAddress,
roles: payload.roles,
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,13 +9,13 @@ datasource db {
model UserAccount {
userId BigInt @id @default(autoincrement()) @map("user_id")
accountSequence BigInt @unique @map("account_sequence")
accountSequence String @unique @map("account_sequence") @db.VarChar(12) // 格式: D + YYMMDD + 5位序号
phoneNumber String? @unique @map("phone_number") @db.VarChar(20)
nickname String @db.VarChar(100)
avatarUrl String? @map("avatar_url") @db.Text
inviterSequence BigInt? @map("inviter_sequence")
inviterSequence String? @map("inviter_sequence") @db.VarChar(12) // 推荐人序列号
referralCode String @unique @map("referral_code") @db.VarChar(10)
kycStatus String @default("NOT_VERIFIED") @map("kyc_status") @db.VarChar(20)
@ -102,10 +102,11 @@ model WalletAddress {
}
model AccountSequenceGenerator {
id Int @id @default(1)
currentSequence BigInt @default(0) @map("current_sequence")
id Int @id @default(autoincrement())
dateKey String @unique @map("date_key") @db.VarChar(6) // 格式: YYMMDD
currentSequence Int @default(0) @map("current_sequence") // 当日序号 (0-99999)
updatedAt DateTime @updatedAt @map("updated_at")
@@map("account_sequence_generator")
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,58 @@
import { DomainError } from '@/shared/exceptions/domain.exception';
/**
*
* 格式: D + (2) + (2) + (2) + 5
* 示例: D2512110008 -> 202512118
*/
export class AccountSequence {
constructor(public readonly value: number) {
if (value <= 0) throw new DomainError('账户序列号必须大于0');
private static readonly PATTERN = /^D\d{11}$/;
constructor(public readonly value: string) {
if (!AccountSequence.PATTERN.test(value)) {
throw new DomainError(`账户序列号格式无效: ${value},应为 D + 年月日(6位) + 序号(5位)`);
}
}
static create(value: number): AccountSequence {
static create(value: string): AccountSequence {
return new AccountSequence(value);
}
static next(current: AccountSequence): AccountSequence {
return new AccountSequence(current.value + 1);
/**
*
* @param date
* @param dailySequence (0-99999)
*/
static generate(date: Date, dailySequence: number): AccountSequence {
if (dailySequence < 0 || dailySequence > 99999) {
throw new DomainError(`当日序号超出范围: ${dailySequence},应为 0-99999`);
}
const year = String(date.getFullYear()).slice(-2);
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const seq = String(dailySequence).padStart(5, '0');
return new AccountSequence(`D${year}${month}${day}${seq}`);
}
/**
* (YYMMDD)
*/
get dateString(): string {
return this.value.slice(1, 7);
}
/**
*
*/
get dailySequence(): number {
return parseInt(this.value.slice(7), 10);
}
equals(other: AccountSequence): boolean {
return this.value === other.value;
}
toString(): string {
return this.value;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@
*/
// 生成用户名: 榴莲女皇x号
export function generateUsername(accountSequence: number): string {
export function generateUsername(accountSequence: string): string {
return `榴莲女皇${accountSequence}`;
}
@ -132,9 +132,9 @@ export function generateRandomAvatarSvg(): string {
/**
*
* @param accountSequence
* @param accountSequence (格式: D + YYMMDD + 5)
*/
export function generateIdentity(accountSequence: number): { username: string; avatarSvg: string } {
export function generateIdentity(accountSequence: string): { username: string; avatarSvg: string } {
return {
username: generateUsername(accountSequence),
avatarSvg: generateRandomAvatarSvg(),

View File

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

View File

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

View File

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

View File

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

View File

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

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