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:
@@ -1085,14 +1085,20 @@ export class AdminController {
|
||||
@Query('pageSize') pageSize?: string,
|
||||
@Query('agentId') agentId?: string,
|
||||
@Query('keyword') keyword?: string,
|
||||
@Query('operatorKeyword') operatorKeyword?: string,
|
||||
@Query('transactionType') transactionType?: string,
|
||||
@Query('dateFrom') dateFrom?: string,
|
||||
@Query('dateTo') dateTo?: string,
|
||||
) {
|
||||
const result = await this.agents.listCreditTransactions({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : 20,
|
||||
agentId: agentId ? BigInt(agentId) : undefined,
|
||||
keyword,
|
||||
operatorKeyword,
|
||||
transactionType,
|
||||
dateFrom: dateFrom ? new Date(dateFrom) : undefined,
|
||||
dateTo: dateTo ? new Date(dateTo) : undefined,
|
||||
});
|
||||
return jsonResponse(result);
|
||||
}
|
||||
@@ -1187,7 +1193,7 @@ export class AdminController {
|
||||
@Post('wallet/withdraw')
|
||||
@RequirePermissions(P.walletWithdraw)
|
||||
async withdraw(@CurrentUser('id') operatorId: bigint, @Body() dto: DepositDto & { userId: string }) {
|
||||
const result = await this.wallet.withdraw(
|
||||
const result = await this.agents.adminWithdrawFromPlayer(
|
||||
BigInt(dto.userId),
|
||||
dto.amount,
|
||||
operatorId,
|
||||
@@ -1204,6 +1210,35 @@ export class AdminController {
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Get('wallet/transfer-transactions')
|
||||
@RequirePermissions(P.walletDeposit, P.walletWithdraw, P.reports)
|
||||
async listWalletTransferTransactions(
|
||||
@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 result = await this.wallet.listTransferTransactions({
|
||||
page: page ? parseInt(page, 10) : 1,
|
||||
pageSize: pageSize ? parseInt(pageSize, 10) : 20,
|
||||
playerId: playerId ? BigInt(playerId) : undefined,
|
||||
parentAgentId: parentAgentId ? BigInt(parentAgentId) : undefined,
|
||||
parentAgentKeyword,
|
||||
keyword,
|
||||
operatorKeyword,
|
||||
transactionType,
|
||||
dateFrom: dateFrom ? new Date(dateFrom) : undefined,
|
||||
dateTo: dateTo ? new Date(dateTo) : undefined,
|
||||
});
|
||||
return jsonResponse(result);
|
||||
}
|
||||
|
||||
@Post('leagues')
|
||||
@RequirePermissions(P.matches)
|
||||
async createLeague(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -308,6 +308,184 @@ export class WalletService {
|
||||
return { items, total, page, pageSize };
|
||||
}
|
||||
|
||||
async listTransferTransactions(params: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
playerId?: bigint;
|
||||
parentAgentId?: bigint;
|
||||
parentAgentKeyword?: string;
|
||||
scopedParentAgentIds?: bigint[];
|
||||
keyword?: string;
|
||||
operatorKeyword?: string;
|
||||
transactionType?: string;
|
||||
dateFrom?: Date;
|
||||
dateTo?: Date;
|
||||
}) {
|
||||
const page = Math.max(1, params.page ?? 1);
|
||||
const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 20));
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const transferTypes = ['MANUAL_DEPOSIT', 'MANUAL_WITHDRAW'];
|
||||
const where: Prisma.WalletTransactionWhereInput = {
|
||||
transactionType: params.transactionType?.trim()
|
||||
? params.transactionType.trim()
|
||||
: { in: transferTypes },
|
||||
};
|
||||
|
||||
if (params.dateFrom || params.dateTo) {
|
||||
where.createdAt = {};
|
||||
if (params.dateFrom) where.createdAt.gte = params.dateFrom;
|
||||
if (params.dateTo) where.createdAt.lte = params.dateTo;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
let playerIds: bigint[] | undefined;
|
||||
|
||||
if (params.playerId) {
|
||||
playerIds = [params.playerId];
|
||||
} else {
|
||||
const playerWhere: Prisma.UserWhereInput = {
|
||||
userType: 'PLAYER',
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
if (params.parentAgentId) {
|
||||
playerWhere.parentId = params.parentAgentId;
|
||||
} else if (params.parentAgentKeyword?.trim()) {
|
||||
const matchedAgents = await this.prisma.user.findMany({
|
||||
where: {
|
||||
userType: 'AGENT',
|
||||
deletedAt: null,
|
||||
username: { contains: params.parentAgentKeyword.trim(), mode: 'insensitive' },
|
||||
...(params.scopedParentAgentIds?.length
|
||||
? { id: { in: params.scopedParentAgentIds } }
|
||||
: {}),
|
||||
},
|
||||
select: { id: true },
|
||||
take: 50,
|
||||
});
|
||||
const agentIds = matchedAgents.map((a) => a.id);
|
||||
if (!agentIds.length) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
playerWhere.parentId = { in: agentIds };
|
||||
} else if (params.scopedParentAgentIds?.length) {
|
||||
playerWhere.parentId = { in: params.scopedParentAgentIds };
|
||||
}
|
||||
|
||||
const keyword = params.keyword?.trim();
|
||||
if (keyword) {
|
||||
playerWhere.username = { contains: keyword, mode: 'insensitive' };
|
||||
}
|
||||
|
||||
if (params.parentAgentId || params.parentAgentKeyword?.trim() || params.scopedParentAgentIds?.length || keyword) {
|
||||
const players = await this.prisma.user.findMany({
|
||||
where: playerWhere,
|
||||
select: { id: true },
|
||||
take: 500,
|
||||
});
|
||||
playerIds = players.map((p) => p.id);
|
||||
if (!playerIds.length) {
|
||||
return { items: [], total: 0, page, pageSize };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playerIds) {
|
||||
where.userId = { in: playerIds };
|
||||
}
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
this.prisma.walletTransaction.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip,
|
||||
take: pageSize,
|
||||
}),
|
||||
this.prisma.walletTransaction.count({ where }),
|
||||
]);
|
||||
|
||||
const userIds = [...new Set(rows.map((r) => r.userId))];
|
||||
const operatorIds = [
|
||||
...new Set(rows.map((r) => r.operatorId).filter((id): id is bigint => id != null)),
|
||||
];
|
||||
|
||||
const [players, operators] = await Promise.all([
|
||||
userIds.length
|
||||
? this.prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true, parentId: true },
|
||||
})
|
||||
: [],
|
||||
operatorIds.length
|
||||
? this.prisma.user.findMany({
|
||||
where: { id: { in: operatorIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [],
|
||||
]);
|
||||
|
||||
const parentIds = [
|
||||
...new Set(players.map((p) => p.parentId).filter((id): id is bigint => id != null)),
|
||||
];
|
||||
const parentAgents = parentIds.length
|
||||
? await this.prisma.user.findMany({
|
||||
where: { id: { in: parentIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
: [];
|
||||
|
||||
const playerById = new Map(players.map((p) => [p.id.toString(), p]));
|
||||
const operatorById = new Map(operators.map((u) => [u.id.toString(), u.username]));
|
||||
const parentById = new Map(parentAgents.map((a) => [a.id.toString(), a.username]));
|
||||
|
||||
return {
|
||||
items: rows.map((row) => {
|
||||
const player = playerById.get(row.userId.toString());
|
||||
const parentId = player?.parentId;
|
||||
return {
|
||||
id: row.id.toString(),
|
||||
transactionId: row.transactionId,
|
||||
playerId: row.userId.toString(),
|
||||
playerUsername: player?.username ?? null,
|
||||
parentAgentId: parentId?.toString() ?? null,
|
||||
parentAgentUsername: parentId ? (parentById.get(parentId.toString()) ?? null) : null,
|
||||
transactionType: row.transactionType,
|
||||
amount: row.amount.toString(),
|
||||
balanceBefore: row.balanceBefore.toString(),
|
||||
balanceAfter: row.balanceAfter.toString(),
|
||||
frozenBefore: row.frozenBefore.toString(),
|
||||
frozenAfter: row.frozenAfter.toString(),
|
||||
operatorId: row.operatorId?.toString() ?? null,
|
||||
operatorUsername: row.operatorId
|
||||
? (operatorById.get(row.operatorId.toString()) ?? null)
|
||||
: null,
|
||||
remark: row.remark,
|
||||
createdAt: row.createdAt,
|
||||
};
|
||||
}),
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
async getTransactionStats(userId: bigint) {
|
||||
const [aggregates, byType] = await Promise.all([
|
||||
this.prisma.walletTransaction.aggregate({
|
||||
|
||||
Reference in New Issue
Block a user