feat(system-accounts): add ledger detail API for all system accounts

新增所有系统账户的分类账明细查询功能:
- wallet-service: 添加 getSystemAccountLedger 和 getAllSystemAccountsLedger 方法
- wallet-service: 添加 /statistics/system-account-ledger 和 /statistics/all-system-accounts-ledger API
- reporting-service: 添加 /all-ledger 端点透传分类账数据

🤖 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-04 23:30:38 -08:00
parent 56f2fd206d
commit 229dff1a9d
5 changed files with 387 additions and 41 deletions

View File

@ -78,4 +78,24 @@ export class SystemAccountReportController {
this.logger.log(`[getExpiredRewards] 请求过期收益统计, startDate=${startDate}, endDate=${endDate}`);
return this.systemAccountReportService.getExpiredRewardsSummary({ startDate, endDate });
}
// [2026-01-05] 新增:所有系统账户分类账明细
@Get('all-ledger')
@ApiOperation({ summary: '获取所有系统账户的分类账明细' })
@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 getAllSystemAccountsLedger(
@Query('pageSize') pageSize?: string,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
) {
this.logger.log(`[getAllSystemAccountsLedger] 请求所有系统账户分类账, pageSize=${pageSize}, startDate=${startDate}, endDate=${endDate}`);
return this.systemAccountReportService.getAllSystemAccountsLedger({
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
startDate,
endDate,
});
}
}

View File

@ -5,7 +5,7 @@
* application.module.ts
*/
import { Injectable, Logger } from '@nestjs/common';
import { WalletServiceClient, OfflineSettlementSummary, AllSystemAccountsResponse } from '../../infrastructure/external/wallet-service/wallet-service.client';
import { WalletServiceClient, OfflineSettlementSummary, AllSystemAccountsResponse, AllSystemAccountsLedgerResponse } from '../../infrastructure/external/wallet-service/wallet-service.client';
import { RewardServiceClient, ExpiredRewardsSummary } from '../../infrastructure/external/reward-service/reward-service.client';
/**
@ -184,6 +184,22 @@ export class SystemAccountReportApplicationService {
return this.rewardServiceClient.getExpiredRewardsSummary(params);
}
// [2026-01-05] 新增:获取所有系统账户的分类账明细
/**
*
*
*/
async getAllSystemAccountsLedger(params?: {
pageSize?: number;
startDate?: string;
endDate?: string;
}): Promise<AllSystemAccountsLedgerResponse> {
this.logger.log('[getAllSystemAccountsLedger] 开始获取所有系统账户分类账...');
const result = await this.walletServiceClient.getAllSystemAccountsLedger(params);
this.logger.log(`[getAllSystemAccountsLedger] 完成: 固定=${result.fixedAccountsLedger.length}, 省=${result.provinceAccountsLedger.length}, 市=${result.cityAccountsLedger.length}`);
return result;
}
/**
*
*/

View File

@ -56,6 +56,44 @@ export interface AllSystemAccountsResponse {
}>;
}
// [2026-01-05] 新增:分类账条目类型
export interface LedgerEntryDTO {
id: string;
entryType: string;
amount: number;
assetType: string;
balanceAfter: number | null;
refOrderId: string | null;
refTxHash: string | null;
memo: string | null;
allocationType: string | null;
createdAt: string;
}
// [2026-01-05] 新增:所有系统账户分类账响应类型
export interface AllSystemAccountsLedgerResponse {
fixedAccountsLedger: Array<{
accountSequence: string;
accountType: string;
ledger: LedgerEntryDTO[];
total: number;
}>;
provinceAccountsLedger: Array<{
accountSequence: string;
regionCode: string;
regionName: string;
ledger: LedgerEntryDTO[];
total: number;
}>;
cityAccountsLedger: Array<{
accountSequence: string;
regionCode: string;
regionName: string;
ledger: LedgerEntryDTO[];
total: number;
}>;
}
@Injectable()
export class WalletServiceClient {
private readonly logger = new Logger(WalletServiceClient.name);
@ -148,4 +186,38 @@ export class WalletServiceClient {
};
}
}
// [2026-01-05] 新增:获取所有系统账户的分类账明细
/**
*
*
*/
async getAllSystemAccountsLedger(params?: {
pageSize?: number;
startDate?: string;
endDate?: string;
}): Promise<AllSystemAccountsLedgerResponse> {
try {
const queryParams = new URLSearchParams();
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/all-system-accounts-ledger?${queryParams.toString()}`;
this.logger.debug(`[getAllSystemAccountsLedger] 请求: ${url}`);
const response = await firstValueFrom(
this.httpService.get<{ success: boolean; data: AllSystemAccountsLedgerResponse }>(url),
);
return response.data.data;
} catch (error) {
this.logger.error(`[getAllSystemAccountsLedger] 失败: ${error.message}`);
return {
fixedAccountsLedger: [],
provinceAccountsLedger: [],
cityAccountsLedger: [],
};
}
}
}

View File

@ -12,6 +12,7 @@ import {
} from '@/application/commands';
import { Public } from '@/shared/decorators';
import { FiatWithdrawalStatus } from '@/domain/value-objects/fiat-withdrawal-status.enum';
import { LedgerEntryType } from '@/domain/value-objects';
/**
* API控制器 -
@ -399,4 +400,67 @@ export class InternalWalletController {
this.logger.log(`系统账户查询结果: 固定=${result.fixedAccounts.length}, 省=${result.provinceAccounts.length}, 市=${result.cityAccounts.length}`);
return result;
}
// [2026-01-05] 新增:单个系统账户分类账查询
@Get('statistics/system-account-ledger')
@Public()
@ApiOperation({ summary: '获取单个系统账户分类账(内部API)' })
@ApiQuery({ name: 'accountSequence', required: true, description: '账户序列号' })
@ApiQuery({ name: 'page', required: false, description: '页码默认1' })
@ApiQuery({ name: 'pageSize', required: false, description: '每页条数默认20' })
@ApiQuery({ name: 'entryType', required: false, description: '流水类型筛选' })
@ApiQuery({ name: 'startDate', required: false, description: '开始日期' })
@ApiQuery({ name: 'endDate', required: false, description: '结束日期' })
@ApiResponse({ status: 200, description: '分类账明细列表' })
async getSystemAccountLedger(
@Query('accountSequence') accountSequence: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('entryType') entryType?: string,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
) {
this.logger.log(`========== statistics/system-account-ledger 请求 ==========`);
this.logger.log(`accountSequence=${accountSequence}, page=${page}, pageSize=${pageSize}`);
const result = await this.walletService.getSystemAccountLedger({
accountSequence,
page: page ? parseInt(page, 10) : 1,
pageSize: pageSize ? parseInt(pageSize, 10) : 20,
entryType: entryType as LedgerEntryType | undefined,
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
});
this.logger.log(`系统账户分类账查询结果: ${result.total} 条记录`);
return result;
}
// [2026-01-05] 新增:所有系统账户分类账查询
@Get('statistics/all-system-accounts-ledger')
@Public()
@ApiOperation({ summary: '获取所有系统账户的分类账明细(内部API) - 用于系统账户报表' })
@ApiQuery({ name: 'pageSize', required: false, description: '每个账户显示的条数默认50' })
@ApiQuery({ name: 'startDate', required: false, description: '开始日期' })
@ApiQuery({ name: 'endDate', required: false, description: '结束日期' })
@ApiResponse({ status: 200, description: '所有系统账户的分类账明细' })
async getAllSystemAccountsLedger(
@Query('pageSize') pageSize?: string,
@Query('startDate') startDate?: string,
@Query('endDate') endDate?: string,
) {
this.logger.log(`========== statistics/all-system-accounts-ledger 请求 ==========`);
const result = await this.walletService.getAllSystemAccountsLedger({
pageSize: pageSize ? parseInt(pageSize, 10) : 50,
startDate: startDate ? new Date(startDate) : undefined,
endDate: endDate ? new Date(endDate) : undefined,
});
const totalLedgers = result.fixedAccountsLedger.reduce((sum, a) => sum + a.total, 0) +
result.provinceAccountsLedger.reduce((sum, a) => sum + a.total, 0) +
result.cityAccountsLedger.reduce((sum, a) => sum + a.total, 0);
this.logger.log(`所有系统账户分类账查询完成: 固定=${result.fixedAccountsLedger.length}, 省=${result.provinceAccountsLedger.length}, 市=${result.cityAccountsLedger.length}, 总流水=${totalLedgers}`);
return result;
}
}

View File

@ -1856,6 +1856,180 @@ export class WalletApplicationService {
};
}
// [2026-01-05] 新增:系统账户分类账查询
/**
* accountSequence
*/
async getSystemAccountLedger(params: {
accountSequence: string;
page?: number;
pageSize?: number;
entryType?: LedgerEntryType;
startDate?: Date;
endDate?: Date;
}): Promise<PaginatedLedgerDTO> {
const result = await this.ledgerRepo.findByAccountSequence(
params.accountSequence,
{
entryType: params.entryType,
startDate: params.startDate,
endDate: params.endDate,
},
{
page: params.page ?? 1,
pageSize: params.pageSize ?? 20,
},
);
return {
data: result.data.map(entry => ({
id: entry.id.toString(),
entryType: entry.entryType,
amount: entry.amount.value,
assetType: entry.assetType,
balanceAfter: entry.balanceAfter?.value ?? null,
refOrderId: entry.refOrderId,
refTxHash: entry.refTxHash,
memo: entry.memo,
allocationType: (entry.payloadJson as Record<string, unknown>)?.allocationType as string ?? null,
createdAt: entry.createdAt.toISOString(),
})),
total: result.total,
page: result.page,
pageSize: result.pageSize,
totalPages: result.totalPages,
};
}
/**
*
*
*/
async getAllSystemAccountsLedger(params?: {
pageSize?: number;
startDate?: Date;
endDate?: Date;
}): Promise<{
fixedAccountsLedger: Array<{
accountSequence: string;
accountType: string;
ledger: LedgerEntryDTO[];
total: number;
}>;
provinceAccountsLedger: Array<{
accountSequence: string;
regionCode: string;
regionName: string;
ledger: LedgerEntryDTO[];
total: number;
}>;
cityAccountsLedger: Array<{
accountSequence: string;
regionCode: string;
regionName: string;
ledger: LedgerEntryDTO[];
total: number;
}>;
}> {
const pageSize = params?.pageSize ?? 50; // 每个账户默认显示最近50条
// 1. 获取所有系统账户
const allAccounts = await this.getAllSystemAccounts();
// 2. 并行获取所有账户的分类账
const [fixedLedgers, provinceLedgers, cityLedgers] = await Promise.all([
// 固定账户
Promise.all(
allAccounts.fixedAccounts.map(async account => {
const ledgerResult = await this.ledgerRepo.findByAccountSequence(
account.accountSequence,
{ startDate: params?.startDate, endDate: params?.endDate },
{ page: 1, pageSize },
);
return {
accountSequence: account.accountSequence,
accountType: account.accountType,
ledger: ledgerResult.data.map(entry => ({
id: entry.id.toString(),
entryType: entry.entryType,
amount: entry.amount.value,
assetType: entry.assetType,
balanceAfter: entry.balanceAfter?.value ?? null,
refOrderId: entry.refOrderId,
refTxHash: entry.refTxHash,
memo: entry.memo,
allocationType: (entry.payloadJson as Record<string, unknown>)?.allocationType as string ?? null,
createdAt: entry.createdAt.toISOString(),
})),
total: ledgerResult.total,
};
}),
),
// 省账户
Promise.all(
allAccounts.provinceAccounts.map(async account => {
const ledgerResult = await this.ledgerRepo.findByAccountSequence(
account.accountSequence,
{ startDate: params?.startDate, endDate: params?.endDate },
{ page: 1, pageSize },
);
return {
accountSequence: account.accountSequence,
regionCode: account.regionCode,
regionName: account.regionName,
ledger: ledgerResult.data.map(entry => ({
id: entry.id.toString(),
entryType: entry.entryType,
amount: entry.amount.value,
assetType: entry.assetType,
balanceAfter: entry.balanceAfter?.value ?? null,
refOrderId: entry.refOrderId,
refTxHash: entry.refTxHash,
memo: entry.memo,
allocationType: (entry.payloadJson as Record<string, unknown>)?.allocationType as string ?? null,
createdAt: entry.createdAt.toISOString(),
})),
total: ledgerResult.total,
};
}),
),
// 市账户
Promise.all(
allAccounts.cityAccounts.map(async account => {
const ledgerResult = await this.ledgerRepo.findByAccountSequence(
account.accountSequence,
{ startDate: params?.startDate, endDate: params?.endDate },
{ page: 1, pageSize },
);
return {
accountSequence: account.accountSequence,
regionCode: account.regionCode,
regionName: account.regionName,
ledger: ledgerResult.data.map(entry => ({
id: entry.id.toString(),
entryType: entry.entryType,
amount: entry.amount.value,
assetType: entry.assetType,
balanceAfter: entry.balanceAfter?.value ?? null,
refOrderId: entry.refOrderId,
refTxHash: entry.refTxHash,
memo: entry.memo,
allocationType: (entry.payloadJson as Record<string, unknown>)?.allocationType as string ?? null,
createdAt: entry.createdAt.toISOString(),
})),
total: ledgerResult.total,
};
}),
),
]);
return {
fixedAccountsLedger: fixedLedgers,
provinceAccountsLedger: provinceLedgers,
cityAccountsLedger: cityLedgers,
};
}
// =============== Pending Rewards ===============
/**
@ -2953,43 +3127,43 @@ export class WalletApplicationService {
createdAt: wallet.createdAt.toISOString(),
}));
}
// =============== 系统账户报表统计 API - 增强版 ===============
// [2026-01-05] 新增:获取所有系统账户列表(固定+区域)
// 回滚方式:删除以下方法
async getAllSystemAccounts(): Promise<{
fixedAccounts: Array<{ accountSequence: string; accountType: string; usdtBalance: string; totalReceived: string; totalTransferred: string; status: string; createdAt: string }>;
provinceAccounts: Array<{ accountSequence: string; regionCode: string; regionName: string; usdtBalance: string; totalReceived: string; status: string }>;
cityAccounts: Array<{ accountSequence: string; regionCode: string; regionName: string; usdtBalance: string; totalReceived: string; status: string }>;
}> {
this.logger.log('[getAllSystemAccounts] 查询所有系统账户...');
const wallets = await this.prisma.walletAccount.findMany({
where: { OR: [{ accountSequence: { startsWith: 'S' } }, { accountSequence: { startsWith: '9' } }, { accountSequence: { startsWith: '8' } }] },
select: { accountSequence: true, usdtAvailable: true, status: true, createdAt: true },
});
const accountSeqs = wallets.map(w => w.accountSequence);
const receivedStats2 = await this.prisma.ledgerEntry.groupBy({ by: ['accountSequence'], where: { accountSequence: { in: accountSeqs }, amount: { gt: 0 } }, _sum: { amount: true } });
const transferredStats = await this.prisma.ledgerEntry.groupBy({ by: ['accountSequence'], where: { accountSequence: { in: accountSeqs }, amount: { lt: 0 } }, _sum: { amount: true } });
const receivedMap2 = new Map(receivedStats2.map(s => [s.accountSequence, s._sum.amount]));
const transferredMap = new Map(transferredStats.map(s => [s.accountSequence, s._sum.amount]));
const fixedAccountTypes: Record<string, string> = { 'S0000000001': 'COST_ACCOUNT', 'S0000000002': 'OPERATION_ACCOUNT', 'S0000000003': 'HQ_COMMUNITY', 'S0000000004': 'RWAD_POOL_PENDING', 'S0000000005': 'PLATFORM_FEE' };
const fixedAccounts: Array<{ accountSequence: string; accountType: string; usdtBalance: string; totalReceived: string; totalTransferred: string; status: string; createdAt: string }> = [];
const provinceAccounts: Array<{ accountSequence: string; regionCode: string; regionName: string; usdtBalance: string; totalReceived: string; status: string }> = [];
const cityAccounts: Array<{ accountSequence: string; regionCode: string; regionName: string; usdtBalance: string; totalReceived: string; status: string }> = [];
for (const wallet of wallets) {
const seq = wallet.accountSequence;
const received = Number(receivedMap2.get(seq) || 0);
const transferred = Math.abs(Number(transferredMap.get(seq) || 0));
if (seq.startsWith('S')) {
fixedAccounts.push({ accountSequence: seq, accountType: fixedAccountTypes[seq] || 'UNKNOWN', usdtBalance: String(wallet.usdtAvailable), totalReceived: String(received), totalTransferred: String(transferred), status: wallet.status, createdAt: wallet.createdAt.toISOString() });
} else if (seq.startsWith('9')) {
provinceAccounts.push({ accountSequence: seq, regionCode: seq.substring(1), regionName: '省区域 ' + seq.substring(1), usdtBalance: String(wallet.usdtAvailable), totalReceived: String(received), status: wallet.status });
} else if (seq.startsWith('8')) {
cityAccounts.push({ accountSequence: seq, regionCode: seq.substring(1), regionName: '市区域 ' + seq.substring(1), usdtBalance: String(wallet.usdtAvailable), totalReceived: String(received), status: wallet.status });
}
}
this.logger.log('[getAllSystemAccounts] 固定: ' + fixedAccounts.length + ', 省: ' + provinceAccounts.length + ', 市: ' + cityAccounts.length);
return { fixedAccounts, provinceAccounts, cityAccounts };
}
}
// =============== 系统账户报表统计 API - 增强版 ===============
// [2026-01-05] 新增:获取所有系统账户列表(固定+区域)
// 回滚方式:删除以下方法
async getAllSystemAccounts(): Promise<{
fixedAccounts: Array<{ accountSequence: string; accountType: string; usdtBalance: string; totalReceived: string; totalTransferred: string; status: string; createdAt: string }>;
provinceAccounts: Array<{ accountSequence: string; regionCode: string; regionName: string; usdtBalance: string; totalReceived: string; status: string }>;
cityAccounts: Array<{ accountSequence: string; regionCode: string; regionName: string; usdtBalance: string; totalReceived: string; status: string }>;
}> {
this.logger.log('[getAllSystemAccounts] 查询所有系统账户...');
const wallets = await this.prisma.walletAccount.findMany({
where: { OR: [{ accountSequence: { startsWith: 'S' } }, { accountSequence: { startsWith: '9' } }, { accountSequence: { startsWith: '8' } }] },
select: { accountSequence: true, usdtAvailable: true, status: true, createdAt: true },
});
const accountSeqs = wallets.map(w => w.accountSequence);
const receivedStats2 = await this.prisma.ledgerEntry.groupBy({ by: ['accountSequence'], where: { accountSequence: { in: accountSeqs }, amount: { gt: 0 } }, _sum: { amount: true } });
const transferredStats = await this.prisma.ledgerEntry.groupBy({ by: ['accountSequence'], where: { accountSequence: { in: accountSeqs }, amount: { lt: 0 } }, _sum: { amount: true } });
const receivedMap2 = new Map(receivedStats2.map(s => [s.accountSequence, s._sum.amount]));
const transferredMap = new Map(transferredStats.map(s => [s.accountSequence, s._sum.amount]));
const fixedAccountTypes: Record<string, string> = { 'S0000000001': 'COST_ACCOUNT', 'S0000000002': 'OPERATION_ACCOUNT', 'S0000000003': 'HQ_COMMUNITY', 'S0000000004': 'RWAD_POOL_PENDING', 'S0000000005': 'PLATFORM_FEE' };
const fixedAccounts: Array<{ accountSequence: string; accountType: string; usdtBalance: string; totalReceived: string; totalTransferred: string; status: string; createdAt: string }> = [];
const provinceAccounts: Array<{ accountSequence: string; regionCode: string; regionName: string; usdtBalance: string; totalReceived: string; status: string }> = [];
const cityAccounts: Array<{ accountSequence: string; regionCode: string; regionName: string; usdtBalance: string; totalReceived: string; status: string }> = [];
for (const wallet of wallets) {
const seq = wallet.accountSequence;
const received = Number(receivedMap2.get(seq) || 0);
const transferred = Math.abs(Number(transferredMap.get(seq) || 0));
if (seq.startsWith('S')) {
fixedAccounts.push({ accountSequence: seq, accountType: fixedAccountTypes[seq] || 'UNKNOWN', usdtBalance: String(wallet.usdtAvailable), totalReceived: String(received), totalTransferred: String(transferred), status: wallet.status, createdAt: wallet.createdAt.toISOString() });
} else if (seq.startsWith('9')) {
provinceAccounts.push({ accountSequence: seq, regionCode: seq.substring(1), regionName: '省区域 ' + seq.substring(1), usdtBalance: String(wallet.usdtAvailable), totalReceived: String(received), status: wallet.status });
} else if (seq.startsWith('8')) {
cityAccounts.push({ accountSequence: seq, regionCode: seq.substring(1), regionName: '市区域 ' + seq.substring(1), usdtBalance: String(wallet.usdtAvailable), totalReceived: String(received), status: wallet.status });
}
}
this.logger.log('[getAllSystemAccounts] 固定: ' + fixedAccounts.length + ', 省: ' + provinceAccounts.length + ', 市: ' + cityAccounts.length);
return { fixedAccounts, provinceAccounts, cityAccounts };
}
}