feat: 赛事管理操作日志与审计页体验优化
- API 为赛事/联赛/盘口/赔率/冠军盘写操作写入 CATALOG 审计日志,并新增 catalog-audit-logs 接口 - 管理端将赛事日志独立至赛事管理子页,通用操作日志排除 CATALOG 模块 - 统一赛事子页 list-chrome 布局与面包屑;修复操作日志页表格滚动与分页不可见问题 - 补充中英文/马来语文案及 UAT 验收项 SEC013/SEC014 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -36,13 +36,14 @@ export class AuditService {
|
||||
async list(
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
module?: string,
|
||||
filters?: { module?: string; excludeModule?: string },
|
||||
viewer?: AuditViewerScope,
|
||||
) {
|
||||
const skip = (page - 1) * pageSize;
|
||||
const scopeWhere = viewer ? await buildAuditListScopeWhere(this.prisma, viewer) : undefined;
|
||||
const where: Prisma.AuditLogWhereInput = {
|
||||
...(module ? { module } : {}),
|
||||
...(filters?.module ? { module: filters.module } : {}),
|
||||
...(filters?.excludeModule ? { module: { not: filters.excludeModule } } : {}),
|
||||
...(scopeWhere ?? {}),
|
||||
};
|
||||
const [rows, total] = await Promise.all([
|
||||
|
||||
53
apps/api/src/domains/operations/audit/catalog-audit.spec.ts
Normal file
53
apps/api/src/domains/operations/audit/catalog-audit.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
CatalogAuditAction,
|
||||
CATALOG_AUDIT_MODULE,
|
||||
logCatalogAudit,
|
||||
summarizeOddsUpdates,
|
||||
} from './catalog-audit';
|
||||
import { AuditService } from './audit.service';
|
||||
|
||||
describe('catalog-audit', () => {
|
||||
it('uses CATALOG module and stable action codes', () => {
|
||||
expect(CATALOG_AUDIT_MODULE).toBe('CATALOG');
|
||||
expect(CatalogAuditAction.PUBLISH_MATCH).toBe('PUBLISH_MATCH');
|
||||
expect(CatalogAuditAction.UPDATE_MATCH_ODDS).toBe('UPDATE_MATCH_ODDS');
|
||||
expect(Object.keys(CatalogAuditAction).length).toBeGreaterThanOrEqual(30);
|
||||
});
|
||||
|
||||
it('summarizeOddsUpdates truncates large batches', () => {
|
||||
const ids = Array.from({ length: 25 }, (_, i) => BigInt(i + 1));
|
||||
const summary = summarizeOddsUpdates(ids, 20);
|
||||
expect(summary.updatedCount).toBe(25);
|
||||
expect(summary.selectionIds).toHaveLength(20);
|
||||
expect(summary.selectionIdsTruncated).toBe(true);
|
||||
expect(summary.total).toBe(25);
|
||||
});
|
||||
|
||||
it('summarizeOddsUpdates keeps full list when small', () => {
|
||||
const summary = summarizeOddsUpdates(['1', '2']);
|
||||
expect(summary.selectionIds).toEqual(['1', '2']);
|
||||
expect(summary.selectionIdsTruncated).toBeUndefined();
|
||||
});
|
||||
|
||||
it('logCatalogAudit delegates to AuditService.log', async () => {
|
||||
const audit = {
|
||||
log: jest.fn().mockResolvedValue({ id: 1n }),
|
||||
} as unknown as AuditService;
|
||||
await logCatalogAudit(audit, {
|
||||
operatorId: 99n,
|
||||
action: CatalogAuditAction.PUBLISH_MATCH,
|
||||
targetId: '42',
|
||||
afterData: { status: 'PUBLISHED' },
|
||||
});
|
||||
expect(audit.log).toHaveBeenCalledWith({
|
||||
operatorId: 99n,
|
||||
operatorType: 'ADMIN',
|
||||
action: 'PUBLISH_MATCH',
|
||||
module: CATALOG_AUDIT_MODULE,
|
||||
targetId: '42',
|
||||
beforeData: undefined,
|
||||
afterData: { status: 'PUBLISHED' },
|
||||
ipAddress: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
87
apps/api/src/domains/operations/audit/catalog-audit.ts
Normal file
87
apps/api/src/domains/operations/audit/catalog-audit.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { AuditService } from './audit.service';
|
||||
|
||||
export const CATALOG_AUDIT_MODULE = 'CATALOG';
|
||||
|
||||
export const CatalogAuditAction = {
|
||||
CREATE_LEAGUE: 'CREATE_LEAGUE',
|
||||
UPDATE_LEAGUE: 'UPDATE_LEAGUE',
|
||||
ARCHIVE_LEAGUE: 'ARCHIVE_LEAGUE',
|
||||
CREATE_TEAM: 'CREATE_TEAM',
|
||||
CREATE_MATCH: 'CREATE_MATCH',
|
||||
UPDATE_MATCH: 'UPDATE_MATCH',
|
||||
DELETE_MATCH: 'DELETE_MATCH',
|
||||
ARCHIVE_MATCH: 'ARCHIVE_MATCH',
|
||||
IMPORT_MATCHES: 'IMPORT_MATCHES',
|
||||
PUBLISH_MATCH: 'PUBLISH_MATCH',
|
||||
UNPUBLISH_MATCH: 'UNPUBLISH_MATCH',
|
||||
CLOSE_MATCH: 'CLOSE_MATCH',
|
||||
REOPEN_MATCH: 'REOPEN_MATCH',
|
||||
CANCEL_MATCH: 'CANCEL_MATCH',
|
||||
CREATE_MARKET_TEMPLATE: 'CREATE_MARKET_TEMPLATE',
|
||||
UPDATE_MARKET_TEMPLATE: 'UPDATE_MARKET_TEMPLATE',
|
||||
DUPLICATE_MARKET_TEMPLATE: 'DUPLICATE_MARKET_TEMPLATE',
|
||||
SET_DEFAULT_MARKET_TEMPLATE: 'SET_DEFAULT_MARKET_TEMPLATE',
|
||||
GENERATE_MATCH_MARKETS: 'GENERATE_MATCH_MARKETS',
|
||||
APPLY_MARKET_TEMPLATE: 'APPLY_MARKET_TEMPLATE',
|
||||
BULK_SAVE_MATCH_MARKETS: 'BULK_SAVE_MATCH_MARKETS',
|
||||
UPDATE_MATCH_ODDS: 'UPDATE_MATCH_ODDS',
|
||||
UPDATE_MARKET: 'UPDATE_MARKET',
|
||||
UPDATE_SELECTION: 'UPDATE_SELECTION',
|
||||
CREATE_OUTRIGHT: 'CREATE_OUTRIGHT',
|
||||
UPDATE_OUTRIGHT: 'UPDATE_OUTRIGHT',
|
||||
UPDATE_OUTRIGHT_ODDS: 'UPDATE_OUTRIGHT_ODDS',
|
||||
ADD_OUTRIGHT_SELECTION: 'ADD_OUTRIGHT_SELECTION',
|
||||
BATCH_ADD_OUTRIGHT_SELECTIONS: 'BATCH_ADD_OUTRIGHT_SELECTIONS',
|
||||
UPDATE_OUTRIGHT_SELECTION: 'UPDATE_OUTRIGHT_SELECTION',
|
||||
REMOVE_OUTRIGHT_SELECTION: 'REMOVE_OUTRIGHT_SELECTION',
|
||||
IMPORT_WC2026_OUTRIGHT: 'IMPORT_WC2026_OUTRIGHT',
|
||||
} as const;
|
||||
|
||||
export type CatalogAuditActionCode =
|
||||
(typeof CatalogAuditAction)[keyof typeof CatalogAuditAction];
|
||||
|
||||
export function catalogTargetId(id: string | bigint | null | undefined): string | undefined {
|
||||
if (id == null) return undefined;
|
||||
return String(id);
|
||||
}
|
||||
|
||||
export function summarizeOddsUpdates(
|
||||
selectionIds: Array<string | bigint>,
|
||||
maxIds = 20,
|
||||
): {
|
||||
updatedCount: number;
|
||||
selectionIds: string[];
|
||||
selectionIdsTruncated?: boolean;
|
||||
total?: number;
|
||||
} {
|
||||
const ids = selectionIds.map((id) => String(id));
|
||||
const truncated = ids.length > maxIds;
|
||||
return {
|
||||
updatedCount: ids.length,
|
||||
selectionIds: ids.slice(0, maxIds),
|
||||
...(truncated ? { selectionIdsTruncated: true, total: ids.length } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function logCatalogAudit(
|
||||
audit: AuditService,
|
||||
data: {
|
||||
operatorId: bigint;
|
||||
action: CatalogAuditActionCode;
|
||||
targetId?: string | bigint | null;
|
||||
beforeData?: unknown;
|
||||
afterData?: unknown;
|
||||
ipAddress?: string;
|
||||
},
|
||||
) {
|
||||
return audit.log({
|
||||
operatorId: data.operatorId,
|
||||
operatorType: 'ADMIN',
|
||||
action: data.action,
|
||||
module: CATALOG_AUDIT_MODULE,
|
||||
targetId: catalogTargetId(data.targetId),
|
||||
beforeData: data.beforeData,
|
||||
afterData: data.afterData,
|
||||
ipAddress: data.ipAddress,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user