package jnpf.bizcommon.audit.service.support; import com.alibaba.fastjson.JSON; import jnpf.audit.diff.AuditFieldDiff; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; /** 审计查询响应的字段名装饰与签名凭证脱敏;不修改原始事件。 */ public final class AuditQueryResponseSanitizer { public static final String SIGN_REFERENCE_LABEL = "电子签名已关联"; private AuditQueryResponseSanitizer() { } /** * 装饰 field_diffs 的展示名,并对历史 biz_sign / *_sign 值做响应级脱敏。 * JSON 已损坏时返回 null,避免绕过结构化脱敏。 */ @SuppressWarnings("unchecked") public static String sanitizeFieldDiffs( String json, Map metadata) { if (json == null || json.trim().isEmpty()) { return json; } try { Object parsed = JSON.parse(json); if (!(parsed instanceof List)) { return null; } List effective = new ArrayList<>(); for (Object raw : (List) parsed) { if (!(raw instanceof Map)) { continue; } Map item = (Map) raw; if (isNoopDiff(item)) { continue; } sanitizeChildDiff(item); String field = string(item.get("field")); if (field == null) { continue; } AuditFieldDiff.FieldMeta meta = lookup(metadata, field); if (isCredentialReference(field, item, meta)) { item.put("fieldName", SIGN_REFERENCE_LABEL); item.put("masked", true); item.put("valueType", "masked"); item.put("componentType", "sign"); if (item.containsKey("oldData")) { item.put("oldData", AuditFieldDiff.MASKED); } if (item.containsKey("newData")) { item.put("newData", AuditFieldDiff.MASKED); } item.remove("oldDisplay"); item.remove("newDisplay"); effective.add(item); continue; } String fieldName = string(item.get("fieldName")); if (fieldName == null || (field != null && fieldName.equalsIgnoreCase(field))) { String label = meta == null ? null : meta.label(); item.put("fieldName", label == null || label.trim().isEmpty() ? databaseFieldName(metadata, field) : label); } effective.add(item); } return JSON.toJSONString(effective); } catch (Throwable t) { return null; } } /** 深度脱敏 extra / data_snapshot 中以敏感键承载的凭证值。 */ public static String sanitizeJson(String json) { return sanitizeJson(json, null); } /** 深度脱敏,并用字段元数据识别非固定列名的签名控件。 */ public static String sanitizeJson( String json, Map metadata) { if (json == null || json.trim().isEmpty()) { return json; } try { Object parsed = JSON.parse(json); sanitizeValue(parsed, metadata); return JSON.toJSONString(parsed); } catch (Throwable t) { return null; } } /** 从层 0 extra 取 sourceModelId,只用于查询当前字段元数据。 */ @SuppressWarnings("unchecked") public static String sourceModelId(String extra) { if (extra == null || extra.trim().isEmpty()) { return null; } try { Object parsed = JSON.parse(extra); if (!(parsed instanceof Map)) { return null; } return string(((Map) parsed).get("sourceModelId")); } catch (Throwable t) { return null; } } public static boolean hidesTargetId(String targetTable) { return targetTable != null && "lims_sign".equalsIgnoreCase(targetTable.trim()); } /** 列表摘要与详情共用的有效 diff 计数;会排除空到空及相同值伪变更。 */ @SuppressWarnings("unchecked") public static int effectiveDiffCount(String json) { return effectiveDiffFields(json).size(); } /** 返回有效变更字段,供同一操作的多条业务事件按字段并集合并摘要。 */ @SuppressWarnings("unchecked") public static Set effectiveDiffFields(String json) { Set fields = new LinkedHashSet<>(); if (json == null || json.trim().isEmpty()) { return fields; } try { Object parsed = JSON.parse(json); if (!(parsed instanceof List)) { return fields; } for (Object raw : (List) parsed) { if (!(raw instanceof Map)) { continue; } Map item = (Map) raw; String field = string(item.get("field")); if (field != null && !isNoopDiff(item)) { String targetTable = string(item.get("targetTable")); fields.add(AuditDiffKey.of(targetTable, field)); } } } catch (Throwable ignored) { // 非法历史 JSON 按无变更降级,不能影响列表查询。 } return fields; } public static boolean isCredentialReference(String field) { if (field == null) { return false; } String trimmed = field.trim(); String lower = trimmed.toLowerCase(Locale.ROOT); return "biz_sign".equals(lower) || "bizsign".equals(lower) || "sign_id".equals(lower) || "signid".equals(lower) || lower.endsWith("_sign") || trimmed.endsWith("Sign") || trimmed.endsWith("SignId"); } /** 字段名、事件快照元数据和当前表单元数据任一确认是签名凭证,都必须脱敏。 */ public static boolean isCredentialReference(String field, Map item, AuditFieldDiff.FieldMeta metadata) { return isCredentialReference(field) || bool(item == null ? null : item.get("masked")) || "masked".equalsIgnoreCase(string(item == null ? null : item.get("valueType"))) || isSignComponent(item == null ? null : item.get("componentType")) || isSignComponent(item == null ? null : item.get("jnpfKey")) || AuditFieldDiff.isCredentialMetadata(metadata); } private static boolean isNoopDiff(Map item) { // 历史坏数据可能只保留 field/fieldName。没有完整前后值时无法证明是伪变更, // 应继续返回供证据追踪;只有两侧值键都存在时才过滤空到空或完全相同。 if (!item.containsKey("oldData") || !item.containsKey("newData")) { return false; } Object oldValue = item.get("oldData"); Object newValue = item.get("newData"); if (AuditFieldDiff.MASKED.equals(String.valueOf(oldValue)) || AuditFieldDiff.MASKED.equals(String.valueOf(newValue))) { return false; } return canonical(oldValue).equals(canonical(newValue)); } @SuppressWarnings("unchecked") private static void sanitizeChildDiff(Map item) { Object fieldsRaw = item.get("chidField"); Object dataRaw = item.get("chidData"); if (!(fieldsRaw instanceof List) || !(dataRaw instanceof List)) { return; } for (Object rawField : (List) fieldsRaw) { if (!(rawField instanceof Map)) { continue; } Map field = (Map) rawField; String prop = string(field.get("prop")); boolean credential = prop != null && (isCredentialReference(prop) || isSignComponent(field.get("jnpfKey")) || bool(field.get("masked"))); if (!credential) { continue; } field.put("masked", true); for (Object rawRow : (List) dataRaw) { if (!(rawRow instanceof Map)) { continue; } Map row = (Map) rawRow; if (row.containsKey(prop)) { row.put(prop, AuditFieldDiff.MASKED); } String oldProp = "jnpf_old_" + prop; if (row.containsKey(oldProp)) { row.put(oldProp, AuditFieldDiff.MASKED); } } } } private static String canonical(Object value) { if (value == null) { return ""; } if (value instanceof CharSequence) { String text = value.toString(); return text.trim().isEmpty() ? "" : "string:" + text; } if (value instanceof java.util.Collection && ((java.util.Collection) value).isEmpty()) { return ""; } if (value instanceof Map && ((Map) value).isEmpty()) { return ""; } return "json:" + JSON.toJSONString(value); } @SuppressWarnings("unchecked") private static void sanitizeValue( Object value, Map metadata) { if (value instanceof Map) { Map map = (Map) value; for (Map.Entry entry : map.entrySet()) { String field = String.valueOf(entry.getKey()); if (isCredentialReference(field) || AuditFieldDiff.isCredentialMetadata(lookup(metadata, field))) { entry.setValue(AuditFieldDiff.MASKED); } else { sanitizeValue(entry.getValue(), metadata); } } } else if (value instanceof List) { for (Object item : (List) value) { sanitizeValue(item, metadata); } } } private static boolean isSignComponent(Object value) { String component = string(value); return "sign".equalsIgnoreCase(component) || "signature".equalsIgnoreCase(component); } private static boolean bool(Object value) { return value instanceof Boolean ? (Boolean) value : value != null && "true".equalsIgnoreCase(String.valueOf(value)); } private static AuditFieldDiff.FieldMeta lookup( Map metadata, String field) { if (metadata == null || field == null) { return null; } AuditFieldDiff.FieldMeta exact = metadata.get(field); if (exact != null) { return exact; } AuditFieldDiff.FieldMeta normalized = metadata.get(field.toLowerCase(Locale.ROOT)); return normalized != null ? normalized : metadata.get(camelToSnake(field)); } private static String databaseFieldName( Map metadata, String field) { String snake = camelToSnake(field); return metadata != null && metadata.containsKey(snake) ? snake : field; } private static String camelToSnake(String value) { StringBuilder out = new StringBuilder(value.length() + 8); for (int i = 0; i < value.length(); i++) { char ch = value.charAt(i); if (Character.isUpperCase(ch)) { out.append('_').append(Character.toLowerCase(ch)); } else { out.append(Character.toLowerCase(ch)); } } return out.toString(); } private static String string(Object value) { if (value == null) { return null; } String text = String.valueOf(value).trim(); return text.isEmpty() ? null : text; } }