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

@@ -265,6 +265,31 @@ export class AgentsService {
return result;
}
/** 管理员给玩家下分:扣款后刷新上级代理占用额度 */
async adminWithdrawFromPlayer(
playerId: bigint,
amount: number,
operatorId: bigint,
remark?: string,
requestId?: string,
) {
const result = await this.wallet.withdraw(
playerId,
amount,
operatorId,
remark,
requestId,
);
const player = await this.prisma.user.findUnique({
where: { id: playerId },
select: { parentId: true },
});
if (player?.parentId) {
await this.recalculateUsedCredit(player.parentId);
}
return result;
}
/** 上下分弹窗:玩家余额 + 授信代理可用额度/限额上下文 */
async getPlayerTransferContext(
playerId: bigint,
@@ -732,7 +757,11 @@ export class AgentsService {
pageSize?: number;
agentId?: bigint;
keyword?: string;
operatorKeyword?: string;
transactionType?: string;
scopedAgentIds?: bigint[];
dateFrom?: Date;
dateTo?: Date;
}) {
const page = Math.max(1, params.page ?? 1);
const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 20));
@@ -744,6 +773,31 @@ export class AgentsService {
where.transactionType = params.transactionType.trim();
}
if (params.dateFrom || params.dateTo) {
where.createdAt = {};
if (params.dateFrom) where.createdAt.gte = params.dateFrom;
if (params.dateTo) where.createdAt.lte = params.dateTo;
}
const scopedIds = params.scopedAgentIds?.length ? params.scopedAgentIds : undefined;
const operatorKeyword = params.operatorKeyword?.trim();
if (operatorKeyword) {
const matchedOps = await this.prisma.user.findMany({
where: {
deletedAt: null,
username: { contains: operatorKeyword, mode: 'insensitive' },
},
select: { id: true },
take: 50,
});
const operatorIds = matchedOps.map((u) => u.id);
if (!operatorIds.length) {
return { items: [], total: 0, page, pageSize };
}
where.operatorId = { in: operatorIds };
}
const keyword = params.keyword?.trim();
if (keyword) {
const matched = await this.prisma.user.findMany({
@@ -755,7 +809,11 @@ export class AgentsService {
select: { id: true },
take: 50,
});
const agentUserIds = matched.map((u) => u.id);
let agentUserIds = matched.map((u) => u.id);
if (scopedIds) {
const scopedSet = new Set(scopedIds.map((id) => id.toString()));
agentUserIds = agentUserIds.filter((id) => scopedSet.has(id.toString()));
}
if (!agentUserIds.length) {
return { items: [], total: 0, page, pageSize };
}
@@ -768,7 +826,12 @@ export class AgentsService {
where.agentId = { in: agentUserIds };
}
} else if (params.agentId) {
if (scopedIds && !scopedIds.some((id) => id === params.agentId)) {
return { items: [], total: 0, page, pageSize };
}
where.agentId = params.agentId;
} else if (scopedIds) {
where.agentId = { in: scopedIds };
}
const [rows, total] = await Promise.all([