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/AuditFormRegistry.java | 1129 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 1,129 insertions(+), 0 deletions(-)
diff --git a/jnpf-biz-common/jnpf-biz-common-audit/src/main/java/jnpf/bizcommon/audit/service/AuditFormRegistry.java b/jnpf-biz-common/jnpf-biz-common-audit/src/main/java/jnpf/bizcommon/audit/service/AuditFormRegistry.java
new file mode 100644
index 0000000..a21dd3f
--- /dev/null
+++ b/jnpf-biz-common/jnpf-biz-common-audit/src/main/java/jnpf/bizcommon/audit/service/AuditFormRegistry.java
@@ -0,0 +1,1129 @@
+package jnpf.bizcommon.audit.service;
+
+import jnpf.audit.diff.AuditFieldDiff;
+import jnpf.bizcommon.audit.service.title.AuditListTitleSpecParser;
+import jnpf.bizcommon.audit.service.title.AuditTitleSpec;
+import jnpf.bizcommon.audit.service.title.AuditTitleSpecParser;
+import jnpf.util.JsonUtil;
+import jnpf.util.visiual.JnpfKeyConsts;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 灞� 0 琛ㄥ崟娉ㄥ唽琛紙Task C1锛夛細鍥炵瓟銆岃繖涓湪绾胯〃鍗曡涓嶈瀹¤銆佺畻鍝釜涓氬姟妯″潡銆嶃��
+ *
+ * <p>鍙栦唬 lims 渚� {@code BizFormRegistry} 鐨� 18 鏉$‖缂栫爜闈欐�� Map鈥斺�旈偅寮犺〃姣忔帴涓�涓柊琛ㄥ崟灏辫鏀�
+ * lims 鐨勪唬鐮併�侀噸鏂版墦鍖呫�侀噸鍚湇鍔★紝涓氬姟妯″潡涔嬮棿杩樹細浜掔浉韪┿�傜幇鍦ㄦ敼鎴� <b>Nacos 閰嶇疆椹卞姩</b>
+ * 锛坉ata-id {@code audit-layer0-forms.yaml}锛夛紝鍚勪笟鍔℃ā鍧楄嚜鍔╂敞鍐岃嚜宸辩殑琛ㄥ崟锛宎udit 鏈嶅姟涓嶅姩浠g爜銆�
+ *
+ * <p><b>鎸変富琛ㄥ悕娉ㄥ唽锛岃�屼笉鏄寜 modelId</b>锛歮odelId 鏄棤鎰忎箟鐨勯洩鑺辨暟瀛楋紙{@code 844231923957086149}锛夛紝
+ * 鍐欒繘閰嶇疆娌′汉鐪嬪緱鎳備篃娌′汉鏁㈡敼锛涜�屼笖鍚屼竴寮犱笟鍔′富琛ㄥ父鏈夊涓〃鍗曡鍥撅紙鍒楄〃椤�/璇︽儏椤�/绉诲姩绔悇涓�涓�
+ * modelId锛夛紝鎸夎〃鐧昏鍙渶涓�鏉°�俶odelId 鈫� 涓昏〃鍚嶈繖涓�璺充粛璧� JDBC 鍙嶆煡 {@code base_visual_release}
+ * 骞剁紦瀛橈紝涓庢棫瀹炵幇鍚屽彛寰勨�斺�擜/B 姣斿鏃跺皯涓�涓彉閲忋��
+ *
+ * <p>鏄惁鍙寜娓呭崟瀹¤鐢� {@code includeUnregisteredForms} 鍐冲畾銆傚紑鍚悗锛屽厑璁哥殑搴旂敤鍓嶅彴涓墍鏈夊湪绾�
+ * 琛ㄥ崟閮界撼鍏ュ璁★紱娓呭崟缁х画鎻愪緵妯″潡銆佷笟鍔$被鍨嬨�佽劚鏁忓瓧娈电瓑鏇村噯纭殑鍏冩暟鎹��
+ */
+@Slf4j
+@Component
+@ConfigurationProperties(prefix = "audit.layer0")
+public class AuditFormRegistry {
+
+ /**
+ * 灞� 0 鎬诲紑鍏炽�傞粯璁ゅ叧鈥斺�斾笌灞� 1 鍚岀邯寰嬶細鏂板煁鐐归粯璁や笉涓婏紝鎸夎〃鐏板害寮�銆�
+ * volatile锛歂acos 鍒锋柊绾跨▼鍐欍�丮Q 娑堣垂绾跨▼璇伙紙璇勫 M-1锛夈��
+ */
+ private volatile boolean enabled = false;
+
+ /** 浠呮帴鍙楀墠绔簲鐢ㄥ墠鍙拌姹傦紱鏃ф秷鎭病鏈� appScope 鏃舵寜闈炲墠鍙板鐞嗐�� */
+ private volatile boolean frontendOnly = false;
+
+ /** READ 鏃ュ織鐙珛寮�鍏筹紝榛樿鍏抽棴浠ラ伩鍏嶅垪琛ㄨ疆璇骇鐢熷ぇ閲忔棩蹇椼�� */
+ private volatile boolean recordRead = false;
+
+ /** 鍏佽鐨勫墠绔簲鐢ㄧ紪鐮侊紱绌洪泦鍚堣〃绀轰笉闄愬埗鍏蜂綋搴旂敤銆� */
+ private List<String> allowedAppCodes = new ArrayList<>();
+ private volatile Set<String> allowedAppCodeSet = Collections.emptySet();
+
+ /** true 鏃讹紝搴旂敤鍓嶅彴璁块棶鐨勬湭鏄惧紡鐧昏鍦ㄧ嚎琛ㄥ崟涔熺撼鍏ュ璁°�� */
+ private volatile boolean includeUnregisteredForms = false;
+
+ /**
+ * 娉ㄥ唽娓呭崟锛岀敱 Nacos 閰嶇疆娉ㄥ叆銆�
+ *
+ * <p><b>褰㈡�佸繀椤绘槸 List 鑰屼笉鏄� Map<琛ㄥ悕, 鍏冩暟鎹�></b>锛圕1 瀹炴祴韪╁埌锛夛細Spring Boot Binder
+ * 瀵� Map 灞炴�у仛鐨勬槸<b>鍚堝苟</b>鈥斺�斿埛鏂版椂瀹冨厛璋� getter 鎷垮埌鐜版湁 Map锛屽啀鎶婃柊閰嶇疆 put 杩涘幓锛�
+ * 浜庢槸<b>琚垹鎺夌殑琛ㄥ崟姘歌繙鐣欏湪娉ㄥ唽琛ㄩ噷</b>锛屻�屼粠娓呭崟绉婚櫎涓�寮犺〃銆嶅彧鏈夐噸鍚墠鐢熸晥銆�
+ * 瀹¤鑼冨洿鍙兘鍔犱笉鑳藉噺锛屾槸杩愯惀涓婄殑鐪熼殣鎮o紙瀹炴祴锛氭挙鍥為厤缃悗閭e紶琛ㄧ収璁颁笉璇級銆�
+ * 绱㈠紩闆嗗悎锛圠ist锛夌殑缁戝畾鍒欐槸鏁翠綋鏇挎崲锛宻etter 姣忔閮芥嬁鍒板畬鏁寸殑鏂版竻鍗曪紝澧炲垹閮藉嵆鏃剁敓鏁堛��
+ */
+ private List<FormMeta> forms = new ArrayList<>();
+
+ /** 涓昏〃鍚嶏紙灏忓啓锛夆啋 鍏冩暟鎹�傜敱 {@link #setForms} 浠庢竻鍗曢噸寤猴紝姣忔鍒锋柊鏁翠綋鏇挎崲銆� */
+ private volatile Map<String, FormMeta> formsByTable = Collections.emptyMap();
+
+ /**
+ * modelId 鈫� 涓昏〃鍚� 鐨勮繍琛屾椂缂撳瓨銆�
+ *
+ * <p><b>鍙紦瀛樼‘瀹氭�х粨鏋�</b>锛堢┖涓插崰浣嶈〃绀恒�岀‘瀹氬湴鏌ヤ笉鍒般�嶏級锛氭煡寰楀埌鍙戝竷璁板綍浣嗛噷闈㈡病鏈夎〃銆�
+ * 鎴栭偅娈� JSON 鏍规湰涓嶅悎娉曗�斺�旈噸璇曚竴涓囨涔熶竴鏍枫��**鐬椂鏁呴殰涓�寰嬩笉缂撳瓨**锛堣繛鎺ユ姈鍔ㄣ�佽秴鏃躲��
+ * OOM/SO锛夆�斺�擫061 鈶狅細缂撳瓨"澶辫触"蹇呴』鍖哄垎纭畾鎬т笌鐬椂锛岃�屼笖蹇呴』鐢ㄧ櫧鍚嶅崟鍒ゅ畾銆�
+ * 杩欓噷鐨勭紦瀛樻槸杩涚▼绾с�佹棤杩囨湡鐨勶紝涓�娆¤繛鎺ユ姈鍔ㄨ嫢琚鎴愩�岃繖涓� modelId 娌℃湁涓昏〃銆嶏紝
+ * 璇ヨ〃鍗曚粠姝ゅ埌閲嶅惎鍓�**姘歌繙涓嶈瀹¤**锛岃�屼笖鏃ュ織閲屼粈涔堥兘娌℃湁銆�
+ *
+ * <p>缂撳瓨浣跨敤鏈夐檺 TTL锛涙敞鍐岃〃鐑埛鏂板拰 TTL 閰嶇疆鍙樺寲杩樹細涓诲姩娓呯┖銆傝〃鍗曢噸鏂板彂甯冧絾鏈Е鍙�
+ * 娉ㄥ唽琛ㄥ埛鏂版椂锛屾渶杩熷湪 TTL 鍒版湡鍚庨噸鏂拌鍙栵紝閬垮厤鍘嗗彶鏌ヨ闀挎湡缁戝畾鏃ч厤缃��
+ */
+ private final Map<String, CacheValue<String>> modelIdToMainTable = new ConcurrentHashMap<>();
+
+ /** modelId 鈫� 宸插彂甯冭〃鍗曞瓧娈靛厓鏁版嵁锛涘彧缂撳瓨纭畾鎬ф煡璇�/瑙f瀽缁撴灉銆� */
+ private final Map<String, CacheValue<Map<String, AuditFieldDiff.FieldMeta>>> modelIdToFieldMeta =
+ new ConcurrentHashMap<>();
+
+ /** modelId 鈫� 鏍囬鍕鹃�夐厤缃紱涓庡瓧娈靛厓鏁版嵁鍚屼竴濂� TTL / 鍙紦瀛樼‘瀹氭�х粨鏋滅殑鍙e緞銆� */
+ private final Map<String, CacheValue<AuditTitleSpec>> modelIdToTitleSpec =
+ new ConcurrentHashMap<>();
+
+ /** 鍒楄〃鏍囬瑙勬牸缂撳瓨锛圖6锛夈�備笌 modelIdToTitleSpec 鍒嗗紑锛氬悓涓� modelId 涓ょ鍙e緞涓嶈兘鍏辩敤涓�鏍笺�� */
+ private final Map<String, CacheValue<AuditTitleSpec>> modelIdToListTitleSpec =
+ new ConcurrentHashMap<>();
+
+ /** 绛惧悕瀛楁閰嶇疆缂撳瓨锛圖4锛夈�� */
+ private final Map<String, CacheValue<List<String>>> modelIdToSignFields =
+ new ConcurrentHashMap<>();
+
+ /** 鏁版嵁搴撳瓧娈靛娉ㄧ紦瀛橈紱鏌ヨ寮傚父鏃朵笉鍐欑紦瀛樸�� */
+ private final Map<String, CacheValue<Map<String, AuditFieldDiff.FieldMeta>>> tableToCommentMeta =
+ new ConcurrentHashMap<>();
+
+ /** 鍙戝竷琛ㄥ崟銆佷富琛ㄥ弽鏌ュ強 COMMENT 鍏冩暟鎹紦瀛� TTL锛汵acos 鍒锋柊娉ㄥ唽琛ㄦ椂杩樹細涓诲姩娓呯┖銆� */
+ private volatile long metadataCacheTtlMs = 300_000L;
+
+ /** 鍙嶆煡涓嶅埌涓昏〃鐨� modelId 鍙憡璀︿竴娆★紝閬垮厤楂橀琛ㄥ崟鎶婃棩蹇楀埛鐖嗐�� */
+ private final Set<String> warnedModels = ConcurrentHashMap.newKeySet();
+
+ @Autowired
+ private JdbcTemplate jdbcTemplate;
+
+ /**
+ * 涓�涓湪绾胯〃鍗曞湪瀹¤閲岀殑韬唤銆�
+ * 闄� {@code table} 澶栧瓧娈靛叏閮ㄥ彲閫夛紝缂轰簡鍙槸璁板緱绯欎竴鐐癸紝涓嶅奖鍝嶄簨浠舵槸鍚﹁惤搴撱��
+ */
+ @Data
+ public static class FormMeta {
+ /** 涓氬姟涓昏〃鍚嶏紙娓呭崟鐨勪富閿級銆傜己澶辨垨绌虹櫧鐨勬潯鐩細琚� {@link #setForms} 涓㈠純骞跺憡璀︺�� */
+ private String table;
+ /** 涓氬姟妯″潡璇箟锛岃惤 {@code audit_events.biz_module}锛屽銆岀幆澧冪洃娴�-鍖哄煙绠$悊銆嶃�� */
+ private String bizModule;
+ /** 涓氬姟鍒嗙被锛岃惤 {@code biz_type}锛屼笌鏃� {@code BizLogType} 鍚屼綅銆� */
+ private String bizType;
+ /** 浠庤〃鍗曟暟鎹噷鍙栦笟鍔″崟鍙风殑瀛楁鍚嶏紝钀� {@code biz_code}锛涘彇涓嶅埌灏辩暀绌猴紝**涓嶄涪浜嬩欢**銆� */
+ private String bizCodeField;
+ /** 鑴辨晱瀛楁锛堝皬鍐欙級锛氬彧璁般�屽凡鍙樻洿銆嶄笉璁板�硷紝涓庡眰 1 鐨� masked-columns 鍚岃涔夈�� */
+ private Set<String> maskedFields = Collections.emptySet();
+ }
+
+ /**
+ * 鍚姩瀹屾垚鍚庢墦鍗颁竴娆�**鏉冨▉**鐢熸晥鎬併��
+ *
+ * <p>{@link #setForms} 閲岄偅琛屼篃鎵撲簡 enabled锛屼絾灞炴�х粦瀹氭椂 setter 鐨勮皟鐢ㄩ『搴忎笉瀹氾紝
+ * 閭d竴琛屽彲鑳芥棭浜� {@link #setEnabled} 鑰屾樉绀轰笂涓�鎷嶇殑鍊笺�傝繖閲屽湪缁戝畾鍏ㄩ儴瀹屾垚鍚庡啀鎶ヤ竴娆★紝
+ * 鎺掓煡銆屽埌搴曞紑娌″紑銆嶆椂浠ヨ繖琛屼负鍑嗐��
+ */
+ @jakarta.annotation.PostConstruct
+ public void logEffectiveState() {
+ log.info("[AUDIT-L0] 灞�0 鐢熸晥鎬侊細enabled={}锛屾敞鍐岃〃 {} 寮犺〃 {}",
+ enabled, formsByTable.size(), formsByTable.keySet());
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isRecordRead() {
+ return recordRead;
+ }
+
+ public void setRecordRead(boolean recordRead) {
+ this.recordRead = recordRead;
+ }
+
+ public boolean isFrontendOnly() {
+ return frontendOnly;
+ }
+
+ public void setFrontendOnly(boolean frontendOnly) {
+ this.frontendOnly = frontendOnly;
+ }
+
+ public boolean isIncludeUnregisteredForms() {
+ return includeUnregisteredForms;
+ }
+
+ public void setIncludeUnregisteredForms(boolean includeUnregisteredForms) {
+ this.includeUnregisteredForms = includeUnregisteredForms;
+ clearMetadataCaches();
+ }
+
+ public List<String> getAllowedAppCodes() {
+ return allowedAppCodes;
+ }
+
+ public void setAllowedAppCodes(List<String> allowedAppCodes) {
+ this.allowedAppCodes = allowedAppCodes == null ? new ArrayList<>() : allowedAppCodes;
+ Set<String> normalized = new LinkedHashSet<>();
+ for (String code : this.allowedAppCodes) {
+ if (code != null && !code.trim().isEmpty()) {
+ normalized.add(code.trim().toUpperCase(Locale.ROOT));
+ }
+ }
+ this.allowedAppCodeSet = normalized.isEmpty()
+ ? Collections.emptySet() : Collections.unmodifiableSet(normalized);
+ }
+
+ /** 鍒ゆ柇璇锋眰鏉ユ簮鏄惁灞炰簬閰嶇疆鍏佽鐨勫簲鐢ㄥ墠鍙般�� */
+ public boolean acceptsRequest(String appScope, String appCode) {
+ if (frontendOnly && !"FRONTEND".equalsIgnoreCase(appScope == null ? "" : appScope.trim())) {
+ return false;
+ }
+ if (allowedAppCodeSet.isEmpty()) {
+ return true;
+ }
+ return appCode != null
+ && allowedAppCodeSet.contains(appCode.trim().toUpperCase(Locale.ROOT));
+ }
+
+ public long getMetadataCacheTtlMs() {
+ return metadataCacheTtlMs;
+ }
+
+ public void setMetadataCacheTtlMs(long metadataCacheTtlMs) {
+ this.metadataCacheTtlMs = Math.max(0L, metadataCacheTtlMs);
+ clearMetadataCaches();
+ }
+
+ public List<FormMeta> getForms() {
+ return forms;
+ }
+
+ /**
+ * 浠庢竻鍗曢噸寤烘煡鎵捐〃锛�**鏁翠綋鏇挎崲**锛堣繖姝f槸鐢� List 鐨勬剰涔夛紝瑙� {@link #forms} 娉ㄩ噴锛夈��
+ *
+ * <p>琛ㄥ悕涓庤劚鏁忓瓧娈典竴寰嬪綊涓�鎴愬皬鍐欙細PG 閲岀殑琛ㄥ悕鏄皬鍐欙紝鑰岄厤缃槸鎵嬪啓鐨勶紙澶у皬鍐欍�佺┖鏍奸殢浜猴級锛�
+ * 鍦ㄥ叆鍙e缁熶竴涓�娆★紝姣斿湪姣忎釜鏌ヨ鐐瑰悇鍐欎竴娆� toLowerCase 鍙潬鈥斺�旀紡涓�澶勫氨鏄劚鏁忛潤榛樺け鏁堛��
+ */
+ public void setForms(List<FormMeta> forms) {
+ this.forms = forms == null ? new ArrayList<>() : forms;
+ Map<String, FormMeta> rebuilt = new LinkedHashMap<>();
+ for (FormMeta meta : this.forms) {
+ if (meta == null) {
+ continue;
+ }
+ String table = meta.getTable() == null ? null : lower(meta.getTable().trim());
+ if (table == null || table.isEmpty()) {
+ // 娌℃湁 table 鐨勬潯鐩棤浠庡尮閰嶏紝闈欓粯涓㈠純灏辨垚浜嗐�岄厤浜嗗嵈涓嶇敓鏁堛�嶇殑鍝戣皽
+ log.error("[AUDIT-L0] 娉ㄥ唽琛ㄩ噷鏈変竴鏉$己 table 鐨勮褰曪紝宸插拷鐣ワ細{}", meta);
+ continue;
+ }
+ meta.setTable(table); // 褰掍竴鍖栧啓鍥烇紝鍏嶅緱灏嗘潵鏈変汉鐢� meta.getTable() 鎷垮埌澶у皬鍐欎笉涓�鐨勫師鍊�
+ meta.setMaskedFields(lowerAll(meta.getMaskedFields()));
+ rebuilt.put(table, meta);
+ }
+ this.formsByTable = rebuilt;
+ clearMetadataCaches();
+ // **鎬诲紑鍏充竴骞舵墦鍗�**锛堣瘎瀹� M-5锛夛細鍙姤銆岃浇鍏ヤ簡鍑犲紶琛ㄣ�嶇殑璇濓紝鍏虫�佷笌銆屼竴寮犳病閰嶃�嶅湪鏃ュ織涓�
+ // 闀垮緱涓�妯′竴鏍凤紝鎺掓煡鏃跺垎涓嶅嚭鏄病閰嶇疆杩樻槸琚叧鎺変簡銆�
+ log.info("[AUDIT-L0] 琛ㄥ崟娉ㄥ唽琛ㄥ凡杞藉叆 {} 寮犺〃锛堝眰0 鎬诲紑鍏� enabled={}锛夛細{}",
+ rebuilt.size(), enabled, rebuilt.keySet());
+ }
+
+ /**
+ * 瑙f瀽 modelId 瀵瑰簲鐨勫璁″厓鏁版嵁锛涙湭娉ㄥ唽锛堟垨鍙嶆煡涓嶅埌涓昏〃锛夎繑鍥� null = 涓嶅璁¤琛ㄥ崟銆�
+ */
+ public FormMeta resolve(String modelId) {
+ String table = resolveMainTable(modelId);
+ if (table == null) {
+ return null;
+ }
+ FormMeta configured = formsByTable.get(table);
+ if (configured != null || !includeUnregisteredForms) {
+ return configured;
+ }
+ FormMeta fallback = new FormMeta();
+ fallback.setTable(table);
+ return fallback;
+ }
+
+ /**
+ * 鏌ヨ鍦ㄧ嚎寮�鍙戣〃鍗曞悕绉帮紱鏌ヤ笉鍒版垨鏌ヨ寮傚父鏃惰繑鍥� null锛岀敱璋冪敤鏂规部鐢ㄥ師 bizModule 鍥為��閫昏緫銆�
+ */
+ public String resolveFullName(String modelId) {
+ if (modelId == null || modelId.trim().isEmpty()) {
+ return null;
+ }
+ String normalizedModelId = modelId.trim();
+ try {
+ String fullName = jdbcTemplate.queryForObject(
+ "SELECT f_full_name FROM base_visual_dev WHERE f_id = ?",
+ String.class, normalizedModelId);
+ return fullName == null || fullName.trim().isEmpty() ? null : fullName.trim();
+ } catch (org.springframework.dao.EmptyResultDataAccessException e) {
+ return null;
+ } catch (Throwable t) {
+ log.warn("[AUDIT-L0] 鍙嶆煡鍦ㄧ嚎寮�鍙戣〃鍗曞悕绉板け璐ワ紝缁х画浣跨敤鍘� bizModule modelId={} err={}",
+ normalizedModelId, t.toString());
+ return null;
+ }
+ }
+
+ /**
+ * 鍙栧綋鍓嶅凡鍙戝竷琛ㄥ崟鐨勫瓧娈靛厓鏁版嵁锛屾暟鎹簱瀛楁澶囨敞浠呰ˉ琛ㄥ崟涓己澶辩殑瀛楁銆�
+ * 琛ㄥ悕蹇呴』鍦ㄥ眰 0 娉ㄥ唽琛ㄧ櫧鍚嶅崟鍐咃紝閬垮厤鍏冩暟鎹� SQL 琚换鎰忚〃鍚嶉┍鍔ㄣ��
+ */
+ public Map<String, AuditFieldDiff.FieldMeta> resolveFieldMetadata(String modelId, String table) {
+ String normalizedTable = table == null ? null : lower(table.trim());
+ if (normalizedTable == null
+ || (!includeUnregisteredForms && !formsByTable.containsKey(normalizedTable))) {
+ return Collections.emptyMap();
+ }
+ Map<String, AuditFieldDiff.FieldMeta> comments = commentMetadata(normalizedTable);
+ if (modelId == null || modelId.trim().isEmpty()) {
+ return comments;
+ }
+ // 鏌ヨ鍘嗗彶浜嬩欢鏃� sourceModelId 鏉ヨ嚜浜嬩欢 extra锛屼笉鑳藉亣瀹氬畠涓�瀹氬睘浜� targetTable銆�
+ // 涓嶅尮閰嶆椂鍙繚鐣欏綋鍓嶈〃 COMMENT锛岄伩鍏嶇敤鍙︿竴寮犺〃鐨勫彂甯冨厓鏁版嵁閿欒瑁呴グ瀛楁鍚嶃��
+ if (!normalizedTable.equals(resolveMainTable(modelId.trim()))) {
+ return comments;
+ }
+ Map<String, AuditFieldDiff.FieldMeta> published = publishedFieldMetadata(modelId);
+ return mergeFieldMetadata(comments, published);
+ }
+
+ /**
+ * CREATE 寤虹珛**瀹屾暣** baseline 鐢ㄧ殑銆屽凡鐭ュ彲瀹¤瀛楁銆嶉泦鍚堬紙灏忓啓锛夈��
+ *
+ * <p><b>涓轰粈涔堥渶瑕佽繖涓�</b>锛氬钩鍙版妸鍊间负绌虹殑瀛楁鏁翠釜浠庝簨浠剁殑 {@code newData} 閲屽墺鎺変簡
+ * 锛�2026-07-30 瀹炴祴锛歿@code lims_qingyandan} 鐨� CREATE 蹇収 21 閿� vs 琛ㄥ崟妯″瀷 23 瀛楁锛�
+ * 灏戠殑姝f槸寤哄崟鏃剁暀绌虹殑 {@code youxiaoqi} / {@code piliang} / {@code piliang_danwei}锛夈��
+ * 浜庢槸 baseline 澶╃敓缂鸿繖浜涢敭锛岀敤鎴风涓�娆℃妸瀹冧滑浠庣┖濉垚闈炵┖鏃讹紝UPDATE 鐨勪袱渚т氦闆嗕細鎶婂畠浠�
+ * 鎺掗櫎鎺夆�斺��**鐪熷疄鍙樻洿琚紡璁�**銆侰REATE 鏃舵寜鏈柟娉曟妸杩欎簺瀛楁鏄惧紡琛ユ垚绌哄�煎嵆鍙棴鍚堛��
+ *
+ * <p><b>涓轰粈涔堟槸銆屽凡鍙戝竷琛ㄥ崟瀛楁 鈭� 涓昏〃鐗╃悊鍒椼�嶈繖涓氦闆嗭紝鑰屼笉鏄换涓�鍗曚晶</b>锛�
+ * <ul>
+ * <li>鍙敤鐗╃悊鍒� 鈫� 浼氭妸**鍚庣浠g爜鍐欍�佷笉缁忚〃鍗�**鐨勫垪锛堝 {@code jieshouren}锛変篃瀹gО涓�
+ * 銆屽缓鍗曟椂宸茬煡涓虹┖銆嶃�傞偅浜涘垪鐨勫彉鏇翠笉浜х敓灞� 0 浜嬩欢锛宐aseline 浼氶暱鏈熷仠鍦ㄥ亣鐨勭┖鍊间笂锛�
+ * 涓嬫琛ㄥ崟纰板埌瀹冨氨缂栧嚭涓�鏉� {@code ""鈫扻} 鐨勫亣 diff鈥斺�旀鏄� B3 鍒掔殑銆屾病鏈� before
+ * 涓嶈缁� field_diffs銆嶉偅鏉$孩绾裤��</li>
+ * <li>鍙敤琛ㄥ崟瀛楁 鈫� 浼氬甫杩涘瓙琛� children 鐨� vModel锛坽@link #collectFields} 鏄�掑綊鐨勶級锛�
+ * 閭d簺涓嶆槸涓昏〃鐨勫垪锛岃ˉ杩涗富琛ㄥ揩鐓у彧鏄櫔闊炽��</li>
+ * </ul>
+ * 鍙栦氦闆嗗悗鍓╀笅鐨勬鏄�岃繖寮犺〃鍗曟帶鍒躲�佷笖纭疄鏄富琛ㄤ竴鍒椼�嶇殑瀛楁鈥斺�斿缓鍗曟彁浜ょ殑灏辨槸鏁翠唤琛ㄥ崟锛�
+ * 娌¤鎼哄甫鍗崇瓑浜庣暀绌猴紝杩欎釜鎺ㄦ柇鏈夋嵁鍙緷銆�
+ *
+ * <p><b>浠讳竴渚у彇涓嶅埌灏辫繑鍥炵┖闆�</b>锛堥潪 PG 閮ㄧ讲銆佸厓鏁版嵁鏌ヨ鐬椂澶辫触銆乵odelId 涓� table 瀵逛笉涓婏級锛�
+ * 涓嶈ˉ = 閫�鍖栨垚浠婂ぉ鐨勩�宐aseline 缂哄瓧娈� 鈫� 鏄惧紡鏍囪涓嶅彲鍒ゅ畾銆嶏紝鏄畨鍏ㄦ柟鍚戙��
+ * 瀹佸彲缁х画灏戣涓�鏉★紝涔熶笉鑳藉嚟鐚滈�犲嚭涓�涓棫鍊笺��
+ */
+ public Set<String> resolveBaselineFields(String modelId, String table) {
+ String normalizedTable = table == null ? null : lower(table.trim());
+ if ((normalizedTable == null
+ || (!includeUnregisteredForms && !formsByTable.containsKey(normalizedTable)))
+ || modelId == null || modelId.trim().isEmpty()) {
+ return Collections.emptySet();
+ }
+ // 涓� resolveFieldMetadata 鍚屼竴閬撻椄锛氫笉鑳界敤鍙︿竴寮犺〃鐨勫彂甯冩ā鍨嬬粰褰撳墠琛ㄨˉ baseline
+ if (!normalizedTable.equals(resolveMainTable(modelId.trim()))) {
+ return Collections.emptySet();
+ }
+ Map<String, AuditFieldDiff.FieldMeta> published = publishedFieldMetadata(modelId.trim());
+ Map<String, AuditFieldDiff.FieldMeta> columns = commentMetadata(normalizedTable);
+ if (published.isEmpty() || columns.isEmpty()) {
+ return Collections.emptySet();
+ }
+ Set<String> fields = new LinkedHashSet<>();
+ for (String field : published.keySet()) {
+ if (columns.containsKey(field)) {
+ fields.add(field);
+ }
+ }
+ return fields.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(fields);
+ }
+
+ /**
+ * 杩斿洖鐗╃悊琛ㄧ湡瀹炲垪鍚嶏紙灏忓啓锛夈�傚彧浣跨敤 JDBC 鍏冩暟鎹紝涓嶆嫾鎺� SQL锛涜В鏋愬け璐ヨ繑鍥炵┖闆嗭紝
+ * 璋冪敤鏂规嵁姝ら�夋嫨淇濈暀鍘熷瀹¤鏁版嵁锛屼笉鑳芥妸鍏冩暟鎹灛鏃舵晠闅滆鍒ゆ垚鈥滆〃涓病鏈変换浣曞瓧娈碘�濄��
+ */
+ public Set<String> resolvePhysicalColumns(String table) {
+ String normalized = table == null ? null : lower(table.trim());
+ if (normalized == null || !normalized.matches("[a-z0-9_]+")) {
+ return Collections.emptySet();
+ }
+ Map<String, AuditFieldDiff.FieldMeta> columns = commentMetadata(normalized);
+ return columns.isEmpty() ? Collections.emptySet()
+ : Collections.unmodifiableSet(new LinkedHashSet<>(columns.keySet()));
+ }
+
+ /**
+ * 鍙栧綋鍓嶅凡鍙戝竷琛ㄥ崟鐨勩�屾搷浣滆褰曟爣棰樸�嶅嬀閫夐厤缃��
+ *
+ * <p>涓ら亾闂镐笌 {@link #resolveFieldMetadata} 瀹屽叏鐩稿悓锛屼笉鑳芥澗锛�
+ * <ul>
+ * <li>琛ㄥ悕蹇呴』鍦ㄥ眰 0 娉ㄥ唽琛ㄧ櫧鍚嶅崟鍐呪�斺�斿惁鍒欏厓鏁版嵁 SQL 浼氳浠绘剰琛ㄥ悕椹卞姩锛�</li>
+ * <li>modelId 鍙嶆煡鍑虹殑涓昏〃蹇呴』涓� {@code table} 涓�鑷粹�斺�旀煡璇㈠巻鍙蹭簨浠舵椂
+ * {@code sourceModelId} 鏉ヨ嚜浜嬩欢 extra锛屼笉鑳藉亣瀹氬畠灞炰簬褰撳墠琛紱
+ * 鐢ㄥ彟涓�寮犺〃鐨勫彂甯冩ā鍨嬭В鏋愭爣棰橈紝浼氭妸鍒殑琛ㄧ殑瀛楁鍚嶅綋鎴愭湰琛ㄧ殑鍕鹃�夈��</li>
+ * </ul>
+ * 浠讳竴鏉′笉婊¤冻杩斿洖 {@link AuditTitleSpec#EMPTY} = 涓嶄骇鍑烘爣棰橈紙瀹夊叏鏂瑰悜锛夈��
+ */
+ public AuditTitleSpec resolveTitleSpec(String modelId, String table) {
+ String normalizedTable = table == null ? null : lower(table.trim());
+ if ((normalizedTable == null
+ || (!includeUnregisteredForms && !formsByTable.containsKey(normalizedTable)))
+ || modelId == null || modelId.trim().isEmpty()) {
+ return AuditTitleSpec.EMPTY;
+ }
+ if (!normalizedTable.equals(resolveMainTable(modelId.trim()))) {
+ return AuditTitleSpec.EMPTY;
+ }
+ return publishedTitleSpec(modelId.trim());
+ }
+
+ private AuditTitleSpec publishedTitleSpec(String modelId) {
+ AuditTitleSpec cached = cachedValue(modelIdToTitleSpec, modelId);
+ if (cached != null) {
+ return cached;
+ }
+ String formData;
+ try {
+ formData = jdbcTemplate.queryForObject(
+ "SELECT F_FORM_DATA FROM base_visual_release WHERE F_ID = ?",
+ String.class, modelId);
+ } catch (org.springframework.dao.EmptyResultDataAccessException e) {
+ cacheValue(modelIdToTitleSpec, modelId, AuditTitleSpec.EMPTY);
+ return AuditTitleSpec.EMPTY;
+ } catch (Throwable t) {
+ // 鐬椂鏁呴殰涓嶇紦瀛橈紙L061 鈶狅級锛氱紦瀛樹簡绛変簬璇ヨ〃鍗曞埌閲嶅惎鍓嶆案杩滄病鏈夋爣棰�
+ log.error("[AUDIT-L0] 璇诲彇琛ㄥ崟鏍囬閰嶇疆澶辫触锛堜笉缂撳瓨锛塵odelId={}", modelId, t);
+ return AuditTitleSpec.EMPTY;
+ }
+ AuditTitleSpec spec = AuditTitleSpecParser.parse(formData);
+ cacheValue(modelIdToTitleSpec, modelId, spec);
+ return spec;
+ }
+
+ /**
+ * 瑙f瀽鍒楄〃鍦烘櫙鐨勬爣棰樿鏍硷紙D6锛夛細璇� {@code F_COLUMN_DATA} 鐨�
+ * {@code columnList[].biz_log_enabled}锛屽瓧娈电紪鐮佸彇 {@code prop}銆�
+ *
+ * <p>涓� {@link #resolveTitleSpec} 鍚屾牱鐨勪袱閬撻椄锛岀悊鐢变篃涓�鏍凤細
+ * 琛ㄥ悕椤诲湪灞� 0 娉ㄥ唽琛ㄧ櫧鍚嶅崟鍐咃紙鍚﹀垯 SQL 琚换鎰忚〃鍚嶉┍鍔級锛沵odelId 鍙嶆煡鍑虹殑涓昏〃
+ * 椤讳笌 {@code table} 涓�鑷达紙鍚﹀垯浼氭嬁鍙︿竴寮犺〃鐨勫彂甯冩ā鍨嬭В鏋愭湰琛ㄧ殑鍕鹃�夛級銆�
+ */
+ public AuditTitleSpec resolveListTitleSpec(String modelId, String table) {
+ String normalizedTable = table == null ? null : lower(table.trim());
+ if ((normalizedTable == null
+ || (!includeUnregisteredForms && !formsByTable.containsKey(normalizedTable)))
+ || modelId == null || modelId.trim().isEmpty()) {
+ return AuditTitleSpec.EMPTY;
+ }
+ if (!normalizedTable.equals(resolveMainTable(modelId.trim()))) {
+ return AuditTitleSpec.EMPTY;
+ }
+ return publishedListTitleSpec(modelId.trim());
+ }
+
+ private AuditTitleSpec publishedListTitleSpec(String modelId) {
+ AuditTitleSpec cached = cachedValue(modelIdToListTitleSpec, modelId);
+ if (cached != null) {
+ return cached;
+ }
+ String columnData;
+ try {
+ columnData = jdbcTemplate.queryForObject(
+ "SELECT F_COLUMN_DATA FROM base_visual_release WHERE F_ID = ?",
+ String.class, modelId);
+ } catch (org.springframework.dao.EmptyResultDataAccessException e) {
+ cacheValue(modelIdToListTitleSpec, modelId, AuditTitleSpec.EMPTY);
+ return AuditTitleSpec.EMPTY;
+ } catch (Throwable t) {
+ // 鐬椂鏁呴殰涓嶇紦瀛橈紙L061 鈶狅級锛氱紦瀛樹簡绛変簬璇ヨ〃鍗曞埌閲嶅惎鍓嶆案杩滄病鏈夊垪琛ㄦ爣棰�
+ log.error("[AUDIT-L0] 璇诲彇鍒楄〃鏍囬閰嶇疆澶辫触锛堜笉缂撳瓨锛塵odelId={}", modelId, t);
+ return AuditTitleSpec.EMPTY;
+ }
+ AuditTitleSpec spec = AuditListTitleSpecParser.parse(columnData);
+ cacheValue(modelIdToListTitleSpec, modelId, spec);
+ return spec;
+ }
+
+ /**
+ * 瑙f瀽琛ㄥ崟閰嶇疆閲屾樉寮忔寚瀹氱殑绛惧悕瀛楁锛圖4 鏉冨▉鏉ユ簮锛夈��
+ *
+ * <p>{@code confirmBtnConfig.biz_sign_field}锛堢‘瀹氭寜閽級涓�
+ * {@code reviewBtnConfig.biz_sign_field}锛堝鏍告寜閽級銆傚浜虹鍚嶅満鏅笅瀛楁鍚嶅彲浠ユ槸浠绘剰鐨勶紝
+ * 鍏夐潬 {@code _sign} 鍛藉悕瑙勫垯浼氭紡鈥斺�旇繖灏辨槸涓轰粈涔堝繀椤昏閰嶇疆鑰屼笉鏄彧闈犵寽銆�
+ * 瀹炴祴銆屾祴璇曡〃鍗曘�嶄袱鑰呭垎鍒槸 {@code biz_sign} 涓� {@code fuhe_sign}銆�
+ *
+ * <p>杩斿洖灏忓啓瀛楁鍚嶏紝涓� {@link jnpf.bizcommon.audit.service.sign.AuditSignEvidenceReader#candidateFields}
+ * 鐨勫彛寰勪竴鑷淬�傝В鏋愪笉鍑鸿繑鍥炵┖鍒楄〃锛堣皟鐢ㄦ柟鑷細閫�鍥炲懡鍚嶈鍒�+鎾炲簱鍏滃簳锛夛紝涓嶆姏寮傚父銆�
+ */
+ public List<String> resolveSignFields(String modelId, String table) {
+ String normalizedTable = table == null ? null : lower(table.trim());
+ if ((normalizedTable == null
+ || (!includeUnregisteredForms && !formsByTable.containsKey(normalizedTable)))
+ || modelId == null || modelId.trim().isEmpty()) {
+ return Collections.emptyList();
+ }
+ if (!normalizedTable.equals(resolveMainTable(modelId.trim()))) {
+ return Collections.emptyList();
+ }
+ return publishedSignFields(modelId.trim());
+ }
+
+ private List<String> publishedSignFields(String modelId) {
+ List<String> cached = cachedValue(modelIdToSignFields, modelId);
+ if (cached != null) {
+ return cached;
+ }
+ String formData;
+ try {
+ formData = jdbcTemplate.queryForObject(
+ "SELECT F_FORM_DATA FROM base_visual_release WHERE F_ID = ?",
+ String.class, modelId);
+ } catch (org.springframework.dao.EmptyResultDataAccessException e) {
+ cacheValue(modelIdToSignFields, modelId, Collections.<String>emptyList());
+ return Collections.emptyList();
+ } catch (Throwable t) {
+ // 鐬椂鏁呴殰涓嶇紦瀛橈紙L061 鈶狅級
+ log.error("[AUDIT-L0] 璇诲彇绛惧悕瀛楁閰嶇疆澶辫触锛堜笉缂撳瓨锛塵odelId={}", modelId, t);
+ return Collections.emptyList();
+ }
+ List<String> fields = parseSignFields(formData);
+ cacheValue(modelIdToSignFields, modelId, fields);
+ return fields;
+ }
+
+ /** 浠庤〃鍗曟牴瀵硅薄璇讳袱涓寜閽厤缃殑 biz_sign_field銆傛案涓嶆姏寮傚父锛岃В鏋愪笉鍑鸿繑鍥炵┖琛ㄣ�� */
+ private static List<String> parseSignFields(String formDataJson) {
+ if (formDataJson == null || formDataJson.trim().isEmpty()) {
+ return Collections.emptyList();
+ }
+ try {
+ Map<String, Object> root = JsonUtil.stringToMap(formDataJson);
+ if (root == null) {
+ return Collections.emptyList();
+ }
+ List<String> out = new ArrayList<>();
+ String[] configKeys = {"confirmBtnConfig", "reviewBtnConfig"};
+ for (String key : configKeys) {
+ Object raw = root.get(key);
+ if (!(raw instanceof Map)) {
+ continue;
+ }
+ Object field = ((Map<?, ?>) raw).get("biz_sign_field");
+ if (field == null) {
+ continue;
+ }
+ String name = lower(String.valueOf(field).trim());
+ if (name != null && !name.isEmpty() && !out.contains(name)) {
+ out.add(name);
+ }
+ }
+ return out.isEmpty() ? Collections.<String>emptyList()
+ : Collections.unmodifiableList(out);
+ } catch (Throwable t) {
+ return Collections.emptyList();
+ }
+ }
+
+ /** preferred 鎸夊瓧娈靛悕锛堝拷鐣ュぇ灏忓啓锛夎鐩� fallback銆� */
+ public static Map<String, AuditFieldDiff.FieldMeta> mergeFieldMetadata(
+ Map<String, AuditFieldDiff.FieldMeta> fallback,
+ Map<String, AuditFieldDiff.FieldMeta> preferred) {
+ if ((fallback == null || fallback.isEmpty()) && (preferred == null || preferred.isEmpty())) {
+ return Collections.emptyMap();
+ }
+ Map<String, AuditFieldDiff.FieldMeta> merged = new LinkedHashMap<>();
+ putAllNormalized(merged, fallback);
+ if (preferred != null) {
+ for (Map.Entry<String, AuditFieldDiff.FieldMeta> entry : preferred.entrySet()) {
+ String field = lower(entry.getKey());
+ if (field == null || entry.getValue() == null) {
+ continue;
+ }
+ merged.put(field, mergeFieldMeta(merged.get(field), entry.getValue()));
+ }
+ }
+ return merged;
+ }
+
+ private Map<String, AuditFieldDiff.FieldMeta> publishedFieldMetadata(String modelId) {
+ if (modelId == null || modelId.trim().isEmpty()) {
+ return Collections.emptyMap();
+ }
+ String key = modelId.trim();
+ Map<String, AuditFieldDiff.FieldMeta> cached = cachedValue(modelIdToFieldMeta, key);
+ if (cached != null) {
+ return cached;
+ }
+
+ String formData;
+ try {
+ formData = jdbcTemplate.queryForObject(
+ "SELECT F_FORM_DATA FROM base_visual_release WHERE F_ID = ?",
+ String.class, key);
+ } catch (org.springframework.dao.EmptyResultDataAccessException e) {
+ cacheValue(modelIdToFieldMeta, key, Collections.emptyMap());
+ return Collections.emptyMap();
+ } catch (Throwable t) {
+ log.error("[AUDIT-L0] 璇诲彇鍙戝竷琛ㄥ崟鍏冩暟鎹け璐ワ紙涓嶇紦瀛橈級modelId={}", key, t);
+ return Collections.emptyMap();
+ }
+
+ try {
+ Map<String, AuditFieldDiff.FieldMeta> parsed = parsePublishedFieldMetadata(formData);
+ Map<String, AuditFieldDiff.FieldMeta> immutable = parsed.isEmpty()
+ ? Collections.emptyMap() : Collections.unmodifiableMap(parsed);
+ cacheValue(modelIdToFieldMeta, key, immutable);
+ return immutable;
+ } catch (Throwable t) {
+ if (deterministicParseFailure(t) || t instanceof RuntimeException) {
+ log.warn("[AUDIT-L0] 鍙戝竷琛ㄥ崟鍏冩暟鎹棤娉曡В鏋愶紙宸茬紦瀛樼┖缁撴灉锛塵odelId={} err={}",
+ key, t.toString());
+ cacheValue(modelIdToFieldMeta, key, Collections.emptyMap());
+ } else {
+ log.error("[AUDIT-L0] 瑙f瀽鍙戝竷琛ㄥ崟鍏冩暟鎹け璐ワ紙涓嶇紦瀛橈級modelId={}", key, t);
+ }
+ return Collections.emptyMap();
+ }
+ }
+
+ private Map<String, AuditFieldDiff.FieldMeta> commentMetadata(String table) {
+ Map<String, AuditFieldDiff.FieldMeta> cached = cachedValue(tableToCommentMeta, table);
+ if (cached != null) {
+ return cached;
+ }
+ try (Connection connection = jdbcTemplate.getDataSource().getConnection()) {
+ Map<String, AuditFieldDiff.FieldMeta> parsed = new LinkedHashMap<>();
+ DatabaseMetaData metadata = connection.getMetaData();
+ String catalog = connection.getCatalog();
+ String schema = currentSchema(connection);
+ collectColumnMetadata(metadata, catalog, schema, table, parsed);
+ if (parsed.isEmpty() && schema != null) {
+ collectColumnMetadata(metadata, catalog, null, table, parsed);
+ }
+ if (parsed.isEmpty()) {
+ collectColumnMetadata(metadata, null, schema, table, parsed);
+ }
+ if (parsed.isEmpty()) {
+ collectColumnMetadata(metadata, catalog, schema,
+ table.toUpperCase(Locale.ROOT), parsed);
+ }
+ Map<String, AuditFieldDiff.FieldMeta> immutable = parsed.isEmpty()
+ ? Collections.emptyMap() : Collections.unmodifiableMap(parsed);
+ cacheValue(tableToCommentMeta, table, immutable);
+ return immutable;
+ } catch (Throwable t) {
+ // 鍏冩暟鎹┍鍔ㄤ笉杩斿洖澶囨敞鎴栫煭鏆傝繛鎺ユ晠闅滐細涓嶉樆鏂簨浠讹紝涔熶笉缂撳瓨澶辫触銆�
+ log.warn("[AUDIT-L0] 璇诲彇瀛楁 COMMENT 澶辫触锛堜笉缂撳瓨锛塼able={} err={}", table, t.toString());
+ return Collections.emptyMap();
+ }
+ }
+
+ private static String currentSchema(Connection connection) {
+ try {
+ return connection.getSchema();
+ } catch (Throwable ignored) {
+ return null;
+ }
+ }
+
+ private static void collectColumnMetadata(DatabaseMetaData metadata,
+ String catalog,
+ String schema,
+ String table,
+ Map<String, AuditFieldDiff.FieldMeta> target) throws Exception {
+ try (ResultSet columns = metadata.getColumns(catalog, schema, table, null)) {
+ while (columns.next()) {
+ String field = str(columns.getObject("COLUMN_NAME"));
+ if (field == null) {
+ continue;
+ }
+ String label = str(columns.getObject("REMARKS"));
+ String jdbcType = str(columns.getObject("TYPE_NAME"));
+ putNormalized(target, field, jdbcFieldMeta(label, jdbcType));
+ }
+ }
+ }
+
+ /** 绾В鏋愬叆鍙o紝渚涢拡瀵规�ф帰閽堣鐩栧彂甯冭〃鍗曠殑宓屽 fields 缁撴瀯銆� */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ static Map<String, AuditFieldDiff.FieldMeta> parsePublishedFieldMetadata(String formDataJson) {
+ if (formDataJson == null || formDataJson.trim().isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map<String, Object> root = JsonUtil.stringToMap(formDataJson);
+ if (root == null) {
+ return Collections.emptyMap();
+ }
+ Object rawFields = root.get("fields");
+ List<Map> fields;
+ if (rawFields instanceof List) {
+ fields = (List<Map>) rawFields;
+ } else if (rawFields != null) {
+ fields = JsonUtil.getJsonToList(String.valueOf(rawFields), Map.class);
+ } else {
+ fields = Collections.emptyList();
+ }
+ Map<String, AuditFieldDiff.FieldMeta> result = new LinkedHashMap<>();
+ collectFields(fields, result);
+ return result;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static void collectFields(List<?> fields, Map<String, AuditFieldDiff.FieldMeta> result) {
+ if (fields == null) {
+ return;
+ }
+ for (Object raw : fields) {
+ if (!(raw instanceof Map)) {
+ continue;
+ }
+ Map item = (Map) raw;
+ Object rawConfig = item.containsKey("config") ? item.get("config") : item.get("__config__");
+ if (!(rawConfig instanceof Map)) {
+ continue;
+ }
+ Map config = (Map) rawConfig;
+ String field = str(item.containsKey("vModel") ? item.get("vModel") : item.get("__vModel__"));
+ String label = Boolean.TRUE.equals(bool(config.get("noShow"))) ? null : str(config.get("label"));
+ String jnpfKey = str(config.get("jnpfKey"));
+ String dataType = str(config.get("dataType"));
+ String dictionaryType = str(config.get("dictionaryType"));
+ Boolean hidden = bool(config.get("noShow"));
+ if (field != null) {
+ boolean nameModified = jnpfKey != null
+ && JnpfKeyConsts.getNameModified().contains(jnpfKey)
+ && (JnpfKeyConsts.getNameModifiedNotDynamic().contains(jnpfKey)
+ || "dynamic".equals(dataType));
+ Map<String, String> options = optionLabels(item.get("options"), item.get("props"));
+ String componentType = componentType(jnpfKey);
+ String valueType = valueType(jnpfKey, dataType, options, bool(item.get("multiple")));
+ String format = firstText(item.get("format"), config.get("format"));
+ putNormalized(result, field,
+ new AuditFieldDiff.FieldMeta(label, jnpfKey, nameModified,
+ valueType, componentType, format, options,
+ dictionaryType, hidden));
+ }
+ Object children = config.get("children");
+ if (children instanceof List) {
+ collectFields((List<?>) children, result);
+ } else if (children != null && !String.valueOf(children).trim().isEmpty()) {
+ collectFields(JsonUtil.getJsonToList(String.valueOf(children), Map.class), result);
+ }
+ }
+ }
+
+ private static void putAllNormalized(Map<String, AuditFieldDiff.FieldMeta> target,
+ Map<String, AuditFieldDiff.FieldMeta> source) {
+ if (source == null) {
+ return;
+ }
+ for (Map.Entry<String, AuditFieldDiff.FieldMeta> entry : source.entrySet()) {
+ putNormalized(target, entry.getKey(), entry.getValue());
+ }
+ }
+
+ private static void putNormalized(Map<String, AuditFieldDiff.FieldMeta> target,
+ String field,
+ AuditFieldDiff.FieldMeta meta) {
+ if (field == null || meta == null) {
+ return;
+ }
+ String normalized = lower(field.trim());
+ if (normalized != null && !normalized.isEmpty()) {
+ target.put(normalized, meta);
+ }
+ }
+
+ private static AuditFieldDiff.FieldMeta mergeFieldMeta(
+ AuditFieldDiff.FieldMeta fallback, AuditFieldDiff.FieldMeta preferred) {
+ if (fallback == null) {
+ return preferred;
+ }
+ if (preferred == null) {
+ return fallback;
+ }
+ Boolean hidden = preferred.hidden() != null ? preferred.hidden() : fallback.hidden();
+ return new AuditFieldDiff.FieldMeta(
+ Boolean.TRUE.equals(hidden) ? fallback.label()
+ : firstText(preferred.label(), fallback.label()),
+ firstText(preferred.jnpfKey(), fallback.jnpfKey()),
+ preferred.nameModified() != null ? preferred.nameModified() : fallback.nameModified(),
+ firstText(preferred.valueType(), fallback.valueType()),
+ firstText(preferred.componentType(), fallback.componentType()),
+ firstText(preferred.format(), fallback.format()),
+ preferred.optionLabels().isEmpty() ? fallback.optionLabels() : preferred.optionLabels(),
+ firstText(preferred.dictionaryType(), fallback.dictionaryType()),
+ hidden);
+ }
+
+ private static AuditFieldDiff.FieldMeta jdbcFieldMeta(String label, String jdbcType) {
+ String type = lower(jdbcType);
+ String valueType = null;
+ String componentType = null;
+ if (type != null) {
+ if (type.startsWith("timestamp")) {
+ valueType = "datetime";
+ componentType = "datePicker";
+ } else if (type.equals("date")) {
+ valueType = "date";
+ componentType = "datePicker";
+ } else if (type.startsWith("bool")) {
+ valueType = "boolean";
+ componentType = "switch";
+ } else if (type.startsWith("smallint") || type.startsWith("integer")
+ || type.startsWith("bigint") || type.startsWith("numeric")
+ || type.startsWith("decimal") || type.startsWith("real")
+ || type.startsWith("double")) {
+ valueType = "number";
+ componentType = "inputNumber";
+ } else if (type.startsWith("json") || type.endsWith("[]")) {
+ valueType = "json";
+ componentType = "input";
+ }
+ }
+ return new AuditFieldDiff.FieldMeta(label, null, null,
+ valueType, componentType, null, Collections.emptyMap());
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private static Map<String, String> optionLabels(Object rawOptions, Object rawProps) {
+ List<?> options;
+ if (rawOptions instanceof List) {
+ options = (List<?>) rawOptions;
+ } else if (rawOptions == null || String.valueOf(rawOptions).trim().isEmpty()) {
+ return Collections.emptyMap();
+ } else {
+ options = JsonUtil.getJsonToList(String.valueOf(rawOptions), Map.class);
+ }
+ Map props = rawProps instanceof Map ? (Map) rawProps : Collections.emptyMap();
+ String labelKey = firstText(props.get("label"), "fullName");
+ String valueKey = firstText(props.get("value"), "id");
+ String childrenKey = firstText(props.get("children"), "children");
+ Map<String, String> result = new LinkedHashMap<>();
+ collectOptionLabels(options, labelKey, valueKey, childrenKey, result);
+ return result;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static void collectOptionLabels(List<?> options, String labelKey, String valueKey,
+ String childrenKey, Map<String, String> result) {
+ if (options == null) {
+ return;
+ }
+ for (Object raw : options) {
+ if (!(raw instanceof Map)) {
+ continue;
+ }
+ Map option = (Map) raw;
+ String value = firstText(option.get(valueKey), option.get("enCode"),
+ option.get("id"), option.get("value"));
+ String label = firstText(option.get(labelKey), option.get("fullName"),
+ option.get("label"), option.get("name"));
+ if (value != null && label != null) {
+ result.put(value, label);
+ }
+ Object children = option.get(childrenKey);
+ if (children instanceof List) {
+ collectOptionLabels((List<?>) children, labelKey, valueKey, childrenKey, result);
+ }
+ }
+ }
+
+ private static String componentType(String jnpfKey) {
+ if (jnpfKey == null) {
+ return null;
+ }
+ return "signature".equalsIgnoreCase(jnpfKey) ? "sign" : jnpfKey;
+ }
+
+ private static String valueType(String jnpfKey, String dataType,
+ Map<String, String> options, Boolean multiple) {
+ String key = lower(jnpfKey);
+ String source = lower(dataType);
+ if (key == null) {
+ return null;
+ }
+ if ("sign".equals(key) || "signature".equals(key)) {
+ return "masked";
+ }
+ if ("datepicker".equals(key)) {
+ return "date";
+ }
+ if ("timepicker".equals(key)) {
+ return "datetime";
+ }
+ if ("inputnumber".equals(key) || "slider".equals(key) || "rate".equals(key)) {
+ return "number";
+ }
+ if ("switch".equals(key)) {
+ return "boolean";
+ }
+ if ("table".equals(key) || "uploadfile".equals(key) || "uploadimg".equals(key)) {
+ return "json";
+ }
+ if ("userselect".equals(key) || "departmentselect".equals(key)
+ || "roleselect".equals(key) || "groupselect".equals(key)
+ || "popupselect".equals(key) || "relationform".equals(key)) {
+ return Boolean.TRUE.equals(multiple) ? "multiReference" : "reference";
+ }
+ if ("checkbox".equals(key) || "cascader".equals(key) || "treeselect".equals(key)) {
+ return ("dynamic".equals(source) && (options == null || options.isEmpty()))
+ ? "multiReference" : "multiEnum";
+ }
+ if ("select".equals(key) || "radio".equals(key)) {
+ if ("dynamic".equals(source) && (options == null || options.isEmpty())) {
+ return Boolean.TRUE.equals(multiple) ? "multiReference" : "reference";
+ }
+ return Boolean.TRUE.equals(multiple) ? "multiEnum" : "enum";
+ }
+ return "text";
+ }
+
+ private void clearMetadataCaches() {
+ modelIdToMainTable.clear();
+ modelIdToFieldMeta.clear();
+ modelIdToTitleSpec.clear();
+ modelIdToListTitleSpec.clear();
+ modelIdToSignFields.clear();
+ tableToCommentMeta.clear();
+ }
+
+ private <T> T cachedValue(Map<String, CacheValue<T>> cache, String key) {
+ CacheValue<T> cached = cache.get(key);
+ if (cached == null) {
+ return null;
+ }
+ if (metadataCacheTtlMs <= 0L
+ || System.currentTimeMillis() - cached.createdAt >= metadataCacheTtlMs) {
+ cache.remove(key, cached);
+ return null;
+ }
+ return cached.value;
+ }
+
+ private <T> void cacheValue(Map<String, CacheValue<T>> cache, String key, T value) {
+ if (metadataCacheTtlMs > 0L) {
+ cache.put(key, new CacheValue<>(value));
+ }
+ }
+
+ private static final class CacheValue<T> {
+ private final T value;
+ private final long createdAt = System.currentTimeMillis();
+
+ private CacheValue(T value) {
+ this.value = value;
+ }
+ }
+
+ /** 瑙f瀽 modelId 瀵瑰簲鐨勪富琛ㄥ悕锛堝皬鍐欙級锛涘弽鏌ヤ笉鍒拌繑鍥� null銆� */
+ public String resolveMainTable(String modelId) {
+ if (modelId == null || modelId.isEmpty()) {
+ return null;
+ }
+ // ConcurrentHashMap 瀛樹笉浜� null锛岀敤绌轰覆褰撱�岀‘瀹氭�у湴鏌ヤ笉鍒般�嶇殑鍗犱綅锛圠061 鈶� 鐨勫摠鍏靛啓娉曪級
+ String cached = cachedValue(modelIdToMainTable, modelId);
+ if (cached != null) {
+ return cached.isEmpty() ? null : cached;
+ }
+ Outcome outcome = queryMainTable(modelId);
+ if (outcome.transientFailure) {
+ // 鐬椂澶辫触锛堣繛鎺ユ姈鍔�/瓒呮椂锛�**涓嶈繘缂撳瓨**锛氫笅娆′簨浠堕噸璇曚竴娆★紝浠d环鍙槸澶氫竴娆℃煡璇紱
+ // 缂撳瓨浜嗗垯鏄琛ㄥ崟浠庢闈欓粯涓嶈瀹¤锛屼袱鑰呯殑鍧忓涓嶅湪涓�涓噺绾с��
+ log.error("[AUDIT-L0] 鍙嶆煡涓昏〃澶辫触锛堢灛鏃讹紝涓嶇紦瀛橈紝涓嬫浜嬩欢浼氶噸璇曪級modelId={} err={}",
+ modelId, outcome.error);
+ return null;
+ }
+ cacheValue(modelIdToMainTable, modelId, outcome.table == null ? "" : outcome.table);
+ if (outcome.table == null && warnedModels.add(modelId)) {
+ log.warn("[AUDIT-L0] modelId={} 鍙嶆煡涓嶅埌涓昏〃锛坆ase_visual_release.F_TABLES_DATA 鏃� typeId=1 涓旀棤琛ㄩ」锛夛紝"
+ + "璇ヨ〃鍗曠殑鍙樻洿涓嶄細琚璁°�傚悓涓� modelId 鍙姤涓�娆�", modelId);
+ }
+ return outcome.table;
+ }
+
+ /** 鏌ヨ缁撴灉涓夋�侊細鎷垮埌琛ㄥ悕 / 纭畾鎬у湴娌℃湁 / 鐬椂澶辫触锛堝悗鑰呬笉鍙紦瀛橈級銆� */
+ private static final class Outcome {
+ private String table;
+ private boolean transientFailure;
+ private String error;
+ }
+
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private Outcome queryMainTable(String modelId) {
+ Outcome outcome = new Outcome();
+ String tablesJson;
+ try {
+ tablesJson = jdbcTemplate.queryForObject(
+ "SELECT F_TABLES_DATA FROM base_visual_release WHERE F_ID = ?",
+ String.class, modelId);
+ } catch (org.springframework.dao.EmptyResultDataAccessException e) {
+ // 纭畾鎬э細杩欎釜 modelId 鏍规湰娌℃湁鍙戝竷璁板綍锛屽啀鏌ヤ竴涓囨涔熶竴鏍�
+ return outcome;
+ } catch (Throwable t) {
+ outcome.transientFailure = true;
+ outcome.error = String.valueOf(t);
+ return outcome;
+ }
+ // 浠ヤ笅閮芥槸銆屾煡璇㈡垚鍔熴�佸彧鏄唴瀹归噷娌℃湁涓昏〃銆嶁�斺�旂‘瀹氭�х粨鏋滐紝鍙互缂撳瓨
+ if (tablesJson == null || tablesJson.isEmpty()) {
+ return outcome;
+ }
+ // **瑙f瀽鍗曠嫭涓�涓� try**锛氫笅闈� deterministicParseFailure 鐨勭櫧鍚嶅崟涔嬫墍浠ユ暍璁�
+ // NumberFormatException锛屽墠鎻愭鏄�岃繖涓� try 閲岄櫎浜嗚В鏋愭病鏈夊埆鐨勪笢瑗裤�嶁�斺��
+ // 璁╄繖涓墠鎻愮敱**浠g爜缁撴瀯**淇濊瘉锛岃�屼笉鏄潬娉ㄩ噴绾﹀畾锛堢害瀹氫細鍦ㄤ笅涓�娆℃敼鍔ㄩ噷鎮勬倓澶辨晥锛夈��
+ List<Map> tables;
+ try {
+ tables = JsonUtil.getJsonToList(tablesJson, Map.class);
+ } catch (Throwable t) {
+ // **鐧藉悕鍗曪紝涓嶆槸榛戝悕鍗�**锛圠061 鈶狅紝鐙珛璇勫 I-3 鎸囧嚭杩欓噷閫�鍖栨垚浜嗛粦鍚嶅崟锛夛細
+ // 鍙瑙f瀽鍣ㄥ杩欐鏂囨湰鐨勫浐鏈夊垽鍐筹紱OOM / StackOverflowError 杩欑被**鐬椂**鏁呴殰
+ // 鑻ヨ鍒ゆ垚纭畾鎬у苟姘镐箙璐熺紦瀛橈紝绛変簬璇ヨ〃鍗曚粠姝ら潤榛樹笉琚璁$洿鍒伴噸鍚��
+ if (deterministicParseFailure(t)) {
+ log.warn("[AUDIT-L0] modelId={} 鐨� F_TABLES_DATA 涓嶆槸鍚堟硶 JSON锛堢‘瀹氭�уけ璐ワ紝宸茬紦瀛橈級: {}",
+ modelId, t.toString());
+ return outcome;
+ }
+ outcome.transientFailure = true;
+ outcome.error = String.valueOf(t);
+ return outcome;
+ }
+ if (tables == null || tables.isEmpty()) {
+ return outcome;
+ }
+ // 浠ヤ笅鏄函 Map 鍙栧�硷紝涓庤В鏋愭棤鍏炽�傝繖閲屽嚭鐨勫紓甯革紙鍏冪礌涓嶆槸 Map 绛夛級鍚屾牱鏄繖鏉″彂甯冭褰曠殑
+ // 鍥烘湁褰㈡�併�侀噸璇曠粨鏋滄亽瀹氾紝鏁呬竴寰嬫寜纭畾鎬у鐞嗐��
+ try {
+ for (Map t : tables) {
+ if ("1".equals(String.valueOf(t.get("typeId")))) { // typeId=1 鏄富琛�
+ outcome.table = lower(str(t.get("table")));
+ if (outcome.table != null) {
+ return outcome;
+ }
+ }
+ }
+ // 娌℃湁 typeId=1 鐨勬爣璁帮紙鑰佽〃鍗曠粨鏋勯噷鍑虹幇杩囷級锛氶��鑰屽彇绗竴寮犺〃锛屼笌鏃� BizFormRegistry 鍚屽彛寰�
+ outcome.table = lower(str(tables.get(0).get("table")));
+ return outcome;
+ } catch (Throwable t) {
+ log.warn("[AUDIT-L0] modelId={} 鐨� F_TABLES_DATA 缁撴瀯寮傚父锛堢‘瀹氭�уけ璐ワ紝宸茬紦瀛橈級: {}",
+ modelId, t.toString());
+ outcome.table = null;
+ return outcome;
+ }
+ }
+
+ /**
+ * 瑙f瀽澶辫触鏄惁涓恒�岃繖娈垫枃鏈殑鍥烘湁鍒ゅ喅銆嶏紙閲嶈瘯缁撴灉鎭掑畾锛夛紝鍙湁杩欑鎵嶅厑璁歌繘璐熺紦瀛樸��
+ *
+ * <p>鐧藉悕鍗曚袱椤癸紝閮界粡瀹炴祴纭锛堢浜岄」鐢� codex 鐙珛澶嶆牳鏃剁敤 fastjson 1.2.83 瀛楄妭鐮佺骇鎺㈤拡鍙戠幇锛夛細
+ * <ul>
+ * <li>{@link com.alibaba.fastjson.JSONException}鈥斺�旇娉曢敊銆侀《灞傞潪鏁扮粍銆佸厓绱犻潪瀵硅薄銆�
+ * 瓒呭ぇ鎸囨暟绛夛紝fastjson 缁熶竴鍖呮垚瀹冿紱</li>
+ * <li>{@link NumberFormatException}鈥斺��**鐣稿舰 Unicode 杞箟锛堝 {@code \\uZZZZ}锛変笌鐣稿舰鏁板瓧
+ * 瀛楅潰閲忎細鎶涜8鐨� NFE銆佷笉琚寘瑁�**銆傚畠鍚屾牱鏄枃鏈殑鍥烘湁灞炴�э紝婕忓湪鐧藉悕鍗曞鍙細璁╄繖绫�
+ * 琛ㄥ崟姣忔浜嬩欢閮界櫧鏌ヤ竴娆″簱锛堜笉褰卞搷姝g‘鎬э紝浣嗗睘瀹炴墦瀹炵殑鏁堢巼姝昏锛夈��</li>
+ * </ul>
+ *
+ * <p><b>鍒绘剰涓嶆敹 Error</b>锛歿@code StackOverflowError}锛堟繁灞傚祵濂� JSON锛夊疄娴�**鍘熸牱绌块�忋��
+ * 涓嶈鍖呰鎴� JSONException</b>锛岃惤杩涚灛鏃跺垎鏀笉缂撳瓨鈥斺�旀鏄兂瑕佺殑琛屼负銆�
+ */
+ private static boolean deterministicParseFailure(Throwable t) {
+ for (Throwable c = t; c != null; c = (c.getCause() == c ? null : c.getCause())) {
+ if (c instanceof NumberFormatException) {
+ return true;
+ }
+ if (c instanceof com.alibaba.fastjson.JSONException) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String str(Object value) {
+ if (value == null) {
+ return null;
+ }
+ String s = value.toString().trim();
+ return s.isEmpty() ? null : s;
+ }
+
+ private static Boolean bool(Object value) {
+ if (value instanceof Boolean) {
+ return (Boolean) value;
+ }
+ if (value == null) {
+ return null;
+ }
+ String text = String.valueOf(value).trim();
+ if ("true".equalsIgnoreCase(text)) {
+ return Boolean.TRUE;
+ }
+ if ("false".equalsIgnoreCase(text)) {
+ return Boolean.FALSE;
+ }
+ return null;
+ }
+
+ private static String firstText(Object... values) {
+ if (values == null) {
+ return null;
+ }
+ for (Object value : values) {
+ String text = str(value);
+ if (text != null) {
+ return text;
+ }
+ }
+ return null;
+ }
+
+ private static String lower(String value) {
+ return value == null ? null : value.toLowerCase(Locale.ROOT);
+ }
+
+ private static Set<String> lowerAll(Set<String> values) {
+ if (values == null || values.isEmpty()) {
+ return Collections.emptySet();
+ }
+ Set<String> out = new LinkedHashSet<>(values.size());
+ for (String v : values) {
+ if (v != null && !v.trim().isEmpty()) {
+ out.add(lower(v.trim()));
+ }
+ }
+ return out;
+ }
+}
--
Gitblit v1.8.0