package jnpf.bizcommon.audit.service; import com.alibaba.fastjson.JSON; import jnpf.bizcommon.audit.entity.AuditEventEntity; import jnpf.audit.diff.AuditFieldDiff; import jnpf.bizcommon.audit.entity.model.AuditFieldDiffViewVO; import jnpf.bizcommon.audit.service.support.AuditQueryResponseSanitizer; import jnpf.bizcommon.audit.service.value.AuditFieldValueContext; import jnpf.bizcommon.audit.service.value.AuditFieldValueResolver; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; /** 将兼容字符串 fieldDiffs 装配成结构化、可展示且可脱敏的字段变更。 */ @Service @Slf4j public class AuditFieldDiffViewService { private final List resolvers; private final ZoneId businessZone; @Autowired public AuditFieldDiffViewService( List resolvers, @Value("${audit.query.business-time-zone:Asia/Shanghai}") String businessTimeZone) { this.resolvers = resolvers == null ? Collections.emptyList() : new ArrayList<>(resolvers); this.businessZone = parseZone(businessTimeZone); } /** * sanitizedFieldDiffs 已由查询脱敏器过滤无意义项并处理凭证引用,原始事件不会被修改。 */ @SuppressWarnings("unchecked") public List build(AuditEventEntity event, String sanitizedFieldDiffs, Map currentMetadata) { if (sanitizedFieldDiffs == null || sanitizedFieldDiffs.trim().isEmpty()) { return Collections.emptyList(); } Object parsed; try { parsed = JSON.parse(sanitizedFieldDiffs); } catch (Throwable ignored) { return Collections.emptyList(); } if (!(parsed instanceof List)) { return Collections.emptyList(); } List views = new ArrayList<>(); Map dictionaryTypes = new LinkedHashMap<>(); for (Object raw : (List) parsed) { if (!(raw instanceof Map)) { continue; } Map item = (Map) raw; AuditFieldDiffViewVO view = toView(item, currentMetadata, event == null ? null : event.getActionCode()); if (view != null) { views.add(view); AuditFieldDiff.FieldMeta meta = metadata(currentMetadata, view.getField()); String dictionaryType = firstText(item.get("dictionaryType"), meta == null ? null : meta.dictionaryType()); if (dictionaryType != null) { dictionaryTypes.put(view.getField().toLowerCase(Locale.ROOT), dictionaryType); } } } AuditFieldValueContext context = new AuditFieldValueContext( event == null ? null : event.getTargetTable(), event == null ? null : event.getTenantId(), event == null ? null : event.getEventTime(), dictionaryTypes); List resolvedViews = views; for (AuditFieldValueResolver resolver : resolvers) { if (resolver != null) { try { if (!resolver.supports(context.getTargetTable())) { continue; } List candidate = copyViews(resolvedViews); resolver.resolve(context, candidate); resolvedViews = candidate; } catch (RuntimeException ex) { // 解析失败只能降级当前 resolver,日志不携带字段值或详情正文。 log.warn("[AUDIT-QUERY] 字段值解析器失败 resolver={} err={}", resolver.getClass().getSimpleName(), ex.getClass().getSimpleName()); } } } for (AuditFieldDiffViewVO view : resolvedViews) { finishDisplay(view); } return resolvedViews; } /** * 为新事件固化可可靠解析的类型和展示值,同时保留旧 field_diffs 的 type/jnpfKey 等键。 * 解析失败时原样返回,不能因为展示增强造成 Layer 0 事件丢失。 */ @SuppressWarnings("unchecked") public String enrichForPersistence(AuditEventEntity event, String fieldDiffs, Map metadata) { if (fieldDiffs == null || fieldDiffs.trim().isEmpty()) { return fieldDiffs; } try { Object parsed = JSON.parse(fieldDiffs); if (!(parsed instanceof List)) { return fieldDiffs; } List views = build(event, fieldDiffs, metadata); Map byField = new LinkedHashMap<>(); for (AuditFieldDiffViewVO view : views) { if (view.getField() != null) { byField.put(view.getField().toLowerCase(Locale.ROOT), view); } } for (Object raw : (List) parsed) { if (!(raw instanceof Map)) { continue; } Map item = (Map) raw; String field = text(item.get("field")); AuditFieldDiffViewVO view = field == null ? null : byField.get(field.toLowerCase(Locale.ROOT)); if (view == null) { continue; } putIfMissing(item, "valueType", view.getValueType()); putIfMissing(item, "componentType", view.getComponentType()); if (Boolean.TRUE.equals(view.getMasked())) { 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"); continue; } if (Boolean.TRUE.equals(view.getTechnical())) { item.put("technical", true); } if (snapshotDisplayType(view.getValueType())) { putDisplayIfResolved(item, "oldDisplay", view.getOldDisplay()); putDisplayIfResolved(item, "newDisplay", view.getNewDisplay()); } } return JSON.toJSONString(parsed); } catch (Throwable ex) { log.warn("[AUDIT-L0] 字段展示快照固化失败 err={}", ex.getClass().getSimpleName()); return fieldDiffs; } } private AuditFieldDiffViewVO toView(Map item, Map currentMetadata, String actionCode) { String field = text(item.get("field")); if (field == null) { return null; } AuditFieldDiff.FieldMeta meta = metadata(currentMetadata, field); boolean credential = AuditQueryResponseSanitizer.isCredentialReference(field, item, meta); boolean masked = credential || AuditFieldDiff.MASKED.equals(text(item.get("oldData"))) || AuditFieldDiff.MASKED.equals(text(item.get("newData"))); AuditFieldDiffViewVO view = new AuditFieldDiffViewVO(); view.setField(field); view.setTargetTable(text(item.get("targetTable"))); view.setFieldName(credential ? "电子签名" : fieldName(item, meta)); view.setChangeType(changeType(item, actionCode, masked)); String componentType = credential ? "sign" : firstText(item.get("componentType"), item.get("jnpfKey"), meta == null ? null : meta.componentType()); String valueType = masked ? "masked" : firstText(item.get("valueType"), meta == null ? null : meta.valueType()); if (valueType == null && isTemporalComponent(componentType)) { valueType = inferValueType(componentType); } view.setValueType(valueType); view.setComponentType(componentType); view.setJnpfKey(firstText(item.get("jnpfKey"), componentType)); view.setNameModified(bool(item.get("nameModified"))); view.setType(integer(item.get("type"))); view.setChidData(mapList(item.get("chidData"))); view.setChidField(mapList(item.get("chidField"))); view.setMasked(masked); view.setTechnical(bool(item.get("technical"))); if (masked) { view.setOldData(null); view.setNewData(null); view.setOldDisplay(null); view.setNewDisplay(null); return view; } String oldData = rawText(item.get("oldData")); String newData = rawText(item.get("newData")); view.setOldData(oldData); view.setNewData(newData); view.setOldDisplay(firstTextPreserveEmpty(item.get("oldDisplay"), optionDisplay(meta, oldData), temporalDisplay(view.getValueType(), item, meta, oldData))); view.setNewDisplay(firstTextPreserveEmpty(item.get("newDisplay"), optionDisplay(meta, newData), temporalDisplay(view.getValueType(), item, meta, newData))); return view; } private void finishDisplay(AuditFieldDiffViewVO view) { if (Boolean.TRUE.equals(view.getMasked())) { return; } if (view.getValueType() == null) { view.setValueType(inferValueType(view.getComponentType())); } if (view.getComponentType() == null) { view.setComponentType("input"); } view.setOldDisplay(fallbackDisplay(view.getValueType(), view.getOldData(), view.getOldDisplay())); view.setNewDisplay(fallbackDisplay(view.getValueType(), view.getNewData(), view.getNewDisplay())); if (view.getTechnical() == null) { view.setTechnical(Boolean.FALSE); } if (view.getMasked() == null) { view.setMasked(Boolean.FALSE); } } private String fallbackDisplay(String valueType, String raw, String display) { if (display != null) { return display; } if (raw == null || raw.trim().isEmpty()) { return ""; } if ("reference".equals(valueType) || "multiReference".equals(valueType) || "enum".equals(valueType) || "multiEnum".equals(valueType)) { return "名称暂不可用"; } return raw; } private String temporalDisplay(String valueType, Map item, AuditFieldDiff.FieldMeta meta, String raw) { if (!("date".equals(valueType) || "datetime".equals(valueType)) || raw == null || raw.trim().isEmpty()) { return null; } try { long epochMillis = Long.parseLong(raw.trim()); String pattern = firstText(item.get("format"), meta == null ? null : meta.format(), "date".equals(valueType) ? "yyyy-MM-dd" : "yyyy-MM-dd HH:mm:ss"); return DateTimeFormatter.ofPattern(pattern, Locale.ROOT) .withZone(businessZone).format(Instant.ofEpochMilli(epochMillis)); } catch (RuntimeException ignored) { return null; } } private static String optionDisplay(AuditFieldDiff.FieldMeta meta, String raw) { return meta == null ? null : meta.optionDisplay(raw); } private static boolean snapshotDisplayType(String valueType) { return "enum".equals(valueType) || "multiEnum".equals(valueType) || "reference".equals(valueType) || "multiReference".equals(valueType) || "date".equals(valueType) || "datetime".equals(valueType); } private static void putIfMissing(Map item, String key, String value) { if (!item.containsKey(key) && value != null) { item.put(key, value); } } private static void putDisplayIfResolved(Map item, String key, String display) { if (!item.containsKey(key) && display != null && !display.trim().isEmpty() && !"名称暂不可用".equals(display)) { item.put(key, display); } } private static AuditFieldDiff.FieldMeta metadata( 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)); if (normalized != null) { return normalized; } StringBuilder snake = new StringBuilder(field.length() + 8); for (int i = 0; i < field.length(); i++) { char ch = field.charAt(i); if (Character.isUpperCase(ch)) { snake.append('_').append(Character.toLowerCase(ch)); } else { snake.append(Character.toLowerCase(ch)); } } return metadata.get(snake.toString()); } private static String fieldName(Map item, AuditFieldDiff.FieldMeta meta) { String name = text(item.get("fieldName")); if (name != null) { return name; } return meta != null && meta.label() != null && !meta.label().trim().isEmpty() ? meta.label() : text(item.get("field")); } private static String changeType(Map item, String actionCode, boolean masked) { if (masked) { return "updated"; } String explicit = text(item.get("changeType")); if ("created".equals(explicit) || "updated".equals(explicit) || "deleted".equals(explicit)) { return explicit; } if (actionCode != null && actionCode.toUpperCase(Locale.ROOT).contains("DELETE")) { return "deleted"; } Object type = item.get("type"); return type != null && "0".equals(String.valueOf(type)) ? "created" : "updated"; } private static String inferValueType(String component) { if (component == null) { return "text"; } String key = component.toLowerCase(Locale.ROOT); if (key.contains("time")) { return "datetime"; } if (key.contains("date")) { return "date"; } if (key.contains("number") || "slider".equals(key) || "rate".equals(key)) { return "number"; } if ("switch".equals(key)) { return "boolean"; } if ("checkbox".equals(key)) { return "multiEnum"; } if (key.contains("select") || "radio".equals(key)) { return "enum"; } if ("table".equals(key) || key.contains("upload")) { return "json"; } return "text"; } private static boolean isTemporalComponent(String component) { if (component == null) { return false; } String key = component.toLowerCase(Locale.ROOT); return key.contains("date") || key.contains("time"); } private static List copyViews(List source) { List copy = new ArrayList<>(source.size()); for (AuditFieldDiffViewVO value : source) { AuditFieldDiffViewVO item = new AuditFieldDiffViewVO(); item.setField(value.getField()); item.setTargetTable(value.getTargetTable()); item.setFieldName(value.getFieldName()); item.setChangeType(value.getChangeType()); item.setValueType(value.getValueType()); item.setComponentType(value.getComponentType()); item.setJnpfKey(value.getJnpfKey()); item.setNameModified(value.getNameModified()); item.setType(value.getType()); item.setChidData(value.getChidData()); item.setChidField(value.getChidField()); item.setOldData(value.getOldData()); item.setNewData(value.getNewData()); item.setOldDisplay(value.getOldDisplay()); item.setNewDisplay(value.getNewDisplay()); item.setMasked(value.getMasked()); item.setTechnical(value.getTechnical()); copy.add(item); } return copy; } private static ZoneId parseZone(String zone) { try { return ZoneId.of(zone == null || zone.trim().isEmpty() ? "Asia/Shanghai" : zone.trim()); } catch (RuntimeException ignored) { return ZoneId.of("Asia/Shanghai"); } } private static boolean bool(Object value) { return value instanceof Boolean ? (Boolean) value : value != null && "true".equalsIgnoreCase(String.valueOf(value)); } private static Integer integer(Object value) { if (value instanceof Number) { return ((Number) value).intValue(); } try { return value == null ? null : Integer.valueOf(String.valueOf(value)); } catch (NumberFormatException ignored) { return null; } } @SuppressWarnings("unchecked") private static List> mapList(Object value) { if (!(value instanceof List)) { return null; } List> result = new ArrayList<>(); for (Object item : (List) value) { if (item instanceof Map) { result.add(new LinkedHashMap<>((Map) item)); } } return result.isEmpty() ? null : result; } private static String rawText(Object value) { return value == null ? "" : String.valueOf(value); } private static String text(Object value) { if (value == null) { return null; } String result = String.valueOf(value).trim(); return result.isEmpty() ? null : result; } private static String firstText(Object... values) { if (values != null) { for (Object value : values) { String result = text(value); if (result != null) { return result; } } } return null; } private static String firstTextPreserveEmpty(Object... values) { if (values != null) { for (Object value : values) { if (value != null) { return String.valueOf(value); } } } return null; } }