From 4b5270f130d1862e1edaedaae26fb0799963cbc6 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 6 Jan 2026 09:19:15 -0800 Subject: [PATCH] =?UTF-8?q?feat(admin-web):=20=E6=B7=BB=E5=8A=A0=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E8=B4=A6=E6=88=B7=E6=94=B6=E7=9B=8A=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E8=AF=A6=E7=BB=86=E6=98=8E=E7=BB=86=E5=88=97=E8=A1=A8=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为系统账户报表中的5个收益类型汇总Tab添加详细明细查看功能: - 手续费账户汇总:点击"查看详细明细"展开手续费记录列表 - 省团队收益汇总:展开省团队权益记录列表 - 市团队收益汇总:展开市团队权益记录列表 - 分享引荐收益汇总:展开分享权益记录列表 - 社区收益汇总:展开社区权益记录列表 后端变更: - reward-service: 添加 getRewardEntriesByType、getFeeEntriesDetailed 方法 - reward-service: 添加 /statistics/reward-entries-by-type、/statistics/fee-entries-detailed 接口 - reporting-service: 添加对应的聚合接口 前端变更: - 添加 RewardEntryDTO、RewardEntriesResponse 类型定义 - 添加 getRewardEntriesByType、getFeeEntriesDetailed API 方法 - FeeAccountSection、RewardTypeSummarySection 组件添加详细明细列表展开功能 - 添加分页支持(每页20条) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/settings.local.json | 6 +- .../system-account-report.controller.ts | 38 +++ ...stem-account-report-application.service.ts | 26 ++- .../reward-service/reward-service.client.ts | 90 ++++++++ .../api/controllers/internal.controller.ts | 49 ++++ .../services/reward-application.service.ts | 153 ++++++++++++ .../SystemAccountsTab.module.scss | 78 +++++++ .../SystemAccountsTab.tsx | 217 +++++++++++++++++- .../src/infrastructure/api/endpoints.ts | 3 + .../services/systemAccountReportService.ts | 23 ++ .../src/types/system-account.types.ts | 51 ++++ 11 files changed, 731 insertions(+), 3 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 7bd0cfb9..0330df20 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -597,7 +597,11 @@ "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(reward-service\\): 修复 WalletServiceClient 未正确解析 wallet-service 响应格式的 Bug\n\n问题原因:\nwallet-service 使用全局 TransformInterceptor 拦截器,会将所有响应包装成:\n{ success: true, data: { success: boolean, ... }, timestamp: \"...\" }\n\n原代码直接读取外层的 success 字段(始终为 true),导致即使业务失败\n(内层 data.success = false)也被误判为成功。\n\n具体案例:\n用户 D25122700024 点击结算时,wallet-service 因余额不足返回:\n{ success: true, data: { success: false, error: \"Insufficient...\" }, ... }\nreward-service 误读为成功,导致奖励被标记为 SETTLED 但钱包余额未变更。\n\n修复内容:\n1. settleToBalance: 解析 response_data.data 获取真实业务结果\n2. confirmPlantingDeduction: 同上\n3. allocateFunds: 同上\n\n所有方法现在会:\n- 使用 response_data.data || response_data 兼容包装和非包装格式\n- 严格检查 data.success !== true 来判断业务是否成功\n- 失败时记录详细错误日志\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service\\): 统一奖励分配到 settleable_usdt,与 reward-service 保持一致\n\n问题原因:\nwallet-service 对不同类型奖励的分配方式不一致:\n- SHARE_RIGHT: 正确使用 addSettleableReward\\(\\) → settleable_usdt\n- CITY_TEAM_RIGHT/COMMUNITY_RIGHT: 错误使用 addAvailableBalance\\(\\) → usdt_available\n\n这导致 reward-service 记录的 SETTLEABLE 奖励总额与 wallet-service 的\nsettleable_usdt 字段不匹配。用户 D25122700024 的案例中:\n- reward-service: 3条奖励共 4464 USDT \\(SHARE_RIGHT 3600 + CITY_TEAM_RIGHT 288 + COMMUNITY_RIGHT 576\\)\n- wallet-service: settleable_usdt = 3600 \\(仅 SHARE_RIGHT\\)\n差额 864 USDT 被错误地放入了 usdt_available\n\n修复内容:\n1. allocateCommunityRight: 改用 addSettleableReward\\(\\) 替代 addAvailableBalance\\(\\)\n2. allocateToRegionAccount: 改用 addSettleableReward\\(\\) 替代 addAvailableBalance\\(\\)\n3. 流水类型统一使用 REWARD_TO_SETTLEABLE 替代 SYSTEM_ALLOCATION\n4. 日志和备注更新以反映新的分配方式\n\n设计原则:\n- reward-service 是奖励的权威来源\n- wallet-service 应跟随 reward-service 的设计\n- 所有奖励都应进入 settleable_usdt,用户主动结算后才转入 usdt_available\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", "Bash(ls \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\reward-service\\\\prisma\"\" 2>/dev/null || dir \"c:UsersdongDesktoprwadurianbackendservicesreward-serviceprisma\"\")", - "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service\\): 修复 settleToBalance 方法缺少事务保护的严重 Bug\n\n问题原因:\nsettleToBalance 方法先执行 wallet.save\\(\\) 更新账户余额,再执行\nledgerRepo.save\\(\\) 写入流水记录。两个操作不在同一个事务中。\n\n当流水写入失败时(如 memo 字段超过 VarChar\\(500\\) 限制),账户余额\n已经被修改,但流水记录未写入,导致数据不一致。\n\n具体案例:\n用户 D25122700023 点击结算时,memo 内容超长(66笔奖励详情),\nwallet-service 先把 settleable_usdt 转入 usdt_available,然后\n写流水失败。账户余额被改但没有对应流水。\n\n修复内容:\n1. settleToBalance: 使用 prisma.$transaction 确保原子性\n - 账户余额更新和流水记录在同一事务中\n - 任一操作失败整个事务回滚\n2. schema: memo 字段从 VarChar\\(500\\) 改为 Text 类型,无长度限制\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")" + "Bash(git commit -m \"$\\(cat <<''EOF''\nfix\\(wallet-service\\): 修复 settleToBalance 方法缺少事务保护的严重 Bug\n\n问题原因:\nsettleToBalance 方法先执行 wallet.save\\(\\) 更新账户余额,再执行\nledgerRepo.save\\(\\) 写入流水记录。两个操作不在同一个事务中。\n\n当流水写入失败时(如 memo 字段超过 VarChar\\(500\\) 限制),账户余额\n已经被修改,但流水记录未写入,导致数据不一致。\n\n具体案例:\n用户 D25122700023 点击结算时,memo 内容超长(66笔奖励详情),\nwallet-service 先把 settleable_usdt 转入 usdt_available,然后\n写流水失败。账户余额被改但没有对应流水。\n\n修复内容:\n1. settleToBalance: 使用 prisma.$transaction 确保原子性\n - 账户余额更新和流水记录在同一事务中\n - 任一操作失败整个事务回滚\n2. schema: memo 字段从 VarChar\\(500\\) 改为 Text 类型,无长度限制\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(wallet-service\\): 实现 Unit of Work 模式保证 settleToBalance 事务原子性\n\n- 新增 UnitOfWork 接口和实现,使用 Prisma Interactive Transaction\n- 修改 IWalletAccountRepository 和 ILedgerEntryRepository 接口支持可选事务参数\n- 修改仓库实现,支持在事务中执行数据库操作\n- 修改 settleToBalance 方法使用 UnitOfWork,确保钱包更新和流水记录原子性\n- 注册 UnitOfWorkService 到 InfrastructureModule\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(ls -la \"c:\\\\Users\\\\dong\\\\Desktop\\\\rwadurian\\\\backend\\\\services\\\\wallet-service\\\\prisma\\\\migrations\"\" 2>/dev/null || dir \"c:UsersdongDesktoprwadurianbackendserviceswallet-serviceprismamigrations \")", + "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(admin-web\\): 添加系统账户收益类型汇总统计功能\n\n在数据统计-系统账户中新增5个统计Tab:\n- 手续费账户汇总:统计成本费、运营费、总部社区基础费、RWAD底池注入\n- 省团队收益汇总:统计省团队权益收益\n- 市团队收益汇总:统计市团队权益收益\n- 分享引荐收益汇总:统计分享权益收益\n- 社区收益汇总:统计社区权益收益\n\n后端变更:\n- reward-service: 添加 getRewardsSummaryByType、getAllRewardTypeSummaries 方法\n- reporting-service: 聚合收益类型汇总统计接口\n\n前端变更:\n- 添加 RewardTypeSummary、FeeAccountSummary 类型定义\n- 添加 getRewardTypeSummaries API 方法\n- 添加 FeeAccountSection、RewardTypeSummarySection 组件\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(git commit -m \"$\\(cat <<''EOF''\nfeat\\(wallet-service\\): 实现手续费归集账户功能\n\n- 新增系统账户 S0000000006 \\(user_id=-6\\) 用于归集提现手续费\n- 新增 FEE_COLLECTION 流水类型记录手续费归集\n- 区块链提现完成时使用 UnitOfWork 事务归集手续费\n- 法币提现完成时在事务中归集手续费\n- WithdrawalOrderRepository 添加事务支持\n- 所有手续费归集操作使用乐观锁保护\n\n🤖 Generated with [Claude Code]\\(https://claude.com/claude-code\\)\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")" ], "deny": [], "ask": [] 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 acce81d3..958207ef 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 @@ -109,4 +109,42 @@ export class SystemAccountReportController { this.logger.log(`[getAllRewardTypeSummaries] 请求所有收益类型汇总统计`); return this.systemAccountReportService.getAllRewardTypeSummaries(); } + + // [2026-01-06] 新增:收益类型详细记录列表接口 + // 用于系统账户报表中展示各收益类型的明细列表 + // 回滚方式:删除以下 API 方法即可 + @Get('reward-entries-by-type') + @ApiOperation({ summary: '获取指定收益类型的详细记录列表' }) + @ApiQuery({ name: 'rightType', required: true, description: '收益类型' }) + @ApiQuery({ name: 'page', required: false, description: '页码,默认1' }) + @ApiQuery({ name: 'pageSize', required: false, description: '每页条数,默认50' }) + @ApiResponse({ status: 200, description: '收益类型详细记录列表' }) + async getRewardEntriesByType( + @Query('rightType') rightType: string, + @Query('page') page?: string, + @Query('pageSize') pageSize?: string, + ) { + this.logger.log(`[getRewardEntriesByType] 请求收益类型详细记录, rightType=${rightType}, page=${page}, pageSize=${pageSize}`); + return this.systemAccountReportService.getRewardEntriesByType({ + rightType, + page: page ? parseInt(page, 10) : undefined, + pageSize: pageSize ? parseInt(pageSize, 10) : undefined, + }); + } + + @Get('fee-entries-detailed') + @ApiOperation({ summary: '获取手续费类型的详细记录列表' }) + @ApiQuery({ name: 'page', required: false, description: '页码,默认1' }) + @ApiQuery({ name: 'pageSize', required: false, description: '每页条数,默认50' }) + @ApiResponse({ status: 200, description: '手续费类型详细记录列表' }) + async getFeeEntriesDetailed( + @Query('page') page?: string, + @Query('pageSize') pageSize?: string, + ) { + this.logger.log(`[getFeeEntriesDetailed] 请求手续费类型详细记录, page=${page}, pageSize=${pageSize}`); + return this.systemAccountReportService.getFeeEntriesDetailed({ + page: page ? parseInt(page, 10) : undefined, + pageSize: pageSize ? parseInt(pageSize, 10) : undefined, + }); + } } 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 6eaafcd5..cae1ea67 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 @@ -7,7 +7,7 @@ */ import { Injectable, Logger } from '@nestjs/common'; import { WalletServiceClient, OfflineSettlementSummary, AllSystemAccountsResponse, AllSystemAccountsLedgerResponse } from '../../infrastructure/external/wallet-service/wallet-service.client'; -import { RewardServiceClient, ExpiredRewardsSummary, AllRewardTypeSummaries } from '../../infrastructure/external/reward-service/reward-service.client'; +import { RewardServiceClient, ExpiredRewardsSummary, AllRewardTypeSummaries, RewardEntriesResponse } from '../../infrastructure/external/reward-service/reward-service.client'; /** * 固定系统账户信息 @@ -213,6 +213,30 @@ export class SystemAccountReportApplicationService { return result; } + // [2026-01-06] 新增:获取收益类型详细记录列表 + /** + * 获取指定收益类型的详细记录列表 + */ + async getRewardEntriesByType(params: { + rightType: string; + page?: number; + pageSize?: number; + }): Promise { + this.logger.log(`[getRewardEntriesByType] 获取收益类型详细记录: ${params.rightType}`); + return this.rewardServiceClient.getRewardEntriesByType(params); + } + + /** + * 获取手续费类型的详细记录列表 + */ + async getFeeEntriesDetailed(params?: { + page?: number; + pageSize?: number; + }): Promise { + this.logger.log('[getFeeEntriesDetailed] 获取手续费类型详细记录'); + return this.rewardServiceClient.getFeeEntriesDetailed(params); + } + /** * 组装固定账户数据 */ 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 index f3abe2ff..831e5aea 100644 --- 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 @@ -56,6 +56,29 @@ export interface AllRewardTypeSummaries { communitySummary: RewardTypeSummary; } +// [2026-01-06] 新增:收益记录条目 +export interface RewardEntryDTO { + id: string; + accountSequence: string; + sourceOrderId: string; + rightType: string; + rewardStatus: string; + usdtAmount: number; + hashpowerAmount: number; + createdAt: string; + claimedAt: string | null; + expiredAt: string | null; +} + +// [2026-01-06] 新增:收益记录列表响应 +export interface RewardEntriesResponse { + entries: RewardEntryDTO[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + @Injectable() export class RewardServiceClient { private readonly logger = new Logger(RewardServiceClient.name); @@ -141,4 +164,71 @@ export class RewardServiceClient { }; } } + + // [2026-01-06] 新增:获取收益类型详细记录列表 + /** + * 获取指定收益类型的详细记录列表 + */ + async getRewardEntriesByType(params: { + rightType: string; + page?: number; + pageSize?: number; + }): Promise { + try { + const queryParams = new URLSearchParams(); + queryParams.append('rightType', params.rightType); + if (params.page) queryParams.append('page', params.page.toString()); + if (params.pageSize) queryParams.append('pageSize', params.pageSize.toString()); + + const url = `${this.baseUrl}/api/v1/internal/statistics/reward-entries-by-type?${queryParams.toString()}`; + this.logger.debug(`[getRewardEntriesByType] 请求: ${url}`); + + const response = await firstValueFrom( + this.httpService.get(url), + ); + + return response.data; + } catch (error) { + this.logger.error(`[getRewardEntriesByType] 失败: ${error.message}`); + return { + entries: [], + total: 0, + page: 1, + pageSize: params.pageSize ?? 50, + totalPages: 0, + }; + } + } + + /** + * 获取手续费类型的详细记录列表 + */ + async getFeeEntriesDetailed(params?: { + page?: number; + pageSize?: number; + }): Promise { + try { + const queryParams = new URLSearchParams(); + if (params?.page) queryParams.append('page', params.page.toString()); + if (params?.pageSize) queryParams.append('pageSize', params.pageSize.toString()); + + const url = `${this.baseUrl}/api/v1/internal/statistics/fee-entries-detailed?${queryParams.toString()}`; + this.logger.debug(`[getFeeEntriesDetailed] 请求: ${url}`); + + const response = await firstValueFrom( + this.httpService.get(url), + ); + + return response.data; + } catch (error) { + this.logger.error(`[getFeeEntriesDetailed] 失败: ${error.message}`); + return { + entries: [], + total: 0, + page: 1, + pageSize: params?.pageSize ?? 50, + totalPages: 0, + }; + } + } } 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 5837cc95..87d5ce89 100644 --- a/backend/services/reward-service/src/api/controllers/internal.controller.ts +++ b/backend/services/reward-service/src/api/controllers/internal.controller.ts @@ -118,4 +118,53 @@ export class InternalController { return result; } + + // [2026-01-06] 新增:收益类型详细记录列表接口 + // 用于系统账户报表中展示各收益类型的明细列表 + // 回滚方式:删除以下 API 方法即可 + + @Get('statistics/reward-entries-by-type') + @ApiOperation({ summary: '获取指定收益类型的详细记录列表(内部接口)- 用于系统账户报表' }) + @ApiQuery({ name: 'rightType', required: true, description: '收益类型' }) + @ApiQuery({ name: 'page', required: false, description: '页码,默认1' }) + @ApiQuery({ name: 'pageSize', required: false, description: '每页条数,默认50' }) + @ApiResponse({ status: 200, description: '收益类型详细记录列表' }) + async getRewardEntriesByType( + @Query('rightType') rightType: string, + @Query('page') page?: string, + @Query('pageSize') pageSize?: string, + ) { + this.logger.log(`========== statistics/reward-entries-by-type 请求 ==========`); + this.logger.log(`rightType=${rightType}, page=${page}, pageSize=${pageSize}`); + + const result = await this.rewardService.getRewardEntriesByType({ + rightType, + page: page ? parseInt(page, 10) : undefined, + pageSize: pageSize ? parseInt(pageSize, 10) : undefined, + }); + + this.logger.log(`收益类型详细记录查询结果: total=${result.total}, page=${result.page}, pageSize=${result.pageSize}`); + return result; + } + + @Get('statistics/fee-entries-detailed') + @ApiOperation({ summary: '获取手续费类型的详细记录列表(内部接口)- 用于系统账户报表' }) + @ApiQuery({ name: 'page', required: false, description: '页码,默认1' }) + @ApiQuery({ name: 'pageSize', required: false, description: '每页条数,默认50' }) + @ApiResponse({ status: 200, description: '手续费类型详细记录列表' }) + async getFeeEntriesDetailed( + @Query('page') page?: string, + @Query('pageSize') pageSize?: string, + ) { + this.logger.log(`========== statistics/fee-entries-detailed 请求 ==========`); + this.logger.log(`page=${page}, pageSize=${pageSize}`); + + const result = await this.rewardService.getFeeEntriesDetailed({ + page: page ? parseInt(page, 10) : undefined, + pageSize: pageSize ? parseInt(pageSize, 10) : undefined, + }); + + this.logger.log(`手续费详细记录查询结果: total=${result.total}, page=${result.page}, pageSize=${result.pageSize}`); + 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 8ea9a594..7947362e 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 @@ -1228,4 +1228,157 @@ export class RewardApplicationService { })), }; } + + // [2026-01-06] 新增:获取收益类型的详细记录列表 + // 用于系统账户报表中展示各收益类型的明细 + // 回滚方式:删除以下方法即可 + /** + * 获取指定收益类型的详细记录列表 + * @param rightType 权益类型 + * @param page 页码 + * @param pageSize 每页条数 + */ + async getRewardEntriesByType(params: { + rightType: string; + page?: number; + pageSize?: number; + }): Promise<{ + entries: Array<{ + id: string; + accountSequence: string; + sourceOrderId: string; + rightType: string; + rewardStatus: string; + usdtAmount: number; + hashpowerAmount: number; + createdAt: string; + claimedAt: string | null; + expiredAt: string | null; + }>; + total: number; + page: number; + pageSize: number; + totalPages: number; + }> { + const page = params.page ?? 1; + const pageSize = params.pageSize ?? 50; + const skip = (page - 1) * pageSize; + + this.logger.log(`[getRewardEntriesByType] 查询权益类型详细记录: ${params.rightType}, page=${page}, pageSize=${pageSize}`); + + // 查询总数 + const total = await this.prisma.rewardLedgerEntry.count({ + where: { + rightType: params.rightType, + rewardStatus: { in: ['SETTLEABLE', 'SETTLED'] }, + }, + }); + + // 查询数据 + const entries = await this.prisma.rewardLedgerEntry.findMany({ + where: { + rightType: params.rightType, + rewardStatus: { in: ['SETTLEABLE', 'SETTLED'] }, + }, + orderBy: { createdAt: 'desc' }, + skip, + take: pageSize, + }); + + return { + entries: entries.map(entry => ({ + id: entry.id.toString(), + accountSequence: entry.accountSequence, + sourceOrderId: entry.sourceOrderId, + rightType: entry.rightType, + rewardStatus: entry.rewardStatus, + usdtAmount: Number(entry.usdtAmount), + hashpowerAmount: Number(entry.hashpowerAmount), + createdAt: entry.createdAt.toISOString(), + claimedAt: entry.claimedAt?.toISOString() ?? null, + expiredAt: entry.expiredAt?.toISOString() ?? null, + })), + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize), + }; + } + + /** + * 获取手续费类型的详细记录列表 + * @param page 页码 + * @param pageSize 每页条数 + */ + async getFeeEntriesDetailed(params: { + page?: number; + pageSize?: number; + }): Promise<{ + entries: Array<{ + id: string; + accountSequence: string; + sourceOrderId: string; + rightType: string; + rewardStatus: string; + usdtAmount: number; + hashpowerAmount: number; + createdAt: string; + claimedAt: string | null; + expiredAt: string | null; + }>; + total: number; + page: number; + pageSize: number; + totalPages: number; + }> { + const page = params.page ?? 1; + const pageSize = params.pageSize ?? 50; + const skip = (page - 1) * pageSize; + const feeTypes = [ + RightType.COST_FEE, + RightType.OPERATION_FEE, + RightType.HEADQUARTERS_BASE_FEE, + RightType.RWAD_POOL_INJECTION, + ]; + + this.logger.log(`[getFeeEntriesDetailed] 查询手续费类型详细记录, page=${page}, pageSize=${pageSize}`); + + // 查询总数 + const total = await this.prisma.rewardLedgerEntry.count({ + where: { + rightType: { in: feeTypes }, + rewardStatus: { in: ['SETTLEABLE', 'SETTLED'] }, + }, + }); + + // 查询数据 + const entries = await this.prisma.rewardLedgerEntry.findMany({ + where: { + rightType: { in: feeTypes }, + rewardStatus: { in: ['SETTLEABLE', 'SETTLED'] }, + }, + orderBy: { createdAt: 'desc' }, + skip, + take: pageSize, + }); + + return { + entries: entries.map(entry => ({ + id: entry.id.toString(), + accountSequence: entry.accountSequence, + sourceOrderId: entry.sourceOrderId, + rightType: entry.rightType, + rewardStatus: entry.rewardStatus, + usdtAmount: Number(entry.usdtAmount), + hashpowerAmount: Number(entry.hashpowerAmount), + createdAt: entry.createdAt.toISOString(), + claimedAt: entry.claimedAt?.toISOString() ?? null, + expiredAt: entry.expiredAt?.toISOString() ?? null, + })), + total, + page, + pageSize, + totalPages: Math.ceil(total / pageSize), + }; + } } 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 index 9f1ec2fd..b2c78544 100644 --- 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 @@ -408,3 +408,81 @@ font-size: 13px; color: #6b7280; } + +/* [2026-01-06] 新增:详细明细列表样式 */ +.detailsSection { + margin-top: 24px; + border-top: 1px solid #e5e7eb; + padding-top: 16px; +} + +.toggleDetailsButton { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + background-color: #f3f4f6; + color: #374151; + border: 1px solid #d1d5db; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s ease; + + &:hover { + background-color: #e5e7eb; + } +} + +.detailsContent { + margin-top: 16px; +} + +.orderId { + font-family: monospace; + font-size: 12px; + color: #6b7280; +} + +.statusBadge { + &.settled { + background-color: #dcfce7; + color: #166534; + } +} + +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 16px; + margin-top: 16px; + padding: 12px; + background-color: #f9fafb; + border-radius: 8px; +} + +.pageButton { + padding: 6px 14px; + background-color: #fff; + color: #374151; + border: 1px solid #d1d5db; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + transition: all 0.2s ease; + + &:hover:not(:disabled) { + background-color: #f3f4f6; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.pageInfo { + font-size: 13px; + color: #6b7280; +} 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 index e755035a..efbc5f92 100644 --- a/frontend/admin-web/src/components/features/system-account-report/SystemAccountsTab.tsx +++ b/frontend/admin-web/src/components/features/system-account-report/SystemAccountsTab.tsx @@ -19,8 +19,10 @@ import type { AllRewardTypeSummariesResponse, RewardTypeSummary, FeeAccountSummary, + RewardEntriesResponse, + RewardEntryDTO, } from '@/types'; -import { ENTRY_TYPE_LABELS, ACCOUNT_TYPE_LABELS, FEE_TYPE_LABELS } from '@/types'; +import { ENTRY_TYPE_LABELS, ACCOUNT_TYPE_LABELS, FEE_TYPE_LABELS, REWARD_RIGHT_TYPE_LABELS, REWARD_STATUS_LABELS } from '@/types'; import styles from './SystemAccountsTab.module.scss'; /** @@ -788,6 +790,33 @@ function FeeAccountSection({ loading: boolean; onRefresh: () => void; }) { + const [showDetails, setShowDetails] = useState(false); + const [detailsLoading, setDetailsLoading] = useState(false); + const [detailsData, setDetailsData] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + + const loadDetails = useCallback(async (page: number = 1) => { + setDetailsLoading(true); + try { + const response = await systemAccountReportService.getFeeEntriesDetailed({ page, pageSize: 20 }); + if (response.data) { + setDetailsData(response.data); + setCurrentPage(page); + } + } catch (err) { + console.error('Failed to load fee entries details:', err); + } finally { + setDetailsLoading(false); + } + }, []); + + const handleToggleDetails = () => { + if (!showDetails && !detailsData) { + loadDetails(1); + } + setShowDetails(!showDetails); + }; + if (loading) { return (
@@ -859,11 +888,92 @@ function FeeAccountSection({ ) : (
暂无手续费数据
)} + + {/* 详细明细列表 */} +
+ + + {showDetails && ( +
+ {detailsLoading ? ( +
+
+ 加载详细明细中... +
+ ) : detailsData && detailsData.entries.length > 0 ? ( + <> +
+ + + + + + + + + + + + + {detailsData.entries.map((entry) => ( + + + + + + + + + ))} + +
时间账户订单号类型金额 (绿积分)状态
{new Date(entry.createdAt).toLocaleString('zh-CN')}{entry.accountSequence}{entry.sourceOrderId}{REWARD_RIGHT_TYPE_LABELS[entry.rightType] || entry.rightType}{formatAmount(entry.usdtAmount)} + + {REWARD_STATUS_LABELS[entry.rewardStatus] || entry.rewardStatus} + +
+
+ {/* 分页 */} +
+ + + 第 {currentPage} / {detailsData.totalPages} 页 (共 {detailsData.total} 条) + + +
+ + ) : ( +
暂无详细明细数据
+ )} +
+ )} +
); } // [2026-01-06] 新增:收益类型汇总组件 +// 收益类型映射到 API 使用的 rightType 值 +const REWARD_TYPE_MAP: Record = { + '省团队收益汇总': 'PROVINCE_TEAM_RIGHT', + '市团队收益汇总': 'CITY_TEAM_RIGHT', + '分享引荐收益汇总': 'SHARE_RIGHT', + '社区收益汇总': 'COMMUNITY_RIGHT', +}; + /** * 收益类型汇总区域(省团队、市团队、分享引荐、社区) */ @@ -878,6 +988,40 @@ function RewardTypeSummarySection({ loading: boolean; onRefresh: () => void; }) { + const [showDetails, setShowDetails] = useState(false); + const [detailsLoading, setDetailsLoading] = useState(false); + const [detailsData, setDetailsData] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + + const rightType = REWARD_TYPE_MAP[title] || ''; + + const loadDetails = useCallback(async (page: number = 1) => { + if (!rightType) return; + setDetailsLoading(true); + try { + const response = await systemAccountReportService.getRewardEntriesByType({ + rightType, + page, + pageSize: 20, + }); + if (response.data) { + setDetailsData(response.data); + setCurrentPage(page); + } + } catch (err) { + console.error('Failed to load reward entries details:', err); + } finally { + setDetailsLoading(false); + } + }, [rightType]); + + const handleToggleDetails = () => { + if (!showDetails && !detailsData && rightType) { + loadDetails(1); + } + setShowDetails(!showDetails); + }; + if (loading) { return (
@@ -957,6 +1101,77 @@ function RewardTypeSummarySection({ ) : (
暂无月度数据
)} + + {/* 详细明细列表 */} +
+ + + {showDetails && ( +
+ {detailsLoading ? ( +
+
+ 加载详细明细中... +
+ ) : detailsData && detailsData.entries.length > 0 ? ( + <> +
+ + + + + + + + + + + + {detailsData.entries.map((entry) => ( + + + + + + + + ))} + +
时间账户订单号金额 (绿积分)状态
{new Date(entry.createdAt).toLocaleString('zh-CN')}{entry.accountSequence}{entry.sourceOrderId}{formatAmount(entry.usdtAmount)} + + {REWARD_STATUS_LABELS[entry.rewardStatus] || entry.rewardStatus} + +
+
+ {/* 分页 */} +
+ + + 第 {currentPage} / {detailsData.totalPages} 页 (共 {detailsData.total} 条) + + +
+ + ) : ( +
暂无详细明细数据
+ )} +
+ )} +
); } diff --git a/frontend/admin-web/src/infrastructure/api/endpoints.ts b/frontend/admin-web/src/infrastructure/api/endpoints.ts index c24293f6..4fc7d319 100644 --- a/frontend/admin-web/src/infrastructure/api/endpoints.ts +++ b/frontend/admin-web/src/infrastructure/api/endpoints.ts @@ -218,5 +218,8 @@ export const API_ENDPOINTS = { ALL_LEDGER: '/v1/system-account-reports/all-ledger', // [2026-01-06] 新增:收益类型汇总统计 REWARD_TYPE_SUMMARIES: '/v1/system-account-reports/reward-type-summaries', + // [2026-01-06] 新增:收益类型详细记录列表 + REWARD_ENTRIES_BY_TYPE: '/v1/system-account-reports/reward-entries-by-type', + FEE_ENTRIES_DETAILED: '/v1/system-account-reports/fee-entries-detailed', }, } as const; diff --git a/frontend/admin-web/src/services/systemAccountReportService.ts b/frontend/admin-web/src/services/systemAccountReportService.ts index c4ff5d66..37b46185 100644 --- a/frontend/admin-web/src/services/systemAccountReportService.ts +++ b/frontend/admin-web/src/services/systemAccountReportService.ts @@ -16,6 +16,7 @@ import type { ExpiredRewardsSummary, AllSystemAccountsLedgerResponse, AllRewardTypeSummariesResponse, + RewardEntriesResponse, } from '@/types'; /** @@ -88,6 +89,28 @@ export const systemAccountReportService = { async getRewardTypeSummaries(): Promise> { return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.REWARD_TYPE_SUMMARIES); }, + + // [2026-01-06] 新增:获取收益类型详细记录列表 + /** + * 获取指定收益类型的详细记录列表 + */ + async getRewardEntriesByType(params: { + rightType: string; + page?: number; + pageSize?: number; + }): Promise> { + return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.REWARD_ENTRIES_BY_TYPE, { params }); + }, + + /** + * 获取手续费类型的详细记录列表 + */ + async getFeeEntriesDetailed(params?: { + page?: number; + pageSize?: number; + }): Promise> { + return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.FEE_ENTRIES_DETAILED, { params }); + }, }; export default systemAccountReportService; diff --git a/frontend/admin-web/src/types/system-account.types.ts b/frontend/admin-web/src/types/system-account.types.ts index 51d2b007..a039631c 100644 --- a/frontend/admin-web/src/types/system-account.types.ts +++ b/frontend/admin-web/src/types/system-account.types.ts @@ -236,3 +236,54 @@ export const FEE_TYPE_LABELS: Record = { HEADQUARTERS_BASE_FEE: '总部社区基础费', RWAD_POOL_INJECTION: 'RWAD底池注入', }; + +// [2026-01-06] 新增:收益记录条目 +/** + * 收益记录条目 + */ +export interface RewardEntryDTO { + id: string; + accountSequence: string; + sourceOrderId: string; + rightType: string; + rewardStatus: string; + usdtAmount: number; + hashpowerAmount: number; + createdAt: string; + claimedAt: string | null; + expiredAt: string | null; +} + +/** + * 收益记录列表响应 + */ +export interface RewardEntriesResponse { + entries: RewardEntryDTO[]; + total: number; + page: number; + pageSize: number; + totalPages: number; +} + +/** + * 收益类型显示名称映射 + */ +export const REWARD_RIGHT_TYPE_LABELS: Record = { + PROVINCE_TEAM_RIGHT: '省团队权益', + CITY_TEAM_RIGHT: '市团队权益', + SHARE_RIGHT: '分享权益', + COMMUNITY_RIGHT: '社区权益', + COST_FEE: '成本费', + OPERATION_FEE: '运营费', + HEADQUARTERS_BASE_FEE: '总部社区基础费', + RWAD_POOL_INJECTION: 'RWAD底池注入', +}; + +/** + * 收益状态显示名称映射 + */ +export const REWARD_STATUS_LABELS: Record = { + SETTLEABLE: '可结算', + SETTLED: '已结算', + EXPIRED: '已过期', +};