refactor: use accountSequence as unified user identifier across all services

- planting-service: extract accountSequence from JWT, pass to referral-service
- referral-service: query by accountSequence instead of userId
- reward-service: add accountSequence field to schema and all layers
- wallet-service: prioritize accountSequence lookup over userId
- authorization-service: change userId from String to BigInt, add accountSequence

This change ensures consistent cross-service user identification using
accountSequence (8-digit unique business ID) instead of internal database IDs.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2025-12-10 13:55:03 -08:00
parent 62a21b73a5
commit 034fb53674
41 changed files with 440 additions and 163 deletions

View File

@ -1,30 +1,15 @@
{ {
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" add frontend/mobile-app/lib/features/home/presentation/pages/home_shell_page.dart)", "Bash(git -C c:/Users/dong/Desktop/rwadurian add backend/services/reward-service/prisma/migrations/)",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"$(cat <<''EOF''\nfix(mobile): change update check interval from 24h to 30-90s random\n\nAllows faster detection of urgent updates while preventing excessive\nAPI calls with random cooldown period.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")", "Bash(git -C c:/Users/dong/Desktop/rwadurian commit --amend --no-edit)",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"$(cat <<''EOF''\nfix(mobile): change update check interval to 90-300s random\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")", "Bash(git -C c:/Users/dong/Desktop/rwadurian push)",
"Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" status --short backend/services/blockchain-service/)", "Bash(ssh root@154.204.60.178 \"cd /opt/rwadurian && git pull && docker-compose build blockchain-service && docker-compose up -d blockchain-service\")",
"Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" log --oneline -5)", "Bash(ssh:*)",
"Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" status backend/services/blockchain-service/)", "Bash(cat:*)",
"Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" log --oneline --all -- backend/services/blockchain-service/src/api/controllers/deposit-repair.controller.ts)",
"Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" log --oneline origin/main -10)",
"Bash(npx tsc:*)",
"Bash(flutter analyze:*)",
"Bash(git add:*)", "Bash(git add:*)",
"Bash(git commit:*)", "Bash(git commit:*)",
"Bash(git push:*)", "Bash(git push)"
"Bash(echo:*)",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" add frontend/mobile-app/lib/features/profile/presentation/pages/profile_page.dart)",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" commit -m \"$(cat <<''EOF''\nfix(mobile): reduce direct referral list item spacing\n\n- Row gap: 8px → 4px\n- Vertical padding: 12px → 8px\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
"Bash(git -C \"c:\\Users\\dong\\Desktop\\rwadurian\" push)",
"Bash(git checkout:*)",
"Bash(find:*)",
"Bash(docker exec:*)",
"Bash(npx prisma migrate status)",
"Bash(DATABASE_URL=\"postgresql://rwa_user:your_secure_password_here@localhost:5432/rwa_referral\" npx prisma migrate status:*)",
"Bash(DATABASE_URL=\"postgresql://rwa_user:your_secure_password_here@localhost:5432/rwa_referral\" npx prisma migrate deploy:*)",
"Bash(DATABASE_URL=\"postgresql://rwa_user:your_secure_password_here@localhost:5432/rwa_referral\" npx prisma migrate resolve:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@ -13,7 +13,8 @@ datasource db {
// ============ 授权角色表 ============ // ============ 授权角色表 ============
model AuthorizationRole { model AuthorizationRole {
id String @id @default(uuid()) id String @id @default(uuid())
userId String @map("user_id") userId BigInt @map("user_id")
accountSequence BigInt @map("account_sequence")
roleType RoleType @map("role_type") roleType RoleType @map("role_type")
regionCode String @map("region_code") regionCode String @map("region_code")
regionName String @map("region_name") regionName String @map("region_name")
@ -22,9 +23,9 @@ model AuthorizationRole {
// 授权信息 // 授权信息
authorizedAt DateTime? @map("authorized_at") authorizedAt DateTime? @map("authorized_at")
authorizedBy String? @map("authorized_by") authorizedBy BigInt? @map("authorized_by")
revokedAt DateTime? @map("revoked_at") revokedAt DateTime? @map("revoked_at")
revokedBy String? @map("revoked_by") revokedBy BigInt? @map("revoked_by")
revokeReason String? @map("revoke_reason") revokeReason String? @map("revoke_reason")
// 考核配置 // 考核配置
@ -51,7 +52,8 @@ model AuthorizationRole {
assessments MonthlyAssessment[] assessments MonthlyAssessment[]
bypassRecords MonthlyBypass[] bypassRecords MonthlyBypass[]
@@unique([userId, roleType, regionCode]) @@unique([accountSequence, roleType, regionCode])
@@index([accountSequence])
@@index([userId]) @@index([userId])
@@index([roleType, regionCode]) @@index([roleType, regionCode])
@@index([status]) @@index([status])
@ -63,7 +65,8 @@ model AuthorizationRole {
model MonthlyAssessment { model MonthlyAssessment {
id String @id @default(uuid()) id String @id @default(uuid())
authorizationId String @map("authorization_id") authorizationId String @map("authorization_id")
userId String @map("user_id") userId BigInt @map("user_id")
accountSequence BigInt @map("account_sequence")
roleType RoleType @map("role_type") roleType RoleType @map("role_type")
regionCode String @map("region_code") regionCode String @map("region_code")
@ -98,7 +101,7 @@ model MonthlyAssessment {
// 豁免 // 豁免
isBypassed Boolean @default(false) @map("is_bypassed") isBypassed Boolean @default(false) @map("is_bypassed")
bypassedBy String? @map("bypassed_by") bypassedBy BigInt? @map("bypassed_by")
bypassedAt DateTime? @map("bypassed_at") bypassedAt DateTime? @map("bypassed_at")
// 时间戳 // 时间戳
@ -110,6 +113,7 @@ model MonthlyAssessment {
authorization AuthorizationRole @relation(fields: [authorizationId], references: [id]) authorization AuthorizationRole @relation(fields: [authorizationId], references: [id])
@@unique([authorizationId, assessmentMonth]) @@unique([authorizationId, assessmentMonth])
@@index([accountSequence, assessmentMonth])
@@index([userId, assessmentMonth]) @@index([userId, assessmentMonth])
@@index([roleType, regionCode, assessmentMonth]) @@index([roleType, regionCode, assessmentMonth])
@@index([assessmentMonth, result]) @@index([assessmentMonth, result])
@ -121,21 +125,22 @@ model MonthlyAssessment {
model MonthlyBypass { model MonthlyBypass {
id String @id @default(uuid()) id String @id @default(uuid())
authorizationId String @map("authorization_id") authorizationId String @map("authorization_id")
userId String @map("user_id") userId BigInt @map("user_id")
accountSequence BigInt @map("account_sequence")
roleType RoleType @map("role_type") roleType RoleType @map("role_type")
bypassMonth String @map("bypass_month") // YYYY-MM bypassMonth String @map("bypass_month") // YYYY-MM
// 授权信息 // 授权信息
grantedBy String @map("granted_by") grantedBy BigInt @map("granted_by")
grantedAt DateTime @map("granted_at") grantedAt DateTime @map("granted_at")
reason String? reason String?
// 审批信息(三人授权) // 审批信息(三人授权)
approver1Id String @map("approver1_id") approver1Id BigInt @map("approver1_id")
approver1At DateTime @map("approver1_at") approver1At DateTime @map("approver1_at")
approver2Id String? @map("approver2_id") approver2Id BigInt? @map("approver2_id")
approver2At DateTime? @map("approver2_at") approver2At DateTime? @map("approver2_at")
approver3Id String? @map("approver3_id") approver3Id BigInt? @map("approver3_id")
approver3At DateTime? @map("approver3_at") approver3At DateTime? @map("approver3_at")
approvalStatus ApprovalStatus @default(PENDING) @map("approval_status") approvalStatus ApprovalStatus @default(PENDING) @map("approval_status")
@ -144,6 +149,7 @@ model MonthlyBypass {
authorization AuthorizationRole @relation(fields: [authorizationId], references: [id]) authorization AuthorizationRole @relation(fields: [authorizationId], references: [id])
@@unique([authorizationId, bypassMonth]) @@unique([authorizationId, bypassMonth])
@@index([accountSequence, bypassMonth])
@@index([userId, bypassMonth]) @@index([userId, bypassMonth])
@@map("monthly_bypasses") @@map("monthly_bypasses")
} }
@ -275,7 +281,8 @@ model RegionHeatMap {
// ============ 火柴人排名视图数据表 ============ // ============ 火柴人排名视图数据表 ============
model StickmanRanking { model StickmanRanking {
id String @id @default(uuid()) id String @id @default(uuid())
userId String @map("user_id") userId BigInt @map("user_id")
accountSequence BigInt @map("account_sequence")
authorizationId String @map("authorization_id") authorizationId String @map("authorization_id")
roleType RoleType @map("role_type") roleType RoleType @map("role_type")
regionCode String @map("region_code") regionCode String @map("region_code")
@ -303,6 +310,7 @@ model StickmanRanking {
updatedAt DateTime @updatedAt @map("updated_at") updatedAt DateTime @updatedAt @map("updated_at")
@@unique([authorizationId, currentMonth]) @@unique([authorizationId, currentMonth])
@@index([accountSequence, currentMonth])
@@index([roleType, regionCode, currentMonth]) @@index([roleType, regionCode, currentMonth])
@@map("stickman_rankings") @@map("stickman_rankings")
} }

View File

@ -54,10 +54,10 @@ export class AuthorizationController {
@ApiOperation({ summary: '申请社区授权' }) @ApiOperation({ summary: '申请社区授权' })
@ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) @ApiResponse({ status: 201, type: ApplyAuthorizationResponse })
async applyCommunityAuth( async applyCommunityAuth(
@CurrentUser() user: { userId: string }, @CurrentUser() user: { userId: string; accountSequence: number },
@Body() dto: ApplyCommunityAuthDto, @Body() dto: ApplyCommunityAuthDto,
): Promise<ApplyAuthorizationResponse> { ): Promise<ApplyAuthorizationResponse> {
const command = new ApplyCommunityAuthCommand(user.userId, dto.communityName) const command = new ApplyCommunityAuthCommand(user.userId, user.accountSequence, dto.communityName)
return await this.applicationService.applyCommunityAuth(command) return await this.applicationService.applyCommunityAuth(command)
} }
@ -65,11 +65,12 @@ export class AuthorizationController {
@ApiOperation({ summary: '申请授权省公司' }) @ApiOperation({ summary: '申请授权省公司' })
@ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) @ApiResponse({ status: 201, type: ApplyAuthorizationResponse })
async applyAuthProvinceCompany( async applyAuthProvinceCompany(
@CurrentUser() user: { userId: string }, @CurrentUser() user: { userId: string; accountSequence: number },
@Body() dto: ApplyAuthProvinceDto, @Body() dto: ApplyAuthProvinceDto,
): Promise<ApplyAuthorizationResponse> { ): Promise<ApplyAuthorizationResponse> {
const command = new ApplyAuthProvinceCompanyCommand( const command = new ApplyAuthProvinceCompanyCommand(
user.userId, user.userId,
user.accountSequence,
dto.provinceCode, dto.provinceCode,
dto.provinceName, dto.provinceName,
) )
@ -80,10 +81,10 @@ export class AuthorizationController {
@ApiOperation({ summary: '申请授权市公司' }) @ApiOperation({ summary: '申请授权市公司' })
@ApiResponse({ status: 201, type: ApplyAuthorizationResponse }) @ApiResponse({ status: 201, type: ApplyAuthorizationResponse })
async applyAuthCityCompany( async applyAuthCityCompany(
@CurrentUser() user: { userId: string }, @CurrentUser() user: { userId: string; accountSequence: number },
@Body() dto: ApplyAuthCityDto, @Body() dto: ApplyAuthCityDto,
): Promise<ApplyAuthorizationResponse> { ): Promise<ApplyAuthorizationResponse> {
const command = new ApplyAuthCityCompanyCommand(user.userId, dto.cityCode, dto.cityName) const command = new ApplyAuthCityCompanyCommand(user.userId, user.accountSequence, dto.cityCode, dto.cityName)
return await this.applicationService.applyAuthCityCompany(command) return await this.applicationService.applyAuthCityCompany(command)
} }
@ -91,9 +92,9 @@ export class AuthorizationController {
@ApiOperation({ summary: '获取我的授权列表' }) @ApiOperation({ summary: '获取我的授权列表' })
@ApiResponse({ status: 200, type: [AuthorizationResponse] }) @ApiResponse({ status: 200, type: [AuthorizationResponse] })
async getMyAuthorizations( async getMyAuthorizations(
@CurrentUser() user: { userId: string }, @CurrentUser() user: { userId: string; accountSequence: number },
): Promise<AuthorizationResponse[]> { ): Promise<AuthorizationResponse[]> {
return await this.applicationService.getUserAuthorizations(user.userId) return await this.applicationService.getUserAuthorizations(user.accountSequence)
} }
@Get(':id') @Get(':id')
@ -125,10 +126,10 @@ export class AuthorizationController {
@ApiResponse({ status: 204 }) @ApiResponse({ status: 204 })
async revokeAuthorization( async revokeAuthorization(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: { userId: string }, @CurrentUser() user: { userId: string; accountSequence: number },
@Body() dto: RevokeAuthorizationDto, @Body() dto: RevokeAuthorizationDto,
): Promise<void> { ): Promise<void> {
const command = new RevokeAuthorizationCommand(id, user.userId, dto.reason) const command = new RevokeAuthorizationCommand(id, user.accountSequence, dto.reason)
await this.applicationService.revokeAuthorization(command) await this.applicationService.revokeAuthorization(command)
} }
@ -139,10 +140,10 @@ export class AuthorizationController {
@ApiResponse({ status: 204 }) @ApiResponse({ status: 204 })
async grantMonthlyBypass( async grantMonthlyBypass(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: { userId: string }, @CurrentUser() user: { userId: string; accountSequence: number },
@Body() dto: GrantMonthlyBypassDto, @Body() dto: GrantMonthlyBypassDto,
): Promise<void> { ): Promise<void> {
const command = new GrantMonthlyBypassCommand(id, dto.month, user.userId, dto.reason) const command = new GrantMonthlyBypassCommand(id, dto.month, user.accountSequence, dto.reason)
await this.applicationService.grantMonthlyBypass(command) await this.applicationService.grantMonthlyBypass(command)
} }
@ -153,9 +154,9 @@ export class AuthorizationController {
@ApiResponse({ status: 204 }) @ApiResponse({ status: 204 })
async exemptLocalPercentageCheck( async exemptLocalPercentageCheck(
@Param('id') id: string, @Param('id') id: string,
@CurrentUser() user: { userId: string }, @CurrentUser() user: { userId: string; accountSequence: number },
): Promise<void> { ): Promise<void> {
const command = new ExemptLocalPercentageCheckCommand(id, user.userId) const command = new ExemptLocalPercentageCheckCommand(id, user.accountSequence)
await this.applicationService.exemptLocalPercentageCheck(command) await this.applicationService.exemptLocalPercentageCheck(command)
} }
} }

View File

@ -1,6 +1,7 @@
export class ApplyAuthCityCompanyCommand { export class ApplyAuthCityCompanyCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number,
public readonly cityCode: string, public readonly cityCode: string,
public readonly cityName: string, public readonly cityName: string,
) {} ) {}

View File

@ -1,6 +1,7 @@
export class ApplyAuthProvinceCompanyCommand { export class ApplyAuthProvinceCompanyCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number,
public readonly provinceCode: string, public readonly provinceCode: string,
public readonly provinceName: string, public readonly provinceName: string,
) {} ) {}

View File

@ -1,6 +1,7 @@
export class ApplyCommunityAuthCommand { export class ApplyCommunityAuthCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number,
public readonly communityName: string, public readonly communityName: string,
) {} ) {}
} }

View File

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

View File

@ -1,8 +1,10 @@
export class GrantCityCompanyCommand { export class GrantCityCompanyCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number,
public readonly cityCode: string, public readonly cityCode: string,
public readonly cityName: string, public readonly cityName: string,
public readonly adminId: string, public readonly adminId: string,
public readonly adminAccountSequence: number,
) {} ) {}
} }

View File

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

View File

@ -1,8 +1,10 @@
export class GrantProvinceCompanyCommand { export class GrantProvinceCompanyCommand {
constructor( constructor(
public readonly userId: string, public readonly userId: string,
public readonly accountSequence: number,
public readonly provinceCode: string, public readonly provinceCode: string,
public readonly provinceName: string, public readonly provinceName: string,
public readonly adminId: string, public readonly adminId: string,
public readonly adminAccountSequence: number,
) {} ) {}
} }

View File

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

View File

@ -64,11 +64,11 @@ export class AuthorizationApplicationService {
async applyCommunityAuth( async applyCommunityAuth(
command: ApplyCommunityAuthCommand, command: ApplyCommunityAuthCommand,
): Promise<ApplyCommunityAuthResult> { ): Promise<ApplyCommunityAuthResult> {
const userId = UserId.create(command.userId) const userId = UserId.create(command.userId, command.accountSequence)
// 1. 检查是否已有社区授权 // 1. 检查是否已有社区授权
const existing = await this.authorizationRepository.findByUserIdAndRoleType( const existing = await this.authorizationRepository.findByAccountSequenceAndRoleType(
userId, userId.accountSequence,
RoleType.COMMUNITY, RoleType.COMMUNITY,
) )
@ -83,7 +83,7 @@ export class AuthorizationApplicationService {
}) })
// 3. 检查初始考核10棵 // 3. 检查初始考核10棵
const teamStats = await this.statsRepository.findByUserId(userId.value) const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence)
const totalTreeCount = teamStats?.totalTeamPlantingCount || 0 const totalTreeCount = teamStats?.totalTeamPlantingCount || 0
if (totalTreeCount >= authorization.getInitialTarget()) { if (totalTreeCount >= authorization.getInitialTarget()) {
@ -113,7 +113,7 @@ export class AuthorizationApplicationService {
async applyAuthProvinceCompany( async applyAuthProvinceCompany(
command: ApplyAuthProvinceCompanyCommand, command: ApplyAuthProvinceCompanyCommand,
): Promise<ApplyAuthProvinceCompanyResult> { ): Promise<ApplyAuthProvinceCompanyResult> {
const userId = UserId.create(command.userId) const userId = UserId.create(command.userId, command.accountSequence)
const regionCode = RegionCode.create(command.provinceCode) const regionCode = RegionCode.create(command.provinceCode)
// 1. 验证授权申请(团队内唯一性) // 1. 验证授权申请(团队内唯一性)
@ -137,7 +137,7 @@ export class AuthorizationApplicationService {
}) })
// 3. 检查初始考核500棵 // 3. 检查初始考核500棵
const teamStats = await this.statsRepository.findByUserId(userId.value) const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence)
const totalTreeCount = teamStats?.totalTeamPlantingCount || 0 const totalTreeCount = teamStats?.totalTeamPlantingCount || 0
if (totalTreeCount >= authorization.getInitialTarget()) { if (totalTreeCount >= authorization.getInitialTarget()) {
@ -169,7 +169,7 @@ export class AuthorizationApplicationService {
async applyAuthCityCompany( async applyAuthCityCompany(
command: ApplyAuthCityCompanyCommand, command: ApplyAuthCityCompanyCommand,
): Promise<ApplyAuthCityCompanyResult> { ): Promise<ApplyAuthCityCompanyResult> {
const userId = UserId.create(command.userId) const userId = UserId.create(command.userId, command.accountSequence)
const regionCode = RegionCode.create(command.cityCode) const regionCode = RegionCode.create(command.cityCode)
// 1. 验证 // 1. 验证
@ -193,7 +193,7 @@ export class AuthorizationApplicationService {
}) })
// 3. 检查初始考核100棵 // 3. 检查初始考核100棵
const teamStats = await this.statsRepository.findByUserId(userId.value) const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence)
const totalTreeCount = teamStats?.totalTeamPlantingCount || 0 const totalTreeCount = teamStats?.totalTeamPlantingCount || 0
if (totalTreeCount >= authorization.getInitialTarget()) { if (totalTreeCount >= authorization.getInitialTarget()) {
@ -222,8 +222,8 @@ export class AuthorizationApplicationService {
* *
*/ */
async grantProvinceCompany(command: GrantProvinceCompanyCommand): Promise<void> { async grantProvinceCompany(command: GrantProvinceCompanyCommand): Promise<void> {
const userId = UserId.create(command.userId) const userId = UserId.create(command.userId, command.accountSequence)
const adminId = AdminUserId.create(command.adminId) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence)
const authorization = AuthorizationRole.createProvinceCompany({ const authorization = AuthorizationRole.createProvinceCompany({
userId, userId,
@ -241,8 +241,8 @@ export class AuthorizationApplicationService {
* *
*/ */
async grantCityCompany(command: GrantCityCompanyCommand): Promise<void> { async grantCityCompany(command: GrantCityCompanyCommand): Promise<void> {
const userId = UserId.create(command.userId) const userId = UserId.create(command.userId, command.accountSequence)
const adminId = AdminUserId.create(command.adminId) const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence)
const authorization = AuthorizationRole.createCityCompany({ const authorization = AuthorizationRole.createCityCompany({
userId, userId,
@ -268,7 +268,10 @@ export class AuthorizationApplicationService {
throw new NotFoundError('授权不存在') throw new NotFoundError('授权不存在')
} }
authorization.revoke(AdminUserId.create(command.adminId), command.reason) // Note: We need the adminId from somewhere, for now using a placeholder
// In a real scenario, we would need to fetch the admin's userId from the accountSequence
const adminId = AdminUserId.create('admin', command.adminAccountSequence)
authorization.revoke(adminId, command.reason)
await this.authorizationRepository.save(authorization) await this.authorizationRepository.save(authorization)
await this.eventPublisher.publishAll(authorization.domainEvents) await this.eventPublisher.publishAll(authorization.domainEvents)
@ -288,7 +291,9 @@ export class AuthorizationApplicationService {
throw new NotFoundError('考核记录不存在') throw new NotFoundError('考核记录不存在')
} }
assessment.grantBypass(AdminUserId.create(command.adminId)) // Note: We need the adminId from somewhere, for now using a placeholder
const adminId = AdminUserId.create('admin', command.adminAccountSequence)
assessment.grantBypass(adminId)
await this.assessmentRepository.save(assessment) await this.assessmentRepository.save(assessment)
await this.eventPublisher.publishAll(assessment.domainEvents) await this.eventPublisher.publishAll(assessment.domainEvents)
@ -307,7 +312,9 @@ export class AuthorizationApplicationService {
throw new NotFoundError('授权不存在') throw new NotFoundError('授权不存在')
} }
authorization.exemptLocalPercentageCheck(AdminUserId.create(command.adminId)) // Note: We need the adminId from somewhere, for now using a placeholder
const adminId = AdminUserId.create('admin', command.adminAccountSequence)
authorization.exemptLocalPercentageCheck(adminId)
await this.authorizationRepository.save(authorization) await this.authorizationRepository.save(authorization)
await this.eventPublisher.publishAll(authorization.domainEvents) await this.eventPublisher.publishAll(authorization.domainEvents)
@ -317,13 +324,13 @@ export class AuthorizationApplicationService {
/** /**
* *
*/ */
async getUserAuthorizations(userId: string): Promise<AuthorizationDTO[]> { async getUserAuthorizations(accountSequence: number): Promise<AuthorizationDTO[]> {
const authorizations = await this.authorizationRepository.findByUserId( const authorizations = await this.authorizationRepository.findByAccountSequence(
UserId.create(userId), BigInt(accountSequence),
) )
// 查询用户团队统计数据 // 查询用户团队统计数据
const teamStats = await this.statsRepository.findByUserId(UserId.create(userId).value) const teamStats = await this.statsRepository.findByAccountSequence(BigInt(accountSequence))
const currentTreeCount = teamStats?.totalTeamPlantingCount || 0 const currentTreeCount = teamStats?.totalTeamPlantingCount || 0
return authorizations.map((auth) => this.toAuthorizationDTO(auth, currentTreeCount)) return authorizations.map((auth) => this.toAuthorizationDTO(auth, currentTreeCount))
@ -340,7 +347,7 @@ export class AuthorizationApplicationService {
if (!authorization) return null if (!authorization) return null
// 查询用户团队统计数据 // 查询用户团队统计数据
const teamStats = await this.statsRepository.findByUserId(authorization.userId.value) const teamStats = await this.statsRepository.findByAccountSequence(authorization.userId.accountSequence)
const currentTreeCount = teamStats?.totalTeamPlantingCount || 0 const currentTreeCount = teamStats?.totalTeamPlantingCount || 0
return this.toAuthorizationDTO(authorization, currentTreeCount) return this.toAuthorizationDTO(authorization, currentTreeCount)

View File

@ -559,16 +559,17 @@ export class AuthorizationRole extends AggregateRoot {
toPersistence(): Record<string, any> { toPersistence(): Record<string, any> {
return { return {
id: this._authorizationId.value, id: this._authorizationId.value,
userId: this._userId.value, userId: this._userId.accountSequence,
accountSequence: this._userId.accountSequence,
roleType: this._roleType, roleType: this._roleType,
regionCode: this._regionCode.value, regionCode: this._regionCode.value,
regionName: this._regionName, regionName: this._regionName,
status: this._status, status: this._status,
displayTitle: this._displayTitle, displayTitle: this._displayTitle,
authorizedAt: this._authorizedAt, authorizedAt: this._authorizedAt,
authorizedBy: this._authorizedBy?.value || null, authorizedBy: this._authorizedBy?.accountSequence || null,
revokedAt: this._revokedAt, revokedAt: this._revokedAt,
revokedBy: this._revokedBy?.value || null, revokedBy: this._revokedBy?.accountSequence || null,
revokeReason: this._revokeReason, revokeReason: this._revokeReason,
initialTargetTreeCount: this._assessmentConfig.initialTargetTreeCount, initialTargetTreeCount: this._assessmentConfig.initialTargetTreeCount,
monthlyTargetType: this._assessmentConfig.monthlyTargetType, monthlyTargetType: this._assessmentConfig.monthlyTargetType,

View File

@ -398,7 +398,8 @@ export class MonthlyAssessment extends AggregateRoot {
return { return {
id: this._assessmentId.value, id: this._assessmentId.value,
authorizationId: this._authorizationId.value, authorizationId: this._authorizationId.value,
userId: this._userId.value, userId: this._userId.accountSequence,
accountSequence: this._userId.accountSequence,
roleType: this._roleType, roleType: this._roleType,
regionCode: this._regionCode.value, regionCode: this._regionCode.value,
assessmentMonth: this._assessmentMonth.value, assessmentMonth: this._assessmentMonth.value,
@ -417,7 +418,7 @@ export class MonthlyAssessment extends AggregateRoot {
rankingInRegion: this._rankingInRegion, rankingInRegion: this._rankingInRegion,
isFirstPlace: this._isFirstPlace, isFirstPlace: this._isFirstPlace,
isBypassed: this._isBypassed, isBypassed: this._isBypassed,
bypassedBy: this._bypassedBy?.value || null, bypassedBy: this._bypassedBy?.accountSequence || null,
bypassedAt: this._bypassedAt, bypassedAt: this._bypassedAt,
assessedAt: this._assessedAt, assessedAt: this._assessedAt,
createdAt: this._createdAt, createdAt: this._createdAt,

View File

@ -8,12 +8,14 @@ export interface IAuthorizationRoleRepository {
save(authorization: AuthorizationRole): Promise<void> save(authorization: AuthorizationRole): Promise<void>
findById(authorizationId: AuthorizationId): Promise<AuthorizationRole | null> findById(authorizationId: AuthorizationId): Promise<AuthorizationRole | null>
findByUserIdAndRoleType(userId: UserId, roleType: RoleType): Promise<AuthorizationRole | null> findByUserIdAndRoleType(userId: UserId, roleType: RoleType): Promise<AuthorizationRole | null>
findByAccountSequenceAndRoleType(accountSequence: bigint, roleType: RoleType): Promise<AuthorizationRole | null>
findByUserIdRoleTypeAndRegion( findByUserIdRoleTypeAndRegion(
userId: UserId, userId: UserId,
roleType: RoleType, roleType: RoleType,
regionCode: RegionCode, regionCode: RegionCode,
): Promise<AuthorizationRole | null> ): Promise<AuthorizationRole | null>
findByUserId(userId: UserId): Promise<AuthorizationRole[]> findByUserId(userId: UserId): Promise<AuthorizationRole[]>
findByAccountSequence(accountSequence: bigint): Promise<AuthorizationRole[]>
findActiveByRoleTypeAndRegion( findActiveByRoleTypeAndRegion(
roleType: RoleType, roleType: RoleType,
regionCode: RegionCode, regionCode: RegionCode,

View File

@ -1,18 +1,24 @@
import { DomainError } from '@/shared/exceptions' import { DomainError } from '@/shared/exceptions'
export class UserId { export class UserId {
constructor(public readonly value: string) { constructor(
public readonly value: string,
public readonly accountSequence: bigint,
) {
if (!value) { if (!value) {
throw new DomainError('用户ID不能为空') throw new DomainError('用户ID不能为空')
} }
if (accountSequence === undefined || accountSequence === null) {
throw new DomainError('账户序列号不能为空')
}
} }
static create(value: string): UserId { static create(value: string, accountSequence: number | bigint): UserId {
return new UserId(value) return new UserId(value, BigInt(accountSequence))
} }
equals(other: UserId): boolean { equals(other: UserId): boolean {
return this.value === other.value return this.value === other.value && this.accountSequence === other.accountSequence
} }
toString(): string { toString(): string {
@ -21,18 +27,24 @@ export class UserId {
} }
export class AdminUserId { export class AdminUserId {
constructor(public readonly value: string) { constructor(
public readonly value: string,
public readonly accountSequence: bigint,
) {
if (!value) { if (!value) {
throw new DomainError('管理员ID不能为空') throw new DomainError('管理员ID不能为空')
} }
if (accountSequence === undefined || accountSequence === null) {
throw new DomainError('管理员账户序列号不能为空')
}
} }
static create(value: string): AdminUserId { static create(value: string, accountSequence: number | bigint): AdminUserId {
return new AdminUserId(value) return new AdminUserId(value, BigInt(accountSequence))
} }
equals(other: AdminUserId): boolean { equals(other: AdminUserId): boolean {
return this.value === other.value return this.value === other.value && this.accountSequence === other.accountSequence
} }
toString(): string { toString(): string {

View File

@ -25,6 +25,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
create: { create: {
id: data.id, id: data.id,
userId: data.userId, userId: data.userId,
accountSequence: data.accountSequence,
roleType: data.roleType, roleType: data.roleType,
regionCode: data.regionCode, regionCode: data.regionCode,
regionName: data.regionName, regionName: data.regionName,
@ -97,6 +98,19 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
return record ? this.toDomain(record) : null return record ? this.toDomain(record) : null
} }
async findByAccountSequenceAndRoleType(
accountSequence: bigint,
roleType: RoleType,
): Promise<AuthorizationRole | null> {
const record = await this.prisma.authorizationRole.findFirst({
where: {
accountSequence: accountSequence,
roleType: roleType,
},
})
return record ? this.toDomain(record) : null
}
async findByUserId(userId: UserId): Promise<AuthorizationRole[]> { async findByUserId(userId: UserId): Promise<AuthorizationRole[]> {
const records = await this.prisma.authorizationRole.findMany({ const records = await this.prisma.authorizationRole.findMany({
where: { userId: userId.value }, where: { userId: userId.value },
@ -105,6 +119,14 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
return records.map((record) => this.toDomain(record)) return records.map((record) => this.toDomain(record))
} }
async findByAccountSequence(accountSequence: bigint): Promise<AuthorizationRole[]> {
const records = await this.prisma.authorizationRole.findMany({
where: { accountSequence: accountSequence },
orderBy: { createdAt: 'desc' },
})
return records.map((record) => this.toDomain(record))
}
async findActiveByRoleTypeAndRegion( async findActiveByRoleTypeAndRegion(
roleType: RoleType, roleType: RoleType,
regionCode: RegionCode, regionCode: RegionCode,
@ -157,16 +179,16 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
private toDomain(record: any): AuthorizationRole { private toDomain(record: any): AuthorizationRole {
const props: AuthorizationRoleProps = { const props: AuthorizationRoleProps = {
authorizationId: AuthorizationId.create(record.id), authorizationId: AuthorizationId.create(record.id),
userId: UserId.create(record.userId), userId: UserId.create(record.userId.toString(), record.accountSequence),
roleType: record.roleType as RoleType, roleType: record.roleType as RoleType,
regionCode: RegionCode.create(record.regionCode), regionCode: RegionCode.create(record.regionCode),
regionName: record.regionName, regionName: record.regionName,
status: record.status as AuthorizationStatus, status: record.status as AuthorizationStatus,
displayTitle: record.displayTitle, displayTitle: record.displayTitle,
authorizedAt: record.authorizedAt, authorizedAt: record.authorizedAt,
authorizedBy: record.authorizedBy ? AdminUserId.create(record.authorizedBy) : null, authorizedBy: record.authorizedBy ? AdminUserId.create(record.authorizedBy.toString(), record.authorizedBy) : null,
revokedAt: record.revokedAt, revokedAt: record.revokedAt,
revokedBy: record.revokedBy ? AdminUserId.create(record.revokedBy) : null, revokedBy: record.revokedBy ? AdminUserId.create(record.revokedBy.toString(), record.revokedBy) : null,
revokeReason: record.revokeReason, revokeReason: record.revokeReason,
assessmentConfig: new AssessmentConfig( assessmentConfig: new AssessmentConfig(
record.initialTargetTreeCount, record.initialTargetTreeCount,

View File

@ -27,6 +27,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi
id: data.id, id: data.id,
authorizationId: data.authorizationId, authorizationId: data.authorizationId,
userId: data.userId, userId: data.userId,
accountSequence: data.accountSequence,
roleType: data.roleType, roleType: data.roleType,
regionCode: data.regionCode, regionCode: data.regionCode,
assessmentMonth: data.assessmentMonth, assessmentMonth: data.assessmentMonth,
@ -79,6 +80,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi
id: data.id, id: data.id,
authorizationId: data.authorizationId, authorizationId: data.authorizationId,
userId: data.userId, userId: data.userId,
accountSequence: data.accountSequence,
roleType: data.roleType, roleType: data.roleType,
regionCode: data.regionCode, regionCode: data.regionCode,
assessmentMonth: data.assessmentMonth, assessmentMonth: data.assessmentMonth,
@ -212,7 +214,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi
const props: MonthlyAssessmentProps = { const props: MonthlyAssessmentProps = {
assessmentId: AssessmentId.create(record.id), assessmentId: AssessmentId.create(record.id),
authorizationId: AuthorizationId.create(record.authorizationId), authorizationId: AuthorizationId.create(record.authorizationId),
userId: UserId.create(record.userId), userId: UserId.create(record.userId.toString(), record.accountSequence),
roleType: record.roleType as RoleType, roleType: record.roleType as RoleType,
regionCode: RegionCode.create(record.regionCode), regionCode: RegionCode.create(record.regionCode),
assessmentMonth: Month.create(record.assessmentMonth), assessmentMonth: Month.create(record.assessmentMonth),
@ -231,7 +233,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi
rankingInRegion: record.rankingInRegion, rankingInRegion: record.rankingInRegion,
isFirstPlace: record.isFirstPlace, isFirstPlace: record.isFirstPlace,
isBypassed: record.isBypassed, isBypassed: record.isBypassed,
bypassedBy: record.bypassedBy ? AdminUserId.create(record.bypassedBy) : null, bypassedBy: record.bypassedBy ? AdminUserId.create(record.bypassedBy.toString(), record.bypassedBy) : null,
bypassedAt: record.bypassedAt, bypassedAt: record.bypassedAt,
assessedAt: record.assessedAt, assessedAt: record.assessedAt,
createdAt: record.createdAt, createdAt: record.createdAt,

View File

@ -32,7 +32,7 @@ import {
import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { JwtAuthGuard } from '../guards/jwt-auth.guard';
interface AuthenticatedRequest { interface AuthenticatedRequest {
user: { id: string }; user: { id: string; accountSequence: number };
} }
@ApiTags('认种订单') @ApiTags('认种订单')
@ -124,7 +124,7 @@ export class PlantingOrderController {
@Param('orderNo') orderNo: string, @Param('orderNo') orderNo: string,
): Promise<PayOrderResponse> { ): Promise<PayOrderResponse> {
const userId = BigInt(req.user.id); const userId = BigInt(req.user.id);
return this.plantingService.payOrder(orderNo, userId); return this.plantingService.payOrder(orderNo, userId, req.user.accountSequence);
} }
@Get('orders') @Get('orders')

View File

@ -10,6 +10,7 @@ import * as jwt from 'jsonwebtoken';
export interface JwtPayload { export interface JwtPayload {
sub: string; sub: string;
userId: string; userId: string;
accountSequence: number;
iat: number; iat: number;
exp: number; exp: number;
} }
@ -35,6 +36,7 @@ export class JwtAuthGuard implements CanActivate {
const payload = jwt.verify(token, secret) as JwtPayload; const payload = jwt.verify(token, secret) as JwtPayload;
request.user = { request.user = {
id: payload.userId || payload.sub, id: payload.userId || payload.sub,
accountSequence: payload.accountSequence,
}; };
return true; return true;

View File

@ -173,6 +173,7 @@ export class PlantingApplicationService {
async payOrder( async payOrder(
orderNo: string, orderNo: string,
userId: bigint, userId: bigint,
accountSequence?: number,
): Promise<{ ): Promise<{
orderNo: string; orderNo: string;
status: string; status: string;
@ -208,7 +209,7 @@ export class PlantingApplicationService {
// 3. 获取推荐链上下文 (先获取,确保服务可用) // 3. 获取推荐链上下文 (先获取,确保服务可用)
const referralContext = await this.referralService.getReferralContext( const referralContext = await this.referralService.getReferralContext(
userId.toString(), accountSequence!,
selection.provinceCode, selection.provinceCode,
selection.cityCode, selection.cityCode,
); );

View File

@ -30,14 +30,14 @@ export class ReferralServiceClient {
* *
*/ */
async getReferralContext( async getReferralContext(
userId: string, accountSequence: number,
provinceCode: string, provinceCode: string,
cityCode: string, cityCode: string,
): Promise<ReferralContext> { ): Promise<ReferralContext> {
try { try {
const response = await firstValueFrom( const response = await firstValueFrom(
this.httpService.get<ReferralInfo>( this.httpService.get<ReferralInfo>(
`${this.baseUrl}/api/v1/referrals/${userId}/context`, `${this.baseUrl}/api/v1/referrals/${accountSequence}/context`,
{ {
params: { provinceCode, cityCode }, params: { provinceCode, cityCode },
}, },
@ -52,7 +52,7 @@ export class ReferralServiceClient {
}; };
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
`Failed to get referral context for user ${userId}`, `Failed to get referral context for accountSequence ${accountSequence}`,
error, error,
); );
// 在开发环境返回默认空数据 // 在开发环境返回默认空数据

View File

@ -41,7 +41,7 @@ export class ReferralController {
@ApiOperation({ summary: '获取当前用户推荐信息' }) @ApiOperation({ summary: '获取当前用户推荐信息' })
@ApiResponse({ status: 200, type: ReferralInfoResponseDto }) @ApiResponse({ status: 200, type: ReferralInfoResponseDto })
async getMyReferralInfo(@CurrentUser('userId') userId: bigint): Promise<ReferralInfoResponseDto> { async getMyReferralInfo(@CurrentUser('userId') userId: bigint): Promise<ReferralInfoResponseDto> {
const query = new GetUserReferralInfoQuery(userId); const query = new GetUserReferralInfoQuery(Number(userId));
return this.referralService.getUserReferralInfo(query); return this.referralService.getUserReferralInfo(query);
} }
@ -104,7 +104,7 @@ export class ReferralController {
@ApiParam({ name: 'userId', description: '用户ID' }) @ApiParam({ name: 'userId', description: '用户ID' })
@ApiResponse({ status: 200, type: ReferralInfoResponseDto }) @ApiResponse({ status: 200, type: ReferralInfoResponseDto })
async getUserReferralInfo(@Param('userId') userId: string): Promise<ReferralInfoResponseDto> { async getUserReferralInfo(@Param('userId') userId: string): Promise<ReferralInfoResponseDto> {
const query = new GetUserReferralInfoQuery(BigInt(userId)); const query = new GetUserReferralInfoQuery(Number(userId));
return this.referralService.getUserReferralInfo(query); return this.referralService.getUserReferralInfo(query);
} }
} }
@ -118,23 +118,23 @@ export class ReferralController {
export class InternalReferralController { export class InternalReferralController {
constructor(private readonly referralService: ReferralService) {} constructor(private readonly referralService: ReferralService) {}
@Get(':userId/context') @Get(':accountSequence/context')
@ApiOperation({ summary: '获取用户推荐上下文信息(内部API)' }) @ApiOperation({ summary: '获取用户推荐上下文信息(内部API)' })
@ApiParam({ name: 'userId', description: '用户ID' }) @ApiParam({ name: 'accountSequence', description: '账户序列号' })
@ApiResponse({ status: 200, description: '推荐上下文' }) @ApiResponse({ status: 200, description: '推荐上下文' })
async getReferralContext( async getReferralContext(
@Param('userId') userId: string, @Param('accountSequence') accountSequence: string,
@Query('provinceCode') provinceCode: string, @Query('provinceCode') provinceCode: string,
@Query('cityCode') cityCode: string, @Query('cityCode') cityCode: string,
) { ) {
// 获取用户的推荐链 // 获取用户的推荐链
const query = new GetUserReferralInfoQuery(BigInt(userId)); const query = new GetUserReferralInfoQuery(Number(accountSequence));
const referralInfo = await this.referralService.getUserReferralInfo(query); const referralInfo = await this.referralService.getUserReferralInfo(query);
// 返回推荐上下文信息 // 返回推荐上下文信息
// 目前返回基础信息,后续可以扩展省市授权等信息 // 目前返回基础信息,后续可以扩展省市授权等信息
return { return {
userId, accountSequence,
referralChain: referralInfo.referrerId ? [referralInfo.referrerId] : [], referralChain: referralInfo.referrerId ? [referralInfo.referrerId] : [],
referrerId: referralInfo.referrerId, referrerId: referralInfo.referrerId,
nearestProvinceAuth: null, // 省代账户ID - 需要后续实现 nearestProvinceAuth: null, // 省代账户ID - 需要后续实现

View File

@ -1,5 +1,5 @@
export class GetUserReferralInfoQuery { export class GetUserReferralInfoQuery {
constructor(public readonly userId: bigint) {} constructor(public readonly accountSequence: number) {}
} }
export interface UserReferralInfoResult { export interface UserReferralInfoResult {

View File

@ -114,12 +114,12 @@ export class ReferralService {
* *
*/ */
async getUserReferralInfo(query: GetUserReferralInfoQuery): Promise<UserReferralInfoResult> { async getUserReferralInfo(query: GetUserReferralInfoQuery): Promise<UserReferralInfoResult> {
const relationship = await this.referralRepo.findByUserId(query.userId); const relationship = await this.referralRepo.findByAccountSequence(query.accountSequence);
if (!relationship) { if (!relationship) {
throw new NotFoundException('用户推荐关系不存在'); throw new NotFoundException('用户推荐关系不存在');
} }
const teamStats = await this.teamStatsRepo.findByUserId(query.userId); const teamStats = await this.teamStatsRepo.findByUserId(relationship.userId);
return { return {
userId: relationship.userId.toString(), userId: relationship.userId.toString(),

View File

@ -14,6 +14,7 @@ datasource db {
model RewardLedgerEntry { model RewardLedgerEntry {
id BigInt @id @default(autoincrement()) @map("entry_id") id BigInt @id @default(autoincrement()) @map("entry_id")
userId BigInt @map("user_id") // 接收奖励的用户ID userId BigInt @map("user_id") // 接收奖励的用户ID
accountSequence BigInt @map("account_sequence") // 账户序列号
// === 奖励来源 === // === 奖励来源 ===
sourceOrderNo String @map("source_order_no") @db.VarChar(50) // 来源认种订单号(字符串格式如PLT1765391584505Q0Q6QD) sourceOrderNo String @map("source_order_no") @db.VarChar(50) // 来源认种订单号(字符串格式如PLT1765391584505Q0Q6QD)
@ -40,6 +41,8 @@ model RewardLedgerEntry {
@@map("reward_ledger_entries") @@map("reward_ledger_entries")
@@index([userId, rewardStatus], name: "idx_user_status") @@index([userId, rewardStatus], name: "idx_user_status")
@@index([userId, createdAt(sort: Desc)], name: "idx_user_created") @@index([userId, createdAt(sort: Desc)], name: "idx_user_created")
@@index([accountSequence, rewardStatus], name: "idx_account_status")
@@index([accountSequence, createdAt(sort: Desc)], name: "idx_account_created")
@@index([sourceOrderNo], name: "idx_source_order") @@index([sourceOrderNo], name: "idx_source_order")
@@index([sourceUserId], name: "idx_source_user") @@index([sourceUserId], name: "idx_source_user")
@@index([rightType], name: "idx_right_type") @@index([rightType], name: "idx_right_type")
@ -55,6 +58,7 @@ model RewardLedgerEntry {
model RewardSummary { model RewardSummary {
id BigInt @id @default(autoincrement()) @map("summary_id") id BigInt @id @default(autoincrement()) @map("summary_id")
userId BigInt @unique @map("user_id") userId BigInt @unique @map("user_id")
accountSequence BigInt @unique @map("account_sequence") // 账户序列号
// === 待领取收益 (24h倒计时) === // === 待领取收益 (24h倒计时) ===
pendingUsdt Decimal @default(0) @map("pending_usdt") @db.Decimal(20, 8) pendingUsdt Decimal @default(0) @map("pending_usdt") @db.Decimal(20, 8)
@ -79,6 +83,7 @@ model RewardSummary {
@@map("reward_summaries") @@map("reward_summaries")
@@index([userId], name: "idx_summary_user") @@index([userId], name: "idx_summary_user")
@@index([accountSequence], name: "idx_summary_account")
@@index([settleableUsdt(sort: Desc)], name: "idx_settleable_desc") @@index([settleableUsdt(sort: Desc)], name: "idx_settleable_desc")
@@index([pendingExpireAt], name: "idx_pending_expire") @@index([pendingExpireAt], name: "idx_pending_expire")
} }
@ -119,6 +124,7 @@ model RightDefinition {
model SettlementRecord { model SettlementRecord {
id BigInt @id @default(autoincrement()) @map("settlement_id") id BigInt @id @default(autoincrement()) @map("settlement_id")
userId BigInt @map("user_id") userId BigInt @map("user_id")
accountSequence BigInt @map("account_sequence") // 账户序列号
// === 结算金额 === // === 结算金额 ===
usdtAmount Decimal @map("usdt_amount") @db.Decimal(20, 8) usdtAmount Decimal @map("usdt_amount") @db.Decimal(20, 8)
@ -144,6 +150,7 @@ model SettlementRecord {
@@map("settlement_records") @@map("settlement_records")
@@index([userId], name: "idx_settlement_user") @@index([userId], name: "idx_settlement_user")
@@index([accountSequence], name: "idx_settlement_account")
@@index([status], name: "idx_settlement_status") @@index([status], name: "idx_settlement_status")
@@index([createdAt], name: "idx_settlement_created") @@index([createdAt], name: "idx_settlement_created")
} }

View File

@ -18,8 +18,8 @@ export class RewardController {
@ApiOperation({ summary: '获取我的收益汇总' }) @ApiOperation({ summary: '获取我的收益汇总' })
@ApiResponse({ status: 200, description: '成功', type: RewardSummaryDto }) @ApiResponse({ status: 200, description: '成功', type: RewardSummaryDto })
async getSummary(@Request() req): Promise<RewardSummaryDto> { async getSummary(@Request() req): Promise<RewardSummaryDto> {
const userId = BigInt(req.user.sub); const accountSequence = BigInt(req.user.accountSequence);
const summary = await this.rewardService.getRewardSummary(userId); const summary = await this.rewardService.getRewardSummary(accountSequence);
return { return {
...summary, ...summary,
@ -43,19 +43,19 @@ export class RewardController {
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number = 1, @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number = 1,
@Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number = 20, @Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number = 20,
) { ) {
const userId = BigInt(req.user.sub); const accountSequence = BigInt(req.user.accountSequence);
const filters: any = {}; const filters: any = {};
if (status) filters.status = status; if (status) filters.status = status;
if (rightType) filters.rightType = rightType; if (rightType) filters.rightType = rightType;
return this.rewardService.getRewardDetails(userId, filters, { page, pageSize }); return this.rewardService.getRewardDetails(accountSequence, filters, { page, pageSize });
} }
@Get('pending') @Get('pending')
@ApiOperation({ summary: '获取待领取奖励(含倒计时)' }) @ApiOperation({ summary: '获取待领取奖励(含倒计时)' })
@ApiResponse({ status: 200, description: '成功' }) @ApiResponse({ status: 200, description: '成功' })
async getPending(@Request() req) { async getPending(@Request() req) {
const userId = BigInt(req.user.sub); const accountSequence = BigInt(req.user.accountSequence);
return this.rewardService.getPendingRewards(userId); return this.rewardService.getPendingRewards(accountSequence);
} }
} }

View File

@ -20,10 +20,10 @@ export class SettlementController {
@Request() req, @Request() req,
@Body() dto: SettleRewardsDto, @Body() dto: SettleRewardsDto,
): Promise<SettlementResultDto> { ): Promise<SettlementResultDto> {
const userId = BigInt(req.user.sub); const accountSequence = BigInt(req.user.accountSequence);
return this.rewardService.settleRewards({ return this.rewardService.settleRewards({
userId, accountSequence,
settleCurrency: dto.settleCurrency, settleCurrency: dto.settleCurrency,
}); });
} }

View File

@ -120,7 +120,7 @@ export class RewardApplicationService {
* *
*/ */
async settleRewards(params: { async settleRewards(params: {
userId: bigint; accountSequence: bigint;
settleCurrency: string; // BNB/OG/USDT/DST settleCurrency: string; // BNB/OG/USDT/DST
}): Promise<{ }): Promise<{
success: boolean; success: boolean;
@ -130,10 +130,10 @@ export class RewardApplicationService {
txHash?: string; txHash?: string;
error?: string; error?: string;
}> { }> {
this.logger.log(`Settling rewards for user ${params.userId}`); this.logger.log(`Settling rewards for accountSequence ${params.accountSequence}`);
// 1. 获取可结算奖励 // 1. 获取可结算奖励
const settleableRewards = await this.rewardLedgerEntryRepository.findSettleableByUserId(params.userId); const settleableRewards = await this.rewardLedgerEntryRepository.findSettleableByAccountSequence(params.accountSequence);
if (settleableRewards.length === 0) { if (settleableRewards.length === 0) {
return { return {
@ -149,9 +149,10 @@ export class RewardApplicationService {
const totalUsdt = settleableRewards.reduce((sum, r) => sum + r.usdtAmount.amount, 0); const totalUsdt = settleableRewards.reduce((sum, r) => sum + r.usdtAmount.amount, 0);
const totalHashpower = settleableRewards.reduce((sum, r) => sum + r.hashpowerAmount.value, 0); const totalHashpower = settleableRewards.reduce((sum, r) => sum + r.hashpowerAmount.value, 0);
// 3. 调用钱包服务执行SWAP // 3. 调用钱包服务执行SWAP (使用第一条记录的userId)
const userId = settleableRewards[0].userId;
const swapResult = await this.walletService.executeSwap({ const swapResult = await this.walletService.executeSwap({
userId: params.userId, userId: userId,
usdtAmount: totalUsdt, usdtAmount: totalUsdt,
targetCurrency: params.settleCurrency, targetCurrency: params.settleCurrency,
}); });
@ -175,11 +176,13 @@ export class RewardApplicationService {
} }
// 5. 更新汇总数据 // 5. 更新汇总数据
const summary = await this.rewardSummaryRepository.getOrCreate(params.userId); const summary = await this.rewardSummaryRepository.findByAccountSequence(params.accountSequence);
summary.settle(Money.USDT(totalUsdt), Hashpower.create(totalHashpower)); if (summary) {
await this.rewardSummaryRepository.save(summary); summary.settle(Money.USDT(totalUsdt), Hashpower.create(totalHashpower));
await this.rewardSummaryRepository.save(summary);
}
this.logger.log(`Settled ${totalUsdt} USDT for user ${params.userId}`); this.logger.log(`Settled ${totalUsdt} USDT for accountSequence ${params.accountSequence}`);
return { return {
success: true, success: true,
@ -251,8 +254,8 @@ export class RewardApplicationService {
/** /**
* *
*/ */
async getRewardSummary(userId: bigint) { async getRewardSummary(accountSequence: bigint) {
const summary = await this.rewardSummaryRepository.findByUserId(userId); const summary = await this.rewardSummaryRepository.findByAccountSequence(accountSequence);
if (!summary) { if (!summary) {
return { return {
@ -285,7 +288,7 @@ export class RewardApplicationService {
* *
*/ */
async getRewardDetails( async getRewardDetails(
userId: bigint, accountSequence: bigint,
filters?: { filters?: {
status?: RewardStatus; status?: RewardStatus;
rightType?: RightType; rightType?: RightType;
@ -294,8 +297,8 @@ export class RewardApplicationService {
}, },
pagination?: { page: number; pageSize: number }, pagination?: { page: number; pageSize: number },
) { ) {
const rewards = await this.rewardLedgerEntryRepository.findByUserId(userId, filters, pagination); const rewards = await this.rewardLedgerEntryRepository.findByAccountSequence(accountSequence, filters, pagination);
const total = await this.rewardLedgerEntryRepository.countByUserId(userId, filters?.status); const total = await this.rewardLedgerEntryRepository.countByAccountSequence(accountSequence, filters?.status);
return { return {
data: rewards.map(r => ({ data: rewards.map(r => ({
@ -323,8 +326,8 @@ export class RewardApplicationService {
/** /**
* *
*/ */
async getPendingRewards(userId: bigint) { async getPendingRewards(accountSequence: bigint) {
const rewards = await this.rewardLedgerEntryRepository.findPendingByUserId(userId); const rewards = await this.rewardLedgerEntryRepository.findPendingByAccountSequence(accountSequence);
return rewards.map(r => ({ return rewards.map(r => ({
id: r.id?.toString(), id: r.id?.toString(),

View File

@ -20,6 +20,7 @@ import { Hashpower } from '../../value-objects/hashpower.vo';
export class RewardLedgerEntry { export class RewardLedgerEntry {
private _id: bigint | null = null; private _id: bigint | null = null;
private readonly _userId: bigint; private readonly _userId: bigint;
private readonly _accountSequence: bigint;
private readonly _rewardSource: RewardSource; private readonly _rewardSource: RewardSource;
private readonly _usdtAmount: Money; private readonly _usdtAmount: Money;
private readonly _hashpowerAmount: Hashpower; private readonly _hashpowerAmount: Hashpower;
@ -35,6 +36,7 @@ export class RewardLedgerEntry {
private constructor( private constructor(
userId: bigint, userId: bigint,
accountSequence: bigint,
rewardSource: RewardSource, rewardSource: RewardSource,
usdtAmount: Money, usdtAmount: Money,
hashpowerAmount: Hashpower, hashpowerAmount: Hashpower,
@ -44,6 +46,7 @@ export class RewardLedgerEntry {
memo: string, memo: string,
) { ) {
this._userId = userId; this._userId = userId;
this._accountSequence = accountSequence;
this._rewardSource = rewardSource; this._rewardSource = rewardSource;
this._usdtAmount = usdtAmount; this._usdtAmount = usdtAmount;
this._hashpowerAmount = hashpowerAmount; this._hashpowerAmount = hashpowerAmount;
@ -59,6 +62,7 @@ export class RewardLedgerEntry {
// ============ Getters ============ // ============ Getters ============
get id(): bigint | null { return this._id; } get id(): bigint | null { return this._id; }
get userId(): bigint { return this._userId; } get userId(): bigint { return this._userId; }
get accountSequence(): bigint { return this._accountSequence; }
get rewardSource(): RewardSource { return this._rewardSource; } get rewardSource(): RewardSource { return this._rewardSource; }
get usdtAmount(): Money { return this._usdtAmount; } get usdtAmount(): Money { return this._usdtAmount; }
get hashpowerAmount(): Hashpower { return this._hashpowerAmount; } get hashpowerAmount(): Hashpower { return this._hashpowerAmount; }
@ -84,6 +88,7 @@ export class RewardLedgerEntry {
*/ */
static createPending(params: { static createPending(params: {
userId: bigint; userId: bigint;
accountSequence: bigint;
rewardSource: RewardSource; rewardSource: RewardSource;
usdtAmount: Money; usdtAmount: Money;
hashpowerAmount: Hashpower; hashpowerAmount: Hashpower;
@ -94,6 +99,7 @@ export class RewardLedgerEntry {
const entry = new RewardLedgerEntry( const entry = new RewardLedgerEntry(
params.userId, params.userId,
params.accountSequence,
params.rewardSource, params.rewardSource,
params.usdtAmount, params.usdtAmount,
params.hashpowerAmount, params.hashpowerAmount,
@ -124,6 +130,7 @@ export class RewardLedgerEntry {
*/ */
static createSettleable(params: { static createSettleable(params: {
userId: bigint; userId: bigint;
accountSequence: bigint;
rewardSource: RewardSource; rewardSource: RewardSource;
usdtAmount: Money; usdtAmount: Money;
hashpowerAmount: Hashpower; hashpowerAmount: Hashpower;
@ -131,6 +138,7 @@ export class RewardLedgerEntry {
}): RewardLedgerEntry { }): RewardLedgerEntry {
const entry = new RewardLedgerEntry( const entry = new RewardLedgerEntry(
params.userId, params.userId,
params.accountSequence,
params.rewardSource, params.rewardSource,
params.usdtAmount, params.usdtAmount,
params.hashpowerAmount, params.hashpowerAmount,
@ -252,6 +260,7 @@ export class RewardLedgerEntry {
static reconstitute(data: { static reconstitute(data: {
id: bigint; id: bigint;
userId: bigint; userId: bigint;
accountSequence: bigint;
rewardSource: RewardSource; rewardSource: RewardSource;
usdtAmount: number; usdtAmount: number;
hashpowerAmount: number; hashpowerAmount: number;
@ -265,6 +274,7 @@ export class RewardLedgerEntry {
}): RewardLedgerEntry { }): RewardLedgerEntry {
const entry = new RewardLedgerEntry( const entry = new RewardLedgerEntry(
data.userId, data.userId,
data.accountSequence,
data.rewardSource, data.rewardSource,
Money.USDT(data.usdtAmount), Money.USDT(data.usdtAmount),
Hashpower.create(data.hashpowerAmount), Hashpower.create(data.hashpowerAmount),

View File

@ -8,6 +8,7 @@ import { Hashpower } from '../../value-objects/hashpower.vo';
export class RewardSummary { export class RewardSummary {
private _id: bigint | null = null; private _id: bigint | null = null;
private readonly _userId: bigint; private readonly _userId: bigint;
private readonly _accountSequence: bigint;
// 待领取收益 // 待领取收益
private _pendingUsdt: Money; private _pendingUsdt: Money;
@ -29,8 +30,9 @@ export class RewardSummary {
private _lastUpdateAt: Date; private _lastUpdateAt: Date;
private readonly _createdAt: Date; private readonly _createdAt: Date;
private constructor(userId: bigint) { private constructor(userId: bigint, accountSequence: bigint) {
this._userId = userId; this._userId = userId;
this._accountSequence = accountSequence;
this._pendingUsdt = Money.zero(); this._pendingUsdt = Money.zero();
this._pendingHashpower = Hashpower.zero(); this._pendingHashpower = Hashpower.zero();
this._pendingExpireAt = null; this._pendingExpireAt = null;
@ -47,6 +49,7 @@ export class RewardSummary {
// ============ Getters ============ // ============ Getters ============
get id(): bigint | null { return this._id; } get id(): bigint | null { return this._id; }
get userId(): bigint { return this._userId; } get userId(): bigint { return this._userId; }
get accountSequence(): bigint { return this._accountSequence; }
get pendingUsdt(): Money { return this._pendingUsdt; } get pendingUsdt(): Money { return this._pendingUsdt; }
get pendingHashpower(): Hashpower { return this._pendingHashpower; } get pendingHashpower(): Hashpower { return this._pendingHashpower; }
get pendingExpireAt(): Date | null { return this._pendingExpireAt; } get pendingExpireAt(): Date | null { return this._pendingExpireAt; }
@ -61,8 +64,8 @@ export class RewardSummary {
// ============ 工厂方法 ============ // ============ 工厂方法 ============
static create(userId: bigint): RewardSummary { static create(userId: bigint, accountSequence: bigint): RewardSummary {
return new RewardSummary(userId); return new RewardSummary(userId, accountSequence);
} }
// ============ 领域行为 ============ // ============ 领域行为 ============
@ -140,6 +143,7 @@ export class RewardSummary {
static reconstitute(data: { static reconstitute(data: {
id: bigint; id: bigint;
userId: bigint; userId: bigint;
accountSequence: bigint;
pendingUsdt: number; pendingUsdt: number;
pendingHashpower: number; pendingHashpower: number;
pendingExpireAt: Date | null; pendingExpireAt: Date | null;
@ -152,7 +156,7 @@ export class RewardSummary {
lastUpdateAt: Date; lastUpdateAt: Date;
createdAt: Date; createdAt: Date;
}): RewardSummary { }): RewardSummary {
const summary = new RewardSummary(data.userId); const summary = new RewardSummary(data.userId, data.accountSequence);
summary._id = data.id; summary._id = data.id;
summary._pendingUsdt = Money.USDT(data.pendingUsdt); summary._pendingUsdt = Money.USDT(data.pendingUsdt);
summary._pendingHashpower = Hashpower.create(data.pendingHashpower); summary._pendingHashpower = Hashpower.create(data.pendingHashpower);

View File

@ -16,11 +16,24 @@ export interface IRewardLedgerEntryRepository {
}, },
pagination?: { page: number; pageSize: number }, pagination?: { page: number; pageSize: number },
): Promise<RewardLedgerEntry[]>; ): Promise<RewardLedgerEntry[]>;
findByAccountSequence(
accountSequence: bigint,
filters?: {
status?: RewardStatus;
rightType?: RightType;
startDate?: Date;
endDate?: Date;
},
pagination?: { page: number; pageSize: number },
): Promise<RewardLedgerEntry[]>;
findPendingByUserId(userId: bigint): Promise<RewardLedgerEntry[]>; findPendingByUserId(userId: bigint): Promise<RewardLedgerEntry[]>;
findPendingByAccountSequence(accountSequence: bigint): Promise<RewardLedgerEntry[]>;
findSettleableByUserId(userId: bigint): Promise<RewardLedgerEntry[]>; findSettleableByUserId(userId: bigint): Promise<RewardLedgerEntry[]>;
findSettleableByAccountSequence(accountSequence: bigint): Promise<RewardLedgerEntry[]>;
findExpiredPending(beforeDate: Date): Promise<RewardLedgerEntry[]>; findExpiredPending(beforeDate: Date): Promise<RewardLedgerEntry[]>;
findBySourceOrderNo(sourceOrderNo: string): Promise<RewardLedgerEntry[]>; findBySourceOrderNo(sourceOrderNo: string): Promise<RewardLedgerEntry[]>;
countByUserId(userId: bigint, status?: RewardStatus): Promise<number>; countByUserId(userId: bigint, status?: RewardStatus): Promise<number>;
countByAccountSequence(accountSequence: bigint, status?: RewardStatus): Promise<number>;
} }
export const REWARD_LEDGER_ENTRY_REPOSITORY = Symbol('IRewardLedgerEntryRepository'); export const REWARD_LEDGER_ENTRY_REPOSITORY = Symbol('IRewardLedgerEntryRepository');

View File

@ -3,7 +3,9 @@ import { RewardSummary } from '../aggregates/reward-summary/reward-summary.aggre
export interface IRewardSummaryRepository { export interface IRewardSummaryRepository {
save(summary: RewardSummary): Promise<void>; save(summary: RewardSummary): Promise<void>;
findByUserId(userId: bigint): Promise<RewardSummary | null>; findByUserId(userId: bigint): Promise<RewardSummary | null>;
findByAccountSequence(accountSequence: bigint): Promise<RewardSummary | null>;
getOrCreate(userId: bigint): Promise<RewardSummary>; getOrCreate(userId: bigint): Promise<RewardSummary>;
getOrCreateByAccountSequence(accountSequence: bigint): Promise<RewardSummary>;
findByUserIds(userIds: bigint[]): Promise<Map<string, RewardSummary>>; findByUserIds(userIds: bigint[]): Promise<Map<string, RewardSummary>>;
findTopSettleableUsers(limit: number): Promise<RewardSummary[]>; findTopSettleableUsers(limit: number): Promise<RewardSummary[]>;
} }

View File

@ -9,6 +9,7 @@ export class RewardLedgerEntryMapper {
return RewardLedgerEntry.reconstitute({ return RewardLedgerEntry.reconstitute({
id: raw.id, id: raw.id,
userId: raw.userId, userId: raw.userId,
accountSequence: raw.accountSequence,
rewardSource: RewardSource.create( rewardSource: RewardSource.create(
raw.rightType as RightType, raw.rightType as RightType,
raw.sourceOrderNo, raw.sourceOrderNo,
@ -30,6 +31,7 @@ export class RewardLedgerEntryMapper {
return { return {
id: entry.id || undefined, id: entry.id || undefined,
userId: entry.userId, userId: entry.userId,
accountSequence: entry.accountSequence,
sourceOrderNo: entry.rewardSource.sourceOrderNo, sourceOrderNo: entry.rewardSource.sourceOrderNo,
sourceUserId: entry.rewardSource.sourceUserId, sourceUserId: entry.rewardSource.sourceUserId,
rightType: entry.rewardSource.rightType, rightType: entry.rewardSource.rightType,

View File

@ -6,6 +6,7 @@ export class RewardSummaryMapper {
return RewardSummary.reconstitute({ return RewardSummary.reconstitute({
id: raw.id, id: raw.id,
userId: raw.userId, userId: raw.userId,
accountSequence: raw.accountSequence,
pendingUsdt: Number(raw.pendingUsdt), pendingUsdt: Number(raw.pendingUsdt),
pendingHashpower: Number(raw.pendingHashpower), pendingHashpower: Number(raw.pendingHashpower),
pendingExpireAt: raw.pendingExpireAt, pendingExpireAt: raw.pendingExpireAt,
@ -24,6 +25,7 @@ export class RewardSummaryMapper {
return { return {
id: summary.id || undefined, id: summary.id || undefined,
userId: summary.userId, userId: summary.userId,
accountSequence: summary.accountSequence,
pendingUsdt: new Prisma.Decimal(summary.pendingUsdt.amount), pendingUsdt: new Prisma.Decimal(summary.pendingUsdt.amount),
pendingHashpower: new Prisma.Decimal(summary.pendingHashpower.value), pendingHashpower: new Prisma.Decimal(summary.pendingHashpower.value),
pendingExpireAt: summary.pendingExpireAt, pendingExpireAt: summary.pendingExpireAt,

View File

@ -29,6 +29,7 @@ export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryReposi
const created = await this.prisma.rewardLedgerEntry.create({ const created = await this.prisma.rewardLedgerEntry.create({
data: { data: {
userId: data.userId, userId: data.userId,
accountSequence: data.accountSequence,
sourceOrderNo: data.sourceOrderNo, sourceOrderNo: data.sourceOrderNo,
sourceUserId: data.sourceUserId, sourceUserId: data.sourceUserId,
rightType: data.rightType, rightType: data.rightType,
@ -152,4 +153,77 @@ export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryReposi
} }
return this.prisma.rewardLedgerEntry.count({ where }); return this.prisma.rewardLedgerEntry.count({ where });
} }
async findByAccountSequence(
accountSequence: bigint,
filters?: {
status?: RewardStatus;
rightType?: RightType;
startDate?: Date;
endDate?: Date;
},
pagination?: { page: number; pageSize: number },
): Promise<RewardLedgerEntry[]> {
const where: any = { accountSequence };
if (filters?.status) {
where.rewardStatus = filters.status;
}
if (filters?.rightType) {
where.rightType = filters.rightType;
}
if (filters?.startDate || filters?.endDate) {
where.createdAt = {};
if (filters.startDate) {
where.createdAt.gte = filters.startDate;
}
if (filters.endDate) {
where.createdAt.lte = filters.endDate;
}
}
const skip = pagination ? (pagination.page - 1) * pagination.pageSize : undefined;
const take = pagination?.pageSize;
const rawList = await this.prisma.rewardLedgerEntry.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take,
});
return rawList.map(RewardLedgerEntryMapper.toDomain);
}
async findPendingByAccountSequence(accountSequence: bigint): Promise<RewardLedgerEntry[]> {
const rawList = await this.prisma.rewardLedgerEntry.findMany({
where: {
accountSequence,
rewardStatus: RewardStatus.PENDING,
},
orderBy: { createdAt: 'desc' },
});
return rawList.map(RewardLedgerEntryMapper.toDomain);
}
async findSettleableByAccountSequence(accountSequence: bigint): Promise<RewardLedgerEntry[]> {
const rawList = await this.prisma.rewardLedgerEntry.findMany({
where: {
accountSequence,
rewardStatus: RewardStatus.SETTLEABLE,
},
orderBy: { createdAt: 'desc' },
});
return rawList.map(RewardLedgerEntryMapper.toDomain);
}
async countByAccountSequence(accountSequence: bigint, status?: RewardStatus): Promise<number> {
const where: any = { accountSequence };
if (status) {
where.rewardStatus = status;
}
return this.prisma.rewardLedgerEntry.count({ where });
}
} }

View File

@ -30,6 +30,7 @@ export class RewardSummaryRepositoryImpl implements IRewardSummaryRepository {
const created = await this.prisma.rewardSummary.create({ const created = await this.prisma.rewardSummary.create({
data: { data: {
userId: data.userId, userId: data.userId,
accountSequence: data.accountSequence,
pendingUsdt: data.pendingUsdt, pendingUsdt: data.pendingUsdt,
pendingHashpower: data.pendingHashpower, pendingHashpower: data.pendingHashpower,
pendingExpireAt: data.pendingExpireAt, pendingExpireAt: data.pendingExpireAt,
@ -88,4 +89,22 @@ export class RewardSummaryRepositoryImpl implements IRewardSummaryRepository {
return rawList.map(RewardSummaryMapper.toDomain); return rawList.map(RewardSummaryMapper.toDomain);
} }
async findByAccountSequence(accountSequence: bigint): Promise<RewardSummary | null> {
const raw = await this.prisma.rewardSummary.findUnique({
where: { accountSequence },
});
return raw ? RewardSummaryMapper.toDomain(raw) : null;
}
async getOrCreateByAccountSequence(accountSequence: bigint): Promise<RewardSummary> {
const existing = await this.findByAccountSequence(accountSequence);
if (existing) {
return existing;
}
// Need to find userId by accountSequence - this requires user service integration
// For now, we'll throw an error indicating this needs to be implemented
throw new Error('getOrCreateByAccountSequence requires userId mapping - use getOrCreate with userId instead');
}
} }

View File

@ -18,6 +18,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
sub: payload.sub, sub: payload.sub,
username: payload.username, username: payload.username,
roles: payload.roles, roles: payload.roles,
accountSequence: payload.accountSequence,
}; };
} }
} }

View File

@ -26,9 +26,10 @@ export class InternalWalletController {
@Get(':userId/balance') @Get(':userId/balance')
@Public() @Public()
@ApiOperation({ summary: '获取用户钱包余额(内部API)' }) @ApiOperation({ summary: '获取用户钱包余额(内部API)' })
@ApiParam({ name: 'userId', description: '用户ID' }) @ApiParam({ name: 'userId', description: '用户ID或accountSequence' })
@ApiResponse({ status: 200, description: '余额信息' }) @ApiResponse({ status: 200, description: '余额信息' })
async getBalance(@Param('userId') userId: string) { async getBalance(@Param('userId') userId: string) {
// 优先使用 accountSequence如果相同则使用 userId
const query = new GetMyWalletQuery(userId, userId); const query = new GetMyWalletQuery(userId, userId);
const wallet = await this.walletService.getMyWallet(query); const wallet = await this.walletService.getMyWallet(query);
return { return {
@ -44,10 +45,12 @@ export class InternalWalletController {
@ApiOperation({ summary: '认种扣款(内部API) - 直接扣款模式' }) @ApiOperation({ summary: '认种扣款(内部API) - 直接扣款模式' })
@ApiResponse({ status: 200, description: '扣款结果' }) @ApiResponse({ status: 200, description: '扣款结果' })
async deductForPlanting( async deductForPlanting(
@Body() dto: { userId: string; amount: number; orderId: string }, @Body() dto: { userId: string; accountSequence?: string; amount: number; orderId: string },
) { ) {
// 优先使用 accountSequence如果未提供则使用 userId
const userIdentifier = dto.accountSequence || dto.userId;
const command = new DeductForPlantingCommand( const command = new DeductForPlantingCommand(
dto.userId, userIdentifier,
dto.amount, dto.amount,
dto.orderId, dto.orderId,
); );
@ -60,17 +63,22 @@ export class InternalWalletController {
@ApiOperation({ summary: '认种冻结资金(内部API) - 预扣款模式第一步' }) @ApiOperation({ summary: '认种冻结资金(内部API) - 预扣款模式第一步' })
@ApiResponse({ status: 200, description: '冻结结果' }) @ApiResponse({ status: 200, description: '冻结结果' })
async freezeForPlanting( async freezeForPlanting(
@Body() dto: { userId: string; amount: number; orderId: string }, @Body() dto: { userId: string; accountSequence?: string; amount: number; orderId: string },
) { ) {
this.logger.log(`========== freeze-for-planting 请求 ==========`); this.logger.log(`========== freeze-for-planting 请求 ==========`);
this.logger.log(`请求参数: ${JSON.stringify(dto)}`); this.logger.log(`请求参数: ${JSON.stringify(dto)}`);
this.logger.log(` userId: ${dto.userId}`); this.logger.log(` userId: ${dto.userId}`);
this.logger.log(` accountSequence: ${dto.accountSequence || '未提供'}`);
this.logger.log(` amount: ${dto.amount}`); this.logger.log(` amount: ${dto.amount}`);
this.logger.log(` orderId: ${dto.orderId}`); this.logger.log(` orderId: ${dto.orderId}`);
try { try {
// 优先使用 accountSequence如果未提供则使用 userId
const userIdentifier = dto.accountSequence || dto.userId;
this.logger.log(` 使用标识符: ${userIdentifier}`);
const command = new FreezeForPlantingCommand( const command = new FreezeForPlantingCommand(
dto.userId, userIdentifier,
dto.amount, dto.amount,
dto.orderId, dto.orderId,
); );
@ -89,10 +97,12 @@ export class InternalWalletController {
@ApiOperation({ summary: '确认认种扣款(内部API) - 预扣款模式第二步' }) @ApiOperation({ summary: '确认认种扣款(内部API) - 预扣款模式第二步' })
@ApiResponse({ status: 200, description: '确认结果' }) @ApiResponse({ status: 200, description: '确认结果' })
async confirmPlantingDeduction( async confirmPlantingDeduction(
@Body() dto: { userId: string; orderId: string }, @Body() dto: { userId: string; accountSequence?: string; orderId: string },
) { ) {
// 优先使用 accountSequence如果未提供则使用 userId
const userIdentifier = dto.accountSequence || dto.userId;
const command = new ConfirmPlantingDeductionCommand( const command = new ConfirmPlantingDeductionCommand(
dto.userId, userIdentifier,
dto.orderId, dto.orderId,
); );
const success = await this.walletService.confirmPlantingDeduction(command); const success = await this.walletService.confirmPlantingDeduction(command);
@ -104,10 +114,12 @@ export class InternalWalletController {
@ApiOperation({ summary: '解冻认种资金(内部API) - 认种失败时回滚' }) @ApiOperation({ summary: '解冻认种资金(内部API) - 认种失败时回滚' })
@ApiResponse({ status: 200, description: '解冻结果' }) @ApiResponse({ status: 200, description: '解冻结果' })
async unfreezeForPlanting( async unfreezeForPlanting(
@Body() dto: { userId: string; orderId: string }, @Body() dto: { userId: string; accountSequence?: string; orderId: string },
) { ) {
// 优先使用 accountSequence如果未提供则使用 userId
const userIdentifier = dto.accountSequence || dto.userId;
const command = new UnfreezeForPlantingCommand( const command = new UnfreezeForPlantingCommand(
dto.userId, userIdentifier,
dto.orderId, dto.orderId,
); );
const success = await this.walletService.unfreezeForPlanting(command); const success = await this.walletService.unfreezeForPlanting(command);

View File

@ -153,9 +153,13 @@ export class WalletApplicationService {
return true; return true;
} }
const wallet = await this.walletRepo.findByUserId(userId); // 优先按 accountSequence 查找,如果未找到则按 userId 查找
let wallet = await this.walletRepo.findByAccountSequence(userId);
if (!wallet) { if (!wallet) {
throw new WalletNotFoundError(`userId: ${command.userId}`); wallet = await this.walletRepo.findByUserId(userId);
}
if (!wallet) {
throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`);
} }
// Deduct from wallet // Deduct from wallet
@ -188,7 +192,7 @@ export class WalletApplicationService {
frozenAmount: number; frozenAmount: number;
}> { }> {
this.logger.log(`[freezeForPlanting] ========== 开始处理 ==========`); this.logger.log(`[freezeForPlanting] ========== 开始处理 ==========`);
this.logger.log(`[freezeForPlanting] userId: ${command.userId}`); this.logger.log(`[freezeForPlanting] userId/accountSequence: ${command.userId}`);
this.logger.log(`[freezeForPlanting] amount: ${command.amount}`); this.logger.log(`[freezeForPlanting] amount: ${command.amount}`);
this.logger.log(`[freezeForPlanting] orderId: ${command.orderId}`); this.logger.log(`[freezeForPlanting] orderId: ${command.orderId}`);
@ -209,10 +213,14 @@ export class WalletApplicationService {
return { success: true, frozenAmount: command.amount }; return { success: true, frozenAmount: command.amount };
} }
const wallet = await this.walletRepo.findByUserId(userId); // 优先按 accountSequence 查找,如果未找到则按 userId 查找
let wallet = await this.walletRepo.findByAccountSequence(userId);
if (!wallet) { if (!wallet) {
this.logger.error(`[freezeForPlanting] 钱包不存在: userId=${command.userId}`); wallet = await this.walletRepo.findByUserId(userId);
throw new WalletNotFoundError(`userId: ${command.userId}`); }
if (!wallet) {
this.logger.error(`[freezeForPlanting] 钱包不存在: userId/accountSequence=${command.userId}`);
throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`);
} }
this.logger.log(`[freezeForPlanting] 钱包信息:`); this.logger.log(`[freezeForPlanting] 钱包信息:`);
@ -289,9 +297,13 @@ export class WalletApplicationService {
// 获取冻结金额(流水中是负数,取绝对值) // 获取冻结金额(流水中是负数,取绝对值)
const frozenAmount = Money.USDT(Math.abs(freezeEntry.amount.value)); const frozenAmount = Money.USDT(Math.abs(freezeEntry.amount.value));
const wallet = await this.walletRepo.findByUserId(userId); // 优先按 accountSequence 查找,如果未找到则按 userId 查找
let wallet = await this.walletRepo.findByAccountSequence(userId);
if (!wallet) { if (!wallet) {
throw new WalletNotFoundError(`userId: ${command.userId}`); wallet = await this.walletRepo.findByUserId(userId);
}
if (!wallet) {
throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`);
} }
// 从冻结金额扣款 // 从冻结金额扣款
@ -363,9 +375,13 @@ export class WalletApplicationService {
// 获取冻结金额 // 获取冻结金额
const frozenAmount = Money.USDT(Math.abs(freezeEntry.amount.value)); const frozenAmount = Money.USDT(Math.abs(freezeEntry.amount.value));
const wallet = await this.walletRepo.findByUserId(userId); // 优先按 accountSequence 查找,如果未找到则按 userId 查找
let wallet = await this.walletRepo.findByAccountSequence(userId);
if (!wallet) { if (!wallet) {
throw new WalletNotFoundError(`userId: ${command.userId}`); wallet = await this.walletRepo.findByUserId(userId);
}
if (!wallet) {
throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`);
} }
// 解冻资金 // 解冻资金
@ -393,10 +409,13 @@ export class WalletApplicationService {
async addRewards(command: AddRewardsCommand): Promise<void> { async addRewards(command: AddRewardsCommand): Promise<void> {
const userId = BigInt(command.userId); const userId = BigInt(command.userId);
// 先通过 userId 查找钱包addRewards 是内部调用,钱包应该已存在) // 优先按 accountSequence 查找,如果未找到则按 userId 查找
const wallet = await this.walletRepo.findByUserId(userId); let wallet = await this.walletRepo.findByAccountSequence(userId);
if (!wallet) { if (!wallet) {
throw new WalletNotFoundError(`userId: ${command.userId}`); wallet = await this.walletRepo.findByUserId(userId);
}
if (!wallet) {
throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`);
} }
const usdtAmount = Money.USDT(command.usdtAmount); const usdtAmount = Money.USDT(command.usdtAmount);
@ -440,9 +459,13 @@ export class WalletApplicationService {
async claimRewards(command: ClaimRewardsCommand): Promise<void> { async claimRewards(command: ClaimRewardsCommand): Promise<void> {
const userId = BigInt(command.userId); const userId = BigInt(command.userId);
const wallet = await this.walletRepo.findByUserId(userId); // 优先按 accountSequence 查找,如果未找到则按 userId 查找
let wallet = await this.walletRepo.findByAccountSequence(userId);
if (!wallet) { if (!wallet) {
throw new WalletNotFoundError(`userId: ${command.userId}`); wallet = await this.walletRepo.findByUserId(userId);
}
if (!wallet) {
throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`);
} }
const pendingUsdt = wallet.rewards.pendingUsdt.value; const pendingUsdt = wallet.rewards.pendingUsdt.value;
@ -483,9 +506,13 @@ export class WalletApplicationService {
const userId = BigInt(command.userId); const userId = BigInt(command.userId);
const usdtAmount = Money.USDT(command.usdtAmount); const usdtAmount = Money.USDT(command.usdtAmount);
const wallet = await this.walletRepo.findByUserId(userId); // 优先按 accountSequence 查找,如果未找到则按 userId 查找
let wallet = await this.walletRepo.findByAccountSequence(userId);
if (!wallet) { if (!wallet) {
throw new WalletNotFoundError(`userId: ${command.userId}`); wallet = await this.walletRepo.findByUserId(userId);
}
if (!wallet) {
throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`);
} }
// Create settlement order // Create settlement order
@ -586,10 +613,14 @@ export class WalletApplicationService {
orderId: string, orderId: string,
): Promise<void> { ): Promise<void> {
const userId = BigInt(allocation.targetId); const userId = BigInt(allocation.targetId);
const wallet = await this.walletRepo.findByUserId(userId);
// 优先按 accountSequence 查找,如果未找到则按 userId 查找
let wallet = await this.walletRepo.findByAccountSequence(userId);
if (!wallet) { if (!wallet) {
this.logger.warn(`Wallet not found for user ${allocation.targetId}, skipping allocation`); wallet = await this.walletRepo.findByUserId(userId);
}
if (!wallet) {
this.logger.warn(`Wallet not found for user/accountSequence ${allocation.targetId}, skipping allocation`);
return; return;
} }
@ -695,10 +726,13 @@ export class WalletApplicationService {
throw new Error(`最小提现金额为 ${this.MIN_WITHDRAWAL_AMOUNT} USDT`); throw new Error(`最小提现金额为 ${this.MIN_WITHDRAWAL_AMOUNT} USDT`);
} }
// 获取钱包 // 优先按 accountSequence 查找,如果未找到则按 userId 查找
const wallet = await this.walletRepo.findByUserId(userId); let wallet = await this.walletRepo.findByAccountSequence(userId);
if (!wallet) { if (!wallet) {
throw new WalletNotFoundError(`userId: ${command.userId}`); wallet = await this.walletRepo.findByUserId(userId);
}
if (!wallet) {
throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`);
} }
// 验证余额是否足够 // 验证余额是否足够

View File

@ -105,6 +105,27 @@ class CreateOrderResponse {
} }
} }
///
class PlantingPosition {
final int totalTreeCount;
final int effectiveTreeCount;
final int pendingTreeCount;
PlantingPosition({
required this.totalTreeCount,
required this.effectiveTreeCount,
required this.pendingTreeCount,
});
factory PlantingPosition.fromJson(Map<String, dynamic> json) {
return PlantingPosition(
totalTreeCount: json['totalTreeCount'] ?? 0,
effectiveTreeCount: json['effectiveTreeCount'] ?? 0,
pendingTreeCount: json['pendingTreeCount'] ?? 0,
);
}
}
/// ///
/// ///
/// ///
@ -113,6 +134,28 @@ class PlantingService {
PlantingService({required ApiClient apiClient}) : _apiClient = apiClient; PlantingService({required ApiClient apiClient}) : _apiClient = apiClient;
///
///
///
Future<PlantingPosition> getMyPosition() async {
try {
debugPrint('[PlantingService] 获取我的持仓信息');
final response = await _apiClient.get('/planting/position');
if (response.statusCode == 200) {
final data = response.data as Map<String, dynamic>;
debugPrint('[PlantingService] 持仓信息: totalTreeCount=${data['totalTreeCount']}');
return PlantingPosition.fromJson(data);
}
throw Exception('获取持仓信息失败: ${response.statusCode}');
} catch (e) {
debugPrint('[PlantingService] 获取持仓信息失败: $e');
rethrow;
}
}
/// ///
/// ///
/// [treeCount] /// [treeCount]