export interface AuditFieldDiff {
|
[key: string]: unknown;
|
changeType?: string;
|
chidData?: Array<Record<string, unknown>>;
|
chidField?: Array<Record<string, unknown>>;
|
componentType?: string;
|
field?: string;
|
fieldName?: string;
|
jnpfKey?: string;
|
masked?: boolean;
|
nameModified?: boolean;
|
newData?: unknown;
|
newDisplay?: unknown;
|
oldData?: unknown;
|
oldDisplay?: unknown;
|
technical?: boolean;
|
type?: number | string;
|
valueType?: string;
|
}
|
|
export interface AuditFieldDiffParseResult {
|
diffs: AuditFieldDiff[];
|
parseError: boolean;
|
}
|
|
const SENSITIVE_ENGLISH_FIELD_PATTERN =
|
/\b(?:password|passwd|pwd|secret|token|credential|authorization|cookie|passport|mobile|phone|telephone|email)\b|\b(?:id|identity)\s+card\b|\bbank\s+(?:account|card)\b/i;
|
const SENSITIVE_CHINESE_FIELD_PATTERN = /密码|口令|密钥|令牌|凭证|身份证|证件号|护照|银行卡|银行账号|手机号|联系电话|电话号码|电子邮箱|邮箱/;
|
const SIGN_REFERENCE_FIELD_PATTERN = /^(?:biz[_\s-]*sign|sign(?:ature)?[_\s-]*(?:id|ref|reference))$/i;
|
const TECHNICAL_IDENTIFIER_PATTERN = /^[a-z_]\w*$/i;
|
const UNAVAILABLE_DISPLAY_VALUE = '名称暂不可用';
|
|
/** Parse audit diff JSON without exposing malformed or masked field values. */
|
export function parseAuditFieldDiffs(value: unknown): AuditFieldDiffParseResult {
|
if (value === undefined || value === null || value === '') return { diffs: [], parseError: false };
|
|
let parsed = value;
|
if (typeof value === 'string') {
|
try {
|
parsed = JSON.parse(value);
|
} catch {
|
return { diffs: [], parseError: true };
|
}
|
}
|
|
if (!Array.isArray(parsed)) return { diffs: [], parseError: true };
|
|
const diffs = parsed
|
.filter((item) => isRecord(item))
|
.map((item) => {
|
const diff: AuditFieldDiff = {
|
...item,
|
changeType: toOptionalString(item.changeType),
|
chidData: toRecordList(item.chidData),
|
chidField: toRecordList(item.chidField),
|
componentType: toOptionalString(item.componentType),
|
field: toOptionalString(item.field),
|
fieldName: toOptionalString(item.fieldName),
|
jnpfKey: toOptionalString(item.jnpfKey),
|
masked: toBoolean(item.masked),
|
nameModified: item.nameModified === true || item.nameModified === 'true',
|
technical: toBoolean(item.technical),
|
valueType: toOptionalString(item.valueType),
|
};
|
|
if (diff.masked) {
|
delete diff.oldData;
|
delete diff.newData;
|
delete diff.oldDisplay;
|
delete diff.newDisplay;
|
}
|
return diff;
|
});
|
|
return { diffs, parseError: false };
|
}
|
|
/** Prefer the structured API contract while retaining compatibility with old event rows. */
|
export function parseAuditEventFieldDiffs(fieldDiffList: unknown, legacyFieldDiffs: unknown): AuditFieldDiffParseResult {
|
const source = fieldDiffList === undefined || fieldDiffList === null ? legacyFieldDiffs : fieldDiffList;
|
return parseAuditFieldDiffs(source);
|
}
|
|
/** Convert a diff value into bounded plain text for Vue text interpolation. */
|
export function formatAuditDiffValue(value: unknown): string {
|
if (value === undefined || value === null) return '';
|
if (typeof value === 'string') return value;
|
if (typeof value === 'number' || typeof value === 'bigint') return String(value);
|
if (typeof value === 'boolean') return value ? '是' : '否';
|
try {
|
return JSON.stringify(value);
|
} catch {
|
return '[无法显示的值]';
|
}
|
}
|
|
export function hasAuditDiffValue(value: unknown): boolean {
|
return value !== undefined && value !== null && value !== '';
|
}
|
|
/** Prefer backend-resolved display semantics and fall back to the stored raw value. */
|
export function getAuditDiffDisplayValue(diff: AuditFieldDiff, side: 'new' | 'old'): unknown {
|
const displayValue = side === 'old' ? diff.oldDisplay : diff.newDisplay;
|
if (displayValue !== undefined && displayValue !== null && !isUnavailableDisplayValue(displayValue)) return displayValue;
|
return side === 'old' ? diff.oldData : diff.newData;
|
}
|
|
function isUnavailableDisplayValue(value: unknown): boolean {
|
return typeof value === 'string' && value.trim() === UNAVAILABLE_DISPLAY_VALUE;
|
}
|
|
/** Return a business-facing label without falling back to a database identifier. */
|
export function getSafeAuditDiffFieldLabel(diff: AuditFieldDiff, index = 0): string {
|
if (isSignReferenceAuditDiff(diff)) return '电子签名';
|
|
const fieldName = diff.fieldName?.trim();
|
const field = diff.field?.trim();
|
if (fieldName && (!field || fieldName.toLocaleLowerCase() !== field.toLocaleLowerCase() || !TECHNICAL_IDENTIFIER_PATTERN.test(fieldName))) {
|
return fieldName;
|
}
|
return `未配置字段名称 ${index + 1}`;
|
}
|
|
/** Detect fields whose values should be masked from audit detail views. */
|
export function isSensitiveAuditDiff(diff: AuditFieldDiff): boolean {
|
if (diff.masked) return true;
|
const jnpfKey = diff.jnpfKey?.trim().toLocaleLowerCase();
|
if (jnpfKey === 'sign' || jnpfKey === 'signature') return true;
|
const componentType = diff.componentType?.trim().toLocaleLowerCase();
|
if (componentType === 'sign' || componentType === 'signature') return true;
|
if (diff.valueType?.trim().toLocaleLowerCase() === 'masked') return true;
|
if (isSignReferenceAuditDiff(diff)) return true;
|
return [diff.field, diff.fieldName].some((value) => {
|
if (typeof value !== 'string') return false;
|
const normalizedValue = value.replaceAll(/([a-z\d])([A-Z])/g, '$1 $2').replaceAll(/[^\p{L}\p{N}]+/gu, ' ');
|
return SENSITIVE_ENGLISH_FIELD_PATTERN.test(normalizedValue) || SENSITIVE_CHINESE_FIELD_PATTERN.test(normalizedValue);
|
});
|
}
|
|
function isSignReferenceAuditDiff(diff: AuditFieldDiff): boolean {
|
return [diff.field, diff.fieldName].some((value) => typeof value === 'string' && SIGN_REFERENCE_FIELD_PATTERN.test(value.trim()));
|
}
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
}
|
|
function toOptionalString(value: unknown): string | undefined {
|
return typeof value === 'string' && value ? value : undefined;
|
}
|
|
function toBoolean(value: unknown): boolean | undefined {
|
if (value === true || value === 'true') return true;
|
if (value === false || value === 'false') return false;
|
return undefined;
|
}
|
|
function toRecordList(value: unknown): Array<Record<string, unknown>> | undefined {
|
if (!Array.isArray(value)) return undefined;
|
const records = value.filter(isRecord);
|
return records.length ? records : undefined;
|
}
|