刘光辉
10 小时以前 0dfe84494048ce27ba8449831782128412d3eb13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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;
}