feat(api): A5 审计采集、检索与导出
AuditInterceptor 成功 / 失败双写 audit_logs(响应前 await、脱敏、sink 缺失退化 Pino);登录 / 权限 / 客户 / 字典 / 配置等既有动作补审计;审计列表 / 详情 / 动作目录 / 异步 CSV 导出 / HMAC 签名下载;分区维护与保留归档占位;OpenAPI 快照更新。
This commit is contained in:
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user