feat(db): A5 审计表分区、append-only 与种子
audit_logs 按月 RANGE 分区、复合主键与 6 组索引;ensure_partitions 幂等建分区;UPDATE/DELETE/TRUNCATE 触发器与 lingxi_app 最小权限;audit_export_jobs;种子新增 3 项配置与 6 条演示审计。
This commit is contained in:
1 parent
408dc5e81b
commit
6215f204ce
6 files changed
+441
-4
No files matched your search
+176
@@ -898,6 +898,181 @@ async function seedDicts(): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A5 审计演示数据(幂等):固定 id + 当前时间附近,写入 6 条覆盖登录 / 客户 / 权限 / 配置 / 审计查看 / 导出。
|
||||
* 注意:audit_logs 是 append-only(DB 层禁 UPDATE / DELETE),因此只能“存在即跳过”,不能 upsert 覆盖。
|
||||
* 集成测试使用独立前缀与筛选条件,不受这些演示数据影响。
|
||||
*/
|
||||
const AUDIT_DEMO_IDS = [
|
||||
'01HZZZZZZZZZZZZZZZZZZZZZ01',
|
||||
'01HZZZZZZZZZZZZZZZZZZZZZ02',
|
||||
'01HZZZZZZZZZZZZZZZZZZZZZ03',
|
||||
'01HZZZZZZZZZZZZZZZZZZZZZ04',
|
||||
'01HZZZZZZZZZZZZZZZZZZZZZ05',
|
||||
'01HZZZZZZZZZZZZZZZZZZZZZ06',
|
||||
] as const;
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function, complexity -- 6 条演示数据集中定义便于核对
|
||||
async function seedAuditDemos(): Promise<void> {
|
||||
const admin = await prisma.user.findUnique({
|
||||
where: { account: 'admin' },
|
||||
select: { id: true, name: true, account: true },
|
||||
});
|
||||
if (admin === null) {
|
||||
console.log('[seed] 跳过 A5 审计演示数据(admin 账号不存在)');
|
||||
return;
|
||||
}
|
||||
const customer = await prisma.customer.findFirst({
|
||||
where: { deletedAt: null },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
const role = await prisma.role.findFirst({
|
||||
where: { code: 'platform_admin' },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
const demos = [
|
||||
{
|
||||
id: AUDIT_DEMO_IDS[0],
|
||||
occurredAt: new Date(now - 5 * 60_000),
|
||||
actionCode: 'identity.auth.login',
|
||||
category: 'auth',
|
||||
targetType: 'user',
|
||||
targetId: admin.id,
|
||||
targetName: admin.name,
|
||||
result: 'success',
|
||||
httpMethod: 'POST',
|
||||
httpPath: '/api/v1/auth/login',
|
||||
traceId: 'seed-audit-login',
|
||||
beforeJson: null,
|
||||
afterJson: { account: admin.account },
|
||||
changesJson: null,
|
||||
},
|
||||
{
|
||||
id: AUDIT_DEMO_IDS[1],
|
||||
occurredAt: new Date(now - 4 * 60_000),
|
||||
actionCode: 'client.customer.update',
|
||||
category: 'client',
|
||||
targetType: 'customer',
|
||||
targetId: customer?.id ?? null,
|
||||
targetName: customer?.name ?? '示例客户',
|
||||
customerId: customer?.id ?? null,
|
||||
customerName: customer?.name ?? '示例客户',
|
||||
result: 'success',
|
||||
httpMethod: 'PATCH',
|
||||
httpPath: customer === null ? '/api/v1/customers' : `/api/v1/customers/${customer.id}`,
|
||||
traceId: 'seed-audit-customer',
|
||||
beforeJson: { remark: '旧备注' },
|
||||
afterJson: { remark: 'A5 审计演示:客户备注已更新' },
|
||||
changesJson: null,
|
||||
},
|
||||
{
|
||||
id: AUDIT_DEMO_IDS[2],
|
||||
occurredAt: new Date(now - 3 * 60_000),
|
||||
actionCode: 'identity.role.update',
|
||||
category: 'permission',
|
||||
targetType: 'role',
|
||||
targetId: role?.id ?? null,
|
||||
targetName: role?.name ?? '示例角色',
|
||||
result: 'success',
|
||||
httpMethod: 'PATCH',
|
||||
httpPath: role === null ? '/api/v1/roles' : `/api/v1/roles/${role.id}`,
|
||||
traceId: 'seed-audit-role',
|
||||
beforeJson: { permissions: ['user:view'] },
|
||||
afterJson: { permissions: ['user:view', 'role:view'] },
|
||||
changesJson: { permissions: { added: ['role:view'], removed: [] } },
|
||||
},
|
||||
{
|
||||
id: AUDIT_DEMO_IDS[3],
|
||||
occurredAt: new Date(now - 2 * 60_000),
|
||||
actionCode: 'platform.system_config.update',
|
||||
category: 'config',
|
||||
targetType: 'system_config',
|
||||
targetId: 'platform.page.default_limit',
|
||||
targetName: '列表默认条数',
|
||||
result: 'success',
|
||||
httpMethod: 'PUT',
|
||||
httpPath: '/api/v1/system/configs',
|
||||
traceId: 'seed-audit-config',
|
||||
beforeJson: { 'platform.page.default_limit': 20 },
|
||||
afterJson: { 'platform.page.default_limit': 20 },
|
||||
changesJson: [{ key: 'platform.page.default_limit', oldValue: 20, newValue: 20 }],
|
||||
},
|
||||
{
|
||||
id: AUDIT_DEMO_IDS[4],
|
||||
occurredAt: new Date(now - 60_000),
|
||||
actionCode: 'audit.log.view',
|
||||
category: 'sensitive',
|
||||
targetType: 'audit_log',
|
||||
targetId: null,
|
||||
targetName: null,
|
||||
result: 'success',
|
||||
httpMethod: 'GET',
|
||||
httpPath: '/api/v1/audit-logs',
|
||||
traceId: 'seed-audit-view',
|
||||
beforeJson: null,
|
||||
afterJson: null,
|
||||
changesJson: null,
|
||||
},
|
||||
{
|
||||
id: AUDIT_DEMO_IDS[5],
|
||||
occurredAt: new Date(now - 30_000),
|
||||
actionCode: 'audit.export',
|
||||
category: 'export',
|
||||
targetType: 'audit_export',
|
||||
targetId: null,
|
||||
targetName: '审计导出 CSV',
|
||||
result: 'success',
|
||||
httpMethod: 'POST',
|
||||
httpPath: '/api/v1/audit-logs/export',
|
||||
traceId: 'seed-audit-export',
|
||||
beforeJson: null,
|
||||
afterJson: { format: 'csv', filters: { result: 'failure' }, rowCount: 120 },
|
||||
changesJson: null,
|
||||
},
|
||||
];
|
||||
|
||||
let created = 0;
|
||||
for (const demo of demos) {
|
||||
const existing = await prisma.auditLog.findFirst({
|
||||
where: { id: demo.id },
|
||||
select: { id: true },
|
||||
});
|
||||
if (existing !== null) continue;
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
id: demo.id,
|
||||
occurredAt: demo.occurredAt,
|
||||
actorId: admin.id,
|
||||
actorName: admin.name,
|
||||
actorAccount: admin.account,
|
||||
actionCode: demo.actionCode,
|
||||
category: demo.category,
|
||||
targetType: demo.targetType,
|
||||
targetId: demo.targetId,
|
||||
targetName: demo.targetName,
|
||||
customerId: 'customerId' in demo ? demo.customerId : null,
|
||||
customerName: 'customerName' in demo ? demo.customerName : null,
|
||||
result: demo.result,
|
||||
httpMethod: demo.httpMethod,
|
||||
httpPath: demo.httpPath,
|
||||
traceId: demo.traceId,
|
||||
ip: '127.0.0.1',
|
||||
userAgent: 'seed',
|
||||
beforeJson: demo.beforeJson ?? undefined,
|
||||
afterJson: demo.afterJson ?? undefined,
|
||||
changesJson: demo.changesJson ?? undefined,
|
||||
},
|
||||
});
|
||||
created += 1;
|
||||
}
|
||||
console.log(
|
||||
`[seed] A5 审计演示数据就绪:新增 ${String(created)} 条 / 共 ${String(AUDIT_DEMO_IDS.length)} 条(幂等)`,
|
||||
);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await seedConfigs();
|
||||
await seedPermissions();
|
||||
@@ -907,6 +1082,7 @@ async function main(): Promise<void> {
|
||||
await seedDepartmentLeaders();
|
||||
await seedClient();
|
||||
await seedDicts();
|
||||
await seedAuditDemos();
|
||||
}
|
||||
|
||||
void main()
|
||||
|
||||
Reference in new issue
Block a user