feat(admin-web): 添加系统账户收益类型详细明细列表功能

为系统账户报表中的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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-06 09:19:15 -08:00
parent 283553a474
commit 4b5270f130
11 changed files with 731 additions and 3 deletions

View File

@ -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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\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 <noreply@anthropic.com>\nEOF\n\\)\")"
],
"deny": [],
"ask": []

View File

@ -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,
});
}
}

View File

@ -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<RewardEntriesResponse> {
this.logger.log(`[getRewardEntriesByType] 获取收益类型详细记录: ${params.rightType}`);
return this.rewardServiceClient.getRewardEntriesByType(params);
}
/**
*
*/
async getFeeEntriesDetailed(params?: {
page?: number;
pageSize?: number;
}): Promise<RewardEntriesResponse> {
this.logger.log('[getFeeEntriesDetailed] 获取手续费类型详细记录');
return this.rewardServiceClient.getFeeEntriesDetailed(params);
}
/**
*
*/

View File

@ -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<RewardEntriesResponse> {
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<RewardEntriesResponse>(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<RewardEntriesResponse> {
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<RewardEntriesResponse>(url),
);
return response.data;
} catch (error) {
this.logger.error(`[getFeeEntriesDetailed] 失败: ${error.message}`);
return {
entries: [],
total: 0,
page: 1,
pageSize: params?.pageSize ?? 50,
totalPages: 0,
};
}
}
}

View File

@ -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;
}
}

View File

@ -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),
};
}
}

View File

@ -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;
}

View File

@ -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<RewardEntriesResponse | null>(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 (
<div className={styles.loading}>
@ -859,11 +888,92 @@ function FeeAccountSection({
) : (
<div className={styles.emptyTable}></div>
)}
{/* 详细明细列表 */}
<div className={styles.detailsSection}>
<button onClick={handleToggleDetails} className={styles.toggleDetailsButton}>
{showDetails ? '收起详细明细 ▲' : '查看详细明细 ▼'}
</button>
{showDetails && (
<div className={styles.detailsContent}>
{detailsLoading ? (
<div className={styles.loading}>
<div className={styles.spinner} />
<span>...</span>
</div>
) : detailsData && detailsData.entries.length > 0 ? (
<>
<div className={styles.tableWrapper}>
<table className={styles.table}>
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th></th>
<th> (绿)</th>
<th></th>
</tr>
</thead>
<tbody>
{detailsData.entries.map((entry) => (
<tr key={entry.id}>
<td>{new Date(entry.createdAt).toLocaleString('zh-CN')}</td>
<td>{entry.accountSequence}</td>
<td className={styles.orderId}>{entry.sourceOrderId}</td>
<td>{REWARD_RIGHT_TYPE_LABELS[entry.rightType] || entry.rightType}</td>
<td>{formatAmount(entry.usdtAmount)}</td>
<td>
<span className={`${styles.statusBadge} ${entry.rewardStatus === 'SETTLED' ? styles.settled : ''}`}>
{REWARD_STATUS_LABELS[entry.rewardStatus] || entry.rewardStatus}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 分页 */}
<div className={styles.pagination}>
<button
onClick={() => loadDetails(currentPage - 1)}
disabled={currentPage <= 1}
className={styles.pageButton}
>
</button>
<span className={styles.pageInfo}>
{currentPage} / {detailsData.totalPages} ( {detailsData.total} )
</span>
<button
onClick={() => loadDetails(currentPage + 1)}
disabled={currentPage >= detailsData.totalPages}
className={styles.pageButton}
>
</button>
</div>
</>
) : (
<div className={styles.emptyTable}></div>
)}
</div>
)}
</div>
</div>
);
}
// [2026-01-06] 新增:收益类型汇总组件
// 收益类型映射到 API 使用的 rightType 值
const REWARD_TYPE_MAP: Record<string, string> = {
'省团队收益汇总': '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<RewardEntriesResponse | null>(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 (
<div className={styles.loading}>
@ -957,6 +1101,77 @@ function RewardTypeSummarySection({
) : (
<div className={styles.emptyTable}></div>
)}
{/* 详细明细列表 */}
<div className={styles.detailsSection}>
<button onClick={handleToggleDetails} className={styles.toggleDetailsButton}>
{showDetails ? '收起详细明细 ▲' : '查看详细明细 ▼'}
</button>
{showDetails && (
<div className={styles.detailsContent}>
{detailsLoading ? (
<div className={styles.loading}>
<div className={styles.spinner} />
<span>...</span>
</div>
) : detailsData && detailsData.entries.length > 0 ? (
<>
<div className={styles.tableWrapper}>
<table className={styles.table}>
<thead>
<tr>
<th></th>
<th></th>
<th></th>
<th> (绿)</th>
<th></th>
</tr>
</thead>
<tbody>
{detailsData.entries.map((entry) => (
<tr key={entry.id}>
<td>{new Date(entry.createdAt).toLocaleString('zh-CN')}</td>
<td>{entry.accountSequence}</td>
<td className={styles.orderId}>{entry.sourceOrderId}</td>
<td>{formatAmount(entry.usdtAmount)}</td>
<td>
<span className={`${styles.statusBadge} ${entry.rewardStatus === 'SETTLED' ? styles.settled : ''}`}>
{REWARD_STATUS_LABELS[entry.rewardStatus] || entry.rewardStatus}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 分页 */}
<div className={styles.pagination}>
<button
onClick={() => loadDetails(currentPage - 1)}
disabled={currentPage <= 1}
className={styles.pageButton}
>
</button>
<span className={styles.pageInfo}>
{currentPage} / {detailsData.totalPages} ( {detailsData.total} )
</span>
<button
onClick={() => loadDetails(currentPage + 1)}
disabled={currentPage >= detailsData.totalPages}
className={styles.pageButton}
>
</button>
</div>
</>
) : (
<div className={styles.emptyTable}></div>
)}
</div>
)}
</div>
</div>
);
}

View File

@ -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;

View File

@ -16,6 +16,7 @@ import type {
ExpiredRewardsSummary,
AllSystemAccountsLedgerResponse,
AllRewardTypeSummariesResponse,
RewardEntriesResponse,
} from '@/types';
/**
@ -88,6 +89,28 @@ export const systemAccountReportService = {
async getRewardTypeSummaries(): Promise<ApiResponse<AllRewardTypeSummariesResponse>> {
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.REWARD_TYPE_SUMMARIES);
},
// [2026-01-06] 新增:获取收益类型详细记录列表
/**
*
*/
async getRewardEntriesByType(params: {
rightType: string;
page?: number;
pageSize?: number;
}): Promise<ApiResponse<RewardEntriesResponse>> {
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.REWARD_ENTRIES_BY_TYPE, { params });
},
/**
*
*/
async getFeeEntriesDetailed(params?: {
page?: number;
pageSize?: number;
}): Promise<ApiResponse<RewardEntriesResponse>> {
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.FEE_ENTRIES_DETAILED, { params });
},
};
export default systemAccountReportService;

View File

@ -236,3 +236,54 @@ export const FEE_TYPE_LABELS: Record<string, string> = {
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<string, string> = {
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<string, string> = {
SETTLEABLE: '可结算',
SETTLED: '已结算',
EXPIRED: '已过期',
};