feat(admin-web): 添加过期收益明细查询功能
- reward-service: 添加 getExpiredRewardsEntries API 查询过期收益明细 - reporting-service: 添加过期收益明细转发接口和类型定义 - admin-web: 过期收益统计区域新增"查看明细"按钮 - 支持分页浏览过期收益记录 - 支持按权益类型筛选 - 显示过期时间、用户ID、账户、权益类型、金额、订单号 回滚方式:删除各服务中标注 [2026-01-07] 的代码块 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
90ca62b594
commit
e9c0196d68
|
|
@ -79,6 +79,28 @@ export class SystemAccountReportController {
|
|||
return this.systemAccountReportService.getExpiredRewardsSummary({ startDate, endDate });
|
||||
}
|
||||
|
||||
// [2026-01-07] 新增:过期收益明细列表
|
||||
// 用于系统账户报表中展示过期收益的具体记录
|
||||
// 回滚方式:删除此方法
|
||||
@Get('expired-rewards-entries')
|
||||
@ApiOperation({ summary: '获取过期收益明细列表' })
|
||||
@ApiQuery({ name: 'page', required: false, description: '页码,默认1' })
|
||||
@ApiQuery({ name: 'pageSize', required: false, description: '每页条数,默认50' })
|
||||
@ApiQuery({ name: 'rightType', required: false, description: '权益类型筛选(可选)' })
|
||||
@ApiResponse({ status: 200, description: '过期收益明细列表' })
|
||||
async getExpiredRewardsEntries(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('rightType') rightType?: string,
|
||||
) {
|
||||
this.logger.log(`[getExpiredRewardsEntries] 请求过期收益明细, page=${page}, pageSize=${pageSize}, rightType=${rightType || 'all'}`);
|
||||
return this.systemAccountReportService.getExpiredRewardsEntries({
|
||||
page: page ? parseInt(page, 10) : undefined,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
|
||||
rightType,
|
||||
});
|
||||
}
|
||||
|
||||
// [2026-01-07] 新增:面对面结算明细列表
|
||||
// 回滚方式:删除此方法
|
||||
@Get('offline-settlement-entries')
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { WalletServiceClient, OfflineSettlementSummary, AllSystemAccountsResponse, AllSystemAccountsLedgerResponse, FeeCollectionSummaryResponse, FeeCollectionEntriesResponse, OfflineSettlementEntriesResponse } from '../../infrastructure/external/wallet-service/wallet-service.client';
|
||||
import { RewardServiceClient, ExpiredRewardsSummary, AllRewardTypeSummaries, RewardEntriesResponse } from '../../infrastructure/external/reward-service/reward-service.client';
|
||||
import { RewardServiceClient, ExpiredRewardsSummary, AllRewardTypeSummaries, RewardEntriesResponse, ExpiredRewardsEntriesResponse } from '../../infrastructure/external/reward-service/reward-service.client';
|
||||
|
||||
/**
|
||||
* 固定系统账户信息
|
||||
|
|
@ -185,6 +185,20 @@ export class SystemAccountReportApplicationService {
|
|||
return this.rewardServiceClient.getExpiredRewardsSummary(params);
|
||||
}
|
||||
|
||||
// [2026-01-07] 新增:获取过期收益明细列表
|
||||
// 回滚方式:删除此方法
|
||||
/**
|
||||
* 获取过期收益明细列表
|
||||
*/
|
||||
async getExpiredRewardsEntries(params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
rightType?: string;
|
||||
}): Promise<ExpiredRewardsEntriesResponse> {
|
||||
this.logger.log('[getExpiredRewardsEntries] 获取过期收益明细列表');
|
||||
return this.rewardServiceClient.getExpiredRewardsEntries(params);
|
||||
}
|
||||
|
||||
// [2026-01-07] 新增:获取面对面结算明细列表
|
||||
// 回滚方式:删除此方法
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -83,6 +83,28 @@ export interface RewardEntryDTO {
|
|||
expiredAt: string | null;
|
||||
}
|
||||
|
||||
// [2026-01-07] 新增:过期收益明细条目
|
||||
export interface ExpiredRewardEntryDTO {
|
||||
id: string;
|
||||
accountSequence: string;
|
||||
userId: number;
|
||||
sourceOrderId: string;
|
||||
rightType: string;
|
||||
usdtAmount: number;
|
||||
hashpowerAmount: number;
|
||||
createdAt: string;
|
||||
expiredAt: string | null;
|
||||
}
|
||||
|
||||
// [2026-01-07] 新增:过期收益明细响应
|
||||
export interface ExpiredRewardsEntriesResponse {
|
||||
entries: ExpiredRewardEntryDTO[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
// [2026-01-06] 新增:收益记录列表响应
|
||||
export interface RewardEntriesResponse {
|
||||
entries: RewardEntryDTO[];
|
||||
|
|
@ -251,4 +273,39 @@ export class RewardServiceClient {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
// [2026-01-07] 新增:获取过期收益明细列表
|
||||
/**
|
||||
* 获取过期收益明细列表
|
||||
*/
|
||||
async getExpiredRewardsEntries(params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
rightType?: string;
|
||||
}): Promise<ExpiredRewardsEntriesResponse> {
|
||||
try {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (params?.page) queryParams.append('page', params.page.toString());
|
||||
if (params?.pageSize) queryParams.append('pageSize', params.pageSize.toString());
|
||||
if (params?.rightType) queryParams.append('rightType', params.rightType);
|
||||
|
||||
const url = `${this.baseUrl}/api/v1/internal/statistics/expired-rewards-entries?${queryParams.toString()}`;
|
||||
this.logger.debug(`[getExpiredRewardsEntries] 请求: ${url}`);
|
||||
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.get<ExpiredRewardsEntriesResponse>(url),
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error(`[getExpiredRewardsEntries] 失败: ${error.message}`);
|
||||
return {
|
||||
entries: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
pageSize: params?.pageSize ?? 50,
|
||||
totalPages: 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,4 +167,32 @@ export class InternalController {
|
|||
this.logger.log(`手续费详细记录查询结果: total=${result.total}, page=${result.page}, pageSize=${result.pageSize}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
// [2026-01-07] 新增:获取过期收益明细列表接口
|
||||
// 用于系统账户报表中展示过期收益的具体记录
|
||||
// 回滚方式:删除以下 API 方法即可
|
||||
|
||||
@Get('statistics/expired-rewards-entries')
|
||||
@ApiOperation({ summary: '获取过期收益明细列表(内部接口)- 用于系统账户报表' })
|
||||
@ApiQuery({ name: 'page', required: false, description: '页码,默认1' })
|
||||
@ApiQuery({ name: 'pageSize', required: false, description: '每页条数,默认50' })
|
||||
@ApiQuery({ name: 'rightType', required: false, description: '权益类型筛选(可选)' })
|
||||
@ApiResponse({ status: 200, description: '过期收益明细列表' })
|
||||
async getExpiredRewardsEntries(
|
||||
@Query('page') page?: string,
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('rightType') rightType?: string,
|
||||
) {
|
||||
this.logger.log(`========== statistics/expired-rewards-entries 请求 ==========`);
|
||||
this.logger.log(`page=${page}, pageSize=${pageSize}, rightType=${rightType || 'all'}`);
|
||||
|
||||
const result = await this.rewardService.getExpiredRewardsEntries({
|
||||
page: page ? parseInt(page, 10) : undefined,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : undefined,
|
||||
rightType: rightType || undefined,
|
||||
});
|
||||
|
||||
this.logger.log(`过期收益明细查询结果: total=${result.total}, page=${result.page}, pageSize=${result.pageSize}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1468,4 +1468,80 @@ export class RewardApplicationService {
|
|||
totalPages: Math.ceil(total / pageSize),
|
||||
};
|
||||
}
|
||||
|
||||
// [2026-01-07] 新增:获取过期收益明细列表
|
||||
// 用于系统账户报表中展示过期收益的具体记录
|
||||
// 回滚方式:删除以下方法即可
|
||||
/**
|
||||
* 获取过期收益明细列表
|
||||
* @param page 页码
|
||||
* @param pageSize 每页条数
|
||||
* @param rightType 可选的权益类型筛选
|
||||
*/
|
||||
async getExpiredRewardsEntries(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
rightType?: string;
|
||||
}): Promise<{
|
||||
entries: Array<{
|
||||
id: string;
|
||||
accountSequence: string;
|
||||
userId: number;
|
||||
sourceOrderId: string;
|
||||
rightType: string;
|
||||
usdtAmount: number;
|
||||
hashpowerAmount: number;
|
||||
createdAt: string;
|
||||
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(`[getExpiredRewardsEntries] 查询过期收益明细, page=${page}, pageSize=${pageSize}, rightType=${params.rightType || 'all'}`);
|
||||
|
||||
// 构建查询条件
|
||||
const whereClause: any = {
|
||||
rewardStatus: 'EXPIRED',
|
||||
};
|
||||
if (params.rightType) {
|
||||
whereClause.rightType = params.rightType;
|
||||
}
|
||||
|
||||
// 查询总数
|
||||
const total = await this.prisma.rewardLedgerEntry.count({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
// 查询数据
|
||||
const entries = await this.prisma.rewardLedgerEntry.findMany({
|
||||
where: whereClause,
|
||||
orderBy: { expiredAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
return {
|
||||
entries: entries.map(entry => ({
|
||||
id: entry.id.toString(),
|
||||
accountSequence: entry.accountSequence,
|
||||
userId: entry.userId,
|
||||
sourceOrderId: entry.sourceOrderNo,
|
||||
rightType: entry.rightType,
|
||||
usdtAmount: Number(entry.usdtAmount),
|
||||
hashpowerAmount: Number(entry.hashpowerAmount),
|
||||
createdAt: entry.createdAt.toISOString(),
|
||||
expiredAt: entry.expiredAt?.toISOString() ?? null,
|
||||
})),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages: Math.ceil(total / pageSize),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -340,6 +340,40 @@
|
|||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.entriesHeader {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.filterGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.filterSelect {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background-color: #fff;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.paginationInfo {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import type {
|
|||
FeeCollectionEntriesResponse,
|
||||
OfflineSettlementEntriesResponse,
|
||||
OfflineSettlementEntryDTO,
|
||||
ExpiredRewardsEntriesResponse,
|
||||
ExpiredRewardEntryDTO,
|
||||
} from '@/types';
|
||||
import { ENTRY_TYPE_LABELS, ACCOUNT_TYPE_LABELS, FEE_TYPE_LABELS, REWARD_RIGHT_TYPE_LABELS, REWARD_STATUS_LABELS, FEE_COLLECTION_TYPE_LABELS, getAccountDisplayName, SYSTEM_ACCOUNT_NAMES, PROVINCE_CODE_NAMES } from '@/types';
|
||||
import styles from './SystemAccountsTab.module.scss';
|
||||
|
|
@ -549,8 +551,58 @@ function OfflineSettlementSection({ data }: { data: SystemAccountReportResponse[
|
|||
/**
|
||||
* 过期收益统计区域
|
||||
* [2026-01-05] 更新:USDT改为绿积分,添加空值检查
|
||||
* [2026-01-07] 更新:添加明细列表显示功能
|
||||
*/
|
||||
function ExpiredRewardsSection({ data }: { data: SystemAccountReportResponse['expiredRewards'] | null | undefined }) {
|
||||
const [showEntries, setShowEntries] = useState(false);
|
||||
const [entriesData, setEntriesData] = useState<ExpiredRewardsEntriesResponse | null>(null);
|
||||
const [entriesLoading, setEntriesLoading] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [selectedRightType, setSelectedRightType] = useState<string>('');
|
||||
const pageSize = 20;
|
||||
|
||||
// 加载明细数据
|
||||
const loadEntries = useCallback(async (page: number, rightType?: string) => {
|
||||
setEntriesLoading(true);
|
||||
try {
|
||||
const response = await systemAccountReportService.getExpiredRewardsEntries({
|
||||
page,
|
||||
pageSize,
|
||||
rightType: rightType || undefined,
|
||||
});
|
||||
if (response.success && response.data) {
|
||||
setEntriesData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载过期收益明细失败:', error);
|
||||
} finally {
|
||||
setEntriesLoading(false);
|
||||
}
|
||||
}, [pageSize]);
|
||||
|
||||
// 切换显示明细
|
||||
const toggleEntries = () => {
|
||||
if (!showEntries && !entriesData) {
|
||||
loadEntries(1, selectedRightType);
|
||||
}
|
||||
setShowEntries(!showEntries);
|
||||
};
|
||||
|
||||
// 切换权益类型筛选
|
||||
const handleRightTypeChange = (rightType: string) => {
|
||||
setSelectedRightType(rightType);
|
||||
setCurrentPage(1);
|
||||
if (showEntries) {
|
||||
loadEntries(1, rightType);
|
||||
}
|
||||
};
|
||||
|
||||
// 分页
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
loadEntries(page, selectedRightType);
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
|
|
@ -562,7 +614,15 @@ function ExpiredRewardsSection({ data }: { data: SystemAccountReportResponse['ex
|
|||
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
<h3 className={styles.sectionTitle}>过期收益统计</h3>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h3 className={styles.sectionTitle}>过期收益统计</h3>
|
||||
<button
|
||||
onClick={toggleEntries}
|
||||
className={styles.toggleButton}
|
||||
>
|
||||
{showEntries ? '收起明细' : '查看明细'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 汇总卡片 */}
|
||||
<div className={styles.summaryCards}>
|
||||
|
|
@ -630,7 +690,91 @@ function ExpiredRewardsSection({ data }: { data: SystemAccountReportResponse['ex
|
|||
</>
|
||||
)}
|
||||
|
||||
{(!data.byRightType || data.byRightType.length === 0) && (!data.byMonth || data.byMonth.length === 0) && (
|
||||
{/* [2026-01-07] 新增:明细列表 */}
|
||||
{showEntries && (
|
||||
<div className={styles.entriesSection}>
|
||||
<div className={styles.entriesHeader}>
|
||||
<h4 className={styles.subTitle}>过期收益明细</h4>
|
||||
<div className={styles.filterGroup}>
|
||||
<label>权益类型:</label>
|
||||
<select
|
||||
value={selectedRightType}
|
||||
onChange={(e) => handleRightTypeChange(e.target.value)}
|
||||
className={styles.filterSelect}
|
||||
>
|
||||
<option value="">全部</option>
|
||||
{data.byRightType?.map((item) => (
|
||||
<option key={item.rightType} value={item.rightType}>
|
||||
{getRightTypeName(item.rightType)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{entriesLoading ? (
|
||||
<div className={styles.loading}>
|
||||
<div className={styles.spinner} />
|
||||
<span>加载明细中...</span>
|
||||
</div>
|
||||
) : entriesData && entriesData.entries.length > 0 ? (
|
||||
<>
|
||||
<div className={styles.tableWrapper}>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>过期时间</th>
|
||||
<th>用户ID</th>
|
||||
<th>账户</th>
|
||||
<th>权益类型</th>
|
||||
<th>金额 (绿积分)</th>
|
||||
<th>订单号</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entriesData.entries.map((entry) => (
|
||||
<tr key={entry.id}>
|
||||
<td>{entry.expiredAt ? new Date(entry.expiredAt).toLocaleString('zh-CN') : '-'}</td>
|
||||
<td>{entry.userId}</td>
|
||||
<td>{getAccountDisplayName(entry.accountSequence)}</td>
|
||||
<td>{getRightTypeName(entry.rightType)}</td>
|
||||
<td>{formatAmount(entry.usdtAmount)}</td>
|
||||
<td className={styles.orderId}>{entry.sourceOrderId}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* 分页 */}
|
||||
{entriesData.totalPages > 1 && (
|
||||
<div className={styles.pagination}>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
className={styles.pageButton}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span className={styles.pageInfo}>
|
||||
第 {currentPage} 页 / 共 {entriesData.totalPages} 页 (共 {entriesData.total} 条)
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
disabled={currentPage >= entriesData.totalPages}
|
||||
className={styles.pageButton}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.emptyTable}>暂无过期收益明细数据</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!data.byRightType || data.byRightType.length === 0) && (!data.byMonth || data.byMonth.length === 0) && !showEntries && (
|
||||
<div className={styles.emptyTable}>暂无过期收益数据</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -240,5 +240,7 @@ export const API_ENDPOINTS = {
|
|||
FEE_COLLECTION_ENTRIES: '/v1/system-account-reports/fee-collection-entries',
|
||||
// [2026-01-07] 新增:面对面结算明细列表
|
||||
OFFLINE_SETTLEMENT_ENTRIES: '/v1/system-account-reports/offline-settlement-entries',
|
||||
// [2026-01-07] 新增:过期收益明细列表
|
||||
EXPIRED_REWARDS_ENTRIES: '/v1/system-account-reports/expired-rewards-entries',
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import type {
|
|||
FeeCollectionSummaryResponse,
|
||||
FeeCollectionEntriesResponse,
|
||||
OfflineSettlementEntriesResponse,
|
||||
ExpiredRewardsEntriesResponse,
|
||||
} from '@/types';
|
||||
|
||||
/**
|
||||
|
|
@ -89,6 +90,18 @@ export const systemAccountReportService = {
|
|||
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.EXPIRED_REWARDS, { params });
|
||||
},
|
||||
|
||||
// [2026-01-07] 新增:获取过期收益明细列表
|
||||
/**
|
||||
* 获取过期收益明细列表
|
||||
*/
|
||||
async getExpiredRewardsEntries(params?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
rightType?: string;
|
||||
}): Promise<ApiResponse<ExpiredRewardsEntriesResponse>> {
|
||||
return apiClient.get(API_ENDPOINTS.SYSTEM_ACCOUNT_REPORTS.EXPIRED_REWARDS_ENTRIES, { params });
|
||||
},
|
||||
|
||||
// [2026-01-05] 新增:获取所有系统账户分类账明细
|
||||
/**
|
||||
* 获取所有系统账户的分类账明细
|
||||
|
|
|
|||
|
|
@ -97,6 +97,34 @@ export interface ExpiredRewardsSummary {
|
|||
}>;
|
||||
}
|
||||
|
||||
// [2026-01-07] 新增:过期收益明细条目
|
||||
/**
|
||||
* 过期收益明细条目
|
||||
*/
|
||||
export interface ExpiredRewardEntryDTO {
|
||||
id: string;
|
||||
accountSequence: string;
|
||||
userId: number;
|
||||
sourceOrderId: string;
|
||||
rightType: string;
|
||||
usdtAmount: number;
|
||||
hashpowerAmount: number;
|
||||
createdAt: string;
|
||||
expiredAt: string | null;
|
||||
}
|
||||
|
||||
// [2026-01-07] 新增:过期收益明细响应
|
||||
/**
|
||||
* 过期收益明细响应
|
||||
*/
|
||||
export interface ExpiredRewardsEntriesResponse {
|
||||
entries: ExpiredRewardEntryDTO[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 固定系统账户
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in New Issue