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