From 6e395ce58ce46ac3462b92c864223dd47ed382e6 Mon Sep 17 00:00:00 2001 From: hailin Date: Sun, 4 Jan 2026 22:06:58 -0800 Subject: [PATCH] feat(reporting): add system account report aggregation feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes - Add system account report aggregation APIs in reporting-service - Add internal statistics APIs in wallet-service, reward-service, authorization-service - Add system accounts tab in admin-web statistics page - Enhanced metadata in reward entries for traceability ## Backend Changes - wallet-service: Add offline settlement summary and system accounts balances APIs - reward-service: Add expired rewards summary API - authorization-service: Add fixed accounts list, region accounts summary APIs - reporting-service: Add HTTP clients and aggregation service for system account reports ## Frontend Changes - admin-web: Add SystemAccountsTab component with fixed accounts, region summaries, offline settlement stats, and expired rewards display ## Rollback Instructions Each file includes rollback comments with [2026-01-04] tag marking new additions. To rollback: delete files marked as new, remove code sections marked with date comments. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../internal-authorization.controller.ts | 64 ++- .../system-account-application.service.ts | 75 ++++ .../reporting-service/package-lock.json | 13 + .../services/reporting-service/package.json | 1 + .../reporting-service/src/api/api.module.ts | 9 + .../system-account-report.controller.ts | 81 ++++ .../src/application/application.module.ts | 11 + ...stem-account-report-application.service.ts | 233 +++++++++++ .../authorization-service.client.ts | 120 ++++++ .../reward-service/reward-service.client.ts | 73 ++++ .../wallet-service/wallet-service.client.ts | 96 +++++ .../infrastructure/infrastructure.module.ts | 25 +- .../api/controllers/internal.controller.ts | 29 +- .../services/reward-application.service.ts | 125 ++++++ .../controllers/internal-wallet.controller.ts | 44 ++ .../services/wallet-application.service.ts | 166 ++++++++ .../src/app/(dashboard)/statistics/page.tsx | 34 ++ .../statistics/statistics.module.scss | 37 ++ .../SystemAccountsTab.module.scss | 287 +++++++++++++ .../SystemAccountsTab.tsx | 394 ++++++++++++++++++ .../features/system-account-report/index.ts | 6 + .../src/infrastructure/api/endpoints.ts | 11 + .../services/systemAccountReportService.ts | 73 ++++ frontend/admin-web/src/types/index.ts | 2 + .../src/types/system-account.types.ts | 122 ++++++ 25 files changed, 2126 insertions(+), 5 deletions(-) create mode 100644 backend/services/reporting-service/src/api/controllers/system-account-report.controller.ts create mode 100644 backend/services/reporting-service/src/application/services/system-account-report-application.service.ts create mode 100644 backend/services/reporting-service/src/infrastructure/external/reward-service/reward-service.client.ts create mode 100644 backend/services/reporting-service/src/infrastructure/external/wallet-service/wallet-service.client.ts create mode 100644 frontend/admin-web/src/components/features/system-account-report/SystemAccountsTab.module.scss create mode 100644 frontend/admin-web/src/components/features/system-account-report/SystemAccountsTab.tsx create mode 100644 frontend/admin-web/src/components/features/system-account-report/index.ts create mode 100644 frontend/admin-web/src/services/systemAccountReportService.ts create mode 100644 frontend/admin-web/src/types/system-account.types.ts diff --git a/backend/services/authorization-service/src/api/controllers/internal-authorization.controller.ts b/backend/services/authorization-service/src/api/controllers/internal-authorization.controller.ts index d880a910..53084aec 100644 --- a/backend/services/authorization-service/src/api/controllers/internal-authorization.controller.ts +++ b/backend/services/authorization-service/src/api/controllers/internal-authorization.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, Query, Logger } from '@nestjs/common' import { ApiTags, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger' -import { AuthorizationApplicationService } from '@/application/services' +import { AuthorizationApplicationService, SystemAccountApplicationService } from '@/application/services' /** * 内部授权 API - 供其他微服务调用 @@ -11,7 +11,11 @@ import { AuthorizationApplicationService } from '@/application/services' export class InternalAuthorizationController { private readonly logger = new Logger(InternalAuthorizationController.name) - constructor(private readonly applicationService: AuthorizationApplicationService) {} + constructor( + private readonly applicationService: AuthorizationApplicationService, + // [2026-01-04] 新增:用于系统账户报表统计 + private readonly systemAccountService: SystemAccountApplicationService, + ) {} /** * 查找用户推荐链中最近的社区授权用户 @@ -280,4 +284,60 @@ export class InternalAuthorizationController { Number(treeCount), ) } + + // =============== 系统账户报表统计 API =============== + // [2026-01-04] 新增:用于 reporting-service 聚合系统账户报表数据 + // 回滚方式:删除以下 API 方法即可 + + /** + * 获取所有固定系统账户列表 + * 包括:总部社区、成本账户、运营账户、RWAD底池 + */ + @Get('statistics/fixed-accounts') + @ApiOperation({ summary: '获取固定系统账户列表(内部 API)- 用于系统账户报表' }) + @ApiResponse({ status: 200, description: '固定系统账户列表' }) + async getFixedAccountsList() { + this.logger.log(`========== statistics/fixed-accounts 请求 ==========`) + + const result = await this.systemAccountService.getAllFixedAccounts() + + this.logger.log(`固定账户查询结果: ${result.length} 个账户`) + return result + } + + /** + * 获取区域系统账户列表 + * 支持按省份或城市类型筛选 + */ + @Get('statistics/region-accounts') + @ApiOperation({ summary: '获取区域系统账户列表(内部 API)- 用于系统账户报表' }) + @ApiQuery({ name: 'type', description: '账户类型: province 或 city' }) + @ApiResponse({ status: 200, description: '区域系统账户列表' }) + async getRegionAccountsList( + @Query('type') type: 'province' | 'city', + ) { + this.logger.log(`========== statistics/region-accounts 请求 ==========`) + this.logger.log(`type: ${type}`) + + const result = await this.systemAccountService.getRegionAccountsSummary(type) + + this.logger.log(`区域账户查询结果: ${result.accounts.length} 个账户, 总余额: ${result.summary.totalBalance}`) + return result + } + + /** + * 获取所有系统账户汇总统计 + * 包括固定账户和区域账户的汇总 + */ + @Get('statistics/all-accounts-summary') + @ApiOperation({ summary: '获取所有系统账户汇总(内部 API)- 用于系统账户报表' }) + @ApiResponse({ status: 200, description: '所有系统账户汇总' }) + async getAllAccountsSummary() { + this.logger.log(`========== statistics/all-accounts-summary 请求 ==========`) + + const result = await this.systemAccountService.getAllAccountsSummary() + + this.logger.log(`所有账户汇总: 固定${result.fixedAccounts.length}个, 省${result.provinceSummary.count}个, 市${result.citySummary.count}个`) + return result + } } diff --git a/backend/services/authorization-service/src/application/services/system-account-application.service.ts b/backend/services/authorization-service/src/application/services/system-account-application.service.ts index ece64224..2ec40d75 100644 --- a/backend/services/authorization-service/src/application/services/system-account-application.service.ts +++ b/backend/services/authorization-service/src/application/services/system-account-application.service.ts @@ -434,4 +434,79 @@ export class SystemAccountApplicationService implements OnModuleInit { createdAt: entry.createdAt, } } + + // =============== 系统账户报表统计方法 =============== + // [2026-01-04] 新增:用于 reporting-service 聚合系统账户报表数据 + // 回滚方式:删除以下方法即可 + + /** + * 获取区域系统账户汇总 + * 用于系统账户报表展示省/市区域账户列表和汇总 + */ + async getRegionAccountsSummary(type: 'province' | 'city'): Promise<{ + accounts: SystemAccountDTO[] + summary: { + totalBalance: string + totalReceived: string + count: number + } + }> { + const accountType = type === 'province' + ? SystemAccountType.SYSTEM_PROVINCE + : SystemAccountType.SYSTEM_CITY + + const accounts = await this.systemAccountRepository.findAllRegionAccounts(accountType) + + // 计算汇总 + let totalBalance = new Decimal(0) + let totalReceived = new Decimal(0) + + const accountDTOs = accounts.map((account) => { + totalBalance = totalBalance.plus(account.usdtBalance) + totalReceived = totalReceived.plus(account.totalReceived) + return this.toDTO(account) + }) + + return { + accounts: accountDTOs, + summary: { + totalBalance: totalBalance.toString(), + totalReceived: totalReceived.toString(), + count: accounts.length, + }, + } + } + + /** + * 获取所有系统账户汇总(固定+区域) + * 用于系统账户报表概览 + */ + async getAllAccountsSummary(): Promise<{ + fixedAccounts: SystemAccountDTO[] + provinceSummary: { + totalBalance: string + totalReceived: string + count: number + } + citySummary: { + totalBalance: string + totalReceived: string + count: number + } + }> { + // 获取固定账户 + const fixedAccounts = await this.getAllFixedAccounts() + + // 获取省区域账户汇总 + const provinceSummary = await this.getRegionAccountsSummary('province') + + // 获取市区域账户汇总 + const citySummary = await this.getRegionAccountsSummary('city') + + return { + fixedAccounts, + provinceSummary: provinceSummary.summary, + citySummary: citySummary.summary, + } + } } diff --git a/backend/services/reporting-service/package-lock.json b/backend/services/reporting-service/package-lock.json index 0613a7a8..4ef021e7 100644 --- a/backend/services/reporting-service/package-lock.json +++ b/backend/services/reporting-service/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "UNLICENSED", "dependencies": { + "@nestjs/axios": "^4.0.1", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", @@ -1651,6 +1652,17 @@ "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "license": "MIT" }, + "node_modules/@nestjs/axios": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz", + "integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "axios": "^1.3.1", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.4.9", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", @@ -3559,6 +3571,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", diff --git a/backend/services/reporting-service/package.json b/backend/services/reporting-service/package.json index 74560f06..6c00a452 100644 --- a/backend/services/reporting-service/package.json +++ b/backend/services/reporting-service/package.json @@ -25,6 +25,7 @@ "prisma:studio": "prisma studio" }, "dependencies": { + "@nestjs/axios": "^4.0.1", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", diff --git a/backend/services/reporting-service/src/api/api.module.ts b/backend/services/reporting-service/src/api/api.module.ts index 47cdc41c..f562737a 100644 --- a/backend/services/reporting-service/src/api/api.module.ts +++ b/backend/services/reporting-service/src/api/api.module.ts @@ -1,9 +1,16 @@ +/** + * API 模块 + * [2026-01-04] 更新:新增 SystemAccountReportController 用于系统账户报表 + * 回滚方式:删除 SystemAccountReportController 的导入和注册 + */ import { Module } from '@nestjs/common'; import { ApplicationModule } from '../application/application.module'; import { HealthController } from './controllers/health.controller'; import { ReportController } from './controllers/report.controller'; import { ExportController } from './controllers/export.controller'; import { DashboardController } from './controllers/dashboard.controller'; +// [2026-01-04] 新增:系统账户报表控制器 +import { SystemAccountReportController } from './controllers/system-account-report.controller'; @Module({ imports: [ApplicationModule], @@ -12,6 +19,8 @@ import { DashboardController } from './controllers/dashboard.controller'; ReportController, ExportController, DashboardController, + // [2026-01-04] 新增:系统账户报表控制器 + SystemAccountReportController, ], }) export class ApiModule {} 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 new file mode 100644 index 00000000..35603490 --- /dev/null +++ b/backend/services/reporting-service/src/api/controllers/system-account-report.controller.ts @@ -0,0 +1,81 @@ +/** + * 系统账户报表控制器 + * [2026-01-04] 新增:提供系统账户报表相关API + * 回滚方式:删除此文件,并从 api.module.ts 中移除注册 + */ +import { Controller, Get, Query, Logger } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger'; +import { SystemAccountReportApplicationService } from '../../application/services/system-account-report-application.service'; + +@ApiTags('System Account Reports') +@Controller('system-account-reports') +export class SystemAccountReportController { + private readonly logger = new Logger(SystemAccountReportController.name); + + constructor( + private readonly systemAccountReportService: SystemAccountReportApplicationService, + ) {} + + @Get() + @ApiOperation({ summary: '获取完整系统账户报表' }) + @ApiQuery({ name: 'startDate', required: false, description: '开始日期 (YYYY-MM-DD)' }) + @ApiQuery({ name: 'endDate', required: false, description: '结束日期 (YYYY-MM-DD)' }) + @ApiResponse({ status: 200, description: '系统账户报表数据' }) + async getFullReport( + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + ) { + this.logger.log(`[getFullReport] 请求系统账户报表, startDate=${startDate}, endDate=${endDate}`); + return this.systemAccountReportService.getFullReport({ startDate, endDate }); + } + + @Get('fixed-accounts') + @ApiOperation({ summary: '获取固定系统账户列表(带余额)' }) + @ApiResponse({ status: 200, description: '固定系统账户列表' }) + async getFixedAccounts() { + this.logger.log('[getFixedAccounts] 请求固定系统账户列表'); + return this.systemAccountReportService.getFixedAccountsWithBalances(); + } + + @Get('province-summary') + @ApiOperation({ summary: '获取省区域账户汇总' }) + @ApiResponse({ status: 200, description: '省区域账户汇总' }) + async getProvinceSummary() { + this.logger.log('[getProvinceSummary] 请求省区域账户汇总'); + return this.systemAccountReportService.getProvinceAccountsSummary(); + } + + @Get('city-summary') + @ApiOperation({ summary: '获取市区域账户汇总' }) + @ApiResponse({ status: 200, description: '市区域账户汇总' }) + async getCitySummary() { + this.logger.log('[getCitySummary] 请求市区域账户汇总'); + return this.systemAccountReportService.getCityAccountsSummary(); + } + + @Get('offline-settlement') + @ApiOperation({ summary: '获取面对面结算统计' }) + @ApiQuery({ name: 'startDate', required: false, description: '开始日期 (YYYY-MM-DD)' }) + @ApiQuery({ name: 'endDate', required: false, description: '结束日期 (YYYY-MM-DD)' }) + @ApiResponse({ status: 200, description: '面对面结算统计' }) + async getOfflineSettlement( + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + ) { + this.logger.log(`[getOfflineSettlement] 请求面对面结算统计, startDate=${startDate}, endDate=${endDate}`); + return this.systemAccountReportService.getOfflineSettlementSummary({ startDate, endDate }); + } + + @Get('expired-rewards') + @ApiOperation({ summary: '获取过期收益统计' }) + @ApiQuery({ name: 'startDate', required: false, description: '开始日期 (YYYY-MM-DD)' }) + @ApiQuery({ name: 'endDate', required: false, description: '结束日期 (YYYY-MM-DD)' }) + @ApiResponse({ status: 200, description: '过期收益统计' }) + async getExpiredRewards( + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + ) { + this.logger.log(`[getExpiredRewards] 请求过期收益统计, startDate=${startDate}, endDate=${endDate}`); + return this.systemAccountReportService.getExpiredRewardsSummary({ startDate, endDate }); + } +} diff --git a/backend/services/reporting-service/src/application/application.module.ts b/backend/services/reporting-service/src/application/application.module.ts index adb483f2..85cef0b6 100644 --- a/backend/services/reporting-service/src/application/application.module.ts +++ b/backend/services/reporting-service/src/application/application.module.ts @@ -1,3 +1,8 @@ +/** + * 应用层模块 + * [2026-01-04] 更新:新增 SystemAccountReportApplicationService 用于系统账户报表聚合 + * 回滚方式:删除 SystemAccountReportApplicationService 的导入和注册 + */ import { Module } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; import { DomainModule } from '../domain/domain.module'; @@ -7,6 +12,8 @@ import { ExportReportHandler } from './commands/export-report/export-report.hand import { GetReportSnapshotHandler } from './queries/get-report-snapshot/get-report-snapshot.handler'; import { ReportingApplicationService } from './services/reporting-application.service'; import { DashboardApplicationService } from './services/dashboard-application.service'; +// [2026-01-04] 新增:系统账户报表聚合服务 +import { SystemAccountReportApplicationService } from './services/system-account-report-application.service'; import { ReportGenerationScheduler } from './schedulers/report-generation.scheduler'; @Module({ @@ -17,6 +24,8 @@ import { ReportGenerationScheduler } from './schedulers/report-generation.schedu GetReportSnapshotHandler, ReportingApplicationService, DashboardApplicationService, + // [2026-01-04] 新增:系统账户报表聚合服务 + SystemAccountReportApplicationService, ReportGenerationScheduler, ], exports: [ @@ -25,6 +34,8 @@ import { ReportGenerationScheduler } from './schedulers/report-generation.schedu GetReportSnapshotHandler, ReportingApplicationService, DashboardApplicationService, + // [2026-01-04] 新增:系统账户报表聚合服务 + SystemAccountReportApplicationService, ], }) export class ApplicationModule {} 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 new file mode 100644 index 00000000..54c01dc6 --- /dev/null +++ b/backend/services/reporting-service/src/application/services/system-account-report-application.service.ts @@ -0,0 +1,233 @@ +/** + * 系统账户报表应用服务 + * [2026-01-04] 新增:用于聚合各服务的系统账户报表数据 + * 回滚方式:删除此文件,并从 application.module.ts 中移除注册 + */ +import { Injectable, Logger } from '@nestjs/common'; +import { WalletServiceClient, OfflineSettlementSummary, SystemAccountBalance } 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 SystemAccountReportResponse { + // 固定系统账户 + fixedAccounts: { + costAccount: SystemAccountWithBalance | null; // 成本账户 S0000000001 + operationAccount: SystemAccountWithBalance | null; // 运营账户 S0000000002 + hqCommunity: SystemAccountWithBalance | null; // 总部社区 S0000000003 + rwadPoolPending: SystemAccountWithBalance | null; // RWAD待发放池 S0000000004 + platformFee: SystemAccountWithBalance | null; // 平台手续费 S0000000005 + }; + // 省区域账户汇总 + provinceSummary: { + accounts: SystemAccountDTO[]; + summary: { + totalBalance: string; + totalReceived: string; + count: number; + }; + }; + // 市区域账户汇总 + citySummary: { + accounts: SystemAccountDTO[]; + summary: { + totalBalance: string; + totalReceived: string; + count: number; + }; + }; + // 面对面结算统计 + offlineSettlement: OfflineSettlementSummary; + // 过期收益统计 + expiredRewards: ExpiredRewardsSummary; + // 报表生成时间 + generatedAt: string; +} + +/** + * 系统账户带钱包余额信息 + */ +export interface SystemAccountWithBalance extends SystemAccountDTO { + walletBalance?: number; +} + +/** + * 固定账户类型映射 + */ +const FIXED_ACCOUNT_TYPES = { + 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() +export class SystemAccountReportApplicationService { + private readonly logger = new Logger(SystemAccountReportApplicationService.name); + + constructor( + private readonly walletServiceClient: WalletServiceClient, + private readonly rewardServiceClient: RewardServiceClient, + private readonly authorizationServiceClient: AuthorizationServiceClient, + ) {} + + /** + * 获取完整的系统账户报表 + */ + async getFullReport(params?: { + startDate?: string; + endDate?: string; + }): Promise { + this.logger.log('[getFullReport] 开始聚合系统账户报表数据...'); + + // 并行获取所有数据 + const [ + allAccountsSummary, + provinceSummary, + citySummary, + offlineSettlement, + expiredRewards, + fixedAccountsBalances, + ] = await Promise.all([ + this.authorizationServiceClient.getAllAccountsSummary(), + this.authorizationServiceClient.getRegionAccountsList('province'), + this.authorizationServiceClient.getRegionAccountsList('city'), + this.walletServiceClient.getOfflineSettlementSummary(params), + this.rewardServiceClient.getExpiredRewardsSummary(params), + this.getFixedAccountsBalances(), + ]); + + // 组装固定账户数据 + const fixedAccounts = this.assembleFixedAccounts( + allAccountsSummary.fixedAccounts, + fixedAccountsBalances, + ); + + const report: SystemAccountReportResponse = { + fixedAccounts, + provinceSummary, + citySummary, + offlineSettlement, + expiredRewards, + generatedAt: new Date().toISOString(), + }; + + this.logger.log('[getFullReport] 系统账户报表数据聚合完成'); + return report; + } + + /** + * 获取固定账户列表(带余额) + */ + async getFixedAccountsWithBalances(): Promise { + 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 getProvinceAccountsSummary(): Promise { + return this.authorizationServiceClient.getRegionAccountsList('province'); + } + + /** + * 获取市区域账户汇总 + */ + async getCityAccountsSummary(): Promise { + return this.authorizationServiceClient.getRegionAccountsList('city'); + } + + /** + * 获取面对面结算统计 + */ + async getOfflineSettlementSummary(params?: { + startDate?: string; + endDate?: string; + }): Promise { + return this.walletServiceClient.getOfflineSettlementSummary(params); + } + + /** + * 获取过期收益统计 + */ + async getExpiredRewardsSummary(params?: { + startDate?: string; + endDate?: string; + }): Promise { + return this.rewardServiceClient.getExpiredRewardsSummary(params); + } + + /** + * 获取固定账户余额 + */ + private async getFixedAccountsBalances(): Promise { + const sequences = Object.values(FIXED_ACCOUNT_SEQUENCES); + return this.walletServiceClient.getSystemAccountsBalances(sequences); + } + + /** + * 组装固定账户数据 + */ + private assembleFixedAccounts( + fixedAccounts: SystemAccountDTO[], + balances: SystemAccountBalance[], + ): SystemAccountReportResponse['fixedAccounts'] { + const result: SystemAccountReportResponse['fixedAccounts'] = { + costAccount: null, + operationAccount: null, + hqCommunity: null, + rwadPoolPending: null, + platformFee: null, + }; + + 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, + }; + } + } + + return result; + } + + /** + * 根据账户类型获取账户序列号 + */ + 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]; + } + return undefined; + } +} diff --git a/backend/services/reporting-service/src/infrastructure/external/authorization-service/authorization-service.client.ts b/backend/services/reporting-service/src/infrastructure/external/authorization-service/authorization-service.client.ts index e8df1702..41a8b002 100644 --- a/backend/services/reporting-service/src/infrastructure/external/authorization-service/authorization-service.client.ts +++ b/backend/services/reporting-service/src/infrastructure/external/authorization-service/authorization-service.client.ts @@ -1,3 +1,8 @@ +/** + * Authorization Service HTTP 客户端 + * [2026-01-04] 更新:新增系统账户报表统计相关方法 + * 回滚方式:删除 getFixedAccountsList, getRegionAccountsList, getAllAccountsSummary 方法 + */ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import axios from 'axios'; @@ -14,6 +19,47 @@ export interface CompanyStatsChange { cityCompanyChange: number; } +// =============== 系统账户报表相关接口 =============== +// [2026-01-04] 新增:用于系统账户报表统计 + +export interface SystemAccountDTO { + id: string; + accountType: string; + regionCode: string | null; + regionName: string | null; + walletAddress: string | null; + usdtBalance: string; + hashpower: string; + totalReceived: string; + totalTransferred: string; + status: string; + createdAt: string; + updatedAt: string; +} + +export interface RegionAccountsSummary { + accounts: SystemAccountDTO[]; + summary: { + totalBalance: string; + totalReceived: string; + count: number; + }; +} + +export interface AllAccountsSummary { + fixedAccounts: SystemAccountDTO[]; + provinceSummary: { + totalBalance: string; + totalReceived: string; + count: number; + }; + citySummary: { + totalBalance: string; + totalReceived: string; + count: number; + }; +} + @Injectable() export class AuthorizationServiceClient { private readonly logger = new Logger(AuthorizationServiceClient.name); @@ -79,4 +125,78 @@ export class AuthorizationServiceClient { }; } } + + // =============== 系统账户报表统计方法 =============== + // [2026-01-04] 新增:用于系统账户报表统计 + // 回滚方式:删除以下方法即可 + + /** + * 获取固定系统账户列表 + */ + async getFixedAccountsList(): Promise { + this.logger.debug(`[getFixedAccountsList] 请求: ${this.baseUrl}/internal/statistics/fixed-accounts`); + + try { + const response = await axios.get( + `${this.baseUrl}/internal/statistics/fixed-accounts`, + ); + return response.data; + } catch (error) { + this.logger.error(`[getFixedAccountsList] 失败: ${error.message}`); + return []; + } + } + + /** + * 获取区域系统账户列表和汇总 + */ + async getRegionAccountsList(type: 'province' | 'city'): Promise { + this.logger.debug(`[getRegionAccountsList] 请求: ${this.baseUrl}/internal/statistics/region-accounts?type=${type}`); + + try { + const response = await axios.get( + `${this.baseUrl}/internal/statistics/region-accounts?type=${type}`, + ); + return response.data; + } catch (error) { + this.logger.error(`[getRegionAccountsList] 失败: ${error.message}`); + return { + accounts: [], + summary: { + totalBalance: '0', + totalReceived: '0', + count: 0, + }, + }; + } + } + + /** + * 获取所有系统账户汇总 + */ + async getAllAccountsSummary(): Promise { + this.logger.debug(`[getAllAccountsSummary] 请求: ${this.baseUrl}/internal/statistics/all-accounts-summary`); + + try { + const response = await axios.get( + `${this.baseUrl}/internal/statistics/all-accounts-summary`, + ); + return response.data; + } catch (error) { + this.logger.error(`[getAllAccountsSummary] 失败: ${error.message}`); + return { + fixedAccounts: [], + provinceSummary: { + totalBalance: '0', + totalReceived: '0', + count: 0, + }, + citySummary: { + totalBalance: '0', + totalReceived: '0', + count: 0, + }, + }; + } + } } diff --git a/backend/services/reporting-service/src/infrastructure/external/reward-service/reward-service.client.ts b/backend/services/reporting-service/src/infrastructure/external/reward-service/reward-service.client.ts new file mode 100644 index 00000000..16da012f --- /dev/null +++ b/backend/services/reporting-service/src/infrastructure/external/reward-service/reward-service.client.ts @@ -0,0 +1,73 @@ +/** + * Reward Service HTTP 客户端 + * [2026-01-04] 新增:用于系统账户报表统计 + * 回滚方式:删除此文件 + */ + +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; + +export interface ExpiredRewardsSummary { + totalAmount: number; + totalCount: number; + byMonth: Array<{ + month: string; + amount: number; + count: number; + }>; + byRightType: Array<{ + rightType: string; + amount: number; + count: number; + }>; +} + +@Injectable() +export class RewardServiceClient { + private readonly logger = new Logger(RewardServiceClient.name); + private readonly baseUrl: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.baseUrl = this.configService.get( + 'REWARD_SERVICE_URL', + 'http://reward-service:3004', + ); + } + + /** + * 获取过期收益统计汇总 + */ + async getExpiredRewardsSummary(params?: { + startDate?: string; + endDate?: string; + }): Promise { + try { + const queryParams = new URLSearchParams(); + if (params?.startDate) queryParams.append('startDate', params.startDate); + if (params?.endDate) queryParams.append('endDate', params.endDate); + + const url = `${this.baseUrl}/internal/statistics/expired-rewards-summary?${queryParams.toString()}`; + this.logger.debug(`[getExpiredRewardsSummary] 请求: ${url}`); + + const response = await firstValueFrom( + this.httpService.get(url), + ); + + return response.data; + } catch (error) { + this.logger.error(`[getExpiredRewardsSummary] 失败: ${error.message}`); + // 返回默认值,不阻塞报表 + return { + totalAmount: 0, + totalCount: 0, + byMonth: [], + byRightType: [], + }; + } + } +} 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 new file mode 100644 index 00000000..418b4c16 --- /dev/null +++ b/backend/services/reporting-service/src/infrastructure/external/wallet-service/wallet-service.client.ts @@ -0,0 +1,96 @@ +/** + * Wallet Service HTTP 客户端 + * [2026-01-04] 新增:用于系统账户报表统计 + * 回滚方式:删除此文件 + */ + +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; + +export interface OfflineSettlementSummary { + totalAmount: number; + totalCount: number; + byMonth: Array<{ + month: string; + amount: number; + count: number; + }>; +} + +export interface SystemAccountBalance { + accountSequence: string; + name: string; + balance: number; + totalReceived: number; + createdAt: string; +} + +@Injectable() +export class WalletServiceClient { + private readonly logger = new Logger(WalletServiceClient.name); + private readonly baseUrl: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) { + this.baseUrl = this.configService.get( + 'WALLET_SERVICE_URL', + 'http://wallet-service:3002', + ); + } + + /** + * 获取面对面结算统计汇总 + */ + async getOfflineSettlementSummary(params?: { + startDate?: string; + endDate?: string; + }): Promise { + try { + const queryParams = new URLSearchParams(); + if (params?.startDate) queryParams.append('startDate', params.startDate); + if (params?.endDate) queryParams.append('endDate', params.endDate); + + const url = `${this.baseUrl}/wallets/statistics/offline-settlement-summary?${queryParams.toString()}`; + this.logger.debug(`[getOfflineSettlementSummary] 请求: ${url}`); + + const response = await firstValueFrom( + this.httpService.get(url), + ); + + return response.data; + } catch (error) { + this.logger.error(`[getOfflineSettlementSummary] 失败: ${error.message}`); + // 返回默认值,不阻塞报表 + return { + totalAmount: 0, + totalCount: 0, + byMonth: [], + }; + } + } + + /** + * 获取系统账户余额 + */ + async getSystemAccountsBalances( + accountSequences: string[], + ): Promise { + try { + const url = `${this.baseUrl}/wallets/statistics/system-accounts-balances?sequences=${accountSequences.join(',')}`; + this.logger.debug(`[getSystemAccountsBalances] 请求: ${url}`); + + const response = await firstValueFrom( + this.httpService.get(url), + ); + + return response.data; + } catch (error) { + this.logger.error(`[getSystemAccountsBalances] 失败: ${error.message}`); + return []; + } + } +} diff --git a/backend/services/reporting-service/src/infrastructure/infrastructure.module.ts b/backend/services/reporting-service/src/infrastructure/infrastructure.module.ts index 034ec7cb..412f0ea5 100644 --- a/backend/services/reporting-service/src/infrastructure/infrastructure.module.ts +++ b/backend/services/reporting-service/src/infrastructure/infrastructure.module.ts @@ -1,4 +1,10 @@ +/** + * 基础设施模块 + * [2026-01-04] 更新:新增 WalletServiceClient, RewardServiceClient 用于系统账户报表统计 + * 回滚方式:删除 HttpModule 导入及 WalletServiceClient, RewardServiceClient 的注册 + */ import { Module } from '@nestjs/common'; +import { HttpModule } from '@nestjs/axios'; import { PrismaService } from './persistence/prisma/prisma.service'; import { ReportDefinitionRepository } from './persistence/repositories/report-definition.repository.impl'; import { ReportSnapshotRepository } from './persistence/repositories/report-snapshot.repository.impl'; @@ -22,11 +28,22 @@ import { LeaderboardServiceClient } from './external/leaderboard-service/leaderb import { PlantingServiceClient } from './external/planting-service/planting-service.client'; import { AuthorizationServiceClient } from './external/authorization-service/authorization-service.client'; import { IdentityServiceClient } from './external/identity-service/identity-service.client'; +// [2026-01-04] 新增:系统账户报表统计 HTTP 客户端 +import { WalletServiceClient } from './external/wallet-service/wallet-service.client'; +import { RewardServiceClient } from './external/reward-service/reward-service.client'; import { ExportModule } from './export/export.module'; import { RedisModule } from './redis/redis.module'; @Module({ - imports: [ExportModule, RedisModule], + imports: [ + // [2026-01-04] 新增:HttpModule 用于系统账户报表统计的 HTTP 调用 + HttpModule.register({ + timeout: 30000, + maxRedirects: 5, + }), + ExportModule, + RedisModule, + ], providers: [ PrismaService, { @@ -65,6 +82,9 @@ import { RedisModule } from './redis/redis.module'; PlantingServiceClient, AuthorizationServiceClient, IdentityServiceClient, + // [2026-01-04] 新增:系统账户报表统计 HTTP 客户端 + WalletServiceClient, + RewardServiceClient, ], exports: [ PrismaService, @@ -80,6 +100,9 @@ import { RedisModule } from './redis/redis.module'; PlantingServiceClient, AuthorizationServiceClient, IdentityServiceClient, + // [2026-01-04] 新增:系统账户报表统计 HTTP 客户端 + WalletServiceClient, + RewardServiceClient, ExportModule, RedisModule, ], diff --git a/backend/services/reward-service/src/api/controllers/internal.controller.ts b/backend/services/reward-service/src/api/controllers/internal.controller.ts index 34df3b3b..2b6aa3d1 100644 --- a/backend/services/reward-service/src/api/controllers/internal.controller.ts +++ b/backend/services/reward-service/src/api/controllers/internal.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Post, Body, Logger } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { Controller, Post, Body, Logger, Get, Query } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger'; import { IsArray, IsString, IsOptional } from 'class-validator'; import { RewardApplicationService } from '../../application/services/reward-application.service'; @@ -71,4 +71,29 @@ export class InternalController { return result; } + + // =============== 系统账户报表统计 API =============== + // [2026-01-04] 新增:用于 reporting-service 聚合系统账户报表数据 + // 回滚方式:删除以下 API 方法即可 + + @Get('statistics/expired-rewards-summary') + @ApiOperation({ summary: '过期收益统计汇总(内部接口)- 用于系统账户报表' }) + @ApiQuery({ name: 'startDate', required: false, description: '开始日期 (YYYY-MM-DD)' }) + @ApiQuery({ name: 'endDate', required: false, description: '结束日期 (YYYY-MM-DD)' }) + @ApiResponse({ status: 200, description: '过期收益统计汇总' }) + async getExpiredRewardsSummary( + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + ) { + this.logger.log(`========== statistics/expired-rewards-summary 请求 ==========`); + this.logger.log(`startDate: ${startDate}, endDate: ${endDate}`); + + const result = await this.rewardService.getExpiredRewardsSummary({ + startDate: startDate ? new Date(startDate) : undefined, + endDate: endDate ? new Date(endDate) : undefined, + }); + + this.logger.log(`过期收益统计结果: totalAmount=${result.totalAmount}, totalCount=${result.totalCount}`); + return result; + } } diff --git a/backend/services/reward-service/src/application/services/reward-application.service.ts b/backend/services/reward-service/src/application/services/reward-application.service.ts index ea87470c..5faba28a 100644 --- a/backend/services/reward-service/src/application/services/reward-application.service.ts +++ b/backend/services/reward-service/src/application/services/reward-application.service.ts @@ -15,6 +15,8 @@ import { WalletServiceClient } from '../../infrastructure/external/wallet-servic import { OutboxRepository, OutboxEventData } from '../../infrastructure/persistence/repositories/outbox.repository'; import { SettlementRecordRepository, SettlementRecordDto } from '../../infrastructure/persistence/repositories/settlement-record.repository'; import { RewardSummary } from '../../domain/aggregates/reward-summary/reward-summary.aggregate'; +// [2026-01-04] 新增:用于过期收益统计查询 +import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; // 总部社区账户ID const HEADQUARTERS_COMMUNITY_USER_ID = BigInt(1); @@ -34,6 +36,8 @@ export class RewardApplicationService { private readonly walletService: WalletServiceClient, private readonly outboxRepository: OutboxRepository, private readonly settlementRecordRepository: SettlementRecordRepository, + // [2026-01-04] 新增:用于过期收益统计查询 + private readonly prisma: PrismaService, ) {} /** @@ -80,6 +84,12 @@ export class RewardApplicationService { rightType: reward.rewardSource.rightType, sourceOrderNo: params.sourceOrderNo, sourceUserId: params.sourceUserId.toString(), + // [2026-01-04] 新增字段:用于系统账户报表统计,支持追溯每笔收益的来源 + // 回滚方式:删除以下4行即可恢复原状 + sourceAccountSequence: params.sourceAccountSequence ?? null, // 来源用户账户序列号 + treeCount: params.treeCount, // 认种数量 + provinceCode: params.provinceCode, // 认种省份代码 + cityCode: params.cityCode, // 认种城市代码 memo: reward.memo, }, })); @@ -183,6 +193,12 @@ export class RewardApplicationService { rightType: reward.rewardSource.rightType, sourceOrderNo: params.sourceOrderNo, sourceUserId: params.sourceUserId.toString(), + // [2026-01-04] 新增字段:用于系统账户报表统计,支持追溯每笔收益的来源 + // 回滚方式:删除以下4行即可恢复原状 + sourceAccountSequence: params.sourceAccountSequence ?? null, // 来源用户账户序列号 + treeCount: params.treeCount, // 认种数量 + provinceCode: params.provinceCode, // 认种省份代码 + cityCode: params.cityCode, // 认种城市代码 memo: reward.memo, reason: 'CONTRACT_EXPIRED', // 标记为合同超时 }, @@ -884,4 +900,113 @@ export class RewardApplicationService { }, }; } + + // =============== 系统账户报表统计方法 =============== + // [2026-01-04] 新增:用于 reporting-service 聚合系统账户报表数据 + // 回滚方式:删除以下方法和构造函数中的 prisma 注入即可 + + /** + * 获取过期收益统计汇总 + * 用于系统账户报表展示过期收益总额 + * + * 统计 reward_status = 'EXPIRED' 的奖励流水 + */ + async getExpiredRewardsSummary(params: { + startDate?: Date; + endDate?: Date; + }): Promise<{ + totalAmount: number; + totalCount: number; + byMonth: Array<{ + month: string; + amount: number; + count: number; + }>; + byRightType: Array<{ + rightType: string; + amount: number; + count: number; + }>; + }> { + this.logger.log(`[getExpiredRewardsSummary] 查询过期收益统计`); + + // 构建日期筛选条件 + const whereClause: any = { + rewardStatus: 'EXPIRED', + }; + if (params.startDate || params.endDate) { + whereClause.expiredAt = {}; + if (params.startDate) { + whereClause.expiredAt.gte = params.startDate; + } + if (params.endDate) { + whereClause.expiredAt.lte = params.endDate; + } + } + + // 查询总计 + const aggregateResult = await this.prisma.rewardLedgerEntry.aggregate({ + where: whereClause, + _sum: { + usdtAmount: true, + }, + _count: { + id: true, + }, + }); + + const totalAmount = aggregateResult._sum.usdtAmount + ? Number(aggregateResult._sum.usdtAmount) + : 0; + const totalCount = aggregateResult._count.id || 0; + + // 查询按月统计 + const byMonth = await this.prisma.$queryRaw>` + SELECT + TO_CHAR(expired_at, 'YYYY-MM') as month, + SUM(usdt_amount) as amount, + COUNT(*) as count + FROM reward_ledger_entries + WHERE reward_status = 'EXPIRED' + AND expired_at IS NOT NULL + GROUP BY TO_CHAR(expired_at, 'YYYY-MM') + ORDER BY month DESC + LIMIT 12 + `; + + // 查询按权益类型统计 + const byRightType = await this.prisma.$queryRaw>` + SELECT + right_type as "rightType", + SUM(usdt_amount) as amount, + COUNT(*) as count + FROM reward_ledger_entries + WHERE reward_status = 'EXPIRED' + GROUP BY right_type + ORDER BY amount DESC + `; + + return { + totalAmount, + totalCount, + byMonth: byMonth.map(row => ({ + month: row.month, + amount: Number(row.amount) || 0, + count: Number(row.count) || 0, + })), + byRightType: byRightType.map(row => ({ + rightType: row.rightType, + amount: Number(row.amount) || 0, + count: Number(row.count) || 0, + })), + }; + } } 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 05725725..263b5e78 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 @@ -344,4 +344,48 @@ export class InternalWalletController { this.logger.log(`全额线下结算扣减结果: ${JSON.stringify(result)}`); return result; } + + // =============== 系统账户报表统计 API =============== + // [2026-01-04] 新增:用于 reporting-service 聚合系统账户报表数据 + // 回滚方式:删除以下 API 方法即可 + + @Get('statistics/offline-settlement-summary') + @Public() + @ApiOperation({ summary: '面对面(线下)结算统计汇总(内部API) - 用于系统账户报表' }) + @ApiQuery({ name: 'startDate', required: false, description: '开始日期 (YYYY-MM-DD)' }) + @ApiQuery({ name: 'endDate', required: false, description: '结束日期 (YYYY-MM-DD)' }) + @ApiResponse({ status: 200, description: '面对面结算统计汇总' }) + async getOfflineSettlementSummary( + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + ) { + this.logger.log(`========== statistics/offline-settlement-summary 请求 ==========`); + this.logger.log(`startDate: ${startDate}, endDate: ${endDate}`); + + const result = await this.walletService.getOfflineSettlementSummary({ + startDate: startDate ? new Date(startDate) : undefined, + endDate: endDate ? new Date(endDate) : undefined, + }); + + this.logger.log(`面对面结算统计结果: totalAmount=${result.totalAmount}, totalCount=${result.totalCount}`); + return result; + } + + @Get('statistics/system-accounts-balances') + @Public() + @ApiOperation({ summary: '批量查询系统账户余额(内部API) - 用于系统账户报表' }) + @ApiQuery({ name: 'sequences', description: '账户序列号列表,逗号分隔 (如: S0000000001,S0000000002)' }) + @ApiResponse({ status: 200, description: '系统账户余额列表' }) + async getSystemAccountsBalances( + @Query('sequences') sequences: string, + ) { + this.logger.log(`========== statistics/system-accounts-balances 请求 ==========`); + this.logger.log(`sequences: ${sequences}`); + + const accountSequences = sequences.split(',').map(s => s.trim()).filter(s => s); + const result = await this.walletService.getSystemAccountsBalances(accountSequences); + + this.logger.log(`系统账户余额查询结果: ${result.length} 个账户`); + 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 3f200b87..3def8b6f 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 @@ -2787,4 +2787,170 @@ export class WalletApplicationService { }; return nameMap[allocationType] || allocationType; } + + // =============== 系统账户报表统计方法 =============== + // [2026-01-04] 新增:用于 reporting-service 聚合系统账户报表数据 + // 回滚方式:删除以下方法即可 + + /** + * 获取面对面(线下)结算统计汇总 + * 用于系统账户报表展示面对面结算收益总额 + */ + async getOfflineSettlementSummary(params: { + startDate?: Date; + endDate?: Date; + }): Promise<{ + totalAmount: number; + totalCount: number; + byMonth: Array<{ + month: string; + amount: number; + count: number; + }>; + }> { + this.logger.log(`[getOfflineSettlementSummary] 查询面对面结算统计`); + + // 构建日期筛选条件 + const whereClause: any = {}; + if (params.startDate || params.endDate) { + whereClause.createdAt = {}; + if (params.startDate) { + whereClause.createdAt.gte = params.startDate; + } + if (params.endDate) { + whereClause.createdAt.lte = params.endDate; + } + } + + // 查询总计 + const aggregateResult = await this.prisma.offlineSettlementDeduction.aggregate({ + where: whereClause, + _sum: { + deductedAmount: true, + }, + _count: { + id: true, + }, + }); + + const totalAmount = aggregateResult._sum.deductedAmount + ? Number(aggregateResult._sum.deductedAmount) + : 0; + const totalCount = aggregateResult._count.id || 0; + + // 查询按月统计 + const monthlyStats = await this.prisma.$queryRaw>` + SELECT + TO_CHAR(created_at, 'YYYY-MM') as month, + SUM(deducted_amount) as amount, + COUNT(*) as count + FROM offline_settlement_deductions + ${params.startDate ? this.prisma.$queryRaw`WHERE created_at >= ${params.startDate}` : this.prisma.$queryRaw``} + ${params.endDate ? this.prisma.$queryRaw`AND created_at <= ${params.endDate}` : this.prisma.$queryRaw``} + GROUP BY TO_CHAR(created_at, 'YYYY-MM') + ORDER BY month DESC + LIMIT 12 + `; + + // 简化查询:不使用条件拼接 + const byMonth = await this.prisma.$queryRaw>` + SELECT + TO_CHAR(created_at, 'YYYY-MM') as month, + SUM(deducted_amount) as amount, + COUNT(*) as count + FROM offline_settlement_deductions + GROUP BY TO_CHAR(created_at, 'YYYY-MM') + ORDER BY month DESC + LIMIT 12 + `; + + return { + totalAmount, + totalCount, + byMonth: byMonth.map(row => ({ + month: row.month, + amount: Number(row.amount) || 0, + count: Number(row.count) || 0, + })), + }; + } + + /** + * 批量查询系统账户余额 + * 用于系统账户报表展示固定账户(总部、成本、运营)的余额 + */ + async getSystemAccountsBalances(accountSequences: string[]): Promise> { + this.logger.log(`[getSystemAccountsBalances] 查询系统账户余额: ${accountSequences.join(', ')}`); + + if (accountSequences.length === 0) { + return []; + } + + // 系统账户名称映射 + const accountNames: Record = { + 'S0000000001': '总部社区账户', + 'S0000000002': '成本费账户', + 'S0000000003': '运营费账户', + 'S0000000004': 'RWAD底池账户', + 'S0000000005': '分享权益池账户', + }; + + // 查询钱包账户 + const wallets = await this.prisma.walletAccount.findMany({ + where: { + accountSequence: { + in: accountSequences, + }, + }, + select: { + accountSequence: true, + usdtAvailable: true, + createdAt: true, + }, + }); + + // 查询每个账户的累计收入 (SYSTEM_ALLOCATION 类型的流水总和) + const receivedStats = await this.prisma.ledgerEntry.groupBy({ + by: ['accountSequence'], + where: { + accountSequence: { + in: accountSequences, + }, + entryType: 'SYSTEM_ALLOCATION', + amount: { + gt: 0, + }, + }, + _sum: { + amount: true, + }, + }); + + // 构建结果 + const receivedMap = new Map( + receivedStats.map(stat => [stat.accountSequence, Number(stat._sum.amount) || 0]) + ); + + return wallets.map(wallet => ({ + accountSequence: wallet.accountSequence, + name: accountNames[wallet.accountSequence] || `系统账户 ${wallet.accountSequence}`, + balance: Number(wallet.usdtAvailable) || 0, + totalReceived: receivedMap.get(wallet.accountSequence) || 0, + createdAt: wallet.createdAt.toISOString(), + })); + } } diff --git a/frontend/admin-web/src/app/(dashboard)/statistics/page.tsx b/frontend/admin-web/src/app/(dashboard)/statistics/page.tsx index 93450503..79c2b79e 100644 --- a/frontend/admin-web/src/app/(dashboard)/statistics/page.tsx +++ b/frontend/admin-web/src/app/(dashboard)/statistics/page.tsx @@ -1,8 +1,15 @@ +/** + * 数据统计页面 + * [2026-01-04] 更新:新增系统账户报表Tab + * 回滚方式:删除 SystemAccountsTab 导入及相关 mainTab state 和切换逻辑 + */ 'use client'; import { useState } from 'react'; import { PageContainer } from '@/components/layout'; import { cn } from '@/utils/helpers'; +// [2026-01-04] 新增:系统账户报表Tab +import { SystemAccountsTab } from '@/components/features/system-account-report'; import styles from './statistics.module.scss'; /** @@ -95,8 +102,11 @@ const metricsData = [ /** * 数据统计页面 * 基于 UIPro Figma 设计实现 + * [2026-01-04] 更新:新增系统账户报表Tab */ export default function StatisticsPage() { + // [2026-01-04] 新增:主Tab切换 - 数据统计 vs 系统账户 + const [mainTab, setMainTab] = useState<'statistics' | 'system-accounts'>('statistics'); // 趋势图时间维度 const [trendPeriod, setTrendPeriod] = useState<'day' | 'week' | 'month' | 'quarter' | 'year'>('day'); // 龙虎榜时间维度 @@ -110,6 +120,28 @@ export default function StatisticsPage() { {/* 页面标题 */}

数据统计

+ {/* [2026-01-04] 新增:主Tab切换 */} +
+ + +
+ + {/* [2026-01-04] 新增:系统账户报表Tab内容 */} + {mainTab === 'system-accounts' && } + + {/* 原有统计内容 - 仅在 statistics tab 显示 */} + {mainTab === 'statistics' && ( + <> {/* 统计概览卡片 */}
@@ -353,6 +385,8 @@ export default function StatisticsPage() {
+ + )} ); diff --git a/frontend/admin-web/src/app/(dashboard)/statistics/statistics.module.scss b/frontend/admin-web/src/app/(dashboard)/statistics/statistics.module.scss index 8b224fa9..c35ad198 100644 --- a/frontend/admin-web/src/app/(dashboard)/statistics/statistics.module.scss +++ b/frontend/admin-web/src/app/(dashboard)/statistics/statistics.module.scss @@ -21,6 +21,43 @@ color: #0f172a; } +/* [2026-01-04] 新增:主Tab切换样式 */ +/* 回滚方式:删除以下 mainTabs 和 mainTab 样式 */ +.statistics__mainTabs { + align-self: stretch; + display: flex; + gap: 8px; + padding: 4px; + background-color: #f3f4f6; + border-radius: 10px; + width: fit-content; +} + +.statistics__mainTab { + padding: 10px 24px; + border: none; + border-radius: 8px; + background-color: transparent; + font-size: 15px; + font-weight: 500; + color: #6b7280; + cursor: pointer; + transition: all 0.2s ease; + font-family: inherit; + + &:hover { + color: #374151; + background-color: rgba(255, 255, 255, 0.5); + } + + &--active { + background-color: #fff; + color: #1f2937; + font-weight: 600; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + } +} + /* 统计概览卡片组 */ .statistics__overview { align-self: stretch; diff --git a/frontend/admin-web/src/components/features/system-account-report/SystemAccountsTab.module.scss b/frontend/admin-web/src/components/features/system-account-report/SystemAccountsTab.module.scss new file mode 100644 index 00000000..96c8703f --- /dev/null +++ b/frontend/admin-web/src/components/features/system-account-report/SystemAccountsTab.module.scss @@ -0,0 +1,287 @@ +/** + * 系统账户报表Tab样式 + * [2026-01-04] 新增:用于系统账户报表显示 + * 回滚方式:删除此文件 + */ +@use '@/styles/variables' as *; +@use '@/styles/mixins' as *; + +.container { + width: 100%; + display: flex; + flex-direction: column; + gap: 24px; +} + +/* 加载状态 */ +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px; + gap: 16px; + color: #6b7280; +} + +.spinner { + width: 32px; + height: 32px; + border: 3px solid #e5e7eb; + border-top-color: #3b82f6; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* 错误状态 */ +.error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px; + gap: 16px; + color: #ef4444; +} + +.retryButton { + padding: 8px 16px; + background-color: #3b82f6; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + + &:hover { + background-color: #2563eb; + } +} + +/* 空状态 */ +.empty { + text-align: center; + padding: 48px; + color: #9ca3af; +} + +/* Tab 切换 */ +.tabs { + display: flex; + gap: 4px; + padding: 4px; + background-color: #f3f4f6; + border-radius: 8px; + flex-wrap: wrap; +} + +.tab { + padding: 8px 16px; + background-color: transparent; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + color: #4b5563; + transition: all 0.2s ease; + + &:hover { + background-color: rgba(255, 255, 255, 0.5); + } + + &.active { + background-color: #fff; + color: #1f2937; + font-weight: 600; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + } +} + +/* 内容区域 */ +.content { + background-color: #fff; + border-radius: 8px; + padding: 24px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +/* 区块标题 */ +.section { + display: flex; + flex-direction: column; + gap: 20px; +} + +.sectionTitle { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #1f2937; +} + +.subTitle { + margin: 16px 0 8px; + font-size: 15px; + font-weight: 600; + color: #374151; +} + +/* 卡片网格 */ +.cardGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 16px; +} + +/* 账户卡片 */ +.accountCard { + background-color: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.cardHeader { + display: flex; + justify-content: space-between; + align-items: center; +} + +.accountLabel { + font-size: 15px; + font-weight: 600; + color: #1f2937; +} + +.accountSequence { + font-size: 12px; + color: #9ca3af; + font-family: monospace; +} + +.cardBody { + display: flex; + flex-direction: column; + gap: 8px; +} + +.statRow { + display: flex; + justify-content: space-between; + align-items: center; +} + +.statLabel { + font-size: 13px; + color: #6b7280; +} + +.statValue { + font-size: 14px; + font-weight: 500; + color: #1f2937; +} + +/* 汇总卡片 */ +.summaryCards { + display: flex; + gap: 16px; + flex-wrap: wrap; +} + +.summaryCard { + flex: 1; + min-width: 150px; + background-color: #f0f9ff; + border: 1px solid #bae6fd; + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.summaryLabel { + font-size: 13px; + color: #0369a1; +} + +.summaryValue { + font-size: 20px; + font-weight: 600; + color: #0c4a6e; +} + +/* 表格 */ +.tableWrapper { + overflow-x: auto; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + + th, + td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid #e5e7eb; + } + + th { + background-color: #f9fafb; + font-weight: 600; + color: #374151; + white-space: nowrap; + } + + td { + color: #4b5563; + } + + tbody tr:hover { + background-color: #f9fafb; + } +} + +.emptyTable { + text-align: center; + padding: 32px; + color: #9ca3af; + background-color: #f9fafb; + border-radius: 8px; +} + +/* 状态标签 */ +.statusBadge { + display: inline-block; + padding: 2px 8px; + font-size: 12px; + border-radius: 4px; + background-color: #e5e7eb; + color: #4b5563; + + &.active { + background-color: #dcfce7; + color: #166534; + } +} + +/* 页脚 */ +.footer { + text-align: right; + font-size: 12px; + color: #9ca3af; + padding-top: 16px; + border-top: 1px solid #e5e7eb; +} diff --git a/frontend/admin-web/src/components/features/system-account-report/SystemAccountsTab.tsx b/frontend/admin-web/src/components/features/system-account-report/SystemAccountsTab.tsx new file mode 100644 index 00000000..6168e925 --- /dev/null +++ b/frontend/admin-web/src/components/features/system-account-report/SystemAccountsTab.tsx @@ -0,0 +1,394 @@ +/** + * 系统账户报表Tab组件 + * [2026-01-04] 新增:显示系统账户统计数据 + * 回滚方式:删除此文件及整个 system-account-report 目录 + */ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { systemAccountReportService } from '@/services/systemAccountReportService'; +import type { + SystemAccountReportResponse, + RegionAccountsSummary, +} from '@/types'; +import styles from './SystemAccountsTab.module.scss'; + +/** + * 格式化金额显示 + */ +const formatAmount = (value: string | number | undefined): string => { + if (value === undefined || value === null) return '0.00'; + const num = typeof value === 'string' ? parseFloat(value) : value; + if (isNaN(num)) return '0.00'; + return num.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); +}; + +/** + * 权益类型显示名称 + */ +const getRightTypeName = (type: string): string => { + const labels: Record = { + SETTLEABLE: '可结算收益', + SHARE: '分享权益', + PROVINCE_TEAM: '省团队权益', + CITY_TEAM: '市团队权益', + PROVINCE_REGION: '省区域权益', + CITY_REGION: '市区域权益', + COMMUNITY: '社区权益', + }; + return labels[type] || type; +}; + +/** + * 系统账户报表Tab组件 + */ +export default function SystemAccountsTab() { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [reportData, setReportData] = useState(null); + const [activeTab, setActiveTab] = useState<'fixed' | 'province' | 'city' | 'settlement' | 'expired'>('fixed'); + + // 加载报表数据 + const loadReportData = useCallback(async () => { + setLoading(true); + setError(null); + try { + const response = await systemAccountReportService.getFullReport(); + if (response.data) { + setReportData(response.data); + } + } catch (err) { + setError('加载系统账户报表失败'); + console.error('Failed to load system account report:', err); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + loadReportData(); + }, [loadReportData]); + + if (loading) { + return ( +
+
+ 加载中... +
+ ); + } + + if (error) { + return ( +
+ {error} + +
+ ); + } + + if (!reportData) { + return
暂无数据
; + } + + return ( +
+ {/* Tab 切换 */} +
+ + + + + +
+ + {/* 内容区域 */} +
+ {activeTab === 'fixed' && } + {activeTab === 'province' && } + {activeTab === 'city' && } + {activeTab === 'settlement' && } + {activeTab === 'expired' && } +
+ + {/* 报表生成时间 */} +
+ 报表生成时间: {new Date(reportData.generatedAt).toLocaleString('zh-CN')} +
+
+ ); +} + +/** + * 固定系统账户区域 + */ +function FixedAccountsSection({ data }: { data: SystemAccountReportResponse['fixedAccounts'] }) { + const accounts = [ + { key: 'costAccount', label: '成本账户', sequence: 'S0000000001', data: data.costAccount }, + { key: 'operationAccount', label: '运营账户', sequence: 'S0000000002', data: data.operationAccount }, + { key: 'hqCommunity', label: '总部社区', sequence: 'S0000000003', data: data.hqCommunity }, + { key: 'rwadPoolPending', label: 'RWAD待发放池', sequence: 'S0000000004', data: data.rwadPoolPending }, + { key: 'platformFee', label: '平台手续费', sequence: 'S0000000005', data: data.platformFee }, + ]; + + return ( +
+

固定系统账户

+
+ {accounts.map(({ key, label, sequence, data: accountData }) => ( +
+
+ {label} + {sequence} +
+
+
+ 账户余额 + + {accountData ? formatAmount(accountData.usdtBalance) : '0.00'} USDT + +
+
+ 累计收入 + + {accountData ? formatAmount(accountData.totalReceived) : '0.00'} USDT + +
+
+ 累计转出 + + {accountData ? formatAmount(accountData.totalTransferred) : '0.00'} USDT + +
+
+
+ ))} +
+
+ ); +} + +/** + * 区域账户汇总区域 + */ +function RegionAccountsSection({ data, type }: { data: RegionAccountsSummary; type: 'province' | 'city' }) { + const typeLabel = type === 'province' ? '省' : '市'; + + return ( +
+

{typeLabel}区域账户汇总

+ + {/* 汇总卡片 */} +
+
+ 账户数量 + {data.summary.count} +
+
+ 总余额 + {formatAmount(data.summary.totalBalance)} USDT +
+
+ 累计收入 + {formatAmount(data.summary.totalReceived)} USDT +
+
+ + {/* 账户列表 */} + {data.accounts.length > 0 ? ( +
+ + + + + + + + + + + + {data.accounts.map((account) => ( + + + + + + + + ))} + +
区域代码区域名称余额 (USDT)累计收入 (USDT)状态
{account.regionCode || '-'}{account.regionName || '-'}{formatAmount(account.usdtBalance)}{formatAmount(account.totalReceived)} + + {account.status === 'ACTIVE' ? '正常' : account.status} + +
+
+ ) : ( +
暂无{typeLabel}区域账户数据
+ )} +
+ ); +} + +/** + * 面对面结算统计区域 + */ +function OfflineSettlementSection({ data }: { data: SystemAccountReportResponse['offlineSettlement'] }) { + return ( +
+

面对面结算统计

+ + {/* 汇总卡片 */} +
+
+ 总笔数 + {data.totalCount} +
+
+ 总金额 + {formatAmount(data.totalAmount)} USDT +
+
+ + {/* 按月统计 */} + {data.byMonth.length > 0 && ( + <> +

按月统计

+
+ + + + + + + + + + {data.byMonth.map((item) => ( + + + + + + ))} + +
月份笔数金额 (USDT)
{item.month}{item.count}{formatAmount(item.amount)}
+
+ + )} + + {data.byMonth.length === 0 && ( +
暂无面对面结算数据
+ )} +
+ ); +} + +/** + * 过期收益统计区域 + */ +function ExpiredRewardsSection({ data }: { data: SystemAccountReportResponse['expiredRewards'] }) { + return ( +
+

过期收益统计

+ + {/* 汇总卡片 */} +
+
+ 总笔数 + {data.totalCount} +
+
+ 总金额 + {formatAmount(data.totalAmount)} USDT +
+
+ + {/* 按权益类型统计 */} + {data.byRightType.length > 0 && ( + <> +

按权益类型统计

+
+ + + + + + + + + + {data.byRightType.map((item) => ( + + + + + + ))} + +
权益类型笔数金额 (USDT)
{getRightTypeName(item.rightType)}{item.count}{formatAmount(item.amount)}
+
+ + )} + + {/* 按月统计 */} + {data.byMonth.length > 0 && ( + <> +

按月统计

+
+ + + + + + + + + + {data.byMonth.map((item) => ( + + + + + + ))} + +
月份笔数金额 (USDT)
{item.month}{item.count}{formatAmount(item.amount)}
+
+ + )} + + {data.byRightType.length === 0 && data.byMonth.length === 0 && ( +
暂无过期收益数据
+ )} +
+ ); +} diff --git a/frontend/admin-web/src/components/features/system-account-report/index.ts b/frontend/admin-web/src/components/features/system-account-report/index.ts new file mode 100644 index 00000000..19cb0ee8 --- /dev/null +++ b/frontend/admin-web/src/components/features/system-account-report/index.ts @@ -0,0 +1,6 @@ +/** + * 系统账户报表组件导出 + * [2026-01-04] 新增 + * 回滚方式:删除此文件 + */ +export { default as SystemAccountsTab } from './SystemAccountsTab'; diff --git a/frontend/admin-web/src/infrastructure/api/endpoints.ts b/frontend/admin-web/src/infrastructure/api/endpoints.ts index d58519c1..c528d766 100644 --- a/frontend/admin-web/src/infrastructure/api/endpoints.ts +++ b/frontend/admin-web/src/infrastructure/api/endpoints.ts @@ -204,4 +204,15 @@ export const API_ENDPOINTS = { START_PAYMENT: (orderNo: string) => `/v1/wallets/fiat-withdrawals/${orderNo}/start-payment`, COMPLETE_PAYMENT: (orderNo: string) => `/v1/wallets/fiat-withdrawals/${orderNo}/complete-payment`, }, + + // [2026-01-04] 新增:系统账户报表 (reporting-service) + // 回滚方式:删除此部分即可 + SYSTEM_ACCOUNT_REPORTS: { + FULL_REPORT: '/v1/system-account-reports', + FIXED_ACCOUNTS: '/v1/system-account-reports/fixed-accounts', + PROVINCE_SUMMARY: '/v1/system-account-reports/province-summary', + CITY_SUMMARY: '/v1/system-account-reports/city-summary', + OFFLINE_SETTLEMENT: '/v1/system-account-reports/offline-settlement', + EXPIRED_REWARDS: '/v1/system-account-reports/expired-rewards', + }, } as const; diff --git a/frontend/admin-web/src/services/systemAccountReportService.ts b/frontend/admin-web/src/services/systemAccountReportService.ts new file mode 100644 index 00000000..1dd99ce0 --- /dev/null +++ b/frontend/admin-web/src/services/systemAccountReportService.ts @@ -0,0 +1,73 @@ +/** + * 系统账户报表服务 + * [2026-01-04] 新增:负责系统账户报表数据的API调用 + * 回滚方式:删除此文件 + */ + +import apiClient from '@/infrastructure/api/client'; +import { API_ENDPOINTS } from '@/infrastructure/api/endpoints'; +import type { + ApiResponse, + SystemAccountReportResponse, + SystemAccountWithBalance, + RegionAccountsSummary, + OfflineSettlementSummary, + ExpiredRewardsSummary, +} from '@/types'; + +/** + * 查询参数 + */ +interface DateRangeParams { + startDate?: string; + endDate?: string; +} + +/** + * 系统账户报表服务 + */ +export const systemAccountReportService = { + /** + * 获取完整系统账户报表 + */ + async getFullReport(params?: DateRangeParams): Promise> { + return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.FULL_REPORT, { params }); + }, + + /** + * 获取固定系统账户列表(带余额) + */ + async getFixedAccounts(): Promise> { + return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.FIXED_ACCOUNTS); + }, + + /** + * 获取省区域账户汇总 + */ + async getProvinceSummary(): Promise> { + return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.PROVINCE_SUMMARY); + }, + + /** + * 获取市区域账户汇总 + */ + async getCitySummary(): Promise> { + return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.CITY_SUMMARY); + }, + + /** + * 获取面对面结算统计 + */ + async getOfflineSettlement(params?: DateRangeParams): Promise> { + return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.OFFLINE_SETTLEMENT, { params }); + }, + + /** + * 获取过期收益统计 + */ + async getExpiredRewards(params?: DateRangeParams): Promise> { + return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.EXPIRED_REWARDS, { params }); + }, +}; + +export default systemAccountReportService; diff --git a/frontend/admin-web/src/types/index.ts b/frontend/admin-web/src/types/index.ts index b478c233..853f06de 100644 --- a/frontend/admin-web/src/types/index.ts +++ b/frontend/admin-web/src/types/index.ts @@ -9,3 +9,5 @@ export * from './dashboard.types'; export * from './pending-action.types'; export * from './withdrawal.types'; export * from './authorization.types'; +// [2026-01-04] 新增:系统账户报表类型 +export * from './system-account.types'; diff --git a/frontend/admin-web/src/types/system-account.types.ts b/frontend/admin-web/src/types/system-account.types.ts new file mode 100644 index 00000000..26c26048 --- /dev/null +++ b/frontend/admin-web/src/types/system-account.types.ts @@ -0,0 +1,122 @@ +/** + * 系统账户报表类型定义 + * [2026-01-04] 新增:用于系统账户报表显示 + * 回滚方式:删除此文件并从 index.ts 中移除导出 + */ + +/** + * 系统账户 DTO + */ +export interface SystemAccountDTO { + id: string; + accountType: string; + regionCode: string | null; + regionName: string | null; + walletAddress: string | null; + usdtBalance: string; + hashpower: string; + totalReceived: string; + totalTransferred: string; + status: string; + createdAt: string; + updatedAt: string; +} + +/** + * 带钱包余额的系统账户 + */ +export interface SystemAccountWithBalance extends SystemAccountDTO { + walletBalance?: number; +} + +/** + * 区域账户汇总 + */ +export interface RegionAccountsSummary { + accounts: SystemAccountDTO[]; + summary: { + totalBalance: string; + totalReceived: string; + count: number; + }; +} + +/** + * 面对面结算统计 + */ +export interface OfflineSettlementSummary { + totalAmount: number; + totalCount: number; + byMonth: Array<{ + month: string; + amount: number; + count: number; + }>; +} + +/** + * 过期收益统计 + */ +export interface ExpiredRewardsSummary { + totalAmount: number; + totalCount: number; + byMonth: Array<{ + month: string; + amount: number; + count: number; + }>; + byRightType: Array<{ + rightType: string; + amount: number; + count: number; + }>; +} + +/** + * 固定系统账户 + */ +export interface FixedAccounts { + costAccount: SystemAccountWithBalance | null; // 成本账户 S0000000001 + operationAccount: SystemAccountWithBalance | null; // 运营账户 S0000000002 + hqCommunity: SystemAccountWithBalance | null; // 总部社区 S0000000003 + rwadPoolPending: SystemAccountWithBalance | null; // RWAD待发放池 S0000000004 + platformFee: SystemAccountWithBalance | null; // 平台手续费 S0000000005 +} + +/** + * 完整系统账户报表响应 + */ +export interface SystemAccountReportResponse { + fixedAccounts: FixedAccounts; + provinceSummary: RegionAccountsSummary; + citySummary: RegionAccountsSummary; + offlineSettlement: OfflineSettlementSummary; + expiredRewards: ExpiredRewardsSummary; + generatedAt: string; +} + +/** + * 账户类型显示名称映射 + */ +export const ACCOUNT_TYPE_LABELS: Record = { + COST_ACCOUNT: '成本账户', + OPERATION_ACCOUNT: '运营账户', + HQ_COMMUNITY: '总部社区', + RWAD_POOL_PENDING: 'RWAD待发放池', + PLATFORM_FEE: '平台手续费', + SYSTEM_PROVINCE: '系统省账户', + SYSTEM_CITY: '系统市账户', +}; + +/** + * 权益类型显示名称映射 + */ +export const RIGHT_TYPE_LABELS: Record = { + SETTLEABLE: '可结算收益', + SHARE: '分享权益', + PROVINCE_TEAM: '省团队权益', + CITY_TEAM: '市团队权益', + PROVINCE_REGION: '省区域权益', + CITY_REGION: '市区域权益', + COMMUNITY: '社区权益', +};