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-audit-api/src/main/java/jnpf/audit/diff/AuditFieldDiff.java |  476 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 476 insertions(+), 0 deletions(-)

diff --git a/jnpf-biz-common/jnpf-audit-api/src/main/java/jnpf/audit/diff/AuditFieldDiff.java b/jnpf-biz-common/jnpf-audit-api/src/main/java/jnpf/audit/diff/AuditFieldDiff.java
new file mode 100644
index 0000000..0be4d87
--- /dev/null
+++ b/jnpf-biz-common/jnpf-audit-api/src/main/java/jnpf/audit/diff/AuditFieldDiff.java
@@ -0,0 +1,476 @@
+package jnpf.audit.diff;
+
+import com.alibaba.fastjson.JSON;
+import jnpf.util.JsonUtil;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * 瀹¤浜嬩欢鐨勬硾鍖栧瓧娈� diff鈥斺��**灞� 0 涓庡眰 1 鍏辩敤鐨勬暟鎹绾�**锛圔3 寤虹珛锛孋1 涓嬫矇鍏变韩锛夈��
+ *
+ * <p>浣忓湪 {@code jnpf-audit-entity} 鑰屼笉鏄煇涓�灞傜殑瀹炵幇鍖呴噷锛屾槸鍥犱负瀹冧骇鍑虹殑姝f槸
+ * {@code audit_events.field_diffs} 杩欎竴鍒楃殑褰㈢姸锛氫袱灞傚悇鍐欎竴浠藉氨蹇呯劧婕傜Щ锛岃��
+ * {@code field_diffs} 涓�鏃﹁法灞備笉鍚屾瀯锛屾煡璇晶涓庡墠绔氨寰楁寜灞傚垎鏀覆鏌擄紝瀹¤鏁版嵁涔熶笉鍐嶅彲姣斻��
+ *
+ * <p>涓� lims 鏃㈡湁鐨� {@code LimsFieldDiff} 鏄�**鍚屼竴濂楄緭鍑哄舰鐘�**锛坽@code VisualLogModel[]}锛�
+ * field / fieldName / oldData / newData / type锛夛紝鍓嶇娓叉煋鍙互鐩存帴澶嶇敤锛涘尯鍒湪浜庢湰绫婚潰瀵圭殑鏄�
+ * 銆屼袱寮犳寜鍒楀悕/瀛楁鍚嶇储寮曠殑 Map銆嶈�岄潪瀹炰綋绫伙細
+ * <ul>
+ *   <li>姣旇緝鐨勬槸**鏁版嵁搴撳垪**锛堝眰 1锛屽垪鍏ㄩ泦鏉ヨ嚜 {@code SELECT *}锛夋垨**琛ㄥ崟瀛楁**锛堝眰 0锛�
+ *       鏉ヨ嚜鍦ㄧ嚎琛ㄥ崟浜嬩欢鐨� newData锛夛紝鏂板姞鐨勫垪/瀛楁鑷姩绾冲叆锛�</li>
+ *   <li>{@code fieldName} 榛樿鍥炶惤鎴愬垪鍚嶏紝鍙敱 {@code labels} 鍙傛暟琛ヤ腑鏂囨爣绛俱��
+ *       <b>鏍囩鐨勬潵婧愬繀椤绘槸鍏冩暟鎹�屼笉鏄墜鍐欓厤缃�</b>鈥斺��66 寮� lims 琛ㄦ墜鍐欒涓婂崈鏉°�佷笖蹇呯劧涓庤〃缁撴瀯
+ *       婕傜Щ锛岄偅鏄�岀湅璧锋潵鍋氬畬浜嗐�佸叾瀹炴病鏈夈�嶇殑鍏稿瀷銆傚眰 0 鐨勬爣绛惧彇鑷湪绾胯〃鍗曚簨浠惰嚜甯︾殑
+ *       {@code listLog}锛堣〃鍗曟ā鍨嬪厓鏁版嵁锛岄殢琛ㄥ崟鍙戝竷鑷姩鏇存柊锛夛紱灞� 1 鏆傛棤鍏冩暟鎹潵婧愶紝
+ *       鏁呬紶 null 璧板垪鍚嶅洖钀姐��</li>
+ * </ul>
+ *
+ * <h3>涓轰粈涔堣鍋氱被鍨嬪綊涓�锛圠006 鐨勫眰 1 鐗堟湰锛�</h3>
+ * <p>before-image 涓� after-image 铏界劧閮芥潵鑷� JDBC锛屼絾**鍚屼竴鍒楀湪涓ゆ鏌ヨ閲屽彲鑳芥嬁鍒颁笉鍚岀殑 Java 绫诲瀷**
+ * 鈥斺�旀渶鍏稿瀷鐨勬槸鏁板�煎垪锛坽@code BigDecimal} vs {@code Integer} vs {@code Long}锛屽彇鍐充簬椹卞姩涓庡垪瀹氫箟锛�
+ * 涓庢椂闂村垪锛坽@code Timestamp} vs {@code Date}锛夈�傜洿鎺� {@code equals} 浼氭妸銆屾病鏀硅繃銆嶅垽鎴愩�屾敼浜嗐�嶏紝
+ * 浜у嚭涓�鍫嗗亣 diff锛屽璁℃暟鎹殑鍙俊搴﹀氨姣佷簡銆傛晠锛氭暟鍊间竴寰嬫寜 {@link BigDecimal#compareTo} 姣斻��
+ * 鏃堕棿涓�寰嬫寜 epoch 姣姣斻�佸瓧鑺傛暟缁勬寜鍐呭姣斻��
+ *
+ * <h3>鑴辨晱</h3>
+ * <p>鍛戒腑鑴辨晱鍒楋紙灞� 1 鏉ヨ嚜 {@code AuditTableRegistry#maskedColumns}锛屽眰 0 鏉ヨ嚜 Nacos 琛ㄥ崟娉ㄥ唽琛級
+ * 鏃�**鍙銆屽凡鍙樻洿銆嶄笉璁板��**鈥斺��
+ * 鏂版棫鍊奸兘鍐欏崰浣嶇銆傛敞鎰忓崰浣嶇瑕�**鎴愬鍑虹幇**锛氬彧鑴辨晱鏂板�间細璁╂棫鍊兼妸绉樺瘑婕忓嚭鍘汇��
+ */
+public final class AuditFieldDiff {
+
+    /** 鑴辨晱鍗犱綅绗︼細鍓嶇鐩存帴灞曠ず杩欎釜涓诧紝璇箟鏄�屽彉浜嗭紝浣嗗唴瀹逛笉浜堣褰曘�嶃�� */
+    public static final String MASKED = "***";
+
+    /** 鍓嶇鎸� {@code jnpfKey=='table'} 娓叉煋瀛愯〃琛岀骇鍙樻洿鍏ュ彛銆� */
+    public static final String JNPF_KEY_TABLE = "table";
+
+    private AuditFieldDiff() {
+    }
+
+    /**
+     * 鍗曚釜瀛楁鐨�**灞曠ず鍏冩暟鎹�**锛堜笉鏄�硷級銆傛潵婧愬繀椤绘槸琛ㄥ崟妯″瀷鍏冩暟鎹紝涓嶆槸鎵嬪啓閰嶇疆銆�
+     *
+     * <p>D1 闂ㄧ鏌ュ嚭锛氭柊 {@code field_diffs} 姣� lims 鏃� {@code data_log} 灏戝洓涓敭锛�
+     * 鑰�**姣忎竴涓兘鏈夊墠绔秷璐规柟**锛坄FormExtraPanel/DataLogList.vue` 鐨勫洓鏉℃覆鏌撳垎鏀級銆�
+     * 鏈被琛ヤ笂鍏朵腑涓や釜鈥斺�斿畠浠笌宸插湪鐢ㄧ殑 {@code fieldName} 鍚屾簮鍚屾�ц川锛堥兘鍙栬嚜浜嬩欢鑷甫鐨�
+     * {@code listLog}锛屾槸**鍏冩暟鎹笉鏄��**锛屾晠涓嶅彈 L003銆屽钩鍙� oldData 涓嶅彲淇°�嶇害鏉燂級锛�
+     *
+     * <ul>
+     *   <li>{@link #nameModified}鈥斺��<b>杩欎竴涓槸鏈夊疄闄呭嵄瀹崇殑閭f潯</b>銆傚墠绔湪瀹冧负 true 鏃�
+     *       鍙覆鏌撱�屽凡淇敼銆嶃��**涓嶆墦鍗板��**銆傜己浜嗗畠锛屽瓧鍏�/鍔ㄦ�佸瓧娈典細鎶�**瀛樺偍鍘熷��**
+     *       锛堥洩鑺� ID / 瀛楀吀鐮侊級褰撴槑鏂囨覆鏌撳嚭鏉ワ細鏃㈡病鏈夊彲璇绘�э紝鍙堟妸涓嶈鐩存帴绀轰汉鐨勫師鍊兼憜鍒颁簡椤甸潰涓娿��
+     *       D1 瀹炴祴鏃ф暟鎹� 318 涓� diff 椤逛腑 <b>63 椤�</b>涓� true锛坰elect 42 + userSelect 21锛夈��</li>
+     *   <li>{@link #jnpfKey}鈥斺�旀帶浠剁被鍨嬶紝椹卞姩鍒嗘敮娓叉煋锛歿@code sign}/{@code signature} 娓叉煋鎴�
+     *       鍙偣寮�鐨勬棫/鏂扮鍚嶅浘鐗囬瑙堬紝鍏朵綑璧版槑鏂囨柊鏃у�笺�傜己浜嗗畠绛惧悕浼氶��鍖栨垚涓�涓� URL 鏂囨湰銆�</li>
+     * </ul>
+     *
+     * <h3>瀛愯〃锛坽@code chidData} / {@code chidField}锛夌敱璋冪敤鏂硅拷鍔�</h3>
+     * <p>杩欎袱涓敭鍚屾牱鍦� {@code listLog} 閲岋紝浣嗕笉鏄櫘閫氭爣閲忓瓧娈碉細
+     * <ul>
+     *   <li>鏅�氬瓧娈典粛鐢辨湰绫绘瘮杈冧富琛ㄥ揩鐓э紱瀛愯〃蹇呴』鎸夊瓙琛ㄤ富閿尮閰嶈锛屼笉鑳芥妸鏁存 JSON 褰撴爣閲忋��</li>
+     *   <li>visualdev 鍦ㄤ笟鍔″啓鍏ュ墠鍚庤鍙栨暟鎹簱锛屽苟鐢ㄥ師鐢熸棩蹇楃畻娉曠敓鎴愬畬鏁寸殑
+     *       {@code chidData/chidField}锛涚粺涓�瀹¤鐩戝惉鍣ㄦ牎楠屼袱鑰呭悓鏃跺瓨鍦ㄥ悗鍘熸牱杩藉姞銆�</li>
+     *   <li>鏃т簨浠舵垨闈炴爣鍑嗗彂甯冩柟缂哄皯鍙俊 {@code listLog} 鏃讹紝璋冪敤鏂圭户缁啓
+     *       {@code extra.subTableUndecidable}锛屼笉鏋勯�犱吉宸紓銆�</li>
+     * </ul>
+     * <p>鏈被鍙壙杞藉瓙琛ㄦ帶浠跺厓鏁版嵁锛涜绾х粨鏋勭敱鐩戝惉鍣ㄨ拷鍔狅紝閬垮厤鐮村潖灞� 1 鐨勬暟鎹簱鍒� diff 濂戠害銆�
+     */
+    public static final class FieldMeta {
+        private final String label;
+        private final String jnpfKey;
+        private final Boolean nameModified;
+        private final String valueType;
+        private final String componentType;
+        private final String format;
+        private final Map<String, String> optionLabels;
+        private final String dictionaryType;
+        private final Boolean hidden;
+
+        public FieldMeta(String label, String jnpfKey, Boolean nameModified) {
+            this(label, jnpfKey, nameModified, null, null, null, null, null, null);
+        }
+
+        public FieldMeta(String label, String jnpfKey, Boolean nameModified,
+                         String valueType, String componentType, String format,
+                         Map<String, String> optionLabels) {
+            this(label, jnpfKey, nameModified, valueType, componentType, format,
+                    optionLabels, null, null);
+        }
+
+        public FieldMeta(String label, String jnpfKey, Boolean nameModified,
+                         String valueType, String componentType, String format,
+                         Map<String, String> optionLabels, String dictionaryType,
+                         Boolean hidden) {
+            this.label = label;
+            this.jnpfKey = jnpfKey;
+            this.nameModified = nameModified;
+            this.valueType = valueType;
+            this.componentType = componentType;
+            this.format = format;
+            this.optionLabels = optionLabels == null || optionLabels.isEmpty()
+                    ? Collections.emptyMap()
+                    : Collections.unmodifiableMap(new LinkedHashMap<>(optionLabels));
+            this.dictionaryType = dictionaryType;
+            this.hidden = hidden;
+        }
+
+        public String label() {
+            return label;
+        }
+
+        public String jnpfKey() {
+            return jnpfKey;
+        }
+
+        public Boolean nameModified() {
+            return nameModified;
+        }
+
+        public String valueType() {
+            return valueType;
+        }
+
+        public String componentType() {
+            return componentType;
+        }
+
+        public String format() {
+            return format;
+        }
+
+        public Map<String, String> optionLabels() {
+            return optionLabels;
+        }
+
+        public String dictionaryType() {
+            return dictionaryType;
+        }
+
+        public Boolean hidden() {
+            return hidden;
+        }
+
+        /** 浣跨敤琛ㄥ崟鍦ㄤ簨浠跺彂鐢熸椂鎼哄甫鐨� options 鐢熸垚灞曠ず蹇収锛涙棤娉曞畬鏁磋В鏋愭椂杩斿洖 null銆� */
+        public String optionDisplay(String raw) {
+            if (raw == null || raw.trim().isEmpty() || optionLabels.isEmpty()) {
+                return raw == null || raw.trim().isEmpty() ? "" : null;
+            }
+            List<String> values = optionValues(raw);
+            if (values.isEmpty()) {
+                return null;
+            }
+            List<String> labels = new ArrayList<>(values.size());
+            for (String value : values) {
+                String label = optionLabels.get(value);
+                if (label == null) {
+                    return null;
+                }
+                labels.add(label);
+            }
+            return String.join("銆�", labels);
+        }
+
+        @SuppressWarnings("unchecked")
+        private static List<String> optionValues(String raw) {
+            String trimmed = raw.trim();
+            if (!trimmed.startsWith("[")) {
+                return Collections.singletonList(trimmed);
+            }
+            try {
+                Object parsed = JSON.parse(trimmed);
+                if (!(parsed instanceof Collection)) {
+                    return Collections.emptyList();
+                }
+                List<String> values = new ArrayList<>();
+                for (Object value : (Collection<Object>) parsed) {
+                    if (value != null) {
+                        values.add(String.valueOf(value));
+                    }
+                }
+                return values;
+            } catch (Throwable ignored) {
+                return Collections.emptyList();
+            }
+        }
+    }
+
+    /**
+     * 璁$畻 before 鈫� after 鐨勫瓧娈电骇 diff锛堟棤鏍囩鏉ユ簮锛寋@code fieldName} 鍥炶惤鍒楀悕锛夈��
+     *
+     * @param before before-image 琛岋紙DELETE 鍦烘櫙浼犲垹闄ゅ墠鐨勮锛宎fter 浼� null锛�
+     * @param after  after-image 琛岋紙INSERT 鍦烘櫙浼犳彃鍏ュ悗鐨勮锛宐efore 浼� null锛�
+     * @param masked 鑴辨晱鍒楋紙灏忓啓鍒楀悕锛�
+     * @return {@code VisualLogModel[]} 褰㈢姸鐨� JSON 瀛楃涓诧紱鏃犲樊寮傛椂杩斿洖 {@code "[]"}锛堜笉杩斿洖 null锛�
+     */
+    public static String diff(Map<String, Object> before, Map<String, Object> after, Set<String> masked) {
+        return diff(before, after, masked, null);
+    }
+
+    /**
+     * 璁$畻 before 鈫� after 鐨勫瓧娈电骇 diff锛屽苟鐢� {@code meta} 琛ュ睍绀哄厓鏁版嵁銆�
+     *
+     * @param meta 瀛楁鍚� 鈫� {@link FieldMeta}锛堜腑鏂囨爣绛� / 鎺т欢绫诲瀷 / 鏄惁鍙樉绀恒�屽凡淇敼銆嶏級銆�
+     *             null 鎴栨煡涓嶅埌鐨勫瓧娈碉細{@code fieldName} 鍥炶惤鎴愬瓧娈靛悕鏈韩锛�
+     *             {@code jnpfKey}/{@code nameModified} 涓や釜閿�**鏁翠釜涓嶅嚭鐜�**銆�
+     *             <p><b>銆屾煡涓嶅埌灏变笉鍑鸿繖涓敭銆嶆槸鍒绘剰鐨勶紝涓嶆槸鐪佷簨</b>锛氬眰 1 娌℃湁鍏冩暟鎹潵婧�
+     *             锛堝畠闈㈠鐨勬槸鏁版嵁搴撳垪锛屾病鏈夎〃鍗曟ā鍨嬶級锛屾亽浼� null銆傝嫢鏃犳潯浠惰ˉ涓�
+     *             {@code "jnpfKey":null} 涔嬬被锛屽眰 1 鐨� 190 鏉℃棦鏈夋柇瑷�浼氬洜褰㈢姸鍙樺寲鑰岀孩锛�
+     *             鑰屽畠鐨勮涔変竴鐐规病鍙樷�斺�旈偅鏄函绮圭殑鑷激銆傜己甯嵆銆屾湰灞傜粰涓嶅嚭銆嶏紝
+     *             鍓嶇鎸� key 瀛樺湪涓庡惁鍒嗘敮锛屽ぉ鐒跺吋瀹广��
+     *             <p>鍏冩暟鎹�**涓嶅弬涓庢瘮杈冦�佷笉褰卞搷鏄惁璁板綍**锛屾墍浠ュ畠缂哄け鏈�鍧忓彧鏄樉绀哄緱绯欎竴鐐癸紝
+     *             缁濅笉浼氳涓�澶勭湡瀹炲彉鏇存秷澶便��
+     */
+    public static String diff(Map<String, Object> before, Map<String, Object> after,
+                              Set<String> masked, Map<String, FieldMeta> meta) {
+        List<Map<String, Object>> items = new ArrayList<>();
+        for (String column : unionColumns(before, after)) {
+            Object oldValue = before == null ? null : before.get(column);
+            Object newValue = after == null ? null : after.get(column);
+            if (valuesEqual(oldValue, newValue)) {
+                continue;
+            }
+            FieldMeta fm = fieldMeta(meta, column);
+            boolean hide = isMasked(column, masked) || isCredentialMetadata(fm);
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("field", column);
+            String label = fm == null ? null : fm.label();
+            item.put("fieldName", label == null || label.isEmpty() ? column : label);
+            // 椤哄簭瀵归綈 lims 鏃� VisualLogModel锛屾柟渚夸笌鏃� data_log 閫愰」瀵圭収锛涘墠绔寜 key 鍙栵紝涓嶄緷璧栭『搴忋��
+            if (fm != null && fm.jnpfKey() != null && !fm.jnpfKey().isEmpty()) {
+                item.put("jnpfKey", fm.jnpfKey());
+            }
+            String oldData = hide ? MASKED : stringify(oldValue);
+            String newData = hide ? MASKED : stringify(newValue);
+            item.put("oldData", oldData);
+            item.put("newData", newData);
+            if (fm != null && fm.valueType() != null) {
+                item.put("valueType", fm.valueType());
+            }
+            if (fm != null && fm.componentType() != null) {
+                item.put("componentType", fm.componentType());
+            }
+            if (fm != null && fm.format() != null) {
+                item.put("format", fm.format());
+            }
+            if (fm != null && fm.dictionaryType() != null) {
+                item.put("dictionaryType", fm.dictionaryType());
+            }
+            if (!hide && fm != null) {
+                String oldDisplay = fm.optionDisplay(oldData);
+                String newDisplay = fm.optionDisplay(newData);
+                if (oldDisplay != null) {
+                    item.put("oldDisplay", oldDisplay);
+                }
+                if (newDisplay != null) {
+                    item.put("newDisplay", newDisplay);
+                }
+            }
+            // nameModified=true 鏃跺墠绔彧娓叉煋銆屽凡淇敼銆嶄笉鎵撳嵃鍊笺��**鑴辨晱鍒椾笂鐓ф牱甯�**鈥斺��
+            // 涓よ�呬笉鍐茬獊锛氳劚鏁忕鐨勬槸銆屽�间笉璁般�嶏紝nameModified 绠$殑鏄�屽�间笉灞曠ず銆嶏紝
+            // 鍚屾椂鎴愮珛鏃跺墠绔樉绀恒�屽凡淇敼銆嶈�屼笉鏄� `***`锛岃涔夋洿鍑嗐��
+            if (fm != null && fm.nameModified() != null) {
+                item.put("nameModified", fm.nameModified());
+            }
+            // 涓� LimsFieldDiff 鍚屼箟锛�0=浠庢棤鍒版湁锛堟柊澧烇級锛�1=淇敼銆�
+            // **鑴辨晱鍒楁亽璁� 1**锛圔5 璇勫 M-3锛夛細type=0 绛変簬鍛婅瘔璇昏�呫�岃繖鍒楁鍓嶆病鏈夊�笺�嶁�斺��
+            // 鍐呭娌℃紡锛屽瓨鍦ㄦ�ф紡浜嗗崐鏍硷紙銆屽垵濮嬪瘑鐮佹槸绗竴娆¤缃�嶆湰韬氨鏄俊鎭級銆�
+            // 鑴辨晱鐨勬壙璇烘槸銆屽彧璁板凡鍙樻洿銆嶏紝閭e氨杩炶繖鍗婃牸涔熶笉缁欍��
+            item.put("type", !hide && isEmpty(oldValue) ? 0 : 1);
+            items.add(item);
+        }
+        return JsonUtil.getObjectToString(items);
+    }
+
+    /**
+     * 琛ㄥ崟 vModel 淇濈暀 camelCase锛岃�� PostgreSQL 鏈姞寮曞彿鐨勫垪鍚嶄細鎶樺彔涓哄皬鍐欍��
+     * 鍏冩暟鎹敞鍐岃〃缁熶竴瀛樺皬鍐欓敭锛屽洜姝ゅ厛绮剧‘鍖归厤锛屽啀鎸夊皬鍐欓敭鍥為��銆�
+     */
+    private static FieldMeta fieldMeta(Map<String, FieldMeta> meta, String column) {
+        if (meta == null || column == null) {
+            return null;
+        }
+        FieldMeta exact = meta.get(column);
+        return exact != null ? exact : meta.get(column.toLowerCase(Locale.ROOT));
+    }
+
+    /**
+     * 鏁磋蹇収锛堣惤 {@code data_snapshot}锛夈�侱ELETE 鐢� before銆佸叾浣欑敤 after鈥斺��
+     * 蹇収鐨勬剰涔夋槸銆岃繖娆℃搷浣滀箣鍚庤繖琛岄暱浠�涔堟牱銆嶏紝鍒犻櫎鍒欐槸銆屽垹鎺夌殑鏄粈涔堛�嶃��
+     */
+    public static String snapshot(Map<String, Object> row, Set<String> masked) {
+        return snapshot(row, masked, null);
+    }
+
+    /** 鏁磋蹇収锛岄澶栦娇鐢ㄥ瓧娈靛厓鏁版嵁璇嗗埆闈炲浐瀹氬垪鍚嶇殑绛惧悕鎺т欢銆� */
+    public static String snapshot(Map<String, Object> row, Set<String> masked,
+                                  Map<String, FieldMeta> meta) {
+        if (row == null) {
+            return null;
+        }
+        Map<String, Object> out = new LinkedHashMap<>();
+        for (Map.Entry<String, Object> entry : row.entrySet()) {
+            String column = entry.getKey();
+            FieldMeta fm = fieldMeta(meta, column);
+            out.put(column, isMasked(column, masked) || isCredentialMetadata(fm)
+                    ? MASKED : stringify(entry.getValue()));
+        }
+        return JsonUtil.getObjectToString(out);
+    }
+
+    /** 绛惧悕鎺т欢鐨勮劚鏁忕敱鍏冩暟鎹喅瀹氾紝涓嶄緷璧栧瓧娈靛悕绾﹀畾銆� */
+    public static boolean isCredentialMetadata(FieldMeta meta) {
+        if (meta == null) {
+            return false;
+        }
+        String valueType = lower(meta.valueType());
+        String componentType = lower(meta.componentType());
+        String jnpfKey = lower(meta.jnpfKey());
+        return "masked".equals(valueType)
+                || "sign".equals(componentType)
+                || "signature".equals(componentType)
+                || "sign".equals(jnpfKey)
+                || "signature".equals(jnpfKey);
+    }
+
+    private static String lower(String value) {
+        return value == null ? null : value.trim().toLowerCase(Locale.ROOT);
+    }
+
+    /** after 鐨勫垪鍦ㄥ墠銆乥efore 鐙湁鐨勫垪琛ュ湪鍚庯細杈撳嚭椤哄簭瀵逛汉璇� diff 鏇村弸濂斤紝涓旂ǔ瀹氬彲鏂█銆� */
+    private static Set<String> unionColumns(Map<String, Object> before, Map<String, Object> after) {
+        Set<String> columns = new LinkedHashSet<>();
+        if (after != null) {
+            columns.addAll(after.keySet());
+        }
+        if (before != null) {
+            columns.addAll(before.keySet());
+        }
+        return columns;
+    }
+
+    private static boolean isMasked(String column, Set<String> masked) {
+        return masked != null && column != null && masked.contains(column.toLowerCase(Locale.ROOT));
+    }
+
+    /**
+     * 褰掍竴鍖栨瘮杈冦��**椤哄簭鏈夎绌�**锛氬厛澶勭悊 null锛屽啀鎸夈�屾暟鍊� / 鏃堕棿 / 瀛楄妭鏁扮粍銆嶄笁绫诲綊涓�锛�
+     * 鏈�鍚庢墠閫�鍥� equals鈥斺�斿弽杩囨潵鍐欎細璁� {@code BigDecimal("1.0")} 涓� {@code 1} 鍒ゆ垚涓嶇瓑銆�
+     *
+     * <p><b>鏁存鍖� try锛圔3 璇勫 I-1锛�</b>锛氭瘮杈冨崟涓�兼椂鎶涘嚭鐨勫紓甯告浘鑳界┛鍑� {@code diff()}锛�
+     * 琚� emitter 鐨勬�诲厹搴曞悆鎺夛紝缁撴灉鏄�**璇ヨ杩炲悓鍏跺悗鎵�鏈夎鐨勪簨浠跺叏閮ㄩ潤榛樻秷澶�**鈥斺��
+     * 婕忚鐪熷疄鍙樻洿姣旇閿欐洿涓ラ噸銆傜幇鍦ㄩ��鍖栨垚銆屾寜涓嶇浉绛夊鐞嗐�嶏細鏈�鍧忔槸澶氳涓�鏉″瓧娈靛彉鏇达紝
+     * 涓嶄細璁╂暣鏉′簨浠舵秷澶便��
+     */
+    public static boolean valuesEqual(Object a, Object b) {
+        try {
+            if (isEmpty(a) && isEmpty(b)) {
+                return true;
+            }
+            if (a == null || b == null) {
+                return a == null && b == null;
+            }
+            if (a instanceof Number && b instanceof Number) {
+                return numbersEqual((Number) a, (Number) b);
+            }
+            if (a instanceof Date && b instanceof Date) {
+                // 鐢� compareTo 鑰岄潪 getTime()锛堣瘎瀹� M-3锛夛細java.sql.Timestamp 鐨勭撼绉掔簿搴︿笉鍦�
+                // getTime() 閲岋紝浜氭绉掑樊寮備細琚垽鎴�"娌″彉"鈥斺�旈偅鏄紡璁扮湡瀹炲彉鏇淬��
+                // Timestamp.compareTo(Date) 涓� Date.compareTo 閮芥寜鍚勮嚜瀹屾暣绮惧害姣斻��
+                return compareDates((Date) a, (Date) b);
+            }
+            if (a instanceof byte[] && b instanceof byte[]) {
+                // 瀛楄妭鏁扮粍鐨� equals 鏄紩鐢ㄦ瘮杈冿紝涓嶅綊涓�浼氳姣忔 diff 閮芥妸 BLOB 鍒楁姤鎴愩�屽凡鍙樻洿銆�
+                return Arrays.equals((byte[]) a, (byte[]) b);
+            }
+            return Objects.equals(a, b);
+        } catch (Throwable t) {
+            return false;   // 姣旇緝涓嶄簡灏卞綋"鍙樹簡"锛氬畞鍙璁颁竴鏉★紝涔熶笉璁╂暣鏉′簨浠舵秷澶�
+        }
+    }
+
+    /**
+     * 鏁板�兼瘮杈冦��**鍏堟尅浣忛潪鏈夐檺鍊�**锛圢aN / 卤Infinity锛夛細瀹冧滑鏄� PG float8/real 鐨勫悎娉曞�硷紝
+     * 鑰� {@code BigDecimal.valueOf(NaN)} 鐩存帴鎶� {@code NumberFormatException}锛堣瘎瀹� I-1 瀹炶瘉锛�
+     * 杩� {@code NaN vs NaN} 閮界偢锛夈�傞潪鏈夐檺鍊间竴寰嬮��鍥� {@code Double.compare} 鐨勮涔夆�斺��
+     * 瀹冩妸 {@code NaN == NaN} 鍒や负鐩哥瓑銆亄@code NaN != 1.0} 鍒や负涓嶇瓑锛屾鏄璁℃兂瑕佺殑銆�
+     */
+    private static boolean numbersEqual(Number a, Number b) {
+        if (isNonFinite(a) || isNonFinite(b)) {
+            return Double.compare(a.doubleValue(), b.doubleValue()) == 0;
+        }
+        return toBigDecimal(a).compareTo(toBigDecimal(b)) == 0;
+    }
+
+    private static boolean isNonFinite(Number number) {
+        if (number instanceof Double || number instanceof Float) {
+            double value = number.doubleValue();
+            return Double.isNaN(value) || Double.isInfinite(value);
+        }
+        return false;
+    }
+
+    /** {@code Timestamp} 涓� {@code Date} 娣锋瘮鏃朵互 Timestamp 涓�渚х殑瀹屾暣绮惧害涓哄噯銆� */
+    private static boolean compareDates(Date a, Date b) {
+        if (a instanceof java.sql.Timestamp || b instanceof java.sql.Timestamp) {
+            java.sql.Timestamp left = a instanceof java.sql.Timestamp
+                    ? (java.sql.Timestamp) a : new java.sql.Timestamp(a.getTime());
+            java.sql.Timestamp right = b instanceof java.sql.Timestamp
+                    ? (java.sql.Timestamp) b : new java.sql.Timestamp(b.getTime());
+            return left.compareTo(right) == 0;
+        }
+        return a.getTime() == b.getTime();
+    }
+
+    private static BigDecimal toBigDecimal(Number number) {
+        if (number instanceof BigDecimal) {
+            return (BigDecimal) number;
+        }
+        if (number instanceof Double || number instanceof Float) {
+            return BigDecimal.valueOf(number.doubleValue());
+        }
+        if (number instanceof java.math.BigInteger) {
+            return new BigDecimal((java.math.BigInteger) number);   // 鍒粡 longValue() 鎴柇
+        }
+        return BigDecimal.valueOf(number.longValue());
+    }
+
+    /** 灞曠ず鐢ㄥ瓧绗︿覆銆俷ull 鈫� 绌轰覆锛堜笌 LimsFieldDiff 涓�鑷达紝鍓嶇鎸夌┖涓插垽銆屾棤鍊笺�嶏級銆� */
+    public static String stringify(Object value) {
+        if (value == null) {
+            return "";
+        }
+        if (value instanceof Date) {
+            return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) value);
+        }
+        if (value instanceof byte[]) {
+            return "byte[" + ((byte[]) value).length + "]";   // 涓嶆妸浜岃繘鍒跺杩涘璁℃枃鏈�
+        }
+        return String.valueOf(value);
+    }
+
+    private static boolean isEmpty(Object value) {
+        if (value == null) {
+            return true;
+        }
+        if (value instanceof CharSequence) {
+            return value.toString().trim().isEmpty();
+        }
+        if (value instanceof Collection) {
+            return ((Collection<?>) value).isEmpty();
+        }
+        return value instanceof Map && ((Map<?, ?>) value).isEmpty();
+    }
+}

--
Gitblit v1.8.0