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

@@ -1,14 +1,21 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import imageCompression from 'browser-image-compression';
import api from '../api';
import GoldSpinner from '../components/GoldSpinner.vue';
const router = useRouter();
const route = useRoute();
const { t } = useI18n();
const reapplyOrderId = computed(() => {
const id = route.query.orderId;
return typeof id === 'string' && id ? id : '';
});
const isReapply = computed(() => !!reapplyOrderId.value);
interface PaymentMethod {
id: string;
methodType: string;
@@ -36,13 +43,39 @@ const bankMethods = computed(() => methods.value.filter((m) => m.methodType ===
const usdtMethods = computed(() => methods.value.filter((m) => m.methodType === 'USDT'));
const currentMethods = computed(() => methodType.value === 'BANK' ? bankMethods.value : usdtMethods.value);
function applyReapplyQuery() {
const type = route.query.methodType;
if (type === 'BANK' || type === 'USDT') {
methodType.value = type;
}
const methodId = typeof route.query.methodId === 'string' ? route.query.methodId : '';
if (methodId) {
const match = methods.value.find((m) => m.id === methodId);
if (match) {
selectedMethod.value = match;
}
}
if (!selectedMethod.value && currentMethods.value.length) {
selectedMethod.value = currentMethods.value[0];
}
const amountQuery = typeof route.query.amount === 'string' ? route.query.amount : '';
const parsedAmount = parseFloat(amountQuery);
if (amountQuery && parsedAmount > 0) {
amount.value = amountQuery;
}
}
async function fetchMethods() {
loading.value = true;
try {
const { data } = await api.get('/player/payment-methods');
methods.value = (data.data ?? []).map((m: any) => ({ ...m, id: String(m.id) }));
// Auto-select first available
if (currentMethods.value.length) {
if (isReapply.value) {
applyReapplyQuery();
} else if (currentMethods.value.length) {
selectedMethod.value = currentMethods.value[0];
}
} catch { /* */ } finally {
@@ -145,10 +178,19 @@ async function handleSubmit() {
submitting.value = true;
try {
const fd = new FormData();
fd.append('paymentMethodId', selectedMethod.value.id);
fd.append('amount', String(amt));
fd.append('screenshot', screenshotFile.value);
if (isReapply.value) {
fd.append('paymentMethodId', selectedMethod.value.id);
const { data } = await api.post(`/player/deposit-orders/${reapplyOrderId.value}/reapply`, fd);
const result = data.data;
orderNo.value = result?.orderNo ?? '';
success.value = true;
return;
}
fd.append('paymentMethodId', selectedMethod.value.id);
const { data } = await api.post('/player/deposit-orders', fd);
const result = data.data;
orderNo.value = result?.orderNo ?? '';
@@ -206,10 +248,12 @@ onMounted(fetchMethods);
<h3>{{ t('recharge.submitted') }}</h3>
<p class="order-no">{{ orderNo }}</p>
<p class="success-hint">{{ t('recharge.pending_review') }}</p>
<button class="btn-primary" @click="resetForm">{{ t('recharge.new_recharge') }}</button>
<button v-if="isReapply" class="btn-primary" @click="goHistory">{{ t('recharge.back_to_history') }}</button>
<button v-else class="btn-primary" @click="resetForm">{{ t('recharge.new_recharge') }}</button>
</div>
<template v-else>
<div v-if="isReapply" class="reapply-banner">{{ t('recharge.reapply_hint') }}</div>
<div class="type-tabs">
<button
:class="['tab', methodType === 'BANK' && 'active']"
@@ -330,6 +374,17 @@ onMounted(fetchMethods);
.state { display: flex; justify-content: center; padding: 48px; }
.reapply-banner {
margin-bottom: 12px;
padding: 10px 12px;
border-radius: 8px;
font-size: 12px;
line-height: 1.45;
color: #ffd0d0;
background: rgba(245, 108, 108, 0.12);
border: 1px solid rgba(245, 108, 108, 0.25);
}
.type-tabs {
display: flex; margin-bottom: 12px;
border-radius: 6px; overflow: hidden;