From afb5c5437ebc58eb6406a5a3d16566463b90e451 Mon Sep 17 00:00:00 2001 From: Mars <3361409208a@gmail.com> Date: Mon, 15 Jun 2026 14:52:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=85=85=E5=80=BC=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E4=B8=8E=E9=87=8D=E6=96=B0=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E8=B5=9B=E4=BA=8B=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E5=92=8C=E4=BD=99=E9=A2=9D=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 deposit_order_audit_logs 表,记录提交/审批/拒绝/撤销/重提全链路 - 管理端充值单页增加审计历史;玩家端充值历史支持时间线与重新申请 - 已拒绝订单可原单号重提;撤销入账使用 PLAYER_DEPOSIT_REVERSAL 并加强幂等 - 结算后清除热门标记,允许归档已结算赛事,完善今日赛事时区窗口 - 足球页今日/早盘独立折叠;资料与余额在进入钱包/个人页及下注后自动刷新 - 补充投注玩法、结算返水规则文档;新增 smoke/settlement CLI 脚本 Co-authored-by: Cursor --- apps/admin/src/i18n/admin-messages.ts | 39 ++ apps/admin/src/main.ts | 3 + .../src/plugins/element-plus-defaults.ts | 6 + apps/admin/src/views/DepositOrders.vue | 106 ++- apps/admin/src/views/MediaLibrary.vue | 2 +- apps/admin/src/views/PaymentMethods.vue | 2 +- apps/api/package.json | 2 + .../migrations/20260615040059/migration.sql | 8 + .../migration.sql | 25 + apps/api/prisma/schema.prisma | 21 + .../applications/admin/admin.controller.ts | 7 + .../applications/player/player.controller.ts | 52 +- .../catalog/catalog-archive.service.spec.ts | 42 +- .../catalog/catalog-archive.service.ts | 9 +- .../src/domains/catalog/matches.service.ts | 2 +- .../domains/deposit/deposit.service.spec.ts | 160 ++++- .../src/domains/deposit/deposit.service.ts | 318 ++++++++- .../domains/ledger/funds-posting.service.ts | 2 + apps/api/src/domains/ledger/wallet.service.ts | 8 +- .../smoke-tests/smoke-test.bet-flow-probes.ts | 6 +- .../smoke-tests/smoke-test.cases.ts | 14 +- .../smoke-tests/smoke-test.helpers.ts | 31 + .../settlement/settlement.service.spec.ts | 9 +- .../domains/settlement/settlement.service.ts | 2 +- .../database/run-settlement-audit.ts | 147 +++++ .../infrastructure/database/run-smoke-cli.ts | 30 + .../database/seed-demo-markets.ts | 24 +- apps/player/src/components/BetSlipDrawer.vue | 4 +- .../player/src/components/CashBalanceChip.vue | 6 +- .../src/components/LeagueAccordionItem.vue | 2 + apps/player/src/components/MatchBetCard.vue | 4 + .../components/outright/OutrightBetModal.vue | 3 + .../src/composables/usePlayerProfile.ts | 17 + apps/player/src/i18n/en-US.ts | 23 +- apps/player/src/i18n/ms-MY.ts | 23 +- apps/player/src/i18n/zh-CN.ts | 23 +- apps/player/src/layouts/MainLayout.vue | 15 +- apps/player/src/utils/cashback.ts | 26 + apps/player/src/utils/depositAuditDisplay.ts | 112 ++++ apps/player/src/utils/walletTx.ts | 23 +- apps/player/src/views/CashbackRecordsView.vue | 20 +- apps/player/src/views/FootballView.vue | 103 ++- apps/player/src/views/ProfileView.vue | 52 +- apps/player/src/views/RechargeHistoryView.vue | 616 +++++++++++++++++- apps/player/src/views/RechargeView.vue | 65 +- .../src/views/WalletTransactionDetailView.vue | 14 +- apps/player/src/views/WalletView.vue | 7 +- docs/UAT_CHECKLIST.md | 2 + docs/投注玩法说明.md | 430 ++++++++++++ docs/结算与返水金额规则.md | 410 ++++++++++++ docs/默认数据说明.md | 2 + package.json | 1 + packages/shared/src/api-errors.js | 10 + packages/shared/src/api-errors.ts | 10 + packages/shared/src/match-time.js | 110 +++- 55 files changed, 3050 insertions(+), 160 deletions(-) create mode 100644 apps/admin/src/plugins/element-plus-defaults.ts create mode 100644 apps/api/prisma/migrations/20260615040059/migration.sql create mode 100644 apps/api/prisma/migrations/20260615120000_add_deposit_order_audit_logs/migration.sql create mode 100644 apps/api/src/infrastructure/database/run-settlement-audit.ts create mode 100644 apps/api/src/infrastructure/database/run-smoke-cli.ts create mode 100644 apps/player/src/utils/cashback.ts create mode 100644 apps/player/src/utils/depositAuditDisplay.ts create mode 100644 docs/投注玩法说明.md create mode 100644 docs/结算与返水金额规则.md diff --git a/apps/admin/src/i18n/admin-messages.ts b/apps/admin/src/i18n/admin-messages.ts index d9d3515..c3b34a5 100644 --- a/apps/admin/src/i18n/admin-messages.ts +++ b/apps/admin/src/i18n/admin-messages.ts @@ -87,8 +87,21 @@ const zh: Record = { 'deposit.confirm_reopen': '订单将恢复为待审核,可再次批准或拒绝。确定继续?', 'deposit.delete': '删除', 'deposit.confirm_delete': '【仅删除记录】将永久删除本订单记录及截图文件。确定继续?', + 'deposit.delete_failed': '删除失败', 'deposit.prev': '上一页', 'deposit.next': '下一页', + 'deposit.audit_history': '审核记录', + 'deposit.audit_action': '操作', + 'deposit.audit_actor': '操作人', + 'deposit.audit_time': '时间', + 'deposit.audit_remark': '备注', + 'deposit.audit_submitted': '提交申请', + 'deposit.audit_approved': '审核通过', + 'deposit.audit_rejected': '审核拒绝', + 'deposit.audit_revoked': '撤销审核', + 'deposit.audit_reopened': '重新审核', + 'deposit.audit_deleted': '已删除', + 'deposit.audit_empty': '暂无审核记录', 'deposit.add_method': '+ 添加', 'deposit.display_name': '展示名称', 'deposit.details': '详情', @@ -357,8 +370,21 @@ const en: Record = { 'deposit.confirm_reopen': 'The order will return to pending for approve or reject. Continue?', 'deposit.delete': 'Delete', 'deposit.confirm_delete': '[Records only] Permanently delete this order record and screenshot. Continue?', + 'deposit.delete_failed': 'Delete failed', 'deposit.prev': 'Prev', 'deposit.next': 'Next', + 'deposit.audit_history': 'Review history', + 'deposit.audit_action': 'Action', + 'deposit.audit_actor': 'Actor', + 'deposit.audit_time': 'Time', + 'deposit.audit_remark': 'Remark', + 'deposit.audit_submitted': 'Submitted', + 'deposit.audit_approved': 'Approved', + 'deposit.audit_rejected': 'Rejected', + 'deposit.audit_revoked': 'Approval revoked', + 'deposit.audit_reopened': 'Reopened for review', + 'deposit.audit_deleted': 'Deleted', + 'deposit.audit_empty': 'No review history yet', 'deposit.add_method': '+ Add', 'deposit.display_name': 'Display Name', 'deposit.details': 'Details', @@ -625,8 +651,21 @@ const ms: Record = { 'deposit.confirm_reopen': 'Pesanan kembali menunggu untuk lulus atau tolak. Teruskan?', 'deposit.delete': 'Padam', 'deposit.confirm_delete': '[Rekod sahaja] Padam rekod pesanan dan tangkapan skrin. Teruskan?', + 'deposit.delete_failed': 'Gagal memadam', 'deposit.prev': 'Sebelum', 'deposit.next': 'Seterus', + 'deposit.audit_history': 'Sejarah semakan', + 'deposit.audit_action': 'Tindakan', + 'deposit.audit_actor': 'Pengendali', + 'deposit.audit_time': 'Masa', + 'deposit.audit_remark': 'Catatan', + 'deposit.audit_submitted': 'Dihantar', + 'deposit.audit_approved': 'Diluluskan', + 'deposit.audit_rejected': 'Ditolak', + 'deposit.audit_revoked': 'Kelulusan dibatalkan', + 'deposit.audit_reopened': 'Dibuka semula', + 'deposit.audit_deleted': 'Dipadam', + 'deposit.audit_empty': 'Tiada sejarah semakan', 'deposit.add_method': '+ Tambah', 'deposit.display_name': 'Nama Paparan', 'deposit.details': 'Butiran', diff --git a/apps/admin/src/main.ts b/apps/admin/src/main.ts index 5e86e18..71473ec 100644 --- a/apps/admin/src/main.ts +++ b/apps/admin/src/main.ts @@ -4,6 +4,9 @@ import 'element-plus/dist/index.css'; import App from './App.vue'; import router from './router'; import { createAdminI18n } from './i18n'; +import { applyElementPlusDialogDefaults } from './plugins/element-plus-defaults'; + +applyElementPlusDialogDefaults(); async function bootstrap() { const i18n = await createAdminI18n(); diff --git a/apps/admin/src/plugins/element-plus-defaults.ts b/apps/admin/src/plugins/element-plus-defaults.ts new file mode 100644 index 0000000..eb691fe --- /dev/null +++ b/apps/admin/src/plugins/element-plus-defaults.ts @@ -0,0 +1,6 @@ +import { ElDialog } from 'element-plus'; + +/** Prevent accidental form data loss from mask clicks across admin dialogs. */ +export function applyElementPlusDialogDefaults() { + ElDialog.props.closeOnClickModal.default = false; +} diff --git a/apps/admin/src/views/DepositOrders.vue b/apps/admin/src/views/DepositOrders.vue index 86e8b93..b15f078 100644 --- a/apps/admin/src/views/DepositOrders.vue +++ b/apps/admin/src/views/DepositOrders.vue @@ -2,6 +2,7 @@ import { ref, onMounted } from 'vue'; import { ElMessage, ElMessageBox } from 'element-plus'; import { useAdminLocale } from '../composables/useAdminLocale'; +import { resolveApiError } from '../i18n/form-validation'; import api from '../api'; import AdminTableEmpty from '../components/AdminTableEmpty.vue'; import { formatAmount } from '../utils/format-amount'; @@ -27,6 +28,20 @@ interface DepositOrderRow { paymentMethodName: string | null; } +interface DepositAuditLogRow { + id: string; + action: string; + actorId: string | null; + actorType: string; + actorUsername: string | null; + statusBefore: string | null; + statusAfter: string; + amount: string | null; + approvedAmount: string | null; + remark: string | null; + createdAt: string; +} + const items = ref([]); const total = ref(0); const page = ref(1); @@ -53,6 +68,12 @@ const rejectReason = ref(''); const previewUrl = ref(''); const previewVisible = ref(false); +// Audit history dialog +const auditDialogVisible = ref(false); +const auditTarget = ref(null); +const auditLogs = ref([]); +const auditLoading = ref(false); + async function fetchList() { loading.value = true; try { @@ -87,6 +108,32 @@ function openScreenshot(url: string) { previewVisible.value = true; } +async function openAuditHistory(row: DepositOrderRow) { + auditTarget.value = row; + auditDialogVisible.value = true; + auditLoading.value = true; + auditLogs.value = []; + try { + const { data } = await api.get(`/admin/deposit-orders/${row.id}/audit-logs`); + auditLogs.value = data.data?.items ?? []; + } catch { + auditLogs.value = []; + } finally { + auditLoading.value = false; + } +} + +function auditActionLabel(action: string) { + const key = `deposit.audit_${action.toLowerCase()}`; + const translated = t(key); + return translated !== key ? translated : action; +} + +function auditActorLabel(row: DepositAuditLogRow) { + if (row.actorUsername) return row.actorUsername; + return row.actorType === 'PLAYER' ? t('deposit.player') : '-'; +} + async function confirmDepositAction(message: string, title: string): Promise { try { await ElMessageBox.confirm(message, title, { @@ -101,8 +148,7 @@ async function confirmDepositAction(message: string, title: string): Promise{{ row.remark || row.rejectReason || '-' }} {{ new Date(row.createdAt).toLocaleString() }} +