NAC_Blockchain/ops/nac-admin/server/ruleVersions.ts

215 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 };
}