feat(wallet-service): 添加运营1和积分股池到系统划转账户列表

- 添加 S0000000002 (运营1) 和 S0000000004 (积分股池) 到允许转出白名单
- 更新系统账户名称映射与前端保持一致
- 为 S0000000006 手续费归集账户添加兼容逻辑,当余额为0时从提现订单表统计历史手续费
- 优化过期奖励处理,按分配类型分别记录流水便于明细查看

🤖 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 2026-01-07 03:44:46 -08:00
parent 4f55d86050
commit 5c7cb616a7
3 changed files with 130 additions and 21 deletions

View File

@ -632,7 +632,11 @@
"Bash(where psql:*)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(reporting-service\\): 修复面对面结算数据解包问题\n\nwallet-service 返回 { success, data, timestamp } 包装格式,\ngetOfflineSettlementSummary 需要用 response.data.data 解包才能获取真正的数据。\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 commit -m \"$\\(cat <<''EOF''\nfix\\(wallet/reporting\\): 修复手续费归集统计 API 的数据库表名和响应解包问题\n\n- wallet-service: 修复 getFeeCollectionSummary 中原生 SQL 使用错误表名\n - 将 ledger_entries 改为 wallet_ledger_entriesPrisma 映射表名)\n- reporting-service: 修复 getFeeCollectionSummary/Entries 响应解包\n - wallet-service 返回 { success, data, timestamp } 格式需要解包 data\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 commit -m \"$\\(cat <<''EOF''\nfeat\\(wallet-service\\): 添加手续费归集统计的历史数据兼容\n\n当 FEE_COLLECTION 流水为空时,自动从提现订单表查询历史手续费:\n- getFeeCollectionSummary: 从 withdrawal_orders 和 fiat_withdrawal_orders 聚合统计\n- getFeeCollectionEntries: 从两个订单表查询明细列表,支持分页和类型筛选\n- 按月统计使用 UNION ALL 合并两种提现订单数据\n- 明细记录添加备注说明区分来源(区块链/法币)\n\n回滚方式删除 fallback 代码块和两个私有方法\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 commit -m \"$\\(cat <<''EOF''\nfeat\\(wallet-service\\): 添加手续费归集统计的历史数据兼容\n\n当 FEE_COLLECTION 流水为空时,自动从提现订单表查询历史手续费:\n- getFeeCollectionSummary: 从 withdrawal_orders 和 fiat_withdrawal_orders 聚合统计\n- getFeeCollectionEntries: 从两个订单表查询明细列表,支持分页和类型筛选\n- 按月统计使用 UNION ALL 合并两种提现订单数据\n- 明细记录添加备注说明区分来源(区块链/法币)\n\n回滚方式删除 fallback 代码块和两个私有方法\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(dir /s /b *.yml)",
"Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(mobile-app\\): 添加联系客服功能\n\n在个人中心设置菜单中添加\"联系客服\"入口,点击后显示弹窗,\n用户可以查看客服的QQ号和微信号并支持一键复制到剪贴板。\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 commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service, admin-web\\): 修复系统账户划转金额类型问题\n\n- wallet-service: 支持 amount 为字符串或数字类型,添加类型转换\n- admin-web: 改进错误处理,正确提取 Axios 错误消息\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 commit -m \"$\\(cat <<''EOF''\nfeat\\(mobile-app\\): 更新客服联系方式\n\n- 客服微信1: liulianhuanghou1\n- 客服微信2: liulianhuanghou2\n- 客服QQ1: 1502109619\n- 客服QQ2: 2171447109\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")"
],
"deny": [],
"ask": []

View File

@ -17,10 +17,10 @@ import Decimal from 'decimal.js';
// 系统账户名称映射
const SYSTEM_ACCOUNT_NAMES: Record<string, string> = {
'S0000000001': '总部账户',
'S0000000002': '成本账户',
'S0000000003': '运营账户',
'S0000000004': 'RWAD底池',
'S0000000001': '总部储蓄',
'S0000000002': '运营1',
'S0000000003': '运营2',
'S0000000004': '积分股池',
'S0000000005': '分享权益池',
'S0000000006': '手续费归集',
};
@ -28,7 +28,9 @@ const SYSTEM_ACCOUNT_NAMES: Record<string, string> = {
// 允许转出的系统账户白名单
const ALLOWED_WITHDRAWAL_ACCOUNTS = new Set([
'S0000000001', // 总部账户
'S0000000003', // 运营账户
'S0000000002', // 运营1
'S0000000003', // 运营2
'S0000000004', // 积分股池
'S0000000005', // 分享权益池
'S0000000006', // 手续费归集
]);
@ -480,6 +482,7 @@ export class SystemWithdrawalApplicationService {
/**
*
* [2026-01-07] S0000000006 0
*/
async getWithdrawableSystemAccounts(): Promise<{
accountSequence: string;
@ -488,7 +491,9 @@ export class SystemWithdrawalApplicationService {
}[]> {
const accounts: string[] = [
'S0000000001', // 总部账户
'S0000000003', // 运营账户
'S0000000002', // 运营1
'S0000000003', // 运营2
'S0000000004', // 积分股池
'S0000000005', // 分享权益池
'S0000000006', // 手续费归集
];
@ -512,11 +517,71 @@ export class SystemWithdrawalApplicationService {
const allWallets = [...wallets, ...regionWallets];
return allWallets.map((w) => ({
// [2026-01-07] 兼容:检查 S0000000006 手续费归集账户
// 如果账户不存在或余额为0从提现订单表统计历史手续费
const feeAccountSequence = 'S0000000006';
const feeWallet = allWallets.find(w => w.accountSequence === feeAccountSequence);
const feeBalance = feeWallet ? new Decimal(feeWallet.usdtAvailable.toString()) : new Decimal(0);
let adjustedFeeBalance = feeBalance;
if (feeBalance.isZero()) {
this.logger.log('[getWithdrawableSystemAccounts] S0000000006 余额为0从提现订单表统计历史手续费');
const fallbackBalance = await this.calculateFeeCollectionFromOrders();
adjustedFeeBalance = new Decimal(fallbackBalance);
this.logger.log(`[getWithdrawableSystemAccounts] 历史手续费总额: ${adjustedFeeBalance.toFixed(2)}`);
}
// 构建返回结果
const result = allWallets.map((w) => {
// 对于手续费归集账户,使用调整后的余额
if (w.accountSequence === feeAccountSequence) {
return {
accountSequence: w.accountSequence,
accountName: this.getSystemAccountName(w.accountSequence),
balance: adjustedFeeBalance.toString(),
};
}
return {
accountSequence: w.accountSequence,
accountName: this.getSystemAccountName(w.accountSequence),
balance: w.usdtAvailable.toString(),
}));
};
});
// [2026-01-07] 兼容:如果 S0000000006 账户不存在于数据库,但有历史手续费,也要显示
if (!feeWallet && !adjustedFeeBalance.isZero()) {
result.push({
accountSequence: feeAccountSequence,
accountName: this.getSystemAccountName(feeAccountSequence),
balance: adjustedFeeBalance.toString(),
});
}
return result;
}
/**
*
* [2026-01-07] S0000000006 0
*
*/
private async calculateFeeCollectionFromOrders(): Promise<number> {
// 1. 统计区块链提现订单手续费 (CONFIRMED 状态)
const withdrawalStats = await this.prisma.withdrawalOrder.aggregate({
where: { status: 'CONFIRMED' },
_sum: { fee: true },
});
// 2. 统计法币提现订单手续费 (COMPLETED 状态)
const fiatStats = await this.prisma.fiatWithdrawalOrder.aggregate({
where: { status: 'COMPLETED' },
_sum: { fee: true },
});
const withdrawalFeeAmount = Number(withdrawalStats._sum.fee) || 0;
const fiatFeeAmount = Number(fiatStats._sum.fee) || 0;
return withdrawalFeeAmount + fiatFeeAmount;
}
/**

View File

@ -2339,6 +2339,7 @@ export class WalletApplicationService {
*
* PENDING EXPIRED
* (S0000000001)
* [2026-01-07] 便
*/
async processExpiredRewards(batchSize = 100): Promise<{
processedCount: number;
@ -2374,16 +2375,39 @@ export class WalletApplicationService {
hqWallet.addAvailableBalance(Money.USDT(totalExpiredUsdt));
await this.walletRepo.save(hqWallet);
// 记录流水
// [2026-01-07] 更新:按分配类型分别记录流水,便于明细查看过期分享权益等
// 按 allocationType 分组
const groupedByType = new Map<string, { amount: number; count: number; orderIds: string[] }>();
for (const reward of expiredRewards) {
const type = reward.allocationType;
const existing = groupedByType.get(type) || { amount: 0, count: 0, orderIds: [] };
existing.amount += reward.usdtAmount.value;
existing.count += 1;
existing.orderIds.push(reward.sourceOrderId);
groupedByType.set(type, existing);
}
// 为每种类型创建单独的流水记录
for (const [allocationType, data] of groupedByType) {
const ledgerEntry = LedgerEntry.create({
accountSequence: headquartersAccountSequence,
userId: hqWallet.userId,
entryType: LedgerEntryType.SYSTEM_ALLOCATION,
amount: Money.USDT(totalExpiredUsdt),
memo: `Expired rewards from ${expiredRewards.length} pending entries`,
amount: Money.USDT(data.amount),
memo: `过期${this.getAllocationTypeLabel(allocationType)}收入 (${data.count}笔)`,
payloadJson: {
allocationType,
expiredCount: data.count,
sourceOrderIds: data.orderIds.slice(0, 10), // 只保留前10个订单号
},
});
await this.ledgerRepo.save(ledgerEntry);
this.logger.log(
`[processExpiredRewards] Recorded ${allocationType}: ${data.amount} USDT from ${data.count} expired rewards`,
);
}
this.logger.log(
`[processExpiredRewards] Transferred ${totalExpiredUsdt} USDT to headquarters from ${expiredRewards.length} expired rewards`,
);
@ -2396,6 +2420,22 @@ export class WalletApplicationService {
};
}
/**
*
* [2026-01-07]
*/
private getAllocationTypeLabel(allocationType: string): string {
const labels: Record<string, string> = {
SHARE_RIGHT: '分享权益',
PROVINCE_TEAM_RIGHT: '省团队权益',
CITY_TEAM_RIGHT: '市团队权益',
PROVINCE_AREA_RIGHT: '省区域权益',
CITY_AREA_RIGHT: '市区域权益',
COMMUNITY_RIGHT: '社区权益',
};
return labels[allocationType] || allocationType;
}
// =============== Ledger Statistics ===============
/**