feat: add finance logs page, banner upload, and admin withdraw fix

## 财务流水
- 新增 FinanceLogs.vue(/finance-logs):额度流水 + 上下分流水双 Tab,支持时间/代理/玩家/操作人筛选与分页
- 管理员与代理共用页面,API 按角色自动切换(/admin/* 或 /agent/*)
- 侧栏「财务流水」替代原「额度流水」;代理侧栏同步新增入口
- /agent-credit-transactions 重定向至 /finance-logs?tab=credit,旧链接仍可用
- 后端:新增 GET /admin/wallet/transfer-transactions;增强额度/上下分列表筛选
- 代理端:新增 GET /agent/credit-transactions;GET /agent/wallet-transactions 支持分页与筛选
- 修复:管理员下分改为 adminWithdrawFromPlayer(),下分后重算上级代理 usedCredit

## 内容管理 Banner
- Contents.vue:各语言 Banner 支持本地上传、媒体库选择、手动填 URL(≤5MB)
- vite 开发代理 /uploads;生产 nginx 反代 /uploads/ 至 API

## 玩家端 Banner
- BannerCarousel:外链无协议时自动补 https://
- defaultBanner:API 加载中不闪默认图,仅空列表时展示默认 Banner

## 其他
- AgentManager:查看额度流水链接改为 /finance-logs
- i18n:finance.*、nav.finance_logs、content.upload.*(中/英/马来)

未纳入本次提交:.pnpm-store/、release/ 部署包、uploads/banners/ 下测试上传图片

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-10 10:02:53 +08:00
parent df20444be9
commit 6124313369
17 changed files with 1233 additions and 25 deletions

View File

@@ -359,17 +359,84 @@ export class AgentPortalController {
return jsonResponse(summary);
}
@Get('wallet-transactions')
async walletTransactions(@CurrentUser('id') agentId: bigint, @Query('playerId') playerId?: string) {
const players = playerId
? [BigInt(playerId)]
: (await this.agents.getDirectPlayers(agentId)).map((p) => p.id);
const transactions = await this.prisma.walletTransaction.findMany({
where: { userId: { in: players } },
orderBy: { createdAt: 'desc' },
take: 50,
@Get('credit-transactions')
async listCreditTransactions(
@CurrentUser('id') agentId: bigint,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('agentId') filterAgentId?: string,
@Query('keyword') keyword?: string,
@Query('operatorKeyword') operatorKeyword?: string,
@Query('transactionType') transactionType?: string,
@Query('dateFrom') dateFrom?: string,
@Query('dateTo') dateTo?: string,
) {
const scopedAgentIds = await this.agents.getSubtreeAgentIds(agentId);
const parsedAgentId = filterAgentId ? BigInt(filterAgentId) : undefined;
if (parsedAgentId && !scopedAgentIds.some((id) => id === parsedAgentId)) {
return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 });
}
const result = await this.agents.listCreditTransactions({
page: page ? parseInt(page, 10) : 1,
pageSize: pageSize ? parseInt(pageSize, 10) : 20,
agentId: parsedAgentId,
keyword,
operatorKeyword,
transactionType,
scopedAgentIds,
dateFrom: dateFrom ? new Date(dateFrom) : undefined,
dateTo: dateTo ? new Date(dateTo) : undefined,
});
return jsonResponse(transactions);
return jsonResponse(result);
}
@Get('wallet-transactions')
async walletTransactions(
@CurrentUser('id') agentId: bigint,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('playerId') playerId?: string,
@Query('parentAgentId') parentAgentId?: string,
@Query('parentAgentKeyword') parentAgentKeyword?: string,
@Query('keyword') keyword?: string,
@Query('operatorKeyword') operatorKeyword?: string,
@Query('transactionType') transactionType?: string,
@Query('dateFrom') dateFrom?: string,
@Query('dateTo') dateTo?: string,
) {
const scopedParentAgentIds = await this.agents.getSubtreeAgentIds(agentId);
const parsedParentAgentId = parentAgentId ? BigInt(parentAgentId) : undefined;
if (
parsedParentAgentId &&
!scopedParentAgentIds.some((id) => id === parsedParentAgentId)
) {
return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 });
}
if (playerId) {
const player = await this.prisma.user.findFirst({
where: { id: BigInt(playerId), userType: 'PLAYER', deletedAt: null },
select: { id: true, parentId: true },
});
if (
!player?.parentId ||
!scopedParentAgentIds.some((id) => id === player.parentId)
) {
return jsonResponse({ items: [], total: 0, page: 1, pageSize: 20 });
}
}
const result = await this.wallet.listTransferTransactions({
page: page ? parseInt(page, 10) : 1,
pageSize: pageSize ? parseInt(pageSize, 10) : 20,
playerId: playerId ? BigInt(playerId) : undefined,
parentAgentId: parsedParentAgentId,
parentAgentKeyword,
scopedParentAgentIds,
keyword,
operatorKeyword,
transactionType,
dateFrom: dateFrom ? new Date(dateFrom) : undefined,
dateTo: dateTo ? new Date(dateTo) : undefined,
});
return jsonResponse(result);
}
}