feat(reporting): fix system account report to use wallet-service data

The system account balances were showing 0 because data was being fetched
from authorization-service.system_accounts table instead of the actual
wallet-service.wallet_accounts table where funds are stored.

Changes:
- wallet-service: Add getAllSystemAccounts() method to query all system
  accounts (fixed S*, province 9*, city 8*) with actual balances
- wallet-service: Add /wallets/statistics/all-system-accounts API endpoint
- reporting-service: Update SystemAccountReportApplicationService to fetch
  data from wallet-service instead of authorization-service
- reporting-service: Fix default service URLs to use correct container names
  and ports (rwa-wallet-service:3001, rwa-reward-service:3005)
- docker-compose: Add WALLET_SERVICE_URL and REWARD_SERVICE_URL env vars
  for reporting-service

🤖 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:10:20 -08:00
parent 83384ff198
commit 838d5c1d3b
6 changed files with 196 additions and 85 deletions

View File

@ -501,6 +501,9 @@ services:
- KAFKA_BROKERS=kafka:29092
- KAFKA_CLIENT_ID=reporting-service
- KAFKA_GROUP_ID=reporting-service-group
# [2026-01-05] 新增:系统账户报表统计所需的外部服务 URL
- WALLET_SERVICE_URL=http://rwa-wallet-service:3001
- REWARD_SERVICE_URL=http://rwa-reward-service:3005
depends_on:
postgres:
condition: service_healthy

View File

@ -1,12 +1,37 @@
/**
*
* [2026-01-04]
* [2026-01-05] wallet-service
* application.module.ts
*/
import { Injectable, Logger } from '@nestjs/common';
import { WalletServiceClient, OfflineSettlementSummary, SystemAccountBalance } from '../../infrastructure/external/wallet-service/wallet-service.client';
import { WalletServiceClient, OfflineSettlementSummary, AllSystemAccountsResponse } from '../../infrastructure/external/wallet-service/wallet-service.client';
import { RewardServiceClient, ExpiredRewardsSummary } from '../../infrastructure/external/reward-service/reward-service.client';
import { AuthorizationServiceClient, SystemAccountDTO, RegionAccountsSummary, AllAccountsSummary } from '../../infrastructure/external/authorization-service/authorization-service.client';
/**
*
*/
export interface FixedAccountInfo {
accountSequence: string;
accountType: string;
usdtBalance: string;
totalReceived: string;
totalTransferred: string;
status: string;
createdAt: string;
}
/**
*
*/
export interface RegionAccountInfo {
accountSequence: string;
regionCode: string;
regionName: string;
usdtBalance: string;
totalReceived: string;
status: string;
}
/**
*
@ -14,15 +39,15 @@ import { AuthorizationServiceClient, SystemAccountDTO, RegionAccountsSummary, Al
export interface SystemAccountReportResponse {
// 固定系统账户
fixedAccounts: {
costAccount: SystemAccountWithBalance | null; // 成本账户 S0000000001
operationAccount: SystemAccountWithBalance | null; // 运营账户 S0000000002
hqCommunity: SystemAccountWithBalance | null; // 总部社区 S0000000003
rwadPoolPending: SystemAccountWithBalance | null; // RWAD待发放池 S0000000004
platformFee: SystemAccountWithBalance | null; // 平台手续费 S0000000005
costAccount: FixedAccountInfo | null; // 成本账户 S0000000001
operationAccount: FixedAccountInfo | null; // 运营账户 S0000000002
hqCommunity: FixedAccountInfo | null; // 总部社区 S0000000003
rwadPoolPending: FixedAccountInfo | null; // RWAD待发放池 S0000000004
platformFee: FixedAccountInfo | null; // 平台手续费 S0000000005
};
// 省区域账户汇总
provinceSummary: {
accounts: SystemAccountDTO[];
accounts: RegionAccountInfo[];
summary: {
totalBalance: string;
totalReceived: string;
@ -31,7 +56,7 @@ export interface SystemAccountReportResponse {
};
// 市区域账户汇总
citySummary: {
accounts: SystemAccountDTO[];
accounts: RegionAccountInfo[];
summary: {
totalBalance: string;
totalReceived: string;
@ -46,33 +71,15 @@ export interface SystemAccountReportResponse {
generatedAt: string;
}
/**
*
*/
export interface SystemAccountWithBalance extends SystemAccountDTO {
walletBalance?: number;
}
/**
*
*/
const FIXED_ACCOUNT_TYPES = {
const FIXED_ACCOUNT_TYPES: Record<string, string> = {
COST_ACCOUNT: 'costAccount',
OPERATION_ACCOUNT: 'operationAccount',
HQ_COMMUNITY: 'hqCommunity',
RWAD_POOL_PENDING: 'rwadPoolPending',
PLATFORM_FEE: 'platformFee',
} as const;
/**
*
*/
const FIXED_ACCOUNT_SEQUENCES = {
costAccount: 'S0000000001',
operationAccount: 'S0000000002',
hqCommunity: 'S0000000003',
rwadPoolPending: 'S0000000004',
platformFee: 'S0000000005',
};
@Injectable()
@ -82,11 +89,11 @@ export class SystemAccountReportApplicationService {
constructor(
private readonly walletServiceClient: WalletServiceClient,
private readonly rewardServiceClient: RewardServiceClient,
private readonly authorizationServiceClient: AuthorizationServiceClient,
) {}
/**
*
* [2026-01-05] wallet-service
*/
async getFullReport(params?: {
startDate?: string;
@ -96,26 +103,23 @@ export class SystemAccountReportApplicationService {
// 并行获取所有数据
const [
allAccountsSummary,
provinceSummary,
citySummary,
allSystemAccounts,
offlineSettlement,
expiredRewards,
fixedAccountsBalances,
] = await Promise.all([
this.authorizationServiceClient.getAllAccountsSummary(),
this.authorizationServiceClient.getRegionAccountsList('province'),
this.authorizationServiceClient.getRegionAccountsList('city'),
this.walletServiceClient.getAllSystemAccounts(),
this.walletServiceClient.getOfflineSettlementSummary(params),
this.rewardServiceClient.getExpiredRewardsSummary(params),
this.getFixedAccountsBalances(),
]);
// 组装固定账户数据
const fixedAccounts = this.assembleFixedAccounts(
allAccountsSummary.fixedAccounts,
fixedAccountsBalances,
);
const fixedAccounts = this.assembleFixedAccounts(allSystemAccounts.fixedAccounts);
// 组装省账户汇总
const provinceSummary = this.assembleRegionSummary(allSystemAccounts.provinceAccounts);
// 组装市账户汇总
const citySummary = this.assembleRegionSummary(allSystemAccounts.cityAccounts);
const report: SystemAccountReportResponse = {
fixedAccounts,
@ -133,33 +137,31 @@ export class SystemAccountReportApplicationService {
/**
*
*/
async getFixedAccountsWithBalances(): Promise<SystemAccountWithBalance[]> {
const [fixedAccounts, balances] = await Promise.all([
this.authorizationServiceClient.getFixedAccountsList(),
this.getFixedAccountsBalances(),
]);
return fixedAccounts.map(account => {
const balance = balances.find(b => b.accountSequence === this.getAccountSequence(account));
return {
...account,
walletBalance: balance?.balance,
};
});
async getFixedAccountsWithBalances(): Promise<FixedAccountInfo[]> {
const allSystemAccounts = await this.walletServiceClient.getAllSystemAccounts();
return allSystemAccounts.fixedAccounts;
}
/**
*
*/
async getProvinceAccountsSummary(): Promise<RegionAccountsSummary> {
return this.authorizationServiceClient.getRegionAccountsList('province');
async getProvinceAccountsSummary(): Promise<{
accounts: RegionAccountInfo[];
summary: { totalBalance: string; totalReceived: string; count: number };
}> {
const allSystemAccounts = await this.walletServiceClient.getAllSystemAccounts();
return this.assembleRegionSummary(allSystemAccounts.provinceAccounts);
}
/**
*
*/
async getCityAccountsSummary(): Promise<RegionAccountsSummary> {
return this.authorizationServiceClient.getRegionAccountsList('city');
async getCityAccountsSummary(): Promise<{
accounts: RegionAccountInfo[];
summary: { totalBalance: string; totalReceived: string; count: number };
}> {
const allSystemAccounts = await this.walletServiceClient.getAllSystemAccounts();
return this.assembleRegionSummary(allSystemAccounts.cityAccounts);
}
/**
@ -182,20 +184,11 @@ export class SystemAccountReportApplicationService {
return this.rewardServiceClient.getExpiredRewardsSummary(params);
}
/**
*
*/
private async getFixedAccountsBalances(): Promise<SystemAccountBalance[]> {
const sequences = Object.values(FIXED_ACCOUNT_SEQUENCES);
return this.walletServiceClient.getSystemAccountsBalances(sequences);
}
/**
*
*/
private assembleFixedAccounts(
fixedAccounts: SystemAccountDTO[],
balances: SystemAccountBalance[],
fixedAccounts: AllSystemAccountsResponse['fixedAccounts'],
): SystemAccountReportResponse['fixedAccounts'] {
const result: SystemAccountReportResponse['fixedAccounts'] = {
costAccount: null,
@ -206,14 +199,9 @@ export class SystemAccountReportApplicationService {
};
for (const account of fixedAccounts) {
const fieldName = FIXED_ACCOUNT_TYPES[account.accountType as keyof typeof FIXED_ACCOUNT_TYPES];
if (fieldName) {
const sequence = FIXED_ACCOUNT_SEQUENCES[fieldName];
const balance = balances.find(b => b.accountSequence === sequence);
result[fieldName] = {
...account,
walletBalance: balance?.balance,
};
const fieldName = FIXED_ACCOUNT_TYPES[account.accountType];
if (fieldName && fieldName in result) {
(result as any)[fieldName] = account;
}
}
@ -221,13 +209,29 @@ export class SystemAccountReportApplicationService {
}
/**
*
*
*/
private getAccountSequence(account: SystemAccountDTO): string | undefined {
const fieldName = FIXED_ACCOUNT_TYPES[account.accountType as keyof typeof FIXED_ACCOUNT_TYPES];
if (fieldName) {
return FIXED_ACCOUNT_SEQUENCES[fieldName];
private assembleRegionSummary(
accounts: RegionAccountInfo[],
): {
accounts: RegionAccountInfo[];
summary: { totalBalance: string; totalReceived: string; count: number };
} {
let totalBalance = 0;
let totalReceived = 0;
for (const account of accounts) {
totalBalance += parseFloat(account.usdtBalance) || 0;
totalReceived += parseFloat(account.totalReceived) || 0;
}
return undefined;
return {
accounts,
summary: {
totalBalance: totalBalance.toFixed(8),
totalReceived: totalReceived.toFixed(8),
count: accounts.length,
},
};
}
}

View File

@ -35,7 +35,7 @@ export class RewardServiceClient {
) {
this.baseUrl = this.configService.get<string>(
'REWARD_SERVICE_URL',
'http://reward-service:3004',
'http://rwa-reward-service:3005',
);
}

View File

@ -27,6 +27,35 @@ export interface SystemAccountBalance {
createdAt: string;
}
// [2026-01-05] 新增:所有系统账户响应类型
export interface AllSystemAccountsResponse {
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;
}>;
}
@Injectable()
export class WalletServiceClient {
private readonly logger = new Logger(WalletServiceClient.name);
@ -38,7 +67,7 @@ export class WalletServiceClient {
) {
this.baseUrl = this.configService.get<string>(
'WALLET_SERVICE_URL',
'http://wallet-service:3002',
'http://rwa-wallet-service:3001',
);
}
@ -93,4 +122,29 @@ export class WalletServiceClient {
return [];
}
}
// [2026-01-05] 新增:获取所有系统账户(固定+区域)
/**
*
* wallet-service
*/
async getAllSystemAccounts(): Promise<AllSystemAccountsResponse> {
try {
const url = `${this.baseUrl}/wallets/statistics/all-system-accounts`;
this.logger.debug(`[getAllSystemAccounts] 请求: ${url}`);
const response = await firstValueFrom(
this.httpService.get<AllSystemAccountsResponse>(url),
);
return response.data;
} catch (error) {
this.logger.error(`[getAllSystemAccounts] 失败: ${error.message}`);
return {
fixedAccounts: [],
provinceAccounts: [],
cityAccounts: [],
};
}
}
}

View File

@ -388,4 +388,15 @@ export class InternalWalletController {
this.logger.log(`系统账户余额查询结果: ${result.length} 个账户`);
return result;
}
@Get('statistics/all-system-accounts')
@Public()
@ApiOperation({ summary: '获取所有系统账户列表(内部API) - 用于系统账户报表' })
@ApiResponse({ status: 200, description: '所有系统账户列表(固定+区域)' })
async getAllSystemAccounts() {
this.logger.log(`========== statistics/all-system-accounts 请求 ==========`);
const result = await this.walletService.getAllSystemAccounts();
this.logger.log(`系统账户查询结果: 固定=${result.fixedAccounts.length}, 省=${result.provinceAccounts.length}, 市=${result.cityAccounts.length}`);
return result;
}
}

View File

@ -2953,4 +2953,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 };
}
}