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:
parent
4f55d86050
commit
5c7cb616a7
|
|
@ -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_entries(Prisma 映射表名)\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": []
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 ===============
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue