feat(api): A5 审计采集、检索与导出

AuditInterceptor 成功 / 失败双写 audit_logs(响应前 await、脱敏、sink 缺失退化 Pino);登录 / 权限 / 客户 / 字典 / 配置等既有动作补审计;审计列表 / 详情 / 动作目录 / 异步 CSV 导出 / HMAC 签名下载;分区维护与保留归档占位;OpenAPI 快照更新。
This commit is contained in:
Stev_Wang committed 2026-11-04 16:26:42 +08:00
1 parent 6215f204ce
commit 6bdda6ec62
44 files changed
+3845 -127

No files matched your search

@@ -14,6 +14,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { AppException } from '../../common/errors/app-exception';
import { RequestContextService } from '../../common/security/request-context';
import type { ScopeContext } from '../../common/security/scope/scope-context';
import { isCustomerVisible, requireScopeContext } from '../../common/security/scope/scope-context';
import type { PrismaService } from '../../infra/prisma/prisma.service';
@@ -55,6 +56,7 @@ export class CustomerFieldsService {
constructor(
@Inject(PRISMA) private readonly prisma: PrismaService,
@Inject(SystemConfigService) private readonly systemConfig: SystemConfigService,
@Inject(RequestContextService) private readonly requestContext: RequestContextService,
) {}
async list(
@@ -128,7 +130,7 @@ export class CustomerFieldsService {
}
const customer = await this.prisma.customer.findFirst({
where: { id: customerId, deletedAt: null },
select: { id: true, status: true },
select: { id: true, name: true, status: true },
});
if (customer === null) {
throw new AppException({ code: ErrorCode.NOT_FOUND, message: '客户不存在' });
@@ -173,6 +175,21 @@ export class CustomerFieldsService {
where: { id },
include: FIELD_INCLUDE,
});
this.requestContext.patchAudit({
targetType: 'customer_field',
targetId: id,
targetName: input.name,
customerId,
customerName: customer.name,
after: {
name: input.name,
code: input.code,
type: input.type,
purpose: input.purpose,
entersGeneration: input.entersGeneration,
status: input.entersGeneration ? 'pending' : 'active',
},
});
return toCustomerField(row);
}
@@ -185,7 +202,7 @@ export class CustomerFieldsService {
const scope = requireScopeContext(scopeInput);
const existing = await this.prisma.customerField.findUnique({
where: { id },
include: { customer: { select: { id: true, deletedAt: true } } },
include: { customer: { select: { id: true, name: true, deletedAt: true } } },
});
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- 保留显式 null 分支,跨客户统一 404
if (existing === null || existing.customer.deletedAt !== null) {
@@ -222,6 +239,19 @@ export class CustomerFieldsService {
where: { id },
include: FIELD_INCLUDE,
});
this.requestContext.patchAudit({
targetType: 'customer_field',
targetId: id,
targetName: existing.name,
customerId: existing.customerId,
customerName: existing.customer.name,
before: { status: 'pending', approverUserId: null },
after: {
status: input.decision === 'approve' ? 'active' : 'rejected',
approverUserId: actorId,
reason: input.decision === 'approve' ? null : (input.reason ?? ''),
},
});
return toCustomerField(row);
}