feat(admin-web): 添加面对面结算明细列表功能

- wallet-service: 新增 getOfflineSettlementEntries 方法和 API
- reporting-service: 新增客户端方法和 API 转发
- admin-web: 添加明细列表组件和样式,支持展开/收起

🤖 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:40:12 -08:00
parent 9953f0eee5
commit 4e5d9685a1
10 changed files with 886 additions and 38 deletions

View File

@ -79,6 +79,30 @@ export class SystemAccountReportController {
return this.systemAccountReportService.getExpiredRewardsSummary({ startDate, endDate });
}
// [2026-01-07] 新增:面对面结算明细列表
// 回滚方式:删除此方法
@Get('offline-settlement-entries')
@ApiOperation({ summary: '获取面对面结算明细列表' })
@ApiQuery({ name: 'page', required: false, description: '页码默认1' })
@ApiQuery({ name: 'pageSize', required: false, description: '每页条数默认50' })
@ApiQuery({ name: 'startDate', required: false, description: '开始日期 (YYYY-MM-DD)' })
@ApiQuery({ name: 'endDate', required: false, description: '结束日期 (YYYY-MM-DD)' })
@ApiResponse({ status: 200, description: '面对面结算明细列表' })
async getOfflineSettlementEntries(
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
) {
this.logger.log(`[getOfflineSettlementEntries] 请求面对面结算明细, page=${page}, pageSize=${pageSize}`);
return this.systemAccountReportService.getOfflineSettlementEntries({
page: page ? parseInt(page, 10) : undefined,
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
startDate,
endDate,
});
}
// [2026-01-05] 新增:所有系统账户分类账明细
@Get('all-ledger')
@ApiOperation({ summary: '获取所有系统账户的分类账明细' })
@ -133,7 +157,7 @@ export class SystemAccountReportController {
}
@Get('fee-entries-detailed')
@ApiOperation({ summary: '获取手续费类型的详细记录列表' })
@ApiOperation({ summary: '获取手续费类型的详细记录列表(系统费用,非提现手续费)' })
@ApiQuery({ name: 'page', required: false, description: '页码默认1' })
@ApiQuery({ name: 'pageSize', required: false, description: '每页条数默认50' })
@ApiResponse({ status: 200, description: '手续费类型详细记录列表' })
@ -147,4 +171,41 @@ export class SystemAccountReportController {
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
});
}
// [2026-01-06] 新增:手续费归集账户统计接口(真正的提现手续费)
// 来源wallet-service 中 S0000000006 账户的 FEE_COLLECTION 类型流水
// 回滚方式:删除以下 API 方法即可
@Get('fee-collection-summary')
@ApiOperation({ summary: '获取手续费归集账户汇总统计(提现手续费)' })
@ApiResponse({ status: 200, description: '手续费归集账户汇总统计' })
async getFeeCollectionSummary() {
this.logger.log(`[getFeeCollectionSummary] 请求手续费归集账户汇总统计`);
return this.systemAccountReportService.getFeeCollectionSummary();
}
@Get('fee-collection-entries')
@ApiOperation({ summary: '获取手续费归集账户流水明细(提现手续费)' })
@ApiQuery({ name: 'page', required: false, description: '页码默认1' })
@ApiQuery({ name: 'pageSize', required: false, description: '每页条数默认50' })
@ApiQuery({ name: 'feeType', required: false, description: '手续费类型筛选 (WITHDRAWAL_FEE/FIAT_WITHDRAWAL_FEE)' })
@ApiQuery({ name: 'startDate', required: false, description: '开始日期' })
@ApiQuery({ name: 'endDate', required: false, description: '结束日期' })
@ApiResponse({ status: 200, description: '手续费归集流水明细列表' })
async getFeeCollectionEntries(
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('feeType') feeType?: string,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
) {
this.logger.log(`[getFeeCollectionEntries] 请求手续费归集流水明细, page=${page}, pageSize=${pageSize}, feeType=${feeType}`);
return this.systemAccountReportService.getFeeCollectionEntries({
page: page ? parseInt(page, 10) : undefined,
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
feeType,
startDate,
endDate,
});
}
}

View File

@ -6,7 +6,7 @@
* application.module.ts
*/
import { Injectable, Logger } from '@nestjs/common';
import { WalletServiceClient, OfflineSettlementSummary, AllSystemAccountsResponse, AllSystemAccountsLedgerResponse } from '../../infrastructure/external/wallet-service/wallet-service.client';
import { WalletServiceClient, OfflineSettlementSummary, AllSystemAccountsResponse, AllSystemAccountsLedgerResponse, FeeCollectionSummaryResponse, FeeCollectionEntriesResponse, OfflineSettlementEntriesResponse } from '../../infrastructure/external/wallet-service/wallet-service.client';
import { RewardServiceClient, ExpiredRewardsSummary, AllRewardTypeSummaries, RewardEntriesResponse } from '../../infrastructure/external/reward-service/reward-service.client';
/**
@ -185,6 +185,21 @@ export class SystemAccountReportApplicationService {
return this.rewardServiceClient.getExpiredRewardsSummary(params);
}
// [2026-01-07] 新增:获取面对面结算明细列表
// 回滚方式:删除此方法
/**
*
*/
async getOfflineSettlementEntries(params?: {
page?: number;
pageSize?: number;
startDate?: string;
endDate?: string;
}): Promise<OfflineSettlementEntriesResponse> {
this.logger.log('[getOfflineSettlementEntries] 获取面对面结算明细列表');
return this.walletServiceClient.getOfflineSettlementEntries(params);
}
// [2026-01-05] 新增:获取所有系统账户的分类账明细
/**
*
@ -228,6 +243,7 @@ export class SystemAccountReportApplicationService {
/**
*
* @deprecated 使 getFeeCollectionEntries
*/
async getFeeEntriesDetailed(params?: {
page?: number;
@ -237,6 +253,33 @@ export class SystemAccountReportApplicationService {
return this.rewardServiceClient.getFeeEntriesDetailed(params);
}
// [2026-01-06] 新增:获取手续费归集账户统计(真正的提现手续费)
// 来源wallet-service 中 S0000000006 账户的 FEE_COLLECTION 类型流水
// 回滚方式:删除以下方法
/**
*
* S0000000006
*/
async getFeeCollectionSummary(): Promise<FeeCollectionSummaryResponse> {
this.logger.log('[getFeeCollectionSummary] 获取手续费归集账户汇总统计');
return this.walletServiceClient.getFeeCollectionSummary();
}
/**
*
*/
async getFeeCollectionEntries(params?: {
page?: number;
pageSize?: number;
feeType?: string;
startDate?: string;
endDate?: string;
}): Promise<FeeCollectionEntriesResponse> {
this.logger.log('[getFeeCollectionEntries] 获取手续费归集流水明细');
return this.walletServiceClient.getFeeCollectionEntries(params);
}
/**
*
*/

View File

@ -19,6 +19,25 @@ export interface OfflineSettlementSummary {
}>;
}
// [2026-01-07] 新增:面对面结算明细条目
export interface OfflineSettlementEntryDTO {
id: string;
accountSequence: string;
amount: number;
memo: string | null;
refOrderId: string | null;
createdAt: string;
}
// [2026-01-07] 新增:面对面结算明细列表响应
export interface OfflineSettlementEntriesResponse {
entries: OfflineSettlementEntryDTO[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export interface SystemAccountBalance {
accountSequence: string;
name: string;
@ -179,6 +198,46 @@ export class WalletServiceClient {
}
}
// [2026-01-07] 新增:获取面对面结算明细列表
// 回滚方式:删除此方法
/**
*
*/
async getOfflineSettlementEntries(params?: {
page?: number;
pageSize?: number;
startDate?: string;
endDate?: string;
}): Promise<OfflineSettlementEntriesResponse> {
try {
const queryParams = new URLSearchParams();
if (params?.page) queryParams.append('page', params.page.toString());
if (params?.pageSize) queryParams.append('pageSize', params.pageSize.toString());
if (params?.startDate) queryParams.append('startDate', params.startDate);
if (params?.endDate) queryParams.append('endDate', params.endDate);
const url = `${this.baseUrl}/api/v1/wallets/statistics/offline-settlement-entries?${queryParams.toString()}`;
this.logger.debug(`[getOfflineSettlementEntries] 请求: ${url}`);
const response = await firstValueFrom(
this.httpService.get<{ success: boolean; data: OfflineSettlementEntriesResponse; timestamp: string }>(url),
);
// wallet-service 返回 { success, data, timestamp } 包装格式
return response.data.data;
} catch (error) {
this.logger.error(`[getOfflineSettlementEntries] 失败: ${error.message}`);
// 返回默认值,不阻塞报表
return {
entries: [],
total: 0,
page: 1,
pageSize: params?.pageSize ?? 50,
totalPages: 0,
};
}
}
/**
*
*/

View File

@ -372,6 +372,36 @@ export class InternalWalletController {
return result;
}
// [2026-01-07] 新增:面对面结算明细列表 API
// 回滚方式:删除此方法
@Get('statistics/offline-settlement-entries')
@Public()
@ApiOperation({ summary: '面对面(线下)结算明细列表(内部API) - 用于系统账户报表' })
@ApiQuery({ name: 'page', required: false, description: '页码默认1' })
@ApiQuery({ name: 'pageSize', required: false, description: '每页条数默认50' })
@ApiQuery({ name: 'startDate', required: false, description: '开始日期 (YYYY-MM-DD)' })
@ApiQuery({ name: 'endDate', required: false, description: '结束日期 (YYYY-MM-DD)' })
@ApiResponse({ status: 200, description: '面对面结算明细列表' })
async getOfflineSettlementEntries(
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
) {
this.logger.log(`========== statistics/offline-settlement-entries 请求 ==========`);
this.logger.log(`page: ${page}, pageSize: ${pageSize}, startDate: ${startDate}, endDate: ${endDate}`);
const result = await this.walletService.getOfflineSettlementEntries({
page: page ? parseInt(page, 10) : undefined,
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
});
this.logger.log(`面对面结算明细结果: total=${result.total}, page=${result.page}, totalPages=${result.totalPages}`);
return result;
}
@Get('statistics/system-accounts-balances')
@Public()
@ApiOperation({ summary: '批量查询系统账户余额(内部API) - 用于系统账户报表' })
@ -463,4 +493,52 @@ export class InternalWalletController {
this.logger.log(`所有系统账户分类账查询完成: 固定=${result.fixedAccountsLedger.length}, 省=${result.provinceAccountsLedger.length}, 市=${result.cityAccountsLedger.length}, 总流水=${totalLedgers}`);
return result;
}
// [2026-01-06] 新增:手续费归集账户统计 API
// 用于系统账户报表中的"手续费账户汇总" Tab
// 回滚方式:删除以下 API 方法即可
@Get('statistics/fee-collection-summary')
@Public()
@ApiOperation({ summary: '获取手续费归集账户汇总统计(内部API) - 用于系统账户报表' })
@ApiResponse({ status: 200, description: '手续费归集账户汇总统计' })
async getFeeCollectionSummary() {
this.logger.log(`========== statistics/fee-collection-summary 请求 ==========`);
const result = await this.walletService.getFeeCollectionSummary();
this.logger.log(`手续费归集统计结果: totalAmount=${result.totalAmount}, totalCount=${result.totalCount}, balance=${result.accountBalance}`);
return result;
}
@Get('statistics/fee-collection-entries')
@Public()
@ApiOperation({ summary: '获取手续费归集账户流水明细(内部API) - 用于系统账户报表' })
@ApiQuery({ name: 'page', required: false, description: '页码默认1' })
@ApiQuery({ name: 'pageSize', required: false, description: '每页条数默认50' })
@ApiQuery({ name: 'feeType', required: false, description: '手续费类型筛选 (WITHDRAWAL_FEE/FIAT_WITHDRAWAL_FEE)' })
@ApiQuery({ name: 'startDate', required: false, description: '开始日期' })
@ApiQuery({ name: 'endDate', required: false, description: '结束日期' })
@ApiResponse({ status: 200, description: '手续费归集流水明细列表' })
async getFeeCollectionEntries(
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('feeType') feeType?: string,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
) {
this.logger.log(`========== statistics/fee-collection-entries 请求 ==========`);
this.logger.log(`page=${page}, pageSize=${pageSize}, feeType=${feeType}`);
const result = await this.walletService.getFeeCollectionEntries({
page: page ? parseInt(page, 10) : 1,
pageSize: pageSize ? parseInt(pageSize, 10) : 50,
feeType,
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
});
this.logger.log(`手续费归集流水查询结果: ${result.total} 条记录`);
return result;
}
}

View File

@ -3138,6 +3138,91 @@ export class WalletApplicationService {
};
}
// [2026-01-07] 新增:获取面对面结算明细列表
// 回滚方式:删除此方法
/**
*
* SPECIAL_DEDUCTION
*/
async getOfflineSettlementEntries(params: {
page?: number;
pageSize?: number;
startDate?: Date;
endDate?: Date;
}): Promise<{
entries: Array<{
id: string;
accountSequence: string;
amount: number;
memo: string | null;
refOrderId: string | null;
createdAt: string;
}>;
total: number;
page: number;
pageSize: number;
totalPages: number;
}> {
const page = params.page ?? 1;
const pageSize = params.pageSize ?? 50;
const skip = (page - 1) * pageSize;
this.logger.log(`[getOfflineSettlementEntries] 查询面对面结算明细: page=${page}, pageSize=${pageSize}`);
// 构建查询条件
const where: any = {
entryType: 'SPECIAL_DEDUCTION',
};
if (params.startDate || params.endDate) {
where.createdAt = {};
if (params.startDate) {
where.createdAt.gte = params.startDate;
}
if (params.endDate) {
where.createdAt.lte = params.endDate;
}
}
// 查询总数
const total = await this.prisma.ledgerEntry.count({ where });
// 查询数据
const entries = await this.prisma.ledgerEntry.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: pageSize,
select: {
id: true,
accountSequence: true,
amount: true,
memo: true,
refOrderId: true,
createdAt: true,
},
});
const totalPages = Math.ceil(total / pageSize);
this.logger.log(`[getOfflineSettlementEntries] 查询结果: total=${total}, page=${page}, totalPages=${totalPages}`);
return {
entries: entries.map(entry => ({
id: entry.id.toString(),
accountSequence: entry.accountSequence,
amount: Math.abs(Number(entry.amount)), // SPECIAL_DEDUCTION 金额是负数,取绝对值
memo: entry.memo,
refOrderId: entry.refOrderId,
createdAt: entry.createdAt.toISOString(),
})),
total,
page,
pageSize,
totalPages,
};
}
/**
*
* ()
@ -3248,4 +3333,206 @@ export class WalletApplicationService {
this.logger.log('[getAllSystemAccounts] 固定: ' + fixedAccounts.length + ', 省: ' + provinceAccounts.length + ', 市: ' + cityAccounts.length);
return { fixedAccounts, provinceAccounts, cityAccounts };
}
// =============== 手续费归集账户统计 API ===============
// [2026-01-06] 新增:获取手续费归集账户 (S0000000006) 的统计数据
// 用于系统账户报表中的"手续费账户汇总" Tab
// 回滚方式:删除以下方法
/**
*
* S0000000006 FEE_COLLECTION
*/
async getFeeCollectionSummary(): Promise<{
accountSequence: string;
accountBalance: number;
totalAmount: number;
totalCount: number;
breakdown: Array<{
feeType: string;
amount: number;
count: number;
}>;
byMonth: Array<{
month: string;
amount: number;
count: number;
}>;
}> {
this.logger.log('[getFeeCollectionSummary] 查询手续费归集账户统计');
const feeAccountSequence = WalletApplicationService.FEE_COLLECTION_ACCOUNT;
// 1. 获取账户余额
const wallet = await this.prisma.walletAccount.findUnique({
where: { accountSequence: feeAccountSequence },
select: { usdtAvailable: true },
});
const accountBalance = Number(wallet?.usdtAvailable) || 0;
// 2. 统计所有 FEE_COLLECTION 类型的流水总额和总笔数
const aggregateResult = await this.prisma.ledgerEntry.aggregate({
where: {
accountSequence: feeAccountSequence,
entryType: 'FEE_COLLECTION',
},
_sum: {
amount: true,
},
_count: {
id: true,
},
});
const totalAmount = Number(aggregateResult._sum.amount) || 0;
const totalCount = aggregateResult._count.id || 0;
// 3. 按手续费类型分组统计(从 payloadJson 中提取 feeType
// 由于 Prisma 不支持直接 groupBy jsonb 字段,使用原生 SQL
const breakdownRaw = await this.prisma.$queryRaw<Array<{
feeType: string;
amount: any;
count: any;
}>>`
SELECT
COALESCE(payload_json->>'feeType', 'UNKNOWN') as "feeType",
SUM(amount) as amount,
COUNT(*) as count
FROM ledger_entries
WHERE account_sequence = ${feeAccountSequence}
AND entry_type = 'FEE_COLLECTION'
GROUP BY COALESCE(payload_json->>'feeType', 'UNKNOWN')
ORDER BY amount DESC
`;
// 4. 按月统计
const byMonthRaw = await this.prisma.$queryRaw<Array<{
month: string;
amount: any;
count: any;
}>>`
SELECT
TO_CHAR(created_at, 'YYYY-MM') as month,
SUM(amount) as amount,
COUNT(*) as count
FROM ledger_entries
WHERE account_sequence = ${feeAccountSequence}
AND entry_type = 'FEE_COLLECTION'
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
ORDER BY month DESC
LIMIT 12
`;
this.logger.log(`[getFeeCollectionSummary] 统计完成: totalAmount=${totalAmount}, totalCount=${totalCount}`);
return {
accountSequence: feeAccountSequence,
accountBalance,
totalAmount,
totalCount,
breakdown: breakdownRaw.map(row => ({
feeType: row.feeType,
amount: Number(row.amount) || 0,
count: Number(row.count) || 0,
})),
byMonth: byMonthRaw.map(row => ({
month: row.month,
amount: Number(row.amount) || 0,
count: Number(row.count) || 0,
})),
};
}
/**
*
*/
async getFeeCollectionEntries(params: {
page?: number;
pageSize?: number;
feeType?: string;
startDate?: Date;
endDate?: Date;
}): Promise<{
entries: Array<{
id: string;
feeType: string;
amount: number;
refOrderId: string | null;
memo: string | null;
createdAt: string;
}>;
total: number;
page: number;
pageSize: number;
totalPages: number;
}> {
const page = params.page ?? 1;
const pageSize = params.pageSize ?? 50;
const skip = (page - 1) * pageSize;
const feeAccountSequence = WalletApplicationService.FEE_COLLECTION_ACCOUNT;
this.logger.log(`[getFeeCollectionEntries] 查询手续费归集流水: page=${page}, pageSize=${pageSize}, feeType=${params.feeType}`);
// 构建查询条件
const where: any = {
accountSequence: feeAccountSequence,
entryType: 'FEE_COLLECTION',
};
if (params.startDate || params.endDate) {
where.createdAt = {};
if (params.startDate) {
where.createdAt.gte = params.startDate;
}
if (params.endDate) {
where.createdAt.lte = params.endDate;
}
}
// 查询总数
const total = await this.prisma.ledgerEntry.count({ where });
// 查询数据
const entries = await this.prisma.ledgerEntry.findMany({
where,
orderBy: { createdAt: 'desc' },
skip,
take: pageSize,
select: {
id: true,
amount: true,
refOrderId: true,
memo: true,
payloadJson: true,
createdAt: true,
},
});
// 如果指定了 feeType在应用层过滤因为 Prisma 不支持 jsonb 字段的 where 条件)
let filteredEntries = entries;
if (params.feeType) {
filteredEntries = entries.filter(entry => {
const payload = entry.payloadJson as any;
return payload?.feeType === params.feeType;
});
}
return {
entries: filteredEntries.map(entry => {
const payload = entry.payloadJson as any;
return {
id: entry.id.toString(),
feeType: payload?.feeType || 'UNKNOWN',
amount: Number(entry.amount),
refOrderId: entry.refOrderId,
memo: entry.memo,
createdAt: entry.createdAt.toISOString(),
};
}),
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
};
}
}

View File

@ -156,12 +156,31 @@
align-items: center;
}
.accountInfo {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
padding: 8px 12px;
background: #f3f4f6;
border-radius: 6px;
}
.accountLabel {
font-size: 15px;
font-weight: 600;
color: #1f2937;
}
.accountValue {
font-size: 14px;
color: #374151;
font-family: monospace;
background: #fff;
padding: 2px 8px;
border-radius: 4px;
}
.accountSequence {
font-size: 12px;
color: #9ca3af;
@ -293,6 +312,43 @@
}
}
/* [2026-01-07] 新增:明细列表切换区域样式 */
.toggleSection {
margin-top: 16px;
text-align: center;
}
.toggleButton {
padding: 8px 20px;
background-color: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s ease;
&:hover {
background-color: #e5e7eb;
border-color: #9ca3af;
}
}
.entriesSection {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e5e7eb;
}
.paginationInfo {
text-align: center;
padding: 12px;
font-size: 13px;
color: #6b7280;
background-color: #f9fafb;
border-top: 1px solid #e5e7eb;
}
/* 页脚 */
.footer {
text-align: right;

View File

@ -21,8 +21,12 @@ import type {
FeeAccountSummary,
RewardEntriesResponse,
RewardEntryDTO,
FeeCollectionSummaryResponse,
FeeCollectionEntriesResponse,
OfflineSettlementEntriesResponse,
OfflineSettlementEntryDTO,
} from '@/types';
import { ENTRY_TYPE_LABELS, ACCOUNT_TYPE_LABELS, FEE_TYPE_LABELS, REWARD_RIGHT_TYPE_LABELS, REWARD_STATUS_LABELS } from '@/types';
import { ENTRY_TYPE_LABELS, ACCOUNT_TYPE_LABELS, FEE_TYPE_LABELS, REWARD_RIGHT_TYPE_LABELS, REWARD_STATUS_LABELS, FEE_COLLECTION_TYPE_LABELS } from '@/types';
import styles from './SystemAccountsTab.module.scss';
/**
@ -239,11 +243,9 @@ export default function SystemAccountsTab() {
{activeTab === 'settlement' && <OfflineSettlementSection data={reportData.offlineSettlement} />}
{activeTab === 'expired' && <ExpiredRewardsSection data={reportData.expiredRewards} />}
{activeTab === 'ledger' && <LedgerSection data={ledgerData} loading={ledgerLoading} onRefresh={loadLedgerData} />}
{/* [2026-01-06] 新增收益类型汇总Tab内容 */}
{/* [2026-01-06] 更新:手续费账户汇总改用 wallet-service 的真正提现手续费数据 */}
{activeTab === 'feeAccount' && (
<FeeAccountSection
data={rewardTypeSummaries?.feeAccountSummary}
loading={rewardTypeSummariesLoading}
onRefresh={loadRewardTypeSummaries}
/>
)}
@ -406,8 +408,35 @@ function RegionAccountsSection({ data, type }: { data: RegionAccountsSummary; ty
/**
*
* [2026-01-05] USDT改为绿积分
* [2026-01-07]
*/
function OfflineSettlementSection({ data }: { data: SystemAccountReportResponse['offlineSettlement'] | null | undefined }) {
const [entriesData, setEntriesData] = useState<OfflineSettlementEntriesResponse | null>(null);
const [entriesLoading, setEntriesLoading] = useState(false);
const [showEntries, setShowEntries] = useState(false);
// 加载明细列表
const loadEntries = useCallback(async () => {
setEntriesLoading(true);
try {
const response = await systemAccountReportService.getOfflineSettlementEntries({ pageSize: 100 });
if (response.data) {
setEntriesData(response.data);
}
} catch (err) {
console.error('Failed to load offline settlement entries:', err);
} finally {
setEntriesLoading(false);
}
}, []);
// 切换显示明细时加载数据
useEffect(() => {
if (showEntries && !entriesData && !entriesLoading) {
loadEntries();
}
}, [showEntries, entriesData, entriesLoading, loadEntries]);
if (!data) {
return (
<div className={styles.section}>
@ -460,7 +489,57 @@ function OfflineSettlementSection({ data }: { data: SystemAccountReportResponse[
</>
)}
{(!data.byMonth || data.byMonth.length === 0) && (
{/* 明细列表切换按钮 */}
<div className={styles.toggleSection}>
<button
className={styles.toggleButton}
onClick={() => setShowEntries(!showEntries)}
>
{showEntries ? '收起明细' : '查看明细列表'}
</button>
</div>
{/* 明细列表 */}
{showEntries && (
<div className={styles.entriesSection}>
<h4 className={styles.subTitle}></h4>
{entriesLoading ? (
<div className={styles.loading}>...</div>
) : entriesData && entriesData.entries.length > 0 ? (
<div className={styles.tableWrapper}>
<table className={styles.table}>
<thead>
<tr>
<th></th>
<th> (绿)</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{entriesData.entries.map((entry) => (
<tr key={entry.id}>
<td>{entry.accountSequence}</td>
<td>{formatAmount(entry.amount)}</td>
<td>{entry.memo || '-'}</td>
<td>{new Date(entry.createdAt).toLocaleString('zh-CN')}</td>
</tr>
))}
</tbody>
</table>
{entriesData.total > entriesData.entries.length && (
<div className={styles.paginationInfo}>
{entriesData.entries.length} / {entriesData.total}
</div>
)}
</div>
) : (
<div className={styles.emptyTable}></div>
)}
</div>
)}
{(!data.byMonth || data.byMonth.length === 0) && !showEntries && (
<div className={styles.emptyTable}></div>
)}
</div>
@ -777,39 +856,68 @@ function LedgerAccountCard({
);
}
// [2026-01-06] 新增:手续费账户汇总组件
// [2026-01-06] 更新:手续费账户汇总组件 - 使用真正的提现手续费归集数据
// 数据来源: wallet-service 的 S0000000006 账户 FEE_COLLECTION 类型流水
// 而非 reward-service 的系统费用(成本费、运营费等)
/**
*
*/
function FeeAccountSection({
data,
loading,
onRefresh,
}: {
data: FeeAccountSummary | undefined;
loading: boolean;
onRefresh: () => void;
}) {
const [loading, setLoading] = useState(true);
const [data, setData] = useState<FeeCollectionSummaryResponse | null>(null);
const [showDetails, setShowDetails] = useState(false);
const [detailsLoading, setDetailsLoading] = useState(false);
const [detailsData, setDetailsData] = useState<RewardEntriesResponse | null>(null);
const [detailsData, setDetailsData] = useState<FeeCollectionEntriesResponse | null>(null);
const [currentPage, setCurrentPage] = useState(1);
// 加载手续费归集汇总数据
const loadSummary = useCallback(async () => {
setLoading(true);
try {
const response = await systemAccountReportService.getFeeCollectionSummary();
if (response.data) {
setData(response.data);
}
} catch (err) {
console.error('Failed to load fee collection summary:', err);
} finally {
setLoading(false);
}
}, []);
// 加载详细流水
const loadDetails = useCallback(async (page: number = 1) => {
setDetailsLoading(true);
try {
const response = await systemAccountReportService.getFeeEntriesDetailed({ page, pageSize: 20 });
const response = await systemAccountReportService.getFeeCollectionEntries({ page, pageSize: 20 });
if (response.data) {
setDetailsData(response.data);
setCurrentPage(page);
}
} catch (err) {
console.error('Failed to load fee entries details:', err);
console.error('Failed to load fee collection entries:', err);
} finally {
setDetailsLoading(false);
}
}, []);
// 初始加载
useEffect(() => {
loadSummary();
}, [loadSummary]);
const handleRefresh = () => {
loadSummary();
if (showDetails) {
loadDetails(currentPage);
}
onRefresh();
};
const handleToggleDetails = () => {
if (!showDetails && !detailsData) {
loadDetails(1);
@ -831,7 +939,7 @@ function FeeAccountSection({
<div className={styles.section}>
<div className={styles.emptyTable}>
<span></span>
<button onClick={onRefresh} className={styles.retryButton}>
<button onClick={handleRefresh} className={styles.retryButton}>
</button>
</div>
@ -843,15 +951,25 @@ function FeeAccountSection({
<div className={styles.section}>
<div className={styles.sectionHeader}>
<h3 className={styles.sectionTitle}></h3>
<button onClick={onRefresh} className={styles.refreshButton}>
<button onClick={handleRefresh} className={styles.refreshButton}>
</button>
</div>
{/* 账户信息 */}
<div className={styles.accountInfo}>
<span className={styles.accountLabel}>: </span>
<span className={styles.accountValue}>{data.accountSequence}</span>
</div>
{/* 汇总卡片 */}
<div className={styles.summaryCards}>
<div className={styles.summaryCard}>
<span className={styles.summaryLabel}></span>
<span className={styles.summaryLabel}></span>
<span className={styles.summaryValue}>{formatAmount(data.accountBalance)} 绿</span>
</div>
<div className={styles.summaryCard}>
<span className={styles.summaryLabel}></span>
<span className={styles.summaryValue}>{formatAmount(data.totalAmount)} 绿</span>
</div>
<div className={styles.summaryCard}>
@ -860,23 +978,23 @@ function FeeAccountSection({
</div>
</div>
{/* 按类型明细 */}
{/* 按手续费类型明细 */}
{data.breakdown && data.breakdown.length > 0 ? (
<>
<h4 className={styles.subTitle}></h4>
<h4 className={styles.subTitle}></h4>
<div className={styles.tableWrapper}>
<table className={styles.table}>
<thead>
<tr>
<th></th>
<th></th>
<th> (绿)</th>
<th></th>
</tr>
</thead>
<tbody>
{data.breakdown.map((item) => (
<tr key={item.rightType}>
<td>{FEE_TYPE_LABELS[item.rightType] || item.rightType}</td>
<tr key={item.feeType}>
<td>{FEE_COLLECTION_TYPE_LABELS[item.feeType] || item.feeType}</td>
<td>{formatAmount(item.amount)}</td>
<td>{item.count}</td>
</tr>
@ -889,6 +1007,33 @@ function FeeAccountSection({
<div className={styles.emptyTable}></div>
)}
{/* 按月统计 */}
{data.byMonth && data.byMonth.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.byMonth.map((item) => (
<tr key={item.month}>
<td>{item.month}</td>
<td>{formatAmount(item.amount)}</td>
<td>{item.count}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
{/* 详细明细列表 */}
<div className={styles.detailsSection}>
<button onClick={handleToggleDetails} className={styles.toggleDetailsButton}>
@ -909,26 +1054,20 @@ function FeeAccountSection({
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th></th>
<th> (绿)</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{detailsData.entries.map((entry) => (
<tr key={entry.id}>
<td>{new Date(entry.createdAt).toLocaleString('zh-CN')}</td>
<td>{entry.accountSequence}</td>
<td className={styles.orderId}>{entry.sourceOrderId}</td>
<td>{REWARD_RIGHT_TYPE_LABELS[entry.rightType] || entry.rightType}</td>
<td>{formatAmount(entry.usdtAmount)}</td>
<td>
<span className={`${styles.statusBadge} ${entry.rewardStatus === 'SETTLED' ? styles.settled : ''}`}>
{REWARD_STATUS_LABELS[entry.rewardStatus] || entry.rewardStatus}
</span>
</td>
<td>{FEE_COLLECTION_TYPE_LABELS[entry.feeType] || entry.feeType}</td>
<td className={styles.orderId}>{entry.refOrderId || '-'}</td>
<td>{formatAmount(entry.amount)}</td>
<td>{entry.memo || '-'}</td>
</tr>
))}
</tbody>

View File

@ -230,10 +230,15 @@ export const API_ENDPOINTS = {
EXPIRED_REWARDS: '/v1/system-account-reports/expired-rewards',
// [2026-01-05] 新增:所有系统账户分类账明细
ALL_LEDGER: '/v1/system-account-reports/all-ledger',
// [2026-01-06] 新增:收益类型汇总统计
// [2026-01-06] 新增:收益类型汇总统计(系统费用:成本费、运营费等)
REWARD_TYPE_SUMMARIES: '/v1/system-account-reports/reward-type-summaries',
// [2026-01-06] 新增:收益类型详细记录列表
REWARD_ENTRIES_BY_TYPE: '/v1/system-account-reports/reward-entries-by-type',
FEE_ENTRIES_DETAILED: '/v1/system-account-reports/fee-entries-detailed',
// [2026-01-06] 新增:手续费归集账户统计(真正的提现手续费)
FEE_COLLECTION_SUMMARY: '/v1/system-account-reports/fee-collection-summary',
FEE_COLLECTION_ENTRIES: '/v1/system-account-reports/fee-collection-entries',
// [2026-01-07] 新增:面对面结算明细列表
OFFLINE_SETTLEMENT_ENTRIES: '/v1/system-account-reports/offline-settlement-entries',
},
} as const;

View File

@ -17,6 +17,9 @@ import type {
AllSystemAccountsLedgerResponse,
AllRewardTypeSummariesResponse,
RewardEntriesResponse,
FeeCollectionSummaryResponse,
FeeCollectionEntriesResponse,
OfflineSettlementEntriesResponse,
} from '@/types';
/**
@ -66,6 +69,19 @@ export const systemAccountReportService = {
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.OFFLINE_SETTLEMENT, { params });
},
// [2026-01-07] 新增:获取面对面结算明细列表
/**
*
*/
async getOfflineSettlementEntries(params?: {
page?: number;
pageSize?: number;
startDate?: string;
endDate?: string;
}): Promise<ApiResponse<OfflineSettlementEntriesResponse>> {
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.OFFLINE_SETTLEMENT_ENTRIES, { params });
},
/**
*
*/
@ -103,7 +119,8 @@ export const systemAccountReportService = {
},
/**
*
*
* @deprecated 使 getFeeCollectionSummary
*/
async getFeeEntriesDetailed(params?: {
page?: number;
@ -111,6 +128,31 @@ export const systemAccountReportService = {
}): Promise<ApiResponse<RewardEntriesResponse>> {
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.FEE_ENTRIES_DETAILED, { params });
},
// [2026-01-06] 新增:手续费归集账户统计(真正的提现手续费)
// 来源wallet-service 中 S0000000006 账户的 FEE_COLLECTION 类型流水
// 回滚方式:删除以下方法
/**
*
* S0000000006
*/
async getFeeCollectionSummary(): Promise<ApiResponse<FeeCollectionSummaryResponse>> {
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.FEE_COLLECTION_SUMMARY);
},
/**
*
*/
async getFeeCollectionEntries(params?: {
page?: number;
pageSize?: number;
feeType?: string;
startDate?: string;
endDate?: string;
}): Promise<ApiResponse<FeeCollectionEntriesResponse>> {
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.FEE_COLLECTION_ENTRIES, { params });
},
};
export default systemAccountReportService;

View File

@ -54,6 +54,31 @@ export interface OfflineSettlementSummary {
}>;
}
// [2026-01-07] 新增:面对面结算明细条目
/**
*
*/
export interface OfflineSettlementEntryDTO {
id: string;
accountSequence: string;
amount: number;
memo: string | null;
refOrderId: string | null;
createdAt: string;
}
// [2026-01-07] 新增:面对面结算明细列表响应
/**
*
*/
export interface OfflineSettlementEntriesResponse {
entries: OfflineSettlementEntryDTO[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
/**
*
*/
@ -302,3 +327,56 @@ export const REWARD_STATUS_LABELS: Record<string, string> = {
SETTLED: '已结算',
EXPIRED: '已过期',
};
// [2026-01-06] 新增:手续费归集账户汇总类型(真正的提现手续费)
/**
*
*/
export interface FeeCollectionSummaryResponse {
accountSequence: string;
accountBalance: number;
totalAmount: number;
totalCount: number;
breakdown: Array<{
feeType: string;
amount: number;
count: number;
}>;
byMonth: Array<{
month: string;
amount: number;
count: number;
}>;
}
/**
*
*/
export interface FeeCollectionEntryDTO {
id: string;
feeType: string;
amount: number;
refOrderId: string | null;
memo: string | null;
createdAt: string;
}
/**
*
*/
export interface FeeCollectionEntriesResponse {
entries: FeeCollectionEntryDTO[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
/**
*
*/
export const FEE_COLLECTION_TYPE_LABELS: Record<string, string> = {
WITHDRAWAL_FEE: '区块链提现手续费',
FIAT_WITHDRAWAL_FEE: '法币提现手续费',
UNKNOWN: '未知类型',
};