feat(admin-web): 完善系统账户报表收益统计显示

- 分享引荐收益汇总:显示所有状态(PENDING/SETTLEABLE/SETTLED/EXPIRED)的完整数据
- 面对面结算:改为从 wallet_ledger_entries 表查询 SPECIAL_DEDUCTION 类型
- 新增按状态分组统计表格和详细分类卡片

🤖 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-06 20:11:09 -08:00
parent 2c8263754f
commit 4df9895863
6 changed files with 221 additions and 23 deletions

View File

@ -26,11 +26,24 @@ export interface ExpiredRewardsSummary {
} }
// [2026-01-06] 新增:收益类型汇总接口类型 // [2026-01-06] 新增:收益类型汇总接口类型
// [2026-01-06] 更新:添加全量统计字段(包含所有状态)
export interface RewardTypeSummary { export interface RewardTypeSummary {
totalAmount: number; totalAmount: number;
totalCount: number; totalCount: number;
totalSettleableAmount: number; totalSettleableAmount: number;
totalSettledAmount: number; totalSettledAmount: number;
// [2026-01-06] 新增:全量统计(包含所有状态)
allStatusTotalAmount: number;
allStatusTotalCount: number;
pendingAmount: number;
pendingCount: number;
expiredAmount: number;
expiredCount: number;
byStatus: Array<{
status: string;
amount: number;
count: number;
}>;
byMonth: Array<{ byMonth: Array<{
month: string; month: string;
amount: number; amount: number;
@ -149,6 +162,13 @@ export class RewardServiceClient {
totalCount: 0, totalCount: 0,
totalSettleableAmount: 0, totalSettleableAmount: 0,
totalSettledAmount: 0, totalSettledAmount: 0,
allStatusTotalAmount: 0,
allStatusTotalCount: 0,
pendingAmount: 0,
pendingCount: 0,
expiredAmount: 0,
expiredCount: 0,
byStatus: [],
byMonth: [], byMonth: [],
}; };
return { return {

View File

@ -911,7 +911,7 @@ export class RewardApplicationService {
/** /**
* *
* SETTLEABLE + SETTLED * [2026-01-06] PENDINGSETTLEABLESETTLEDEXPIRED
* *
* @param rightType * @param rightType
*/ */
@ -920,6 +920,18 @@ export class RewardApplicationService {
totalCount: number; totalCount: number;
totalSettleableAmount: number; totalSettleableAmount: number;
totalSettledAmount: number; totalSettledAmount: number;
// [2026-01-06] 新增:全量统计(包含所有状态)
allStatusTotalAmount: number;
allStatusTotalCount: number;
pendingAmount: number;
pendingCount: number;
expiredAmount: number;
expiredCount: number;
byStatus: Array<{
status: string;
amount: number;
count: number;
}>;
byMonth: Array<{ byMonth: Array<{
month: string; month: string;
amount: number; amount: number;
@ -928,7 +940,20 @@ export class RewardApplicationService {
}> { }> {
this.logger.log(`[getRewardsSummaryByType] 查询权益类型收益统计: ${rightType}`); this.logger.log(`[getRewardsSummaryByType] 查询权益类型收益统计: ${rightType}`);
// 查询总计SETTLEABLE + SETTLED 状态) // [2026-01-06] 新增:查询所有状态的总计
const allStatusResult = await this.prisma.rewardLedgerEntry.aggregate({
where: {
rightType: rightType,
},
_sum: {
usdtAmount: true,
},
_count: {
id: true,
},
});
// 查询总计SETTLEABLE + SETTLED 状态)- 有效收益
const aggregateResult = await this.prisma.rewardLedgerEntry.aggregate({ const aggregateResult = await this.prisma.rewardLedgerEntry.aggregate({
where: { where: {
rightType: rightType, rightType: rightType,
@ -951,6 +976,9 @@ export class RewardApplicationService {
_sum: { _sum: {
usdtAmount: true, usdtAmount: true,
}, },
_count: {
id: true,
},
}); });
// 查询已结算金额 // 查询已结算金额
@ -962,8 +990,43 @@ export class RewardApplicationService {
_sum: { _sum: {
usdtAmount: true, usdtAmount: true,
}, },
_count: {
id: true,
},
}); });
// [2026-01-06] 新增:查询待领取金额
const pendingResult = await this.prisma.rewardLedgerEntry.aggregate({
where: {
rightType: rightType,
rewardStatus: 'PENDING',
},
_sum: {
usdtAmount: true,
},
_count: {
id: true,
},
});
// [2026-01-06] 新增:查询已过期金额
const expiredResult = await this.prisma.rewardLedgerEntry.aggregate({
where: {
rightType: rightType,
rewardStatus: 'EXPIRED',
},
_sum: {
usdtAmount: true,
},
_count: {
id: true,
},
});
const allStatusTotalAmount = allStatusResult._sum.usdtAmount
? Number(allStatusResult._sum.usdtAmount)
: 0;
const allStatusTotalCount = allStatusResult._count.id || 0;
const totalAmount = aggregateResult._sum.usdtAmount const totalAmount = aggregateResult._sum.usdtAmount
? Number(aggregateResult._sum.usdtAmount) ? Number(aggregateResult._sum.usdtAmount)
: 0; : 0;
@ -971,11 +1034,21 @@ export class RewardApplicationService {
const totalSettleableAmount = settleableResult._sum.usdtAmount const totalSettleableAmount = settleableResult._sum.usdtAmount
? Number(settleableResult._sum.usdtAmount) ? Number(settleableResult._sum.usdtAmount)
: 0; : 0;
const settleableCount = settleableResult._count.id || 0;
const totalSettledAmount = settledResult._sum.usdtAmount const totalSettledAmount = settledResult._sum.usdtAmount
? Number(settledResult._sum.usdtAmount) ? Number(settledResult._sum.usdtAmount)
: 0; : 0;
const settledCount = settledResult._count.id || 0;
const pendingAmount = pendingResult._sum.usdtAmount
? Number(pendingResult._sum.usdtAmount)
: 0;
const pendingCount = pendingResult._count.id || 0;
const expiredAmount = expiredResult._sum.usdtAmount
? Number(expiredResult._sum.usdtAmount)
: 0;
const expiredCount = expiredResult._count.id || 0;
// 查询按月统计 // [2026-01-06] 修改:查询按月统计(所有状态)
const byMonth = await this.prisma.$queryRaw<Array<{ const byMonth = await this.prisma.$queryRaw<Array<{
month: string; month: string;
amount: any; amount: any;
@ -987,17 +1060,31 @@ export class RewardApplicationService {
COUNT(*) as count COUNT(*) as count
FROM reward_ledger_entries FROM reward_ledger_entries
WHERE right_type = ${rightType} WHERE right_type = ${rightType}
AND reward_status IN ('SETTLEABLE', 'SETTLED')
GROUP BY TO_CHAR(created_at, 'YYYY-MM') GROUP BY TO_CHAR(created_at, 'YYYY-MM')
ORDER BY month DESC ORDER BY month DESC
LIMIT 12 LIMIT 12
`; `;
// [2026-01-06] 新增:按状态分组统计
const byStatus = [
{ status: 'PENDING', amount: pendingAmount, count: pendingCount },
{ status: 'SETTLEABLE', amount: totalSettleableAmount, count: settleableCount },
{ status: 'SETTLED', amount: totalSettledAmount, count: settledCount },
{ status: 'EXPIRED', amount: expiredAmount, count: expiredCount },
];
return { return {
totalAmount, totalAmount,
totalCount, totalCount,
totalSettleableAmount, totalSettleableAmount,
totalSettledAmount, totalSettledAmount,
allStatusTotalAmount,
allStatusTotalCount,
pendingAmount,
pendingCount,
expiredAmount,
expiredCount,
byStatus,
byMonth: byMonth.map(row => ({ byMonth: byMonth.map(row => ({
month: row.month, month: row.month,
amount: Number(row.amount) || 0, amount: Number(row.amount) || 0,

View File

@ -3060,6 +3060,8 @@ export class WalletApplicationService {
/** /**
* (线) * (线)
* *
* [2026-01-06] wallet_ledger_entries SPECIAL_DEDUCTION
* offline_settlement_deductions
*/ */
async getOfflineSettlementSummary(params: { async getOfflineSettlementSummary(params: {
startDate?: Date; startDate?: Date;
@ -3073,10 +3075,12 @@ export class WalletApplicationService {
count: number; count: number;
}>; }>;
}> { }> {
this.logger.log(`[getOfflineSettlementSummary] 查询面对面结算统计`); this.logger.log(`[getOfflineSettlementSummary] 查询面对面结算统计(从 SPECIAL_DEDUCTION 流水)`);
// 构建日期筛选条件 // 构建日期筛选条件,包含 entryType 过滤
const whereClause: any = {}; const whereClause: any = {
entryType: 'SPECIAL_DEDUCTION',
};
if (params.startDate || params.endDate) { if (params.startDate || params.endDate) {
whereClause.createdAt = {}; whereClause.createdAt = {};
if (params.startDate) { if (params.startDate) {
@ -3087,23 +3091,24 @@ export class WalletApplicationService {
} }
} }
// 查询总计 // 查询总计(从 wallet_ledger_entries 表)
const aggregateResult = await this.prisma.offlineSettlementDeduction.aggregate({ const aggregateResult = await this.prisma.ledgerEntry.aggregate({
where: whereClause, where: whereClause,
_sum: { _sum: {
deductedAmount: true, amount: true,
}, },
_count: { _count: {
id: true, id: true,
}, },
}); });
const totalAmount = aggregateResult._sum.deductedAmount // SPECIAL_DEDUCTION 的 amount 是负数,取绝对值
? Number(aggregateResult._sum.deductedAmount) const totalAmount = aggregateResult._sum.amount
? Math.abs(Number(aggregateResult._sum.amount))
: 0; : 0;
const totalCount = aggregateResult._count.id || 0; const totalCount = aggregateResult._count.id || 0;
// 查询按月统计(简化版本,不使用日期筛选 // 查询按月统计(从 wallet_ledger_entries 表SPECIAL_DEDUCTION 类型
const byMonth = await this.prisma.$queryRaw<Array<{ const byMonth = await this.prisma.$queryRaw<Array<{
month: string; month: string;
amount: any; amount: any;
@ -3111,14 +3116,17 @@ export class WalletApplicationService {
}>>` }>>`
SELECT SELECT
TO_CHAR(created_at, 'YYYY-MM') as month, TO_CHAR(created_at, 'YYYY-MM') as month,
SUM(deducted_amount) as amount, ABS(SUM(amount)) as amount,
COUNT(*) as count COUNT(*) as count
FROM offline_settlement_deductions FROM wallet_ledger_entries
WHERE entry_type = 'SPECIAL_DEDUCTION'
GROUP BY TO_CHAR(created_at, 'YYYY-MM') GROUP BY TO_CHAR(created_at, 'YYYY-MM')
ORDER BY month DESC ORDER BY month DESC
LIMIT 12 LIMIT 12
`; `;
this.logger.log(`[getOfflineSettlementSummary] 查询结果: totalAmount=${totalAmount}, totalCount=${totalCount}`);
return { return {
totalAmount, totalAmount,
totalCount, totalCount,

View File

@ -275,6 +275,22 @@
background-color: #dcfce7; background-color: #dcfce7;
color: #166534; color: #166534;
} }
/* [2026-01-06] 新增:收益状态样式 */
&.settled {
background-color: #dcfce7;
color: #166534;
}
&.expired {
background-color: #fee2e2;
color: #991b1b;
}
&.pending {
background-color: #fef3c7;
color: #92400e;
}
} }
/* 页脚 */ /* 页脚 */

View File

@ -1053,30 +1053,82 @@ function RewardTypeSummarySection({
</button> </button>
</div> </div>
{/* 汇总卡片 */} {/* [2026-01-06] 更新:全量汇总卡片(所有状态) */}
<div className={styles.summaryCards}> <div className={styles.summaryCards}>
<div className={styles.summaryCard}> <div className={styles.summaryCard}>
<span className={styles.summaryLabel}></span> <span className={styles.summaryLabel}></span>
<span className={styles.summaryValue}>{formatAmount(data.allStatusTotalAmount ?? data.totalAmount)} 绿</span>
</div>
<div className={styles.summaryCard}>
<span className={styles.summaryLabel}></span>
<span className={styles.summaryValue}>{data.allStatusTotalCount ?? data.totalCount}</span>
</div>
<div className={styles.summaryCard}>
<span className={styles.summaryLabel}></span>
<span className={styles.summaryValue}>{formatAmount(data.totalAmount)} 绿</span> <span className={styles.summaryValue}>{formatAmount(data.totalAmount)} 绿</span>
</div> </div>
<div className={styles.summaryCard}> <div className={styles.summaryCard}>
<span className={styles.summaryLabel}></span> <span className={styles.summaryLabel}></span>
<span className={styles.summaryValue}>{data.totalCount}</span> <span className={styles.summaryValue}>{data.totalCount}</span>
</div> </div>
</div>
{/* [2026-01-06] 新增:按状态分组统计 */}
{data.byStatus && data.byStatus.length > 0 && (
<>
<h4 className={styles.subTitle}></h4>
<div className={styles.tableWrapper}>
<table className={styles.table}>
<thead>
<tr>
<th></th>
<th> (绿)</th>
<th></th>
</tr>
</thead>
<tbody>
{data.byStatus.map((item) => (
<tr key={item.status}>
<td>
<span className={`${styles.statusBadge} ${item.status === 'SETTLED' ? styles.settled : ''} ${item.status === 'EXPIRED' ? styles.expired : ''} ${item.status === 'PENDING' ? styles.pending : ''}`}>
{REWARD_STATUS_LABELS[item.status] || item.status}
</span>
</td>
<td>{formatAmount(item.amount)}</td>
<td>{item.count}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
{/* 详细分类卡片 */}
<h4 className={styles.subTitle}></h4>
<div className={styles.summaryCards}>
<div className={styles.summaryCard}> <div className={styles.summaryCard}>
<span className={styles.summaryLabel}></span> <span className={styles.summaryLabel}></span>
<span className={styles.summaryValue}>{formatAmount(data.totalSettleableAmount)} 绿</span> <span className={styles.summaryValue}>{formatAmount(data.pendingAmount ?? 0)} ({data.pendingCount ?? 0})</span>
</div> </div>
<div className={styles.summaryCard}> <div className={styles.summaryCard}>
<span className={styles.summaryLabel}></span> <span className={styles.summaryLabel}></span>
<span className={styles.summaryValue}>{formatAmount(data.totalSettledAmount)} 绿</span> <span className={styles.summaryValue}>{formatAmount(data.totalSettleableAmount)} ({(data.byStatus?.find(s => s.status === 'SETTLEABLE')?.count) ?? 0})</span>
</div>
<div className={styles.summaryCard}>
<span className={styles.summaryLabel}></span>
<span className={styles.summaryValue}>{formatAmount(data.totalSettledAmount)} ({(data.byStatus?.find(s => s.status === 'SETTLED')?.count) ?? 0})</span>
</div>
<div className={styles.summaryCard}>
<span className={styles.summaryLabel}></span>
<span className={styles.summaryValue}>{formatAmount(data.expiredAmount ?? 0)} ({data.expiredCount ?? 0})</span>
</div> </div>
</div> </div>
{/* 按月统计 */} {/* 按月统计 */}
{data.byMonth && data.byMonth.length > 0 ? ( {data.byMonth && data.byMonth.length > 0 ? (
<> <>
<h4 className={styles.subTitle}></h4> <h4 className={styles.subTitle}></h4>
<div className={styles.tableWrapper}> <div className={styles.tableWrapper}>
<table className={styles.table}> <table className={styles.table}>
<thead> <thead>

View File

@ -188,6 +188,7 @@ export const ENTRY_TYPE_LABELS: Record<string, string> = {
}; };
// [2026-01-06] 新增:收益类型汇总统计类型 // [2026-01-06] 新增:收益类型汇总统计类型
// [2026-01-06] 更新:添加全量统计字段(包含所有状态)
/** /**
* *
*/ */
@ -196,6 +197,18 @@ export interface RewardTypeSummary {
totalCount: number; totalCount: number;
totalSettleableAmount: number; totalSettleableAmount: number;
totalSettledAmount: number; totalSettledAmount: number;
// [2026-01-06] 新增:全量统计(包含所有状态)
allStatusTotalAmount: number;
allStatusTotalCount: number;
pendingAmount: number;
pendingCount: number;
expiredAmount: number;
expiredCount: number;
byStatus: Array<{
status: string;
amount: number;
count: number;
}>;
byMonth: Array<{ byMonth: Array<{
month: string; month: string;
amount: number; amount: number;
@ -281,8 +294,10 @@ export const REWARD_RIGHT_TYPE_LABELS: Record<string, string> = {
/** /**
* *
* [2026-01-06] PENDING
*/ */
export const REWARD_STATUS_LABELS: Record<string, string> = { export const REWARD_STATUS_LABELS: Record<string, string> = {
PENDING: '待领取',
SETTLEABLE: '可结算', SETTLEABLE: '可结算',
SETTLED: '已结算', SETTLED: '已结算',
EXPIRED: '已过期', EXPIRED: '已过期',