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:
parent
62a21b73a5
commit
034fb53674
|
|
@ -1,30 +1,15 @@
|
|||
{
|
||||
"permissions": {
|
||||
"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\" 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 -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\" status --short backend/services/blockchain-service/)",
|
||||
"Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" log --oneline -5)",
|
||||
"Bash(git -C \"c:/Users/dong/Desktop/rwadurian\" status backend/services/blockchain-service/)",
|
||||
"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 -C c:/Users/dong/Desktop/rwadurian add backend/services/reward-service/prisma/migrations/)",
|
||||
"Bash(git -C c:/Users/dong/Desktop/rwadurian commit --amend --no-edit)",
|
||||
"Bash(git -C c:/Users/dong/Desktop/rwadurian push)",
|
||||
"Bash(ssh root@154.204.60.178 \"cd /opt/rwadurian && git pull && docker-compose build blockchain-service && docker-compose up -d blockchain-service\")",
|
||||
"Bash(ssh:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"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:*)"
|
||||
"Bash(git push)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ datasource db {
|
|||
// ============ 授权角色表 ============
|
||||
model AuthorizationRole {
|
||||
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")
|
||||
regionCode String @map("region_code")
|
||||
regionName String @map("region_name")
|
||||
|
|
@ -22,9 +23,9 @@ model AuthorizationRole {
|
|||
|
||||
// 授权信息
|
||||
authorizedAt DateTime? @map("authorized_at")
|
||||
authorizedBy String? @map("authorized_by")
|
||||
authorizedBy BigInt? @map("authorized_by")
|
||||
revokedAt DateTime? @map("revoked_at")
|
||||
revokedBy String? @map("revoked_by")
|
||||
revokedBy BigInt? @map("revoked_by")
|
||||
revokeReason String? @map("revoke_reason")
|
||||
|
||||
// 考核配置
|
||||
|
|
@ -51,7 +52,8 @@ model AuthorizationRole {
|
|||
assessments MonthlyAssessment[]
|
||||
bypassRecords MonthlyBypass[]
|
||||
|
||||
@@unique([userId, roleType, regionCode])
|
||||
@@unique([accountSequence, roleType, regionCode])
|
||||
@@index([accountSequence])
|
||||
@@index([userId])
|
||||
@@index([roleType, regionCode])
|
||||
@@index([status])
|
||||
|
|
@ -63,7 +65,8 @@ model AuthorizationRole {
|
|||
model MonthlyAssessment {
|
||||
id String @id @default(uuid())
|
||||
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")
|
||||
regionCode String @map("region_code")
|
||||
|
||||
|
|
@ -98,7 +101,7 @@ model MonthlyAssessment {
|
|||
|
||||
// 豁免
|
||||
isBypassed Boolean @default(false) @map("is_bypassed")
|
||||
bypassedBy String? @map("bypassed_by")
|
||||
bypassedBy BigInt? @map("bypassed_by")
|
||||
bypassedAt DateTime? @map("bypassed_at")
|
||||
|
||||
// 时间戳
|
||||
|
|
@ -110,6 +113,7 @@ model MonthlyAssessment {
|
|||
authorization AuthorizationRole @relation(fields: [authorizationId], references: [id])
|
||||
|
||||
@@unique([authorizationId, assessmentMonth])
|
||||
@@index([accountSequence, assessmentMonth])
|
||||
@@index([userId, assessmentMonth])
|
||||
@@index([roleType, regionCode, assessmentMonth])
|
||||
@@index([assessmentMonth, result])
|
||||
|
|
@ -121,21 +125,22 @@ model MonthlyAssessment {
|
|||
model MonthlyBypass {
|
||||
id String @id @default(uuid())
|
||||
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")
|
||||
bypassMonth String @map("bypass_month") // YYYY-MM
|
||||
|
||||
// 授权信息
|
||||
grantedBy String @map("granted_by")
|
||||
grantedBy BigInt @map("granted_by")
|
||||
grantedAt DateTime @map("granted_at")
|
||||
reason String?
|
||||
|
||||
// 审批信息(三人授权)
|
||||
approver1Id String @map("approver1_id")
|
||||
approver1Id BigInt @map("approver1_id")
|
||||
approver1At DateTime @map("approver1_at")
|
||||
approver2Id String? @map("approver2_id")
|
||||
approver2Id BigInt? @map("approver2_id")
|
||||
approver2At DateTime? @map("approver2_at")
|
||||
approver3Id String? @map("approver3_id")
|
||||
approver3Id BigInt? @map("approver3_id")
|
||||
approver3At DateTime? @map("approver3_at")
|
||||
approvalStatus ApprovalStatus @default(PENDING) @map("approval_status")
|
||||
|
||||
|
|
@ -144,6 +149,7 @@ model MonthlyBypass {
|
|||
authorization AuthorizationRole @relation(fields: [authorizationId], references: [id])
|
||||
|
||||
@@unique([authorizationId, bypassMonth])
|
||||
@@index([accountSequence, bypassMonth])
|
||||
@@index([userId, bypassMonth])
|
||||
@@map("monthly_bypasses")
|
||||
}
|
||||
|
|
@ -275,7 +281,8 @@ model RegionHeatMap {
|
|||
// ============ 火柴人排名视图数据表 ============
|
||||
model StickmanRanking {
|
||||
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")
|
||||
roleType RoleType @map("role_type")
|
||||
regionCode String @map("region_code")
|
||||
|
|
@ -303,6 +310,7 @@ model StickmanRanking {
|
|||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@unique([authorizationId, currentMonth])
|
||||
@@index([accountSequence, currentMonth])
|
||||
@@index([roleType, regionCode, currentMonth])
|
||||
@@map("stickman_rankings")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,10 +54,10 @@ export class AuthorizationController {
|
|||
@ApiOperation({ summary: '申请社区授权' })
|
||||
@ApiResponse({ status: 201, type: ApplyAuthorizationResponse })
|
||||
async applyCommunityAuth(
|
||||
@CurrentUser() user: { userId: string },
|
||||
@CurrentUser() user: { userId: string; accountSequence: number },
|
||||
@Body() dto: ApplyCommunityAuthDto,
|
||||
): 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)
|
||||
}
|
||||
|
||||
|
|
@ -65,11 +65,12 @@ export class AuthorizationController {
|
|||
@ApiOperation({ summary: '申请授权省公司' })
|
||||
@ApiResponse({ status: 201, type: ApplyAuthorizationResponse })
|
||||
async applyAuthProvinceCompany(
|
||||
@CurrentUser() user: { userId: string },
|
||||
@CurrentUser() user: { userId: string; accountSequence: number },
|
||||
@Body() dto: ApplyAuthProvinceDto,
|
||||
): Promise<ApplyAuthorizationResponse> {
|
||||
const command = new ApplyAuthProvinceCompanyCommand(
|
||||
user.userId,
|
||||
user.accountSequence,
|
||||
dto.provinceCode,
|
||||
dto.provinceName,
|
||||
)
|
||||
|
|
@ -80,10 +81,10 @@ export class AuthorizationController {
|
|||
@ApiOperation({ summary: '申请授权市公司' })
|
||||
@ApiResponse({ status: 201, type: ApplyAuthorizationResponse })
|
||||
async applyAuthCityCompany(
|
||||
@CurrentUser() user: { userId: string },
|
||||
@CurrentUser() user: { userId: string; accountSequence: number },
|
||||
@Body() dto: ApplyAuthCityDto,
|
||||
): 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)
|
||||
}
|
||||
|
||||
|
|
@ -91,9 +92,9 @@ export class AuthorizationController {
|
|||
@ApiOperation({ summary: '获取我的授权列表' })
|
||||
@ApiResponse({ status: 200, type: [AuthorizationResponse] })
|
||||
async getMyAuthorizations(
|
||||
@CurrentUser() user: { userId: string },
|
||||
@CurrentUser() user: { userId: string; accountSequence: number },
|
||||
): Promise<AuthorizationResponse[]> {
|
||||
return await this.applicationService.getUserAuthorizations(user.userId)
|
||||
return await this.applicationService.getUserAuthorizations(user.accountSequence)
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
|
@ -125,10 +126,10 @@ export class AuthorizationController {
|
|||
@ApiResponse({ status: 204 })
|
||||
async revokeAuthorization(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: { userId: string },
|
||||
@CurrentUser() user: { userId: string; accountSequence: number },
|
||||
@Body() dto: RevokeAuthorizationDto,
|
||||
): 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)
|
||||
}
|
||||
|
||||
|
|
@ -139,10 +140,10 @@ export class AuthorizationController {
|
|||
@ApiResponse({ status: 204 })
|
||||
async grantMonthlyBypass(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: { userId: string },
|
||||
@CurrentUser() user: { userId: string; accountSequence: number },
|
||||
@Body() dto: GrantMonthlyBypassDto,
|
||||
): 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)
|
||||
}
|
||||
|
||||
|
|
@ -153,9 +154,9 @@ export class AuthorizationController {
|
|||
@ApiResponse({ status: 204 })
|
||||
async exemptLocalPercentageCheck(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser() user: { userId: string },
|
||||
@CurrentUser() user: { userId: string; accountSequence: number },
|
||||
): Promise<void> {
|
||||
const command = new ExemptLocalPercentageCheckCommand(id, user.userId)
|
||||
const command = new ExemptLocalPercentageCheckCommand(id, user.accountSequence)
|
||||
await this.applicationService.exemptLocalPercentageCheck(command)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export class ApplyAuthCityCompanyCommand {
|
||||
constructor(
|
||||
public readonly userId: string,
|
||||
public readonly accountSequence: number,
|
||||
public readonly cityCode: string,
|
||||
public readonly cityName: string,
|
||||
) {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export class ApplyAuthProvinceCompanyCommand {
|
||||
constructor(
|
||||
public readonly userId: string,
|
||||
public readonly accountSequence: number,
|
||||
public readonly provinceCode: string,
|
||||
public readonly provinceName: string,
|
||||
) {}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export class ApplyCommunityAuthCommand {
|
||||
constructor(
|
||||
public readonly userId: string,
|
||||
public readonly accountSequence: number,
|
||||
public readonly communityName: string,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export class ExemptLocalPercentageCheckCommand {
|
||||
constructor(
|
||||
public readonly authorizationId: string,
|
||||
public readonly adminId: string,
|
||||
public readonly adminAccountSequence: number,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
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,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ export class GrantMonthlyBypassCommand {
|
|||
constructor(
|
||||
public readonly authorizationId: string,
|
||||
public readonly month: string,
|
||||
public readonly adminId: string,
|
||||
public readonly adminAccountSequence: number,
|
||||
public readonly reason?: string,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
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,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export class RevokeAuthorizationCommand {
|
||||
constructor(
|
||||
public readonly authorizationId: string,
|
||||
public readonly adminId: string,
|
||||
public readonly adminAccountSequence: number,
|
||||
public readonly reason: string,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,11 +64,11 @@ export class AuthorizationApplicationService {
|
|||
async applyCommunityAuth(
|
||||
command: ApplyCommunityAuthCommand,
|
||||
): Promise<ApplyCommunityAuthResult> {
|
||||
const userId = UserId.create(command.userId)
|
||||
const userId = UserId.create(command.userId, command.accountSequence)
|
||||
|
||||
// 1. 检查是否已有社区授权
|
||||
const existing = await this.authorizationRepository.findByUserIdAndRoleType(
|
||||
userId,
|
||||
const existing = await this.authorizationRepository.findByAccountSequenceAndRoleType(
|
||||
userId.accountSequence,
|
||||
RoleType.COMMUNITY,
|
||||
)
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ export class AuthorizationApplicationService {
|
|||
})
|
||||
|
||||
// 3. 检查初始考核(10棵)
|
||||
const teamStats = await this.statsRepository.findByUserId(userId.value)
|
||||
const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence)
|
||||
const totalTreeCount = teamStats?.totalTeamPlantingCount || 0
|
||||
|
||||
if (totalTreeCount >= authorization.getInitialTarget()) {
|
||||
|
|
@ -113,7 +113,7 @@ export class AuthorizationApplicationService {
|
|||
async applyAuthProvinceCompany(
|
||||
command: ApplyAuthProvinceCompanyCommand,
|
||||
): Promise<ApplyAuthProvinceCompanyResult> {
|
||||
const userId = UserId.create(command.userId)
|
||||
const userId = UserId.create(command.userId, command.accountSequence)
|
||||
const regionCode = RegionCode.create(command.provinceCode)
|
||||
|
||||
// 1. 验证授权申请(团队内唯一性)
|
||||
|
|
@ -137,7 +137,7 @@ export class AuthorizationApplicationService {
|
|||
})
|
||||
|
||||
// 3. 检查初始考核(500棵)
|
||||
const teamStats = await this.statsRepository.findByUserId(userId.value)
|
||||
const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence)
|
||||
const totalTreeCount = teamStats?.totalTeamPlantingCount || 0
|
||||
|
||||
if (totalTreeCount >= authorization.getInitialTarget()) {
|
||||
|
|
@ -169,7 +169,7 @@ export class AuthorizationApplicationService {
|
|||
async applyAuthCityCompany(
|
||||
command: ApplyAuthCityCompanyCommand,
|
||||
): Promise<ApplyAuthCityCompanyResult> {
|
||||
const userId = UserId.create(command.userId)
|
||||
const userId = UserId.create(command.userId, command.accountSequence)
|
||||
const regionCode = RegionCode.create(command.cityCode)
|
||||
|
||||
// 1. 验证
|
||||
|
|
@ -193,7 +193,7 @@ export class AuthorizationApplicationService {
|
|||
})
|
||||
|
||||
// 3. 检查初始考核(100棵)
|
||||
const teamStats = await this.statsRepository.findByUserId(userId.value)
|
||||
const teamStats = await this.statsRepository.findByAccountSequence(userId.accountSequence)
|
||||
const totalTreeCount = teamStats?.totalTeamPlantingCount || 0
|
||||
|
||||
if (totalTreeCount >= authorization.getInitialTarget()) {
|
||||
|
|
@ -222,8 +222,8 @@ export class AuthorizationApplicationService {
|
|||
* 管理员授权正式省公司
|
||||
*/
|
||||
async grantProvinceCompany(command: GrantProvinceCompanyCommand): Promise<void> {
|
||||
const userId = UserId.create(command.userId)
|
||||
const adminId = AdminUserId.create(command.adminId)
|
||||
const userId = UserId.create(command.userId, command.accountSequence)
|
||||
const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence)
|
||||
|
||||
const authorization = AuthorizationRole.createProvinceCompany({
|
||||
userId,
|
||||
|
|
@ -241,8 +241,8 @@ export class AuthorizationApplicationService {
|
|||
* 管理员授权正式市公司
|
||||
*/
|
||||
async grantCityCompany(command: GrantCityCompanyCommand): Promise<void> {
|
||||
const userId = UserId.create(command.userId)
|
||||
const adminId = AdminUserId.create(command.adminId)
|
||||
const userId = UserId.create(command.userId, command.accountSequence)
|
||||
const adminId = AdminUserId.create(command.adminId, command.adminAccountSequence)
|
||||
|
||||
const authorization = AuthorizationRole.createCityCompany({
|
||||
userId,
|
||||
|
|
@ -268,7 +268,10 @@ export class AuthorizationApplicationService {
|
|||
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.eventPublisher.publishAll(authorization.domainEvents)
|
||||
|
|
@ -288,7 +291,9 @@ export class AuthorizationApplicationService {
|
|||
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.eventPublisher.publishAll(assessment.domainEvents)
|
||||
|
|
@ -307,7 +312,9 @@ export class AuthorizationApplicationService {
|
|||
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.eventPublisher.publishAll(authorization.domainEvents)
|
||||
|
|
@ -317,13 +324,13 @@ export class AuthorizationApplicationService {
|
|||
/**
|
||||
* 查询用户授权列表
|
||||
*/
|
||||
async getUserAuthorizations(userId: string): Promise<AuthorizationDTO[]> {
|
||||
const authorizations = await this.authorizationRepository.findByUserId(
|
||||
UserId.create(userId),
|
||||
async getUserAuthorizations(accountSequence: number): Promise<AuthorizationDTO[]> {
|
||||
const authorizations = await this.authorizationRepository.findByAccountSequence(
|
||||
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
|
||||
|
||||
return authorizations.map((auth) => this.toAuthorizationDTO(auth, currentTreeCount))
|
||||
|
|
@ -340,7 +347,7 @@ export class AuthorizationApplicationService {
|
|||
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
|
||||
|
||||
return this.toAuthorizationDTO(authorization, currentTreeCount)
|
||||
|
|
|
|||
|
|
@ -559,16 +559,17 @@ export class AuthorizationRole extends AggregateRoot {
|
|||
toPersistence(): Record<string, any> {
|
||||
return {
|
||||
id: this._authorizationId.value,
|
||||
userId: this._userId.value,
|
||||
userId: this._userId.accountSequence,
|
||||
accountSequence: this._userId.accountSequence,
|
||||
roleType: this._roleType,
|
||||
regionCode: this._regionCode.value,
|
||||
regionName: this._regionName,
|
||||
status: this._status,
|
||||
displayTitle: this._displayTitle,
|
||||
authorizedAt: this._authorizedAt,
|
||||
authorizedBy: this._authorizedBy?.value || null,
|
||||
authorizedBy: this._authorizedBy?.accountSequence || null,
|
||||
revokedAt: this._revokedAt,
|
||||
revokedBy: this._revokedBy?.value || null,
|
||||
revokedBy: this._revokedBy?.accountSequence || null,
|
||||
revokeReason: this._revokeReason,
|
||||
initialTargetTreeCount: this._assessmentConfig.initialTargetTreeCount,
|
||||
monthlyTargetType: this._assessmentConfig.monthlyTargetType,
|
||||
|
|
|
|||
|
|
@ -398,7 +398,8 @@ export class MonthlyAssessment extends AggregateRoot {
|
|||
return {
|
||||
id: this._assessmentId.value,
|
||||
authorizationId: this._authorizationId.value,
|
||||
userId: this._userId.value,
|
||||
userId: this._userId.accountSequence,
|
||||
accountSequence: this._userId.accountSequence,
|
||||
roleType: this._roleType,
|
||||
regionCode: this._regionCode.value,
|
||||
assessmentMonth: this._assessmentMonth.value,
|
||||
|
|
@ -417,7 +418,7 @@ export class MonthlyAssessment extends AggregateRoot {
|
|||
rankingInRegion: this._rankingInRegion,
|
||||
isFirstPlace: this._isFirstPlace,
|
||||
isBypassed: this._isBypassed,
|
||||
bypassedBy: this._bypassedBy?.value || null,
|
||||
bypassedBy: this._bypassedBy?.accountSequence || null,
|
||||
bypassedAt: this._bypassedAt,
|
||||
assessedAt: this._assessedAt,
|
||||
createdAt: this._createdAt,
|
||||
|
|
|
|||
|
|
@ -8,12 +8,14 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,18 +1,24 @@
|
|||
import { DomainError } from '@/shared/exceptions'
|
||||
|
||||
export class UserId {
|
||||
constructor(public readonly value: string) {
|
||||
constructor(
|
||||
public readonly value: string,
|
||||
public readonly accountSequence: bigint,
|
||||
) {
|
||||
if (!value) {
|
||||
throw new DomainError('用户ID不能为空')
|
||||
}
|
||||
if (accountSequence === undefined || accountSequence === null) {
|
||||
throw new DomainError('账户序列号不能为空')
|
||||
}
|
||||
}
|
||||
|
||||
static create(value: string): UserId {
|
||||
return new UserId(value)
|
||||
static create(value: string, accountSequence: number | bigint): UserId {
|
||||
return new UserId(value, BigInt(accountSequence))
|
||||
}
|
||||
|
||||
equals(other: UserId): boolean {
|
||||
return this.value === other.value
|
||||
return this.value === other.value && this.accountSequence === other.accountSequence
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
|
|
@ -21,18 +27,24 @@ export class UserId {
|
|||
}
|
||||
|
||||
export class AdminUserId {
|
||||
constructor(public readonly value: string) {
|
||||
constructor(
|
||||
public readonly value: string,
|
||||
public readonly accountSequence: bigint,
|
||||
) {
|
||||
if (!value) {
|
||||
throw new DomainError('管理员ID不能为空')
|
||||
}
|
||||
if (accountSequence === undefined || accountSequence === null) {
|
||||
throw new DomainError('管理员账户序列号不能为空')
|
||||
}
|
||||
}
|
||||
|
||||
static create(value: string): AdminUserId {
|
||||
return new AdminUserId(value)
|
||||
static create(value: string, accountSequence: number | bigint): AdminUserId {
|
||||
return new AdminUserId(value, BigInt(accountSequence))
|
||||
}
|
||||
|
||||
equals(other: AdminUserId): boolean {
|
||||
return this.value === other.value
|
||||
return this.value === other.value && this.accountSequence === other.accountSequence
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
|
|||
create: {
|
||||
id: data.id,
|
||||
userId: data.userId,
|
||||
accountSequence: data.accountSequence,
|
||||
roleType: data.roleType,
|
||||
regionCode: data.regionCode,
|
||||
regionName: data.regionName,
|
||||
|
|
@ -97,6 +98,19 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
|
|||
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[]> {
|
||||
const records = await this.prisma.authorizationRole.findMany({
|
||||
where: { userId: userId.value },
|
||||
|
|
@ -105,6 +119,14 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
|
|||
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(
|
||||
roleType: RoleType,
|
||||
regionCode: RegionCode,
|
||||
|
|
@ -157,16 +179,16 @@ export class AuthorizationRoleRepositoryImpl implements IAuthorizationRoleReposi
|
|||
private toDomain(record: any): AuthorizationRole {
|
||||
const props: AuthorizationRoleProps = {
|
||||
authorizationId: AuthorizationId.create(record.id),
|
||||
userId: UserId.create(record.userId),
|
||||
userId: UserId.create(record.userId.toString(), 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) : null,
|
||||
authorizedBy: record.authorizedBy ? AdminUserId.create(record.authorizedBy.toString(), record.authorizedBy) : null,
|
||||
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,
|
||||
assessmentConfig: new AssessmentConfig(
|
||||
record.initialTargetTreeCount,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi
|
|||
id: data.id,
|
||||
authorizationId: data.authorizationId,
|
||||
userId: data.userId,
|
||||
accountSequence: data.accountSequence,
|
||||
roleType: data.roleType,
|
||||
regionCode: data.regionCode,
|
||||
assessmentMonth: data.assessmentMonth,
|
||||
|
|
@ -79,6 +80,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi
|
|||
id: data.id,
|
||||
authorizationId: data.authorizationId,
|
||||
userId: data.userId,
|
||||
accountSequence: data.accountSequence,
|
||||
roleType: data.roleType,
|
||||
regionCode: data.regionCode,
|
||||
assessmentMonth: data.assessmentMonth,
|
||||
|
|
@ -212,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),
|
||||
userId: UserId.create(record.userId.toString(), record.accountSequence),
|
||||
roleType: record.roleType as RoleType,
|
||||
regionCode: RegionCode.create(record.regionCode),
|
||||
assessmentMonth: Month.create(record.assessmentMonth),
|
||||
|
|
@ -231,7 +233,7 @@ export class MonthlyAssessmentRepositoryImpl implements IMonthlyAssessmentReposi
|
|||
rankingInRegion: record.rankingInRegion,
|
||||
isFirstPlace: record.isFirstPlace,
|
||||
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,
|
||||
assessedAt: record.assessedAt,
|
||||
createdAt: record.createdAt,
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import {
|
|||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||
|
||||
interface AuthenticatedRequest {
|
||||
user: { id: string };
|
||||
user: { id: string; accountSequence: number };
|
||||
}
|
||||
|
||||
@ApiTags('认种订单')
|
||||
|
|
@ -124,7 +124,7 @@ export class PlantingOrderController {
|
|||
@Param('orderNo') orderNo: string,
|
||||
): Promise<PayOrderResponse> {
|
||||
const userId = BigInt(req.user.id);
|
||||
return this.plantingService.payOrder(orderNo, userId);
|
||||
return this.plantingService.payOrder(orderNo, userId, req.user.accountSequence);
|
||||
}
|
||||
|
||||
@Get('orders')
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import * as jwt from 'jsonwebtoken';
|
|||
export interface JwtPayload {
|
||||
sub: string;
|
||||
userId: string;
|
||||
accountSequence: number;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
|
@ -35,6 +36,7 @@ export class JwtAuthGuard implements CanActivate {
|
|||
const payload = jwt.verify(token, secret) as JwtPayload;
|
||||
request.user = {
|
||||
id: payload.userId || payload.sub,
|
||||
accountSequence: payload.accountSequence,
|
||||
};
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -173,6 +173,7 @@ export class PlantingApplicationService {
|
|||
async payOrder(
|
||||
orderNo: string,
|
||||
userId: bigint,
|
||||
accountSequence?: number,
|
||||
): Promise<{
|
||||
orderNo: string;
|
||||
status: string;
|
||||
|
|
@ -208,7 +209,7 @@ export class PlantingApplicationService {
|
|||
|
||||
// 3. 获取推荐链上下文 (先获取,确保服务可用)
|
||||
const referralContext = await this.referralService.getReferralContext(
|
||||
userId.toString(),
|
||||
accountSequence!,
|
||||
selection.provinceCode,
|
||||
selection.cityCode,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ export class ReferralServiceClient {
|
|||
* 获取用户的推荐链和权限上级信息
|
||||
*/
|
||||
async getReferralContext(
|
||||
userId: string,
|
||||
accountSequence: number,
|
||||
provinceCode: string,
|
||||
cityCode: string,
|
||||
): Promise<ReferralContext> {
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.get<ReferralInfo>(
|
||||
`${this.baseUrl}/api/v1/referrals/${userId}/context`,
|
||||
`${this.baseUrl}/api/v1/referrals/${accountSequence}/context`,
|
||||
{
|
||||
params: { provinceCode, cityCode },
|
||||
},
|
||||
|
|
@ -52,7 +52,7 @@ export class ReferralServiceClient {
|
|||
};
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to get referral context for user ${userId}`,
|
||||
`Failed to get referral context for accountSequence ${accountSequence}`,
|
||||
error,
|
||||
);
|
||||
// 在开发环境返回默认空数据
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export class ReferralController {
|
|||
@ApiOperation({ summary: '获取当前用户推荐信息' })
|
||||
@ApiResponse({ status: 200, type: 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);
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ export class ReferralController {
|
|||
@ApiParam({ name: 'userId', description: '用户ID' })
|
||||
@ApiResponse({ status: 200, type: 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -118,23 +118,23 @@ export class ReferralController {
|
|||
export class InternalReferralController {
|
||||
constructor(private readonly referralService: ReferralService) {}
|
||||
|
||||
@Get(':userId/context')
|
||||
@Get(':accountSequence/context')
|
||||
@ApiOperation({ summary: '获取用户推荐上下文信息(内部API)' })
|
||||
@ApiParam({ name: 'userId', description: '用户ID' })
|
||||
@ApiParam({ name: 'accountSequence', description: '账户序列号' })
|
||||
@ApiResponse({ status: 200, description: '推荐上下文' })
|
||||
async getReferralContext(
|
||||
@Param('userId') userId: string,
|
||||
@Param('accountSequence') accountSequence: string,
|
||||
@Query('provinceCode') provinceCode: string,
|
||||
@Query('cityCode') cityCode: string,
|
||||
) {
|
||||
// 获取用户的推荐链
|
||||
const query = new GetUserReferralInfoQuery(BigInt(userId));
|
||||
const query = new GetUserReferralInfoQuery(Number(accountSequence));
|
||||
const referralInfo = await this.referralService.getUserReferralInfo(query);
|
||||
|
||||
// 返回推荐上下文信息
|
||||
// 目前返回基础信息,后续可以扩展省市授权等信息
|
||||
return {
|
||||
userId,
|
||||
accountSequence,
|
||||
referralChain: referralInfo.referrerId ? [referralInfo.referrerId] : [],
|
||||
referrerId: referralInfo.referrerId,
|
||||
nearestProvinceAuth: null, // 省代账户ID - 需要后续实现
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export class GetUserReferralInfoQuery {
|
||||
constructor(public readonly userId: bigint) {}
|
||||
constructor(public readonly accountSequence: number) {}
|
||||
}
|
||||
|
||||
export interface UserReferralInfoResult {
|
||||
|
|
|
|||
|
|
@ -114,12 +114,12 @@ export class ReferralService {
|
|||
* 获取用户推荐信息
|
||||
*/
|
||||
async getUserReferralInfo(query: GetUserReferralInfoQuery): Promise<UserReferralInfoResult> {
|
||||
const relationship = await this.referralRepo.findByUserId(query.userId);
|
||||
const relationship = await this.referralRepo.findByAccountSequence(query.accountSequence);
|
||||
if (!relationship) {
|
||||
throw new NotFoundException('用户推荐关系不存在');
|
||||
}
|
||||
|
||||
const teamStats = await this.teamStatsRepo.findByUserId(query.userId);
|
||||
const teamStats = await this.teamStatsRepo.findByUserId(relationship.userId);
|
||||
|
||||
return {
|
||||
userId: relationship.userId.toString(),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ datasource db {
|
|||
model RewardLedgerEntry {
|
||||
id BigInt @id @default(autoincrement()) @map("entry_id")
|
||||
userId BigInt @map("user_id") // 接收奖励的用户ID
|
||||
accountSequence BigInt @map("account_sequence") // 账户序列号
|
||||
|
||||
// === 奖励来源 ===
|
||||
sourceOrderNo String @map("source_order_no") @db.VarChar(50) // 来源认种订单号(字符串格式如PLT1765391584505Q0Q6QD)
|
||||
|
|
@ -40,6 +41,8 @@ model RewardLedgerEntry {
|
|||
@@map("reward_ledger_entries")
|
||||
@@index([userId, rewardStatus], name: "idx_user_status")
|
||||
@@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([sourceUserId], name: "idx_source_user")
|
||||
@@index([rightType], name: "idx_right_type")
|
||||
|
|
@ -55,6 +58,7 @@ model RewardLedgerEntry {
|
|||
model RewardSummary {
|
||||
id BigInt @id @default(autoincrement()) @map("summary_id")
|
||||
userId BigInt @unique @map("user_id")
|
||||
accountSequence BigInt @unique @map("account_sequence") // 账户序列号
|
||||
|
||||
// === 待领取收益 (24h倒计时) ===
|
||||
pendingUsdt Decimal @default(0) @map("pending_usdt") @db.Decimal(20, 8)
|
||||
|
|
@ -79,6 +83,7 @@ model RewardSummary {
|
|||
|
||||
@@map("reward_summaries")
|
||||
@@index([userId], name: "idx_summary_user")
|
||||
@@index([accountSequence], name: "idx_summary_account")
|
||||
@@index([settleableUsdt(sort: Desc)], name: "idx_settleable_desc")
|
||||
@@index([pendingExpireAt], name: "idx_pending_expire")
|
||||
}
|
||||
|
|
@ -119,6 +124,7 @@ model RightDefinition {
|
|||
model SettlementRecord {
|
||||
id BigInt @id @default(autoincrement()) @map("settlement_id")
|
||||
userId BigInt @map("user_id")
|
||||
accountSequence BigInt @map("account_sequence") // 账户序列号
|
||||
|
||||
// === 结算金额 ===
|
||||
usdtAmount Decimal @map("usdt_amount") @db.Decimal(20, 8)
|
||||
|
|
@ -144,6 +150,7 @@ model SettlementRecord {
|
|||
|
||||
@@map("settlement_records")
|
||||
@@index([userId], name: "idx_settlement_user")
|
||||
@@index([accountSequence], name: "idx_settlement_account")
|
||||
@@index([status], name: "idx_settlement_status")
|
||||
@@index([createdAt], name: "idx_settlement_created")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ export class RewardController {
|
|||
@ApiOperation({ summary: '获取我的收益汇总' })
|
||||
@ApiResponse({ status: 200, description: '成功', type: RewardSummaryDto })
|
||||
async getSummary(@Request() req): Promise<RewardSummaryDto> {
|
||||
const userId = BigInt(req.user.sub);
|
||||
const summary = await this.rewardService.getRewardSummary(userId);
|
||||
const accountSequence = BigInt(req.user.accountSequence);
|
||||
const summary = await this.rewardService.getRewardSummary(accountSequence);
|
||||
|
||||
return {
|
||||
...summary,
|
||||
|
|
@ -43,19 +43,19 @@ export class RewardController {
|
|||
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number = 1,
|
||||
@Query('pageSize', new DefaultValuePipe(20), ParseIntPipe) pageSize: number = 20,
|
||||
) {
|
||||
const userId = BigInt(req.user.sub);
|
||||
const accountSequence = BigInt(req.user.accountSequence);
|
||||
const filters: any = {};
|
||||
if (status) filters.status = status;
|
||||
if (rightType) filters.rightType = rightType;
|
||||
|
||||
return this.rewardService.getRewardDetails(userId, filters, { page, pageSize });
|
||||
return this.rewardService.getRewardDetails(accountSequence, filters, { page, pageSize });
|
||||
}
|
||||
|
||||
@Get('pending')
|
||||
@ApiOperation({ summary: '获取待领取奖励(含倒计时)' })
|
||||
@ApiResponse({ status: 200, description: '成功' })
|
||||
async getPending(@Request() req) {
|
||||
const userId = BigInt(req.user.sub);
|
||||
return this.rewardService.getPendingRewards(userId);
|
||||
const accountSequence = BigInt(req.user.accountSequence);
|
||||
return this.rewardService.getPendingRewards(accountSequence);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ export class SettlementController {
|
|||
@Request() req,
|
||||
@Body() dto: SettleRewardsDto,
|
||||
): Promise<SettlementResultDto> {
|
||||
const userId = BigInt(req.user.sub);
|
||||
const accountSequence = BigInt(req.user.accountSequence);
|
||||
|
||||
return this.rewardService.settleRewards({
|
||||
userId,
|
||||
accountSequence,
|
||||
settleCurrency: dto.settleCurrency,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ export class RewardApplicationService {
|
|||
* 结算可结算收益
|
||||
*/
|
||||
async settleRewards(params: {
|
||||
userId: bigint;
|
||||
accountSequence: bigint;
|
||||
settleCurrency: string; // BNB/OG/USDT/DST
|
||||
}): Promise<{
|
||||
success: boolean;
|
||||
|
|
@ -130,10 +130,10 @@ export class RewardApplicationService {
|
|||
txHash?: string;
|
||||
error?: string;
|
||||
}> {
|
||||
this.logger.log(`Settling rewards for user ${params.userId}`);
|
||||
this.logger.log(`Settling rewards for accountSequence ${params.accountSequence}`);
|
||||
|
||||
// 1. 获取可结算奖励
|
||||
const settleableRewards = await this.rewardLedgerEntryRepository.findSettleableByUserId(params.userId);
|
||||
const settleableRewards = await this.rewardLedgerEntryRepository.findSettleableByAccountSequence(params.accountSequence);
|
||||
|
||||
if (settleableRewards.length === 0) {
|
||||
return {
|
||||
|
|
@ -149,9 +149,10 @@ export class RewardApplicationService {
|
|||
const totalUsdt = settleableRewards.reduce((sum, r) => sum + r.usdtAmount.amount, 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({
|
||||
userId: params.userId,
|
||||
userId: userId,
|
||||
usdtAmount: totalUsdt,
|
||||
targetCurrency: params.settleCurrency,
|
||||
});
|
||||
|
|
@ -175,11 +176,13 @@ export class RewardApplicationService {
|
|||
}
|
||||
|
||||
// 5. 更新汇总数据
|
||||
const summary = await this.rewardSummaryRepository.getOrCreate(params.userId);
|
||||
summary.settle(Money.USDT(totalUsdt), Hashpower.create(totalHashpower));
|
||||
await this.rewardSummaryRepository.save(summary);
|
||||
const summary = await this.rewardSummaryRepository.findByAccountSequence(params.accountSequence);
|
||||
if (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 {
|
||||
success: true,
|
||||
|
|
@ -251,8 +254,8 @@ export class RewardApplicationService {
|
|||
/**
|
||||
* 获取用户奖励汇总
|
||||
*/
|
||||
async getRewardSummary(userId: bigint) {
|
||||
const summary = await this.rewardSummaryRepository.findByUserId(userId);
|
||||
async getRewardSummary(accountSequence: bigint) {
|
||||
const summary = await this.rewardSummaryRepository.findByAccountSequence(accountSequence);
|
||||
|
||||
if (!summary) {
|
||||
return {
|
||||
|
|
@ -285,7 +288,7 @@ export class RewardApplicationService {
|
|||
* 获取用户奖励明细
|
||||
*/
|
||||
async getRewardDetails(
|
||||
userId: bigint,
|
||||
accountSequence: bigint,
|
||||
filters?: {
|
||||
status?: RewardStatus;
|
||||
rightType?: RightType;
|
||||
|
|
@ -294,8 +297,8 @@ export class RewardApplicationService {
|
|||
},
|
||||
pagination?: { page: number; pageSize: number },
|
||||
) {
|
||||
const rewards = await this.rewardLedgerEntryRepository.findByUserId(userId, filters, pagination);
|
||||
const total = await this.rewardLedgerEntryRepository.countByUserId(userId, filters?.status);
|
||||
const rewards = await this.rewardLedgerEntryRepository.findByAccountSequence(accountSequence, filters, pagination);
|
||||
const total = await this.rewardLedgerEntryRepository.countByAccountSequence(accountSequence, filters?.status);
|
||||
|
||||
return {
|
||||
data: rewards.map(r => ({
|
||||
|
|
@ -323,8 +326,8 @@ export class RewardApplicationService {
|
|||
/**
|
||||
* 获取待领取奖励(含倒计时)
|
||||
*/
|
||||
async getPendingRewards(userId: bigint) {
|
||||
const rewards = await this.rewardLedgerEntryRepository.findPendingByUserId(userId);
|
||||
async getPendingRewards(accountSequence: bigint) {
|
||||
const rewards = await this.rewardLedgerEntryRepository.findPendingByAccountSequence(accountSequence);
|
||||
|
||||
return rewards.map(r => ({
|
||||
id: r.id?.toString(),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { Hashpower } from '../../value-objects/hashpower.vo';
|
|||
export class RewardLedgerEntry {
|
||||
private _id: bigint | null = null;
|
||||
private readonly _userId: bigint;
|
||||
private readonly _accountSequence: bigint;
|
||||
private readonly _rewardSource: RewardSource;
|
||||
private readonly _usdtAmount: Money;
|
||||
private readonly _hashpowerAmount: Hashpower;
|
||||
|
|
@ -35,6 +36,7 @@ export class RewardLedgerEntry {
|
|||
|
||||
private constructor(
|
||||
userId: bigint,
|
||||
accountSequence: bigint,
|
||||
rewardSource: RewardSource,
|
||||
usdtAmount: Money,
|
||||
hashpowerAmount: Hashpower,
|
||||
|
|
@ -44,6 +46,7 @@ export class RewardLedgerEntry {
|
|||
memo: string,
|
||||
) {
|
||||
this._userId = userId;
|
||||
this._accountSequence = accountSequence;
|
||||
this._rewardSource = rewardSource;
|
||||
this._usdtAmount = usdtAmount;
|
||||
this._hashpowerAmount = hashpowerAmount;
|
||||
|
|
@ -59,6 +62,7 @@ export class RewardLedgerEntry {
|
|||
// ============ Getters ============
|
||||
get id(): bigint | null { return this._id; }
|
||||
get userId(): bigint { return this._userId; }
|
||||
get accountSequence(): bigint { return this._accountSequence; }
|
||||
get rewardSource(): RewardSource { return this._rewardSource; }
|
||||
get usdtAmount(): Money { return this._usdtAmount; }
|
||||
get hashpowerAmount(): Hashpower { return this._hashpowerAmount; }
|
||||
|
|
@ -84,6 +88,7 @@ export class RewardLedgerEntry {
|
|||
*/
|
||||
static createPending(params: {
|
||||
userId: bigint;
|
||||
accountSequence: bigint;
|
||||
rewardSource: RewardSource;
|
||||
usdtAmount: Money;
|
||||
hashpowerAmount: Hashpower;
|
||||
|
|
@ -94,6 +99,7 @@ export class RewardLedgerEntry {
|
|||
|
||||
const entry = new RewardLedgerEntry(
|
||||
params.userId,
|
||||
params.accountSequence,
|
||||
params.rewardSource,
|
||||
params.usdtAmount,
|
||||
params.hashpowerAmount,
|
||||
|
|
@ -124,6 +130,7 @@ export class RewardLedgerEntry {
|
|||
*/
|
||||
static createSettleable(params: {
|
||||
userId: bigint;
|
||||
accountSequence: bigint;
|
||||
rewardSource: RewardSource;
|
||||
usdtAmount: Money;
|
||||
hashpowerAmount: Hashpower;
|
||||
|
|
@ -131,6 +138,7 @@ export class RewardLedgerEntry {
|
|||
}): RewardLedgerEntry {
|
||||
const entry = new RewardLedgerEntry(
|
||||
params.userId,
|
||||
params.accountSequence,
|
||||
params.rewardSource,
|
||||
params.usdtAmount,
|
||||
params.hashpowerAmount,
|
||||
|
|
@ -252,6 +260,7 @@ export class RewardLedgerEntry {
|
|||
static reconstitute(data: {
|
||||
id: bigint;
|
||||
userId: bigint;
|
||||
accountSequence: bigint;
|
||||
rewardSource: RewardSource;
|
||||
usdtAmount: number;
|
||||
hashpowerAmount: number;
|
||||
|
|
@ -265,6 +274,7 @@ export class RewardLedgerEntry {
|
|||
}): RewardLedgerEntry {
|
||||
const entry = new RewardLedgerEntry(
|
||||
data.userId,
|
||||
data.accountSequence,
|
||||
data.rewardSource,
|
||||
Money.USDT(data.usdtAmount),
|
||||
Hashpower.create(data.hashpowerAmount),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { Hashpower } from '../../value-objects/hashpower.vo';
|
|||
export class RewardSummary {
|
||||
private _id: bigint | null = null;
|
||||
private readonly _userId: bigint;
|
||||
private readonly _accountSequence: bigint;
|
||||
|
||||
// 待领取收益
|
||||
private _pendingUsdt: Money;
|
||||
|
|
@ -29,8 +30,9 @@ export class RewardSummary {
|
|||
private _lastUpdateAt: Date;
|
||||
private readonly _createdAt: Date;
|
||||
|
||||
private constructor(userId: bigint) {
|
||||
private constructor(userId: bigint, accountSequence: bigint) {
|
||||
this._userId = userId;
|
||||
this._accountSequence = accountSequence;
|
||||
this._pendingUsdt = Money.zero();
|
||||
this._pendingHashpower = Hashpower.zero();
|
||||
this._pendingExpireAt = null;
|
||||
|
|
@ -47,6 +49,7 @@ export class RewardSummary {
|
|||
// ============ Getters ============
|
||||
get id(): bigint | null { return this._id; }
|
||||
get userId(): bigint { return this._userId; }
|
||||
get accountSequence(): bigint { return this._accountSequence; }
|
||||
get pendingUsdt(): Money { return this._pendingUsdt; }
|
||||
get pendingHashpower(): Hashpower { return this._pendingHashpower; }
|
||||
get pendingExpireAt(): Date | null { return this._pendingExpireAt; }
|
||||
|
|
@ -61,8 +64,8 @@ export class RewardSummary {
|
|||
|
||||
// ============ 工厂方法 ============
|
||||
|
||||
static create(userId: bigint): RewardSummary {
|
||||
return new RewardSummary(userId);
|
||||
static create(userId: bigint, accountSequence: bigint): RewardSummary {
|
||||
return new RewardSummary(userId, accountSequence);
|
||||
}
|
||||
|
||||
// ============ 领域行为 ============
|
||||
|
|
@ -140,6 +143,7 @@ export class RewardSummary {
|
|||
static reconstitute(data: {
|
||||
id: bigint;
|
||||
userId: bigint;
|
||||
accountSequence: bigint;
|
||||
pendingUsdt: number;
|
||||
pendingHashpower: number;
|
||||
pendingExpireAt: Date | null;
|
||||
|
|
@ -152,7 +156,7 @@ export class RewardSummary {
|
|||
lastUpdateAt: Date;
|
||||
createdAt: Date;
|
||||
}): RewardSummary {
|
||||
const summary = new RewardSummary(data.userId);
|
||||
const summary = new RewardSummary(data.userId, data.accountSequence);
|
||||
summary._id = data.id;
|
||||
summary._pendingUsdt = Money.USDT(data.pendingUsdt);
|
||||
summary._pendingHashpower = Hashpower.create(data.pendingHashpower);
|
||||
|
|
|
|||
|
|
@ -16,11 +16,24 @@ export interface IRewardLedgerEntryRepository {
|
|||
},
|
||||
pagination?: { page: number; pageSize: number },
|
||||
): 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[]>;
|
||||
findPendingByAccountSequence(accountSequence: bigint): Promise<RewardLedgerEntry[]>;
|
||||
findSettleableByUserId(userId: bigint): Promise<RewardLedgerEntry[]>;
|
||||
findSettleableByAccountSequence(accountSequence: bigint): Promise<RewardLedgerEntry[]>;
|
||||
findExpiredPending(beforeDate: Date): Promise<RewardLedgerEntry[]>;
|
||||
findBySourceOrderNo(sourceOrderNo: string): Promise<RewardLedgerEntry[]>;
|
||||
countByUserId(userId: bigint, status?: RewardStatus): Promise<number>;
|
||||
countByAccountSequence(accountSequence: bigint, status?: RewardStatus): Promise<number>;
|
||||
}
|
||||
|
||||
export const REWARD_LEDGER_ENTRY_REPOSITORY = Symbol('IRewardLedgerEntryRepository');
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import { RewardSummary } from '../aggregates/reward-summary/reward-summary.aggre
|
|||
export interface IRewardSummaryRepository {
|
||||
save(summary: RewardSummary): Promise<void>;
|
||||
findByUserId(userId: bigint): Promise<RewardSummary | null>;
|
||||
findByAccountSequence(accountSequence: bigint): Promise<RewardSummary | null>;
|
||||
getOrCreate(userId: bigint): Promise<RewardSummary>;
|
||||
getOrCreateByAccountSequence(accountSequence: bigint): Promise<RewardSummary>;
|
||||
findByUserIds(userIds: bigint[]): Promise<Map<string, RewardSummary>>;
|
||||
findTopSettleableUsers(limit: number): Promise<RewardSummary[]>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export class RewardLedgerEntryMapper {
|
|||
return RewardLedgerEntry.reconstitute({
|
||||
id: raw.id,
|
||||
userId: raw.userId,
|
||||
accountSequence: raw.accountSequence,
|
||||
rewardSource: RewardSource.create(
|
||||
raw.rightType as RightType,
|
||||
raw.sourceOrderNo,
|
||||
|
|
@ -30,6 +31,7 @@ export class RewardLedgerEntryMapper {
|
|||
return {
|
||||
id: entry.id || undefined,
|
||||
userId: entry.userId,
|
||||
accountSequence: entry.accountSequence,
|
||||
sourceOrderNo: entry.rewardSource.sourceOrderNo,
|
||||
sourceUserId: entry.rewardSource.sourceUserId,
|
||||
rightType: entry.rewardSource.rightType,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export class RewardSummaryMapper {
|
|||
return RewardSummary.reconstitute({
|
||||
id: raw.id,
|
||||
userId: raw.userId,
|
||||
accountSequence: raw.accountSequence,
|
||||
pendingUsdt: Number(raw.pendingUsdt),
|
||||
pendingHashpower: Number(raw.pendingHashpower),
|
||||
pendingExpireAt: raw.pendingExpireAt,
|
||||
|
|
@ -24,6 +25,7 @@ export class RewardSummaryMapper {
|
|||
return {
|
||||
id: summary.id || undefined,
|
||||
userId: summary.userId,
|
||||
accountSequence: summary.accountSequence,
|
||||
pendingUsdt: new Prisma.Decimal(summary.pendingUsdt.amount),
|
||||
pendingHashpower: new Prisma.Decimal(summary.pendingHashpower.value),
|
||||
pendingExpireAt: summary.pendingExpireAt,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryReposi
|
|||
const created = await this.prisma.rewardLedgerEntry.create({
|
||||
data: {
|
||||
userId: data.userId,
|
||||
accountSequence: data.accountSequence,
|
||||
sourceOrderNo: data.sourceOrderNo,
|
||||
sourceUserId: data.sourceUserId,
|
||||
rightType: data.rightType,
|
||||
|
|
@ -152,4 +153,77 @@ export class RewardLedgerEntryRepositoryImpl implements IRewardLedgerEntryReposi
|
|||
}
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export class RewardSummaryRepositoryImpl implements IRewardSummaryRepository {
|
|||
const created = await this.prisma.rewardSummary.create({
|
||||
data: {
|
||||
userId: data.userId,
|
||||
accountSequence: data.accountSequence,
|
||||
pendingUsdt: data.pendingUsdt,
|
||||
pendingHashpower: data.pendingHashpower,
|
||||
pendingExpireAt: data.pendingExpireAt,
|
||||
|
|
@ -88,4 +89,22 @@ export class RewardSummaryRepositoryImpl implements IRewardSummaryRepository {
|
|||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||
sub: payload.sub,
|
||||
username: payload.username,
|
||||
roles: payload.roles,
|
||||
accountSequence: payload.accountSequence,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,9 +26,10 @@ export class InternalWalletController {
|
|||
@Get(':userId/balance')
|
||||
@Public()
|
||||
@ApiOperation({ summary: '获取用户钱包余额(内部API)' })
|
||||
@ApiParam({ name: 'userId', description: '用户ID' })
|
||||
@ApiParam({ name: 'userId', description: '用户ID或accountSequence' })
|
||||
@ApiResponse({ status: 200, description: '余额信息' })
|
||||
async getBalance(@Param('userId') userId: string) {
|
||||
// 优先使用 accountSequence,如果相同则使用 userId
|
||||
const query = new GetMyWalletQuery(userId, userId);
|
||||
const wallet = await this.walletService.getMyWallet(query);
|
||||
return {
|
||||
|
|
@ -44,10 +45,12 @@ export class InternalWalletController {
|
|||
@ApiOperation({ summary: '认种扣款(内部API) - 直接扣款模式' })
|
||||
@ApiResponse({ status: 200, description: '扣款结果' })
|
||||
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(
|
||||
dto.userId,
|
||||
userIdentifier,
|
||||
dto.amount,
|
||||
dto.orderId,
|
||||
);
|
||||
|
|
@ -60,17 +63,22 @@ export class InternalWalletController {
|
|||
@ApiOperation({ summary: '认种冻结资金(内部API) - 预扣款模式第一步' })
|
||||
@ApiResponse({ status: 200, description: '冻结结果' })
|
||||
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(`请求参数: ${JSON.stringify(dto)}`);
|
||||
this.logger.log(` userId: ${dto.userId}`);
|
||||
this.logger.log(` accountSequence: ${dto.accountSequence || '未提供'}`);
|
||||
this.logger.log(` amount: ${dto.amount}`);
|
||||
this.logger.log(` orderId: ${dto.orderId}`);
|
||||
|
||||
try {
|
||||
// 优先使用 accountSequence,如果未提供则使用 userId
|
||||
const userIdentifier = dto.accountSequence || dto.userId;
|
||||
this.logger.log(` 使用标识符: ${userIdentifier}`);
|
||||
|
||||
const command = new FreezeForPlantingCommand(
|
||||
dto.userId,
|
||||
userIdentifier,
|
||||
dto.amount,
|
||||
dto.orderId,
|
||||
);
|
||||
|
|
@ -89,10 +97,12 @@ export class InternalWalletController {
|
|||
@ApiOperation({ summary: '确认认种扣款(内部API) - 预扣款模式第二步' })
|
||||
@ApiResponse({ status: 200, description: '确认结果' })
|
||||
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(
|
||||
dto.userId,
|
||||
userIdentifier,
|
||||
dto.orderId,
|
||||
);
|
||||
const success = await this.walletService.confirmPlantingDeduction(command);
|
||||
|
|
@ -104,10 +114,12 @@ export class InternalWalletController {
|
|||
@ApiOperation({ summary: '解冻认种资金(内部API) - 认种失败时回滚' })
|
||||
@ApiResponse({ status: 200, description: '解冻结果' })
|
||||
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(
|
||||
dto.userId,
|
||||
userIdentifier,
|
||||
dto.orderId,
|
||||
);
|
||||
const success = await this.walletService.unfreezeForPlanting(command);
|
||||
|
|
|
|||
|
|
@ -153,9 +153,13 @@ export class WalletApplicationService {
|
|||
return true;
|
||||
}
|
||||
|
||||
const wallet = await this.walletRepo.findByUserId(userId);
|
||||
// 优先按 accountSequence 查找,如果未找到则按 userId 查找
|
||||
let wallet = await this.walletRepo.findByAccountSequence(userId);
|
||||
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
|
||||
|
|
@ -188,7 +192,7 @@ export class WalletApplicationService {
|
|||
frozenAmount: number;
|
||||
}> {
|
||||
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] orderId: ${command.orderId}`);
|
||||
|
||||
|
|
@ -209,10 +213,14 @@ export class WalletApplicationService {
|
|||
return { success: true, frozenAmount: command.amount };
|
||||
}
|
||||
|
||||
const wallet = await this.walletRepo.findByUserId(userId);
|
||||
// 优先按 accountSequence 查找,如果未找到则按 userId 查找
|
||||
let wallet = await this.walletRepo.findByAccountSequence(userId);
|
||||
if (!wallet) {
|
||||
this.logger.error(`[freezeForPlanting] 钱包不存在: userId=${command.userId}`);
|
||||
throw new WalletNotFoundError(`userId: ${command.userId}`);
|
||||
wallet = await this.walletRepo.findByUserId(userId);
|
||||
}
|
||||
if (!wallet) {
|
||||
this.logger.error(`[freezeForPlanting] 钱包不存在: userId/accountSequence=${command.userId}`);
|
||||
throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`);
|
||||
}
|
||||
|
||||
this.logger.log(`[freezeForPlanting] 钱包信息:`);
|
||||
|
|
@ -289,9 +297,13 @@ export class WalletApplicationService {
|
|||
// 获取冻结金额(流水中是负数,取绝对值)
|
||||
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) {
|
||||
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 wallet = await this.walletRepo.findByUserId(userId);
|
||||
// 优先按 accountSequence 查找,如果未找到则按 userId 查找
|
||||
let wallet = await this.walletRepo.findByAccountSequence(userId);
|
||||
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> {
|
||||
const userId = BigInt(command.userId);
|
||||
|
||||
// 先通过 userId 查找钱包(addRewards 是内部调用,钱包应该已存在)
|
||||
const wallet = await this.walletRepo.findByUserId(userId);
|
||||
// 优先按 accountSequence 查找,如果未找到则按 userId 查找
|
||||
let wallet = await this.walletRepo.findByAccountSequence(userId);
|
||||
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);
|
||||
|
|
@ -440,9 +459,13 @@ export class WalletApplicationService {
|
|||
async claimRewards(command: ClaimRewardsCommand): Promise<void> {
|
||||
const userId = BigInt(command.userId);
|
||||
|
||||
const wallet = await this.walletRepo.findByUserId(userId);
|
||||
// 优先按 accountSequence 查找,如果未找到则按 userId 查找
|
||||
let wallet = await this.walletRepo.findByAccountSequence(userId);
|
||||
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;
|
||||
|
|
@ -483,9 +506,13 @@ export class WalletApplicationService {
|
|||
const userId = BigInt(command.userId);
|
||||
const usdtAmount = Money.USDT(command.usdtAmount);
|
||||
|
||||
const wallet = await this.walletRepo.findByUserId(userId);
|
||||
// 优先按 accountSequence 查找,如果未找到则按 userId 查找
|
||||
let wallet = await this.walletRepo.findByAccountSequence(userId);
|
||||
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
|
||||
|
|
@ -586,10 +613,14 @@ export class WalletApplicationService {
|
|||
orderId: string,
|
||||
): Promise<void> {
|
||||
const userId = BigInt(allocation.targetId);
|
||||
const wallet = await this.walletRepo.findByUserId(userId);
|
||||
|
||||
// 优先按 accountSequence 查找,如果未找到则按 userId 查找
|
||||
let wallet = await this.walletRepo.findByAccountSequence(userId);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -695,10 +726,13 @@ export class WalletApplicationService {
|
|||
throw new Error(`最小提现金额为 ${this.MIN_WITHDRAWAL_AMOUNT} USDT`);
|
||||
}
|
||||
|
||||
// 获取钱包
|
||||
const wallet = await this.walletRepo.findByUserId(userId);
|
||||
// 优先按 accountSequence 查找,如果未找到则按 userId 查找
|
||||
let wallet = await this.walletRepo.findByAccountSequence(userId);
|
||||
if (!wallet) {
|
||||
throw new WalletNotFoundError(`userId: ${command.userId}`);
|
||||
wallet = await this.walletRepo.findByUserId(userId);
|
||||
}
|
||||
if (!wallet) {
|
||||
throw new WalletNotFoundError(`userId/accountSequence: ${command.userId}`);
|
||||
}
|
||||
|
||||
// 验证余额是否足够
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// 获取我的持仓信息
|
||||
///
|
||||
/// 返回用户的总认种数量、有效数量、待生效数量
|
||||
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] 认种数量
|
||||
|
|
|
|||
Loading…
Reference in New Issue