215 lines
7.5 KiB
TypeScript
215 lines
7.5 KiB
TypeScript
/**
|
||
* NAC知识引擎 — 规则版本管理模块 (v15)
|
||
*
|
||
* 功能:
|
||
* 1. 规则更新时自动保存历史版本快照
|
||
* 2. 查询规则的版本历史列表
|
||
* 3. 对比两个版本的差异(字段级别diff)
|
||
* 4. 版本回滚(管理员权限)
|
||
*/
|
||
|
||
import { getMongoDb, COLLECTIONS } from "./mongodb";
|
||
import { ObjectId } from "mongodb";
|
||
|
||
// ─── 类型定义 ──────────────────────────────────────────────────────
|
||
export interface RuleVersion {
|
||
_id?: ObjectId;
|
||
ruleId: string; // 原始规则ID
|
||
version: number; // 版本号(从1开始递增)
|
||
snapshot: Record<string, unknown>; // 规则完整快照
|
||
changedFields: string[]; // 本次变更的字段列表
|
||
changeSummary: string; // 变更摘要(中文描述)
|
||
operatorId: number; // 操作人ID
|
||
operatorEmail: string; // 操作人邮箱
|
||
createdAt: Date;
|
||
}
|
||
|
||
export interface FieldDiff {
|
||
field: string;
|
||
label: string;
|
||
oldValue: unknown;
|
||
newValue: unknown;
|
||
changeType: "added" | "removed" | "modified";
|
||
}
|
||
|
||
// ─── 字段中文标签映射 ─────────────────────────────────────────────
|
||
const FIELD_LABELS: Record<string, string> = {
|
||
ruleName: "规则名称",
|
||
description: "规则描述",
|
||
jurisdiction: "司法辖区",
|
||
assetType: "资产类型",
|
||
status: "状态",
|
||
required: "强制要求",
|
||
tags: "标签",
|
||
ruleNameI18n: "多语言名称",
|
||
descriptionI18n: "多语言描述",
|
||
};
|
||
|
||
// ─── 生成变更摘要 ──────────────────────────────────────────────────
|
||
function buildChangeSummary(changedFields: string[]): string {
|
||
if (changedFields.length === 0) return "无变更";
|
||
const labels = changedFields.map(f => FIELD_LABELS[f] || f);
|
||
if (labels.length === 1) return `更新了${labels[0]}`;
|
||
if (labels.length <= 3) return `更新了${labels.join("、")}`;
|
||
return `更新了${labels.slice(0, 2).join("、")}等${labels.length}个字段`;
|
||
}
|
||
|
||
// ─── 比较两个值是否相等(深度比较) ──────────────────────────────
|
||
function deepEqual(a: unknown, b: unknown): boolean {
|
||
if (a === b) return true;
|
||
if (typeof a !== typeof b) return false;
|
||
if (a === null || b === null) return false;
|
||
if (typeof a === "object") {
|
||
return JSON.stringify(a) === JSON.stringify(b);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ─── 保存版本快照(规则更新时调用) ──────────────────────────────
|
||
export async function saveRuleVersion(
|
||
ruleId: string,
|
||
oldSnapshot: Record<string, unknown>,
|
||
updatedFields: Record<string, unknown>,
|
||
operatorId: number,
|
||
operatorEmail: string
|
||
): Promise<number> {
|
||
const db = await getMongoDb();
|
||
if (!db) return 0;
|
||
|
||
// 计算变更字段
|
||
const changedFields = Object.keys(updatedFields).filter(key => {
|
||
if (key === "updatedAt" || key === "_embeddingUpdatedAt" || key === "_embedding") return false;
|
||
return !deepEqual(oldSnapshot[key], updatedFields[key]);
|
||
});
|
||
|
||
if (changedFields.length === 0) return 0; // 无实质变更,不保存
|
||
|
||
// 获取当前最大版本号
|
||
const lastVersion = await db.collection(COLLECTIONS.RULE_VERSIONS)
|
||
.find({ ruleId })
|
||
.sort({ version: -1 })
|
||
.limit(1)
|
||
.toArray() as any[];
|
||
|
||
const nextVersion = (lastVersion[0]?.version || 0) + 1;
|
||
|
||
const versionDoc: RuleVersion = {
|
||
ruleId,
|
||
version: nextVersion,
|
||
snapshot: { ...oldSnapshot },
|
||
changedFields,
|
||
changeSummary: buildChangeSummary(changedFields),
|
||
operatorId,
|
||
operatorEmail,
|
||
createdAt: new Date(),
|
||
};
|
||
|
||
await db.collection(COLLECTIONS.RULE_VERSIONS).insertOne(versionDoc);
|
||
return nextVersion;
|
||
}
|
||
|
||
// ─── 查询规则版本历史 ──────────────────────────────────────────────
|
||
export async function getRuleVersionHistory(
|
||
ruleId: string,
|
||
limit = 20
|
||
): Promise<RuleVersion[]> {
|
||
const db = await getMongoDb();
|
||
if (!db) return [];
|
||
|
||
const docs = await db.collection(COLLECTIONS.RULE_VERSIONS)
|
||
.find({ ruleId })
|
||
.sort({ version: -1 })
|
||
.limit(limit)
|
||
.toArray();
|
||
return docs as unknown as RuleVersion[];
|
||
}
|
||
|
||
// ─── 对比两个版本的差异 ────────────────────────────────────────────
|
||
export async function compareRuleVersions(
|
||
ruleId: string,
|
||
versionA: number,
|
||
versionB: number
|
||
): Promise<{
|
||
diffs: FieldDiff[];
|
||
versionA: RuleVersion | null;
|
||
versionB: RuleVersion | null;
|
||
}> {
|
||
const db = await getMongoDb();
|
||
if (!db) return { diffs: [], versionA: null, versionB: null };
|
||
|
||
const [docA, docB] = await Promise.all([
|
||
db.collection(COLLECTIONS.RULE_VERSIONS).findOne({ ruleId, version: versionA }) as Promise<any>,
|
||
db.collection(COLLECTIONS.RULE_VERSIONS).findOne({ ruleId, version: versionB }) as Promise<any>,
|
||
]);
|
||
|
||
if (!docA || !docB) return { diffs: [], versionA: docA, versionB: docB };
|
||
|
||
const snapshotA = docA.snapshot || {};
|
||
const snapshotB = docB.snapshot || {};
|
||
|
||
// 比较所有字段
|
||
const allFields = new Set([
|
||
...Object.keys(snapshotA),
|
||
...Object.keys(snapshotB),
|
||
].filter(f => !["_id", "_embedding", "_embeddingUpdatedAt"].includes(f)));
|
||
|
||
const diffs: FieldDiff[] = [];
|
||
|
||
for (const field of Array.from(allFields)) {
|
||
const valA = snapshotA[field];
|
||
const valB = snapshotB[field];
|
||
|
||
if (deepEqual(valA, valB)) continue;
|
||
|
||
let changeType: "added" | "removed" | "modified" = "modified";
|
||
if (valA === undefined || valA === null) changeType = "added";
|
||
else if (valB === undefined || valB === null) changeType = "removed";
|
||
|
||
diffs.push({
|
||
field,
|
||
label: FIELD_LABELS[field] || field,
|
||
oldValue: valA,
|
||
newValue: valB,
|
||
changeType,
|
||
});
|
||
}
|
||
|
||
return { diffs, versionA: docA, versionB: docB };
|
||
}
|
||
|
||
// ─── 版本回滚(恢复到指定版本的快照) ────────────────────────────
|
||
export async function rollbackRuleToVersion(
|
||
ruleId: string,
|
||
targetVersion: number,
|
||
operatorId: number,
|
||
operatorEmail: string
|
||
): Promise<{ success: boolean; restoredFields: string[] }> {
|
||
const db = await getMongoDb();
|
||
if (!db) return { success: false, restoredFields: [] };
|
||
|
||
const versionDoc = await db.collection(COLLECTIONS.RULE_VERSIONS)
|
||
.findOne({ ruleId, version: targetVersion }) as any;
|
||
|
||
if (!versionDoc) return { success: false, restoredFields: [] };
|
||
|
||
// 获取当前规则
|
||
const currentRule = await db.collection(COLLECTIONS.COMPLIANCE_RULES)
|
||
.findOne({ _id: new ObjectId(ruleId) }) as any;
|
||
|
||
if (!currentRule) return { success: false, restoredFields: [] };
|
||
|
||
// 先保存当前版本快照
|
||
await saveRuleVersion(ruleId, currentRule, versionDoc.snapshot, operatorId, operatorEmail);
|
||
|
||
// 恢复快照(保留_id,去除快照中的_id)
|
||
const { _id: _snapId, ...restoreData } = versionDoc.snapshot;
|
||
const restoredFields = Object.keys(restoreData).filter(k => !["_embedding", "_embeddingUpdatedAt"].includes(k));
|
||
|
||
await db.collection(COLLECTIONS.COMPLIANCE_RULES).updateOne(
|
||
{ _id: new ObjectId(ruleId) },
|
||
{ $set: { ...restoreData, updatedAt: new Date(), _rollbackFrom: targetVersion } }
|
||
);
|
||
|
||
return { success: true, restoredFields };
|
||
}
|