diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0d73ed7b..900fd673 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -592,7 +592,11 @@ "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(mobile-app\\): 添加待办操作轮询机制\n\n解决老版本 App 升级后不重启导致无法激活待办事项的问题。\n\n- 新增 PendingActionPollingService 定时轮询服务(每4秒检查)\n- App启动时无待办则启动轮询,有待办则直接进入待办页面\n- 轮询检测到待办后自动停止并跳转,防止重入问题\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(mobile-app\\): 用户资料页添加\"同伴认种\"标题和快捷标签\n\n- 在统计卡片上方添加\"同伴认种\"标题(紫色)\n- 在统计卡片下方添加\"引荐\"、\"同伴\"、\"本人\"快捷标签\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(mobile-app\\): 用户资料页术语修改\n\n- 直推 → 引荐\n- 伞下 → 同伴\n- 个人认种 → 本人认种\n- 团队认种 → 同伴认种\n- 推荐人 → 引荐人\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", - "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(mobile-app\\): 已结算数据改为从流水统计API获取\n\n- 从 wallet-service 的 getLedgerStatistics\\(\\) 获取 REWARD_SETTLED 类型的总金额\n- 与流水明细中的结算记录统计来自同一数据源,确保数据一致性\n- 添加调试日志对比 summary 和流水统计的数据\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")" + "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(mobile-app\\): 已结算数据改为从流水统计API获取\n\n- 从 wallet-service 的 getLedgerStatistics\\(\\) 获取 REWARD_SETTLED 类型的总金额\n- 与流水明细中的结算记录统计来自同一数据源,确保数据一致性\n- 添加调试日志对比 summary 和流水统计的数据\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(authorization\\): 火柴人排名过滤已撤销授权的考核记录\n\n- findRankingsByMonthAndRegion 和 findRankingsByMonthAndRoleType 增加过滤条件\n- 排除 authorization.status = ''REVOKED'' 的记录\n- 解决同一用户因有多条授权记录(含已撤销)而重复显示的问题\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(reward-service\\): 修复 WalletServiceClient 未正确解析 wallet-service 响应格式的 Bug\n\n问题原因:\nwallet-service 使用全局 TransformInterceptor 拦截器,会将所有响应包装成:\n{ success: true, data: { success: boolean, ... }, timestamp: \"...\" }\n\n原代码直接读取外层的 success 字段(始终为 true),导致即使业务失败\n(内层 data.success = false)也被误判为成功。\n\n具体案例:\n用户 D25122700024 点击结算时,wallet-service 因余额不足返回:\n{ success: true, data: { success: false, error: \"Insufficient...\" }, ... }\nreward-service 误读为成功,导致奖励被标记为 SETTLED 但钱包余额未变更。\n\n修复内容:\n1. settleToBalance: 解析 response_data.data 获取真实业务结果\n2. confirmPlantingDeduction: 同上\n3. allocateFunds: 同上\n\n所有方法现在会:\n- 使用 response_data.data || response_data 兼容包装和非包装格式\n- 严格检查 data.success !== true 来判断业务是否成功\n- 失败时记录详细错误日志\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service\\): 统一奖励分配到 settleable_usdt,与 reward-service 保持一致\n\n问题原因:\nwallet-service 对不同类型奖励的分配方式不一致:\n- SHARE_RIGHT: 正确使用 addSettleableReward\\(\\) → settleable_usdt\n- CITY_TEAM_RIGHT/COMMUNITY_RIGHT: 错误使用 addAvailableBalance\\(\\) → usdt_available\n\n这导致 reward-service 记录的 SETTLEABLE 奖励总额与 wallet-service 的\nsettleable_usdt 字段不匹配。用户 D25122700024 的案例中:\n- reward-service: 3条奖励共 4464 USDT \\(SHARE_RIGHT 3600 + CITY_TEAM_RIGHT 288 + COMMUNITY_RIGHT 576\\)\n- wallet-service: settleable_usdt = 3600 \\(仅 SHARE_RIGHT\\)\n差额 864 USDT 被错误地放入了 usdt_available\n\n修复内容:\n1. allocateCommunityRight: 改用 addSettleableReward\\(\\) 替代 addAvailableBalance\\(\\)\n2. allocateToRegionAccount: 改用 addSettleableReward\\(\\) 替代 addAvailableBalance\\(\\)\n3. 流水类型统一使用 REWARD_TO_SETTLEABLE 替代 SYSTEM_ALLOCATION\n4. 日志和备注更新以反映新的分配方式\n\n设计原则:\n- reward-service 是奖励的权威来源\n- wallet-service 应跟随 reward-service 的设计\n- 所有奖励都应进入 settleable_usdt,用户主动结算后才转入 usdt_available\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(ls \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\reward-service\\\\prisma\"\" 2>/dev/null || dir \"c:UsersdongDesktoprwadurianbackendservicesreward-serviceprisma\"\")" ], "deny": [], "ask": [] diff --git a/backend/services/wallet-service/prisma/schema.prisma b/backend/services/wallet-service/prisma/schema.prisma index fa9b3a32..0ed0efb8 100644 --- a/backend/services/wallet-service/prisma/schema.prisma +++ b/backend/services/wallet-service/prisma/schema.prisma @@ -96,8 +96,8 @@ model LedgerEntry { refOrderId String? @map("ref_order_id") @db.VarChar(100) refTxHash String? @map("ref_tx_hash") @db.VarChar(100) - // 备注 - memo String? @map("memo") @db.VarChar(500) + // 备注 (使用 Text 类型,无长度限制) + memo String? @map("memo") @db.Text // 扩展数据 payloadJson Json? @map("payload_json") diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index 854d823c..85c4d397 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -909,42 +909,74 @@ export class WalletApplicationService { }> { this.logger.log(`Settling ${params.usdtAmount} USDT to balance for ${params.accountSequence}`); + // 生成结算ID + const settlementId = `STL_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + let balanceAfter = 0; + let userId: bigint = BigInt(0); + try { - // 1. 查找钱包 - const wallet = await this.walletRepo.findByAccountSequence(params.accountSequence); - if (!wallet) { - throw new WalletNotFoundError(`accountSequence: ${params.accountSequence}`); - } + // 使用事务确保账户余额更新和流水记录的原子性 + await this.prisma.$transaction(async (tx) => { + // 1. 查找钱包 + const walletRecord = await tx.walletAccount.findUnique({ + where: { accountSequence: params.accountSequence }, + }); + if (!walletRecord) { + throw new WalletNotFoundError(`accountSequence: ${params.accountSequence}`); + } - const usdtAmount = Money.USDT(params.usdtAmount); - const userId = wallet.userId.value; + userId = walletRecord.userId; + const Decimal = (await import('decimal.js')).default; + const usdtAmountDecimal = new Decimal(params.usdtAmount); + const currentSettleable = new Decimal(walletRecord.settleableUsdt.toString()); + const currentAvailable = new Decimal(walletRecord.usdtAvailable.toString()); + const currentSettledTotal = new Decimal(walletRecord.settledTotalUsdt.toString()); - // 2. 生成结算ID - const settlementId = `STL_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + // 2. 验证可结算余额足够 + if (currentSettleable.lessThan(usdtAmountDecimal)) { + throw new Error(`Insufficient settleable balance: ${currentSettleable} < ${usdtAmountDecimal}`); + } - // 3. 执行钱包结算 - wallet.settleToBalance(usdtAmount, settlementId); - await this.walletRepo.save(wallet); + // 3. 计算新余额 + const newSettleable = currentSettleable.minus(usdtAmountDecimal); + const newAvailable = currentAvailable.plus(usdtAmountDecimal); + const newSettledTotal = currentSettledTotal.plus(usdtAmountDecimal); + balanceAfter = newAvailable.toNumber(); - // 4. 记录账本流水(含详细来源信息) - const ledgerEntry = LedgerEntry.create({ - accountSequence: wallet.accountSequence, - userId: UserId.create(userId), - entryType: LedgerEntryType.REWARD_SETTLED, - amount: usdtAmount, - balanceAfter: wallet.balances.usdt.available, - refOrderId: settlementId, - memo: params.memo || `结算 ${params.usdtAmount} 绿积分到钱包余额`, - payloadJson: { - settlementType: 'SETTLE_TO_BALANCE', - rewardEntryIds: params.rewardEntryIds, - rewardCount: params.rewardEntryIds.length, - breakdown: params.breakdown, - }, + // 4. 更新钱包账户(在事务内) + await tx.walletAccount.update({ + where: { accountSequence: params.accountSequence }, + data: { + settleableUsdt: newSettleable.toFixed(8), + usdtAvailable: newAvailable.toFixed(8), + settledTotalUsdt: newSettledTotal.toFixed(8), + updatedAt: new Date(), + }, + }); + + // 5. 创建流水记录(在事务内) + await tx.ledgerEntry.create({ + data: { + accountSequence: params.accountSequence, + userId: walletRecord.userId, + entryType: LedgerEntryType.REWARD_SETTLED, + amount: usdtAmountDecimal.toFixed(8), + assetType: 'USDT', + balanceAfter: newAvailable.toFixed(8), + refOrderId: settlementId, + memo: params.memo || `结算 ${params.usdtAmount} 绿积分到钱包余额`, + payloadJson: { + settlementType: 'SETTLE_TO_BALANCE', + rewardEntryIds: params.rewardEntryIds, + rewardCount: params.rewardEntryIds.length, + breakdown: params.breakdown, + }, + createdAt: new Date(), + }, + }); }); - await this.ledgerRepo.save(ledgerEntry); - // 5. 使缓存失效 + // 6. 使缓存失效(在事务外,事务成功后执行) await this.walletCacheService.invalidateWallet(userId); this.logger.log(`Successfully settled ${params.usdtAmount} USDT to balance for ${params.accountSequence}`); @@ -953,7 +985,7 @@ export class WalletApplicationService { success: true, settlementId, settledAmount: params.usdtAmount, - balanceAfter: wallet.balances.usdt.available.value, + balanceAfter, }; } catch (error) { this.logger.error(`Failed to settle to balance for ${params.accountSequence}: ${error.message}`);