操作审计(Audit Log)

This commit is contained in:
Cheng Zhou
2025-12-17 21:19:05 +08:00
parent b0bffbc75e
commit 13efb0a3a9
10 changed files with 420 additions and 74 deletions
+35
View File
@@ -0,0 +1,35 @@
export const auditDict: Record<
string,
{ label: string; actionText: string; targetLabel: string }
> = {
AE_CLOSED: {
label: "关闭 AE(不良事件)",
actionText: "关闭了 AE",
targetLabel: "不良事件",
},
SUBJECT_STATUS_CHANGED: {
label: "受试者状态变更",
actionText: "变更了受试者状态",
targetLabel: "受试者",
},
FINANCE_STATUS_CHANGED: {
label: "费用状态变更",
actionText: "变更了费用状态",
targetLabel: "费用记录",
},
ADMIN_RESET_PASSWORD: {
label: "重置用户密码",
actionText: "重置了用户密码",
targetLabel: "用户账号",
},
TASK_STATUS_CHANGED: {
label: "任务状态变更",
actionText: "更新了任务状态",
targetLabel: "任务",
},
ISSUE_STATUS_CHANGED: {
label: "风险/问题状态变更",
actionText: "更新了风险/问题状态",
targetLabel: "风险/问题",
},
};
+81
View File
@@ -0,0 +1,81 @@
import { auditDict } from "./auditDict";
import { roleDict, getDictLabel, statusDict } from "../dictionaries";
export interface AuditEvent {
eventType: string;
eventLabel: string;
actorId: string;
actorName: string;
actorRole: string;
actorRoleLabel: string;
targetType: string;
targetTypeLabel: string;
targetId: string;
targetName?: string;
actionText: string;
before?: Record<string, any>;
after?: Record<string, any>;
diffText?: string[];
result: "SUCCESS" | "FAIL";
resultLabel: string;
timestamp: string;
}
const buildDiffText = (before?: Record<string, any>, after?: Record<string, any>) => {
if (!before || !after) return [];
const lines: string[] = [];
Object.keys({ ...before, ...after }).forEach((key) => {
const prev = before[key];
const next = after[key];
if (prev === next) return;
if (key.toLowerCase().includes("status")) {
lines.push(`状态:${getDictLabel(statusDict, prev)}${getDictLabel(statusDict, next)}`);
} else {
lines.push(`${key}${prev ?? "-"}${next ?? "-"}`);
}
});
return lines;
};
export const normalizeAuditEvent = (raw: any, userMap: Record<string, string>): AuditEvent => {
const dict = auditDict[raw.action] || {
label: "系统操作",
actionText: "执行了操作",
targetLabel: raw.entity_type || "对象",
};
let detailObj: any = {};
if (raw.detail) {
try {
detailObj = JSON.parse(raw.detail);
} catch {
detailObj = { description: raw.detail };
}
}
const before = detailObj.before;
const after = detailObj.after;
const diffText = detailObj.diffText || buildDiffText(before, after);
return {
eventType: raw.action,
eventLabel: dict.label,
actorId: raw.operator_id,
actorName: userMap[raw.operator_id] || raw.operator_id,
actorRole: raw.operator_role,
actorRoleLabel: getDictLabel(roleDict, raw.operator_role),
targetType: raw.entity_type || "",
targetTypeLabel: dict.targetLabel,
targetId: raw.entity_id || "",
targetName: detailObj.targetName,
actionText: dict.actionText,
before,
after,
diffText: Array.isArray(diffText) ? diffText : diffText ? [diffText] : [],
result: detailObj.result || "SUCCESS",
resultLabel: detailObj.result === "FAIL" ? "失败" : "成功",
timestamp: raw.created_at,
};
};
export { auditDict };
// 前端占位的 logAudit:不修改后端逻辑,失败不影响主流程
export const logAudit = async (_eventType: string, _payload: Record<string, any>) => Promise.resolve();