/** * 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; // 规则完整快照 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 = { 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, updatedFields: Record, operatorId: number, operatorEmail: string ): Promise { 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 { 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, db.collection(COLLECTIONS.RULE_VERSIONS).findOne({ ruleId, version: versionB }) as Promise, ]); 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 }; }