From 34981c30a78e8bbd7791131059a9210f9928b62c Mon Sep 17 00:00:00 2001
From: 刘光辉 <347230014@qq.com>
Date: 星期四, 17 九月 2026 09:24:09 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master' into master

---
 jnpf-biz-common/jnpf-biz-common-audit/src/main/java/jnpf/bizcommon/audit/service/AuditFieldDiffViewService.java |  485 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 485 insertions(+), 0 deletions(-)

diff --git a/jnpf-biz-common/jnpf-biz-common-audit/src/main/java/jnpf/bizcommon/audit/service/AuditFieldDiffViewService.java b/jnpf-biz-common/jnpf-biz-common-audit/src/main/java/jnpf/bizcommon/audit/service/AuditFieldDiffViewService.java
new file mode 100644
index 0000000..fa124e1
--- /dev/null
+++ b/jnpf-biz-common/jnpf-biz-common-audit/src/main/java/jnpf/bizcommon/audit/service/AuditFieldDiffViewService.java
@@ -0,0 +1,485 @@
+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<AuditFieldValueResolver> resolvers;
+    private final ZoneId businessZone;
+
+    @Autowired
+    public AuditFieldDiffViewService(
+            List<AuditFieldValueResolver> 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<AuditFieldDiffViewVO> build(AuditEventEntity event,
+                                             String sanitizedFieldDiffs,
+                                             Map<String, AuditFieldDiff.FieldMeta> 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<AuditFieldDiffViewVO> views = new ArrayList<>();
+        Map<String, String> dictionaryTypes = new LinkedHashMap<>();
+        for (Object raw : (List<?>) parsed) {
+            if (!(raw instanceof Map)) {
+                continue;
+            }
+            Map<String, Object> item = (Map<String, Object>) 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<AuditFieldDiffViewVO> resolvedViews = views;
+        for (AuditFieldValueResolver resolver : resolvers) {
+            if (resolver != null) {
+                try {
+                    if (!resolver.supports(context.getTargetTable())) {
+                        continue;
+                    }
+                    List<AuditFieldDiffViewVO> candidate = copyViews(resolvedViews);
+                    resolver.resolve(context, candidate);
+                    resolvedViews = candidate;
+                } catch (RuntimeException ex) {
+                    // 瑙f瀽澶辫触鍙兘闄嶇骇褰撳墠 resolver锛屾棩蹇椾笉鎼哄甫瀛楁鍊兼垨璇︽儏姝f枃銆�
+                    log.warn("[AUDIT-QUERY] 瀛楁鍊艰В鏋愬櫒澶辫触 resolver={} err={}",
+                            resolver.getClass().getSimpleName(), ex.getClass().getSimpleName());
+                }
+            }
+        }
+        for (AuditFieldDiffViewVO view : resolvedViews) {
+            finishDisplay(view);
+        }
+        return resolvedViews;
+    }
+
+    /**
+     * 涓烘柊浜嬩欢鍥哄寲鍙彲闈犺В鏋愮殑绫诲瀷鍜屽睍绀哄�硷紝鍚屾椂淇濈暀鏃� field_diffs 鐨� type/jnpfKey 绛夐敭銆�
+     * 瑙f瀽澶辫触鏃跺師鏍疯繑鍥烇紝涓嶈兘鍥犱负灞曠ず澧炲己閫犳垚 Layer 0 浜嬩欢涓㈠け銆�
+     */
+    @SuppressWarnings("unchecked")
+    public String enrichForPersistence(AuditEventEntity event,
+                                       String fieldDiffs,
+                                       Map<String, AuditFieldDiff.FieldMeta> metadata) {
+        if (fieldDiffs == null || fieldDiffs.trim().isEmpty()) {
+            return fieldDiffs;
+        }
+        try {
+            Object parsed = JSON.parse(fieldDiffs);
+            if (!(parsed instanceof List)) {
+                return fieldDiffs;
+            }
+            List<AuditFieldDiffViewVO> views = build(event, fieldDiffs, metadata);
+            Map<String, AuditFieldDiffViewVO> 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<String, Object> item = (Map<String, Object>) 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<String, Object> item,
+                                        Map<String, AuditFieldDiff.FieldMeta> 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<String, Object> 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<String, Object> item, String key, String value) {
+        if (!item.containsKey(key) && value != null) {
+            item.put(key, value);
+        }
+    }
+
+    private static void putDisplayIfResolved(Map<String, Object> 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<String, AuditFieldDiff.FieldMeta> 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<String, Object> 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<String, Object> 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<AuditFieldDiffViewVO> copyViews(List<AuditFieldDiffViewVO> source) {
+        List<AuditFieldDiffViewVO> 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<Map<String, Object>> mapList(Object value) {
+        if (!(value instanceof List)) {
+            return null;
+        }
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (Object item : (List<?>) value) {
+            if (item instanceof Map) {
+                result.add(new LinkedHashMap<>((Map<String, Object>) 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;
+    }
+}

--
Gitblit v1.8.0