diff --git a/backend/services/reporting-service/src/api/controllers/system-account-report.controller.ts b/backend/services/reporting-service/src/api/controllers/system-account-report.controller.ts index 35603490..bd7b56ed 100644 --- a/backend/services/reporting-service/src/api/controllers/system-account-report.controller.ts +++ b/backend/services/reporting-service/src/api/controllers/system-account-report.controller.ts @@ -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, + }); + } } diff --git a/backend/services/reporting-service/src/application/services/system-account-report-application.service.ts b/backend/services/reporting-service/src/application/services/system-account-report-application.service.ts index c5ced78e..df33a2d3 100644 --- a/backend/services/reporting-service/src/application/services/system-account-report-application.service.ts +++ b/backend/services/reporting-service/src/application/services/system-account-report-application.service.ts @@ -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 { + 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; + } + /** * 组装固定账户数据 */ diff --git a/backend/services/reporting-service/src/infrastructure/external/wallet-service/wallet-service.client.ts b/backend/services/reporting-service/src/infrastructure/external/wallet-service/wallet-service.client.ts index 71c92a20..eb7d8e6c 100644 --- a/backend/services/reporting-service/src/infrastructure/external/wallet-service/wallet-service.client.ts +++ b/backend/services/reporting-service/src/infrastructure/external/wallet-service/wallet-service.client.ts @@ -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 { + 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: [], + }; + } + } } diff --git a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts index 3d91972e..7e527d4e 100644 --- a/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts +++ b/backend/services/wallet-service/src/api/controllers/internal-wallet.controller.ts @@ -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; + } } diff --git a/backend/services/wallet-service/src/application/services/wallet-application.service.ts b/backend/services/wallet-service/src/application/services/wallet-application.service.ts index e0ce0193..cdac58b3 100644 --- a/backend/services/wallet-service/src/application/services/wallet-application.service.ts +++ b/backend/services/wallet-service/src/application/services/wallet-application.service.ts @@ -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 { + 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)?.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)?.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)?.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)?.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 = { '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 = { '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 }; + } +}