From 2a8b8415e7174bd00e978250c9b11d7c35557613 Mon Sep 17 00:00:00 2001 From: Mars <3361409208a@gmail.com> Date: Tue, 16 Jun 2026 10:00:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=B5=9B=E4=BA=8B=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=97=A5=E5=BF=97=E4=B8=8E=E5=AE=A1=E8=AE=A1?= =?UTF-8?q?=E9=A1=B5=E4=BD=93=E9=AA=8C=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API 为赛事/联赛/盘口/赔率/冠军盘写操作写入 CATALOG 审计日志,并新增 catalog-audit-logs 接口 - 管理端将赛事日志独立至赛事管理子页,通用操作日志排除 CATALOG 模块 - 统一赛事子页 list-chrome 布局与面包屑;修复操作日志页表格滚动与分页不可见问题 - 补充中英文/马来语文案及 UAT 验收项 SEC013/SEC014 Co-authored-by: Cursor --- apps/admin/src/App.vue | 1 - apps/admin/src/components/AuditLogTable.vue | 194 +++++++++ apps/admin/src/components/MatchesSubNav.vue | 16 +- apps/admin/src/i18n/admin-messages.ts | 15 +- apps/admin/src/i18n/admin-pages-ms.ts | 35 +- apps/admin/src/i18n/admin-pages.ts | 70 +++- apps/admin/src/layouts/ManageLayout.vue | 2 +- apps/admin/src/router/index.ts | 5 + apps/admin/src/utils/admin-breadcrumb.ts | 12 - apps/admin/src/views/Audit.vue | 164 +------- apps/admin/src/views/MarketTemplates.vue | 10 +- apps/admin/src/views/MatchesAudit.vue | 21 + .../applications/admin/admin.controller.ts | 374 ++++++++++++++++-- .../domains/operations/audit/audit.service.ts | 5 +- .../operations/audit/catalog-audit.spec.ts | 53 +++ .../domains/operations/audit/catalog-audit.ts | 87 ++++ docs/UAT_CHECKLIST.md | 2 + 17 files changed, 850 insertions(+), 216 deletions(-) create mode 100644 apps/admin/src/components/AuditLogTable.vue create mode 100644 apps/admin/src/views/MatchesAudit.vue create mode 100644 apps/api/src/domains/operations/audit/catalog-audit.spec.ts create mode 100644 apps/api/src/domains/operations/audit/catalog-audit.ts diff --git a/apps/admin/src/App.vue b/apps/admin/src/App.vue index b97c95a..137036e 100644 --- a/apps/admin/src/App.vue +++ b/apps/admin/src/App.vue @@ -261,7 +261,6 @@ body::-webkit-scrollbar { } .list-chrome__left .matches-subnav--embedded { align-self: center; - height: var(--list-chrome-control-h); } .list-chrome__actions { display: flex; diff --git a/apps/admin/src/components/AuditLogTable.vue b/apps/admin/src/components/AuditLogTable.vue new file mode 100644 index 0000000..17b9803 --- /dev/null +++ b/apps/admin/src/components/AuditLogTable.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/apps/admin/src/components/MatchesSubNav.vue b/apps/admin/src/components/MatchesSubNav.vue index f3b7403..6c127aa 100644 --- a/apps/admin/src/components/MatchesSubNav.vue +++ b/apps/admin/src/components/MatchesSubNav.vue @@ -16,6 +16,7 @@ const tabs = [ { path: '/matches', labelKey: 'nav.matches.fixtures' }, { path: '/matches/outrights', labelKey: 'nav.matches.outrights' }, { path: '/matches/market-templates', labelKey: 'nav.matches.market_templates' }, + { path: '/matches/audit-logs', labelKey: 'nav.matches.audit_logs' }, ]; function isActive(path: string) { @@ -25,11 +26,15 @@ function isActive(path: string) { if (path === '/matches/market-templates') { return route.path === '/matches/market-templates'; } + if (path === '/matches/audit-logs') { + return route.path === '/matches/audit-logs'; + } return ( route.path === '/matches' || (route.path.startsWith('/matches/') && !route.path.startsWith('/matches/outrights') && - !route.path.startsWith('/matches/market-templates')) + !route.path.startsWith('/matches/market-templates') && + !route.path.startsWith('/matches/audit-logs')) ); } @@ -73,9 +78,16 @@ function isActive(path: string) { border-right: 1px solid var(--border); background: transparent; flex-shrink: 0; - height: 44px; + min-height: 0; + height: auto; + align-self: center; box-sizing: border-box; } +.matches-subnav--embedded .matches-subnav__item { + height: 32px; + padding: 0 12px; + font-size: 12px; +} .matches-subnav__item { padding: 0 14px; height: 34px; diff --git a/apps/admin/src/i18n/admin-messages.ts b/apps/admin/src/i18n/admin-messages.ts index aba0a42..6a64988 100644 --- a/apps/admin/src/i18n/admin-messages.ts +++ b/apps/admin/src/i18n/admin-messages.ts @@ -247,6 +247,7 @@ const zh: Record = { 'nav.matches.fixtures': '赛事配置', 'nav.matches.outrights': '优胜赛配置(盘口)', 'nav.matches.market_templates': '盘口模板', + 'nav.matches.audit_logs': '赛事日志', 'page.matches.title': '赛事管理', 'page.matches.desc': '草稿可编辑、删除;已发布可改开赛时间与热门;未结算可下架', 'page.bets.title': '注单管理', @@ -254,7 +255,9 @@ const zh: Record = { 'page.cashback.title': '返水管理', 'page.cashback.desc': '按周期生成返水并发放', 'page.audit.title': '操作日志', - 'page.audit.desc': '记录所有管理员操作行为', + 'page.audit.desc': '账务、用户、结算等非赛事类管理操作', + 'page.matches_audit.title': '赛事日志', + 'page.matches_audit.desc': '联赛、赛事、盘口、赔率、冠军盘等赛事管理操作', 'page.settlement.title': '赛事结算', 'page.agent_dash.title': '代理概览', 'page.agent_dash.desc': '下线经营概况与分布', @@ -543,6 +546,7 @@ const en: Record = { 'nav.matches.fixtures': 'Fixtures', 'nav.matches.outrights': 'Outright odds', 'nav.matches.market_templates': 'Market templates', + 'nav.matches.audit_logs': 'Match logs', 'page.matches.title': 'Matches', 'page.matches.desc': 'Edit/delete drafts; adjust kickoff and featured when published; unpublish while unsettled', 'page.bets.title': 'Bets', @@ -550,7 +554,9 @@ const en: Record = { 'page.cashback.title': 'Cashback', 'page.cashback.desc': 'Generate and issue cashback by period', 'page.audit.title': 'Audit Log', - 'page.audit.desc': 'Administrator action history', + 'page.audit.desc': 'Non-catalog admin actions: users, wallet, settlement, etc.', + 'page.matches_audit.title': 'Match Logs', + 'page.matches_audit.desc': 'Leagues, fixtures, markets, odds, and outright operations', 'page.settlement.title': 'Settlement', 'page.agent_dash.title': 'Agent overview', 'page.agent_dash.desc': 'Downline performance at a glance', @@ -839,6 +845,7 @@ const ms: Record = { 'nav.matches.fixtures': 'Konfigurasi perlawanan', 'nav.matches.outrights': 'Odds juara', 'nav.matches.market_templates': 'Templat pasaran', + 'nav.matches.audit_logs': 'Log perlawanan', 'page.matches.title': 'Perlawanan', 'page.matches.desc': 'Edit/padam draf; laraskan masa mula bila diterbitkan; nyahterbit jika belum selesai', 'page.bets.title': 'Pertaruhan', @@ -846,7 +853,9 @@ const ms: Record = { 'page.cashback.title': 'Rebat', 'page.cashback.desc': 'Jana dan keluarkan rebat mengikut tempoh', 'page.audit.title': 'Log audit', - 'page.audit.desc': 'Sejarah tindakan pentadbir', + 'page.audit.desc': 'Tindakan bukan katalog: pengguna, dompet, penyelesaian, dll.', + 'page.matches_audit.title': 'Log perlawanan', + 'page.matches_audit.desc': 'Liga, perlawanan, pasaran, odds, dan outright', 'page.settlement.title': 'Penyelesaian', 'page.agent_dash.title': 'Gambaran ejen', 'page.agent_dash.desc': 'Prestasi downline sepintas lalu', diff --git a/apps/admin/src/i18n/admin-pages-ms.ts b/apps/admin/src/i18n/admin-pages-ms.ts index 8d44e7f..49621f3 100644 --- a/apps/admin/src/i18n/admin-pages-ms.ts +++ b/apps/admin/src/i18n/admin-pages-ms.ts @@ -332,7 +332,7 @@ export const adminPagesMs: Record = { 'bet.col.line': 'Garisan', 'bet.col.result': 'Keputusan', - 'audit.module_ph': 'cth. USERS, AGENTS', + 'audit.module_ph': 'cth. USERS, SETTLEMENT', 'audit.col.time': 'Masa', 'audit.col.operator': 'Pengendali', 'audit.col.action': 'Tindakan', @@ -360,6 +360,39 @@ export const adminPagesMs: Record = { 'audit.action.UPDATE_STAFF': 'Kemas kini kakitangan', 'audit.action.PURGE_UNUSED_FILES': 'Padam media tidak digunakan', 'audit.action.FORGOT_PASSWORD_RESET': 'Pemain set semula kata laluan', + 'audit.action.CREATE_LEAGUE': 'Cipta liga', + 'audit.action.UPDATE_LEAGUE': 'Kemas kini liga', + 'audit.action.ARCHIVE_LEAGUE': 'Arkib liga', + 'audit.action.CREATE_TEAM': 'Cipta pasukan', + 'audit.action.CREATE_MATCH': 'Cipta perlawanan', + 'audit.action.UPDATE_MATCH': 'Kemas kini perlawanan', + 'audit.action.DELETE_MATCH': 'Padam perlawanan', + 'audit.action.ARCHIVE_MATCH': 'Arkib perlawanan', + 'audit.action.IMPORT_MATCHES': 'Import perlawanan', + 'audit.action.PUBLISH_MATCH': 'Terbit perlawanan', + 'audit.action.UNPUBLISH_MATCH': 'Nyahterbit perlawanan', + 'audit.action.CLOSE_MATCH': 'Tutup perlawanan', + 'audit.action.REOPEN_MATCH': 'Buka semula perlawanan', + 'audit.action.CANCEL_MATCH': 'Batalkan perlawanan', + 'audit.action.CREATE_MARKET_TEMPLATE': 'Cipta templat pasaran', + 'audit.action.UPDATE_MARKET_TEMPLATE': 'Kemas kini templat pasaran', + 'audit.action.DUPLICATE_MARKET_TEMPLATE': 'Salin templat pasaran', + 'audit.action.SET_DEFAULT_MARKET_TEMPLATE': 'Tetapkan templat lalai', + 'audit.action.GENERATE_MATCH_MARKETS': 'Jana pasaran perlawanan', + 'audit.action.APPLY_MARKET_TEMPLATE': 'Guna templat pasaran', + 'audit.action.BULK_SAVE_MATCH_MARKETS': 'Simpan pasaran pukal', + 'audit.action.UPDATE_MATCH_ODDS': 'Kemas kini odds perlawanan', + 'audit.action.UPDATE_MARKET': 'Kemas kini pasaran', + 'audit.action.UPDATE_SELECTION': 'Kemas kini pilihan', + 'audit.action.CREATE_OUTRIGHT': 'Cipta outright', + 'audit.action.UPDATE_OUTRIGHT': 'Kemas kini outright', + 'audit.action.UPDATE_OUTRIGHT_ODDS': 'Kemas kini odds outright', + 'audit.action.ADD_OUTRIGHT_SELECTION': 'Tambah pilihan outright', + 'audit.action.BATCH_ADD_OUTRIGHT_SELECTIONS': 'Tambah pilihan outright pukal', + 'audit.action.UPDATE_OUTRIGHT_SELECTION': 'Kemas kini pilihan outright', + 'audit.action.REMOVE_OUTRIGHT_SELECTION': 'Buang pilihan outright', + 'audit.action.IMPORT_WC2026_OUTRIGHT': 'Import outright WC2026', + 'audit.module.CATALOG': 'Pengurusan perlawanan', 'audit.module.USERS': 'Pemain', 'audit.module.AGENTS': 'Ejen', 'audit.module.SYSTEM': 'Sistem', diff --git a/apps/admin/src/i18n/admin-pages.ts b/apps/admin/src/i18n/admin-pages.ts index 1549a4f..21a776b 100644 --- a/apps/admin/src/i18n/admin-pages.ts +++ b/apps/admin/src/i18n/admin-pages.ts @@ -354,7 +354,7 @@ export const adminPagesZh: Record = { 'bet.col.line': '盘口', 'bet.col.result': '赛果', - 'audit.module_ph': '如 USERS、AGENTS', + 'audit.module_ph': '如 USERS、SETTLEMENT', 'audit.col.time': '时间', 'audit.col.operator': '操作人', 'audit.col.action': '操作', @@ -382,6 +382,39 @@ export const adminPagesZh: Record = { 'audit.action.UPDATE_STAFF': '更新后台员工', 'audit.action.PURGE_UNUSED_FILES': '清理未引用媒体', 'audit.action.FORGOT_PASSWORD_RESET': '玩家找回密码', + 'audit.action.CREATE_LEAGUE': '新建联赛', + 'audit.action.UPDATE_LEAGUE': '更新联赛', + 'audit.action.ARCHIVE_LEAGUE': '归档联赛', + 'audit.action.CREATE_TEAM': '新建球队', + 'audit.action.CREATE_MATCH': '新建赛事', + 'audit.action.UPDATE_MATCH': '更新赛事', + 'audit.action.DELETE_MATCH': '删除赛事', + 'audit.action.ARCHIVE_MATCH': '归档赛事', + 'audit.action.IMPORT_MATCHES': '批量导入赛事', + 'audit.action.PUBLISH_MATCH': '发布赛事', + 'audit.action.UNPUBLISH_MATCH': '下架赛事', + 'audit.action.CLOSE_MATCH': '封盘赛事', + 'audit.action.REOPEN_MATCH': '重新开放赛事', + 'audit.action.CANCEL_MATCH': '取消赛事', + 'audit.action.CREATE_MARKET_TEMPLATE': '新建盘口模板', + 'audit.action.UPDATE_MARKET_TEMPLATE': '更新盘口模板', + 'audit.action.DUPLICATE_MARKET_TEMPLATE': '复制盘口模板', + 'audit.action.SET_DEFAULT_MARKET_TEMPLATE': '设为默认盘口模板', + 'audit.action.GENERATE_MATCH_MARKETS': '生成赛事盘口', + 'audit.action.APPLY_MARKET_TEMPLATE': '应用盘口模板', + 'audit.action.BULK_SAVE_MATCH_MARKETS': '批量保存赛事盘口', + 'audit.action.UPDATE_MATCH_ODDS': '更新赛事赔率', + 'audit.action.UPDATE_MARKET': '更新盘口', + 'audit.action.UPDATE_SELECTION': '更新选项', + 'audit.action.CREATE_OUTRIGHT': '新建冠军盘', + 'audit.action.UPDATE_OUTRIGHT': '更新冠军盘', + 'audit.action.UPDATE_OUTRIGHT_ODDS': '更新冠军盘赔率', + 'audit.action.ADD_OUTRIGHT_SELECTION': '添加冠军盘选项', + 'audit.action.BATCH_ADD_OUTRIGHT_SELECTIONS': '批量添加冠军盘选项', + 'audit.action.UPDATE_OUTRIGHT_SELECTION': '更新冠军盘选项', + 'audit.action.REMOVE_OUTRIGHT_SELECTION': '移除冠军盘选项', + 'audit.action.IMPORT_WC2026_OUTRIGHT': '导入世界杯冠军盘', + 'audit.module.CATALOG': '赛事管理', 'audit.module.USERS': '玩家', 'audit.module.AGENTS': '代理', 'audit.module.SYSTEM': '系统', @@ -1393,7 +1426,7 @@ export const adminPagesEn: Record = { 'bet.col.line': 'Line', 'bet.col.result': 'Result', - 'audit.module_ph': 'e.g. USERS, AGENTS', + 'audit.module_ph': 'e.g. USERS, SETTLEMENT', 'audit.col.time': 'Time', 'audit.col.operator': 'Operator', 'audit.col.action': 'Action', @@ -1421,6 +1454,39 @@ export const adminPagesEn: Record = { 'audit.action.UPDATE_STAFF': 'Update staff account', 'audit.action.PURGE_UNUSED_FILES': 'Purge unused media', 'audit.action.FORGOT_PASSWORD_RESET': 'Player forgot-password reset', + 'audit.action.CREATE_LEAGUE': 'Create league', + 'audit.action.UPDATE_LEAGUE': 'Update league', + 'audit.action.ARCHIVE_LEAGUE': 'Archive league', + 'audit.action.CREATE_TEAM': 'Create team', + 'audit.action.CREATE_MATCH': 'Create match', + 'audit.action.UPDATE_MATCH': 'Update match', + 'audit.action.DELETE_MATCH': 'Delete match', + 'audit.action.ARCHIVE_MATCH': 'Archive match', + 'audit.action.IMPORT_MATCHES': 'Import matches', + 'audit.action.PUBLISH_MATCH': 'Publish match', + 'audit.action.UNPUBLISH_MATCH': 'Unpublish match', + 'audit.action.CLOSE_MATCH': 'Close match', + 'audit.action.REOPEN_MATCH': 'Reopen match', + 'audit.action.CANCEL_MATCH': 'Cancel match', + 'audit.action.CREATE_MARKET_TEMPLATE': 'Create market template', + 'audit.action.UPDATE_MARKET_TEMPLATE': 'Update market template', + 'audit.action.DUPLICATE_MARKET_TEMPLATE': 'Duplicate market template', + 'audit.action.SET_DEFAULT_MARKET_TEMPLATE': 'Set default market template', + 'audit.action.GENERATE_MATCH_MARKETS': 'Generate match markets', + 'audit.action.APPLY_MARKET_TEMPLATE': 'Apply market template', + 'audit.action.BULK_SAVE_MATCH_MARKETS': 'Bulk save match markets', + 'audit.action.UPDATE_MATCH_ODDS': 'Update match odds', + 'audit.action.UPDATE_MARKET': 'Update market', + 'audit.action.UPDATE_SELECTION': 'Update selection', + 'audit.action.CREATE_OUTRIGHT': 'Create outright', + 'audit.action.UPDATE_OUTRIGHT': 'Update outright', + 'audit.action.UPDATE_OUTRIGHT_ODDS': 'Update outright odds', + 'audit.action.ADD_OUTRIGHT_SELECTION': 'Add outright selection', + 'audit.action.BATCH_ADD_OUTRIGHT_SELECTIONS': 'Batch add outright selections', + 'audit.action.UPDATE_OUTRIGHT_SELECTION': 'Update outright selection', + 'audit.action.REMOVE_OUTRIGHT_SELECTION': 'Remove outright selection', + 'audit.action.IMPORT_WC2026_OUTRIGHT': 'Import WC2026 outright', + 'audit.module.CATALOG': 'Catalog', 'audit.module.USERS': 'Players', 'audit.module.AGENTS': 'Agents', 'audit.module.SYSTEM': 'System', diff --git a/apps/admin/src/layouts/ManageLayout.vue b/apps/admin/src/layouts/ManageLayout.vue index c1189a1..387e766 100644 --- a/apps/admin/src/layouts/ManageLayout.vue +++ b/apps/admin/src/layouts/ManageLayout.vue @@ -48,7 +48,7 @@ const adminMenus = computed(() => { { path: '/bets', label: t('nav.bets'), icon: 'bets', permissions: [AdminPerm.bets] }, { path: '/contents', label: t('nav.contents'), icon: 'contents', permissions: [AdminPerm.content] }, { path: '/media', label: t('nav.media'), icon: 'media', permissions: [AdminPerm.content, AdminPerm.matches] }, - { path: '/audit', label: t('nav.audit'), icon: 'audit', permissions: [AdminPerm.audit] }, + { path: '/audit', label: t('nav.audit'), icon: 'audit', permissions: [AdminPerm.audit], excludeRoles: ['MATCH_ADMIN'] }, { path: '/staff', label: t('nav.staff'), icon: 'users', permissions: [AdminPerm.settings] }, { path: '/smoke-tests', label: t('nav.smoke_tests'), icon: 'smoke-tests', permissions: [AdminPerm.settings] }, ]; diff --git a/apps/admin/src/router/index.ts b/apps/admin/src/router/index.ts index cd97cab..856d42d 100644 --- a/apps/admin/src/router/index.ts +++ b/apps/admin/src/router/index.ts @@ -86,6 +86,11 @@ const router = createRouter({ component: () => import('../views/MarketTemplates.vue'), meta: { adminOnly: true, permissions: [AdminPerm.matches] }, }, + { + path: 'matches/audit-logs', + component: () => import('../views/MatchesAudit.vue'), + meta: { adminOnly: true, permissions: [AdminPerm.matches] }, + }, { path: 'matches/:matchId/edit', name: 'admin-match-edit', diff --git a/apps/admin/src/utils/admin-breadcrumb.ts b/apps/admin/src/utils/admin-breadcrumb.ts index de11abb..0cbf8f2 100644 --- a/apps/admin/src/utils/admin-breadcrumb.ts +++ b/apps/admin/src/utils/admin-breadcrumb.ts @@ -14,12 +14,6 @@ export function resolveAdminBreadcrumb( { label: t('breadcrumb.settlement') }, ]; } - if (path === '/matches/market-templates') { - return [ - { label: t('nav.matches'), to: '/matches' }, - { label: t('nav.matches.market_templates') }, - ]; - } if (/^\/matches\/[^/]+\/edit/.test(path)) { return [ { label: t('nav.matches'), to: '/matches' }, @@ -32,12 +26,6 @@ export function resolveAdminBreadcrumb( { label: t('breadcrumb.match_markets') }, ]; } - if (path === '/matches/outrights') { - return [ - { label: t('nav.matches'), to: '/matches' }, - { label: t('nav.matches.outrights') }, - ]; - } if (path === '/dashboard/players') { return [ { label: t('nav.dashboard'), to: '/' }, diff --git a/apps/admin/src/views/Audit.vue b/apps/admin/src/views/Audit.vue index 49788ea..26a3df7 100644 --- a/apps/admin/src/views/Audit.vue +++ b/apps/admin/src/views/Audit.vue @@ -1,169 +1,9 @@ - - diff --git a/apps/admin/src/views/MarketTemplates.vue b/apps/admin/src/views/MarketTemplates.vue index 0dc3c76..2edf75d 100644 --- a/apps/admin/src/views/MarketTemplates.vue +++ b/apps/admin/src/views/MarketTemplates.vue @@ -477,8 +477,14 @@ load();