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:
parent
8148d1d127
commit
4be9c1fb82
|
|
@ -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": []
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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: '市团队授权成功' }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ export class GrantAuthCityCompanyDto {
|
|||
userId: string
|
||||
|
||||
@ApiProperty({ description: '账户序列号' })
|
||||
@IsNumber()
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '账户序列号不能为空' })
|
||||
accountSequence: number
|
||||
accountSequence: string
|
||||
|
||||
@ApiProperty({ description: '城市代码', example: '430100' })
|
||||
@IsString()
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ export class GrantAuthProvinceCompanyDto {
|
|||
userId: string
|
||||
|
||||
@ApiProperty({ description: '账户序列号' })
|
||||
@IsNumber()
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '账户序列号不能为空' })
|
||||
accountSequence: number
|
||||
accountSequence: string
|
||||
|
||||
@ApiProperty({ description: '省份代码', example: '430000' })
|
||||
@IsString()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ export class GrantCommunityDto {
|
|||
userId: string
|
||||
|
||||
@ApiProperty({ description: '账户序列号' })
|
||||
@IsNumber()
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '账户序列号不能为空' })
|
||||
accountSequence: number
|
||||
accountSequence: string
|
||||
|
||||
@ApiProperty({ description: '社区名称', example: '深圳社区' })
|
||||
@IsString()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
export interface CommunityInfoDTO {
|
||||
authorizationId: string
|
||||
accountSequence: number
|
||||
accountSequence: string
|
||||
communityName: string
|
||||
userId?: string
|
||||
isHeadquarters: boolean
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export * from './current-user.decorator'
|
||||
export * from './public.decorator'
|
||||
export * from './current-user.decorator'
|
||||
export * from './public.decorator'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export * from './domain.exception'
|
||||
export * from './application.exception'
|
||||
export * from './domain.exception'
|
||||
export * from './application.exception'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export * from './global-exception.filter'
|
||||
export * from './global-exception.filter'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export * from './jwt-auth.guard'
|
||||
export * from './jwt-auth.guard'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export * from './transform.interceptor'
|
||||
export * from './transform.interceptor'
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export * from './jwt.strategy'
|
||||
export * from './jwt.strategy'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@ export class StoreShareDto {
|
|||
userId: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
accountSequence: number;
|
||||
@IsString()
|
||||
accountSequence: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 开头)',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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个单词,空格分隔)',
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export class MpcKeygenCompletedHandler implements OnModuleInit {
|
|||
|
||||
const result = await this.addressDerivationService.deriveAndRegister({
|
||||
userId: BigInt(userId),
|
||||
accountSequence: Number(accountSequence),
|
||||
accountSequence: accountSequence,
|
||||
publicKey,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ConfigService } from '@nestjs/config';
|
|||
|
||||
interface JwtPayload {
|
||||
userId: string;
|
||||
accountSequence: number;
|
||||
accountSequence: string;
|
||||
deviceId: string;
|
||||
type: 'access' | 'refresh';
|
||||
iat: number;
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,19 +1,58 @@
|
|||
import { DomainError } from '@/shared/exceptions/domain.exception';
|
||||
|
||||
/**
|
||||
* 账户序列号值对象
|
||||
* 格式: D + 年(2位) + 月(2位) + 日(2位) + 5位序号
|
||||
* 示例: D2512110008 -> 2025年12月11日的第8个注册用户
|
||||
*/
|
||||
export class AccountSequence {
|
||||
constructor(public readonly value: number) {
|
||||
if (value <= 0) throw new DomainError('账户序列号必须大于0');
|
||||
private static readonly PATTERN = /^D\d{11}$/;
|
||||
|
||||
constructor(public readonly value: string) {
|
||||
if (!AccountSequence.PATTERN.test(value)) {
|
||||
throw new DomainError(`账户序列号格式无效: ${value},应为 D + 年月日(6位) + 序号(5位)`);
|
||||
}
|
||||
}
|
||||
|
||||
static create(value: number): AccountSequence {
|
||||
static create(value: string): AccountSequence {
|
||||
return new AccountSequence(value);
|
||||
}
|
||||
|
||||
static next(current: AccountSequence): AccountSequence {
|
||||
return new AccountSequence(current.value + 1);
|
||||
/**
|
||||
* 根据日期和当日序号生成新的账户序列号
|
||||
* @param date 日期
|
||||
* @param dailySequence 当日序号 (0-99999)
|
||||
*/
|
||||
static generate(date: Date, dailySequence: number): AccountSequence {
|
||||
if (dailySequence < 0 || dailySequence > 99999) {
|
||||
throw new DomainError(`当日序号超出范围: ${dailySequence},应为 0-99999`);
|
||||
}
|
||||
const year = String(date.getFullYear()).slice(-2);
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const seq = String(dailySequence).padStart(5, '0');
|
||||
return new AccountSequence(`D${year}${month}${day}${seq}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从序列号中提取日期字符串 (YYMMDD)
|
||||
*/
|
||||
get dateString(): string {
|
||||
return this.value.slice(1, 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从序列号中提取当日序号
|
||||
*/
|
||||
get dailySequence(): number {
|
||||
return parseInt(this.value.slice(7), 10);
|
||||
}
|
||||
|
||||
equals(other: AccountSequence): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,284 +1,269 @@
|
|||
import { DomainError } from '@/shared/exceptions/domain.exception';
|
||||
import { createHash, createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'crypto';
|
||||
import * as bip39 from '@scure/bip39';
|
||||
import { wordlist } from '@scure/bip39/wordlists/english';
|
||||
|
||||
// ============ UserId ============
|
||||
export class UserId {
|
||||
constructor(public readonly value: bigint) {
|
||||
// 允许 0 作为临时值(表示未持久化的新账户)
|
||||
if (value === null || value === undefined) {
|
||||
throw new DomainError('UserId不能为空');
|
||||
}
|
||||
}
|
||||
|
||||
static create(value: bigint | string | number): UserId {
|
||||
if (typeof value === 'string') {
|
||||
return new UserId(BigInt(value));
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return new UserId(BigInt(value));
|
||||
}
|
||||
return new UserId(value);
|
||||
}
|
||||
|
||||
equals(other: UserId): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ AccountSequence ============
|
||||
export class AccountSequence {
|
||||
constructor(public readonly value: number) {
|
||||
if (value <= 0) throw new DomainError('账户序列号必须大于0');
|
||||
}
|
||||
|
||||
static create(value: number): AccountSequence {
|
||||
return new AccountSequence(value);
|
||||
}
|
||||
|
||||
static next(current: AccountSequence): AccountSequence {
|
||||
return new AccountSequence(current.value + 1);
|
||||
}
|
||||
|
||||
equals(other: AccountSequence): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ PhoneNumber ============
|
||||
export class PhoneNumber {
|
||||
constructor(public readonly value: string) {
|
||||
if (!/^1[3-9]\d{9}$/.test(value)) {
|
||||
throw new DomainError('手机号格式错误');
|
||||
}
|
||||
}
|
||||
|
||||
static create(value: string): PhoneNumber {
|
||||
return new PhoneNumber(value);
|
||||
}
|
||||
|
||||
equals(other: PhoneNumber): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
|
||||
masked(): string {
|
||||
return this.value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
||||
}
|
||||
}
|
||||
|
||||
// ============ ReferralCode ============
|
||||
export class ReferralCode {
|
||||
constructor(public readonly value: string) {
|
||||
if (!/^[A-Z0-9]{6}$/.test(value)) {
|
||||
throw new DomainError('推荐码格式错误');
|
||||
}
|
||||
}
|
||||
|
||||
static generate(): ReferralCode {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let code = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return new ReferralCode(code);
|
||||
}
|
||||
|
||||
static create(value: string): ReferralCode {
|
||||
return new ReferralCode(value.toUpperCase());
|
||||
}
|
||||
|
||||
equals(other: ReferralCode): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Mnemonic ============
|
||||
export class Mnemonic {
|
||||
constructor(public readonly value: string) {
|
||||
if (!bip39.validateMnemonic(value, wordlist)) {
|
||||
throw new DomainError('助记词格式错误');
|
||||
}
|
||||
}
|
||||
|
||||
static generate(): Mnemonic {
|
||||
const mnemonic = bip39.generateMnemonic(wordlist, 128);
|
||||
return new Mnemonic(mnemonic);
|
||||
}
|
||||
|
||||
static create(value: string): Mnemonic {
|
||||
return new Mnemonic(value);
|
||||
}
|
||||
|
||||
toSeed(): Uint8Array {
|
||||
return bip39.mnemonicToSeedSync(this.value);
|
||||
}
|
||||
|
||||
getWords(): string[] {
|
||||
return this.value.split(' ');
|
||||
}
|
||||
|
||||
equals(other: Mnemonic): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ DeviceInfo ============
|
||||
// deviceInfo: 完整的设备信息 JSON,100% 保持前端传递的原样
|
||||
export class DeviceInfo {
|
||||
private _lastActiveAt: Date;
|
||||
private _deviceInfo: Record<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: 完整的设备信息 JSON,100% 保持前端传递的原样
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,64 +1,64 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
|
||||
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
|
||||
import { DeviceInfo, KYCInfo, KYCStatus, AccountStatus, ChainType, AddressStatus } from '@/domain/value-objects';
|
||||
import { UserAccountEntity } from '../entities/user-account.entity';
|
||||
import { toMpcSignatureString } from '../entities/wallet-address.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserAccountMapper {
|
||||
toDomain(entity: UserAccountEntity): UserAccount {
|
||||
const devices = (entity.devices || []).map((d) => {
|
||||
// 直接使用完整的 deviceInfo JSON,100% 保持原样
|
||||
return new DeviceInfo(
|
||||
d.deviceId,
|
||||
d.deviceName || '未命名设备',
|
||||
d.addedAt,
|
||||
d.lastActiveAt,
|
||||
d.deviceInfo || undefined,
|
||||
);
|
||||
});
|
||||
|
||||
const wallets = (entity.walletAddresses || []).map((w) =>
|
||||
WalletAddress.reconstruct({
|
||||
addressId: w.addressId.toString(),
|
||||
userId: w.userId.toString(),
|
||||
chainType: w.chainType as ChainType,
|
||||
address: w.address,
|
||||
publicKey: w.publicKey,
|
||||
addressDigest: w.addressDigest,
|
||||
mpcSignature: toMpcSignatureString(w), // 64 bytes hex (r + s)
|
||||
status: w.status as AddressStatus,
|
||||
boundAt: w.boundAt,
|
||||
}),
|
||||
);
|
||||
|
||||
const kycInfo =
|
||||
entity.realName && entity.idCardNumber
|
||||
? KYCInfo.create({
|
||||
realName: entity.realName,
|
||||
idCardNumber: entity.idCardNumber,
|
||||
idCardFrontUrl: entity.idCardFrontUrl || '',
|
||||
idCardBackUrl: entity.idCardBackUrl || '',
|
||||
})
|
||||
: null;
|
||||
|
||||
return UserAccount.reconstruct({
|
||||
userId: entity.userId.toString(),
|
||||
accountSequence: Number(entity.accountSequence),
|
||||
devices,
|
||||
phoneNumber: entity.phoneNumber,
|
||||
nickname: entity.nickname,
|
||||
avatarUrl: entity.avatarUrl,
|
||||
inviterSequence: entity.inviterSequence ? Number(entity.inviterSequence) : null,
|
||||
referralCode: entity.referralCode,
|
||||
walletAddresses: wallets,
|
||||
kycInfo,
|
||||
kycStatus: entity.kycStatus as KYCStatus,
|
||||
status: entity.status as AccountStatus,
|
||||
registeredAt: entity.registeredAt,
|
||||
lastLoginAt: entity.lastLoginAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UserAccount } from '@/domain/aggregates/user-account/user-account.aggregate';
|
||||
import { WalletAddress } from '@/domain/entities/wallet-address.entity';
|
||||
import { DeviceInfo, KYCInfo, KYCStatus, AccountStatus, ChainType, AddressStatus } from '@/domain/value-objects';
|
||||
import { UserAccountEntity } from '../entities/user-account.entity';
|
||||
import { toMpcSignatureString } from '../entities/wallet-address.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UserAccountMapper {
|
||||
toDomain(entity: UserAccountEntity): UserAccount {
|
||||
const devices = (entity.devices || []).map((d) => {
|
||||
// 直接使用完整的 deviceInfo JSON,100% 保持原样
|
||||
return new DeviceInfo(
|
||||
d.deviceId,
|
||||
d.deviceName || '未命名设备',
|
||||
d.addedAt,
|
||||
d.lastActiveAt,
|
||||
d.deviceInfo || undefined,
|
||||
);
|
||||
});
|
||||
|
||||
const wallets = (entity.walletAddresses || []).map((w) =>
|
||||
WalletAddress.reconstruct({
|
||||
addressId: w.addressId.toString(),
|
||||
userId: w.userId.toString(),
|
||||
chainType: w.chainType as ChainType,
|
||||
address: w.address,
|
||||
publicKey: w.publicKey,
|
||||
addressDigest: w.addressDigest,
|
||||
mpcSignature: toMpcSignatureString(w), // 64 bytes hex (r + s)
|
||||
status: w.status as AddressStatus,
|
||||
boundAt: w.boundAt,
|
||||
}),
|
||||
);
|
||||
|
||||
const kycInfo =
|
||||
entity.realName && entity.idCardNumber
|
||||
? KYCInfo.create({
|
||||
realName: entity.realName,
|
||||
idCardNumber: entity.idCardNumber,
|
||||
idCardFrontUrl: entity.idCardFrontUrl || '',
|
||||
idCardBackUrl: entity.idCardBackUrl || '',
|
||||
})
|
||||
: null;
|
||||
|
||||
return UserAccount.reconstruct({
|
||||
userId: entity.userId.toString(),
|
||||
accountSequence: entity.accountSequence, // 现在是字符串类型
|
||||
devices,
|
||||
phoneNumber: entity.phoneNumber,
|
||||
nickname: entity.nickname,
|
||||
avatarUrl: entity.avatarUrl,
|
||||
inviterSequence: entity.inviterSequence, // 现在是字符串类型
|
||||
referralCode: entity.referralCode,
|
||||
walletAddresses: wallets,
|
||||
kycInfo,
|
||||
kycStatus: entity.kycStatus as KYCStatus,
|
||||
status: entity.status as AccountStatus,
|
||||
registeredAt: entity.registeredAt,
|
||||
lastLoginAt: entity.lastLoginAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import * as jwt from 'jsonwebtoken';
|
|||
|
||||
export interface StoreBackupShareParams {
|
||||
userId: string;
|
||||
accountSequence: number;
|
||||
accountSequence: string;
|
||||
username: string;
|
||||
publicKey: string;
|
||||
partyId: string;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { firstValueFrom } from 'rxjs';
|
|||
|
||||
export interface DeriveAddressParams {
|
||||
userId: string;
|
||||
accountSequence: number; // 8位账户序列号,用于关联恢复助记词
|
||||
accountSequence: string; // 账户序列号,格式: D + YYMMDD + 5位序号,如 D2512110008
|
||||
publicKey: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue