feat: 充值订单审计与重新申请,优化赛事展示和余额刷新

- 新增 deposit_order_audit_logs 表,记录提交/审批/拒绝/撤销/重提全链路
- 管理端充值单页增加审计历史;玩家端充值历史支持时间线与重新申请
- 已拒绝订单可原单号重提;撤销入账使用 PLAYER_DEPOSIT_REVERSAL 并加强幂等
- 结算后清除热门标记,允许归档已结算赛事,完善今日赛事时区窗口
- 足球页今日/早盘独立折叠;资料与余额在进入钱包/个人页及下注后自动刷新
- 补充投注玩法、结算返水规则文档;新增 smoke/settlement CLI 脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-15 14:52:05 +08:00
parent 73a94e6be3
commit afb5c5437e
55 changed files with 3050 additions and 160 deletions

View File

@@ -182,7 +182,9 @@ export class PlayerController {
]);
const timeZone = safeTimeZone(headerTimeZone);
const now = new Date();
const hotMatches = (allMatches as Array<{ isHot?: boolean }>).filter((m) => m.isHot);
const hotMatches = (allMatches as Array<{ isHot?: boolean; status?: string }>).filter(
(m) => m.isHot && m.status !== 'SETTLED',
);
const todayMatches = (allMatches as Array<{ startTime: string }>).filter((m) => {
const kickoff = new Date(m.startTime);
return !Number.isNaN(kickoff.getTime()) && isInLocalTodayMatchWindow(kickoff, now, timeZone);
@@ -399,4 +401,52 @@ export class PlayerController {
);
return jsonResponse(result);
}
@Get('deposit-orders/:id/audit-logs')
async myDepositOrderAuditLogs(
@CurrentUser('id') userId: bigint,
@Param('id') id: string,
) {
const items = await this.deposit.getPlayerDepositOrderAuditLogs(BigInt(id), userId);
return jsonResponse({ items });
}
@Post('deposit-orders/:id/reapply')
@UseInterceptors(FileInterceptor('screenshot', { limits: { fileSize: 5 * 1024 * 1024 } }))
async reapplyDepositOrder(
@CurrentUser('id') userId: bigint,
@Param('id') id: string,
@UploadedFile() file: { originalname: string; mimetype: string; buffer: Buffer; size: number } | undefined,
@Body() body: { paymentMethodId?: string; amount?: string },
) {
if (!file) throw appBadRequest('SCREENSHOT_REQUIRED');
if (!file.mimetype.startsWith('image/')) throw appBadRequest('FILE_MUST_BE_IMAGE');
const amount = body.amount != null && body.amount !== '' ? parseFloat(body.amount) : undefined;
if (amount != null && (!amount || amount <= 0)) throw appBadRequest('INVALID_AMOUNT');
const ext = extname(file.originalname || '.jpg').toLowerCase() || '.jpg';
const filename = `${Date.now()}-${randomUUID().slice(0, 8)}${ext}`;
const root = getUploadRoot();
const targetDir = join(root, 'deposits');
await mkdir(targetDir, { recursive: true });
await writeFile(join(targetDir, filename), file.buffer);
const screenshotUrl = `/uploads/deposits/${filename}`;
const order = await this.deposit.reapplyDepositOrder(
userId,
BigInt(id),
screenshotUrl,
amount,
body.paymentMethodId ? BigInt(body.paymentMethodId) : undefined,
);
return jsonResponse({
id: order!.id.toString(),
orderNo: order!.orderNo,
amount: order!.amount.toString(),
status: order!.status,
createdAt: order!.createdAt,
});
}
}