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/title/AuditRecordTitleBuilder.java |  443 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 443 insertions(+), 0 deletions(-)

diff --git a/jnpf-biz-common/jnpf-biz-common-audit/src/main/java/jnpf/bizcommon/audit/service/title/AuditRecordTitleBuilder.java b/jnpf-biz-common/jnpf-biz-common-audit/src/main/java/jnpf/bizcommon/audit/service/title/AuditRecordTitleBuilder.java
new file mode 100644
index 0000000..a907608
--- /dev/null
+++ b/jnpf-biz-common/jnpf-biz-common-audit/src/main/java/jnpf/bizcommon/audit/service/title/AuditRecordTitleBuilder.java
@@ -0,0 +1,443 @@
+package jnpf.bizcommon.audit.service.title;
+
+import jnpf.bizcommon.audit.entity.AuditEventEntity;
+import jnpf.audit.diff.AuditFieldDiff;
+import jnpf.bizcommon.audit.entity.model.AuditFieldDiffViewVO;
+import jnpf.bizcommon.audit.service.AuditFieldDiffViewService;
+import jnpf.util.JsonUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 鎶婅〃鍗曞嬀閫夋覆鏌撴垚浜哄彲璇荤殑銆屾搷浣滆褰曟爣棰樸�嶃��
+ *
+ * <p><b>鐢ㄥ睍绀哄�间笉鐢ㄥ瓨鍌ㄥ��</b>锛氬瓧鍏�/寮曠敤/鏋氫妇瀛楁鐨勫瓨鍌ㄥ�兼槸闆姳 ID 鎴栧瓧鍏哥爜锛岀洿鎺ユ嫾杩涙爣棰�
+ * 灏辨槸涓�涓叉暟瀛椼�傝繖閲屽鐢� {@link AuditFieldDiffViewService#build}鈥斺�斾篃灏辨槸 {@code field_diffs}
+ * 璧扮殑<b>鍚屼竴鏉�</b>灞曠ず瑙f瀽璺緞锛屼繚璇佹爣棰樹笌瀛楁鏄庣粏閲岀殑鍚屼竴涓瓧娈垫樉绀烘垚鍚屼竴涓�笺��
+ *
+ * <p><b>涓夌被瀛楁缁濅笉杩涙爣棰�</b>锛堥『搴忓嵆浼樺厛绾э級锛�
+ * <ol>
+ *   <li>璋冪敤鏂逛紶鍏ョ殑 {@code excludedFields}鈥斺�旇劚鏁忓垪涓庡垽瀹氫负绛惧悕瀛楁鐨勫垪銆�
+ *       鏍囬鏄垪琛ㄦ槑鏂囧垪锛屽嚟璇�/鏁忔劅鍊艰繘鍘诲氨娲椾笉鎺夛紙瀹¤琛� append-only 淇濈暀 10 骞达級锛�</li>
+ *   <li>灞曠ず瑙f瀽鍚庝粛琚爣璁� {@code masked} 鐨勨�斺�斿弻淇濋櫓锛岄槻璋冪敤鏂规紡浼狅紱</li>
+ *   <li>鍊间负绌虹殑鈥斺�旇烦杩囪�屼笉鏄暀涓�涓┖鐨� {@code /} 娈点��</li>
+ * </ol>
+ */
+@Slf4j
+@Component
+public class AuditRecordTitleBuilder {
+
+    /** 瀛楁闂村垎闅旂銆備笟鍔″彛寰勶細a/b/c銆� */
+    public static final String SEPARATOR = "/";
+
+    /** 鍗曡鏍囬鐨勫瓧绗︿笂闄愩�備富琛ㄨ钀� varchar(500)锛岀暀鍑轰綑閲忥紱瀛愯〃琛屽悓鍙e緞閬垮厤 extra 鏃犻檺鑶ㄨ儉銆� */
+    private static final int MAX_LINE = 400;
+
+    /**
+     * 瀛愯〃**鏍囬鏉℃暟**涓婇檺锛堜骇鍑轰晶锛夈�傝秴鍑虹殑涓㈠純鈥斺�斿垪琛ㄥ彧鏄剧ず鍓� 3 琛岋紝鍏ㄩ噺缁欒鎯呫��
+     * 2026-08-05 璧蜂涪寮冭繖浠朵簨浼氬瀹炵暀鐥曪紙{@link AuditRecordTitle#SCOPE_ROWS_TRUNCATED}锛夛紝
+     * 姝ゅ墠鏄倓鎮勬埅鐨勶細璇荤殑浜虹湅鍒� 200 鏉′細浠ヤ负閭e氨鏄叏閮ㄣ��
+     */
+    private static final int MAX_SUB_ROWS = 200;
+
+    /**
+     * 瀛愯〃**鎵弿琛屾暟**涓婇檺锛堣緭鍏ヤ晶锛�2026-08-03 鍚堟壒鍚庢柊澧烇級銆�
+     *
+     * <p>姣� {@link #MAX_SUB_ROWS} 澶т竴妗o細鏁磋鏍囬瀛楁涓虹┖鐨勮娓叉煋涓嶅嚭鏍囬鍗翠粛瑕佸崰涓�涓壂鎻忓悕棰濓紝
+     * 涓よ�呭悓鍊兼椂銆屽墠 200 琛屾伆濂藉叏绌恒�嶅氨浼氳 recordSubTitles 鐩存帴绌烘帀锛圕odex 缁堝缂洪櫡 6锛夈��
+     * 鏀惧ぇ鍒� 4 鍊嶆槸鏉冭 鈥斺�斿鍚告敹甯歌鐨勭┖琛屾瘮渚嬶紝鍙堟妸鍚堟垚 diff 鐨勮妯¢拤鍦ㄥ父鏁扮骇銆�
+     *
+     * <p>2026-08-05 璧峰畠鍚屾椂鏄�<b>鐗╁寲棰勭畻</b>锛歿@link #subRows} 鎸夊墿浣欓搴︽埅鏂鍒讹紝
+     * 澶栧眰鍚嶉鐢ㄥ敖鐩存帴 break 鎺夊墿浣欏瓙琛紙Codex 澶嶅 Medium鈶o級銆傛鍓嶅畠鍙尅娓叉煋銆佹尅涓嶄綇澶嶅埗銆�
+     */
+    private static final int MAX_SUB_SCAN = MAX_SUB_ROWS * 4;
+
+    @Autowired
+    private AuditFieldDiffViewService fieldDiffViewService;
+
+    public AuditRecordTitle build(AuditTitleSpec spec,
+                                  Map<String, Object> row,
+                                  AuditEventEntity valueContext,
+                                  Map<String, AuditFieldDiff.FieldMeta> labels,
+                                  Set<String> excludedFields) {
+        return build(spec, row, valueContext, labels, excludedFields,
+                Collections.<String, String>emptyMap());
+    }
+
+    public AuditRecordTitle build(AuditTitleSpec spec,
+                                  Map<String, Object> row,
+                                  AuditEventEntity valueContext,
+                                  Map<String, AuditFieldDiff.FieldMeta> labels,
+                                  Set<String> excludedFields,
+                                  Map<String, String> frontendDisplayValues) {
+        if (spec == null || spec.isEmpty() || row == null || row.isEmpty()) {
+            return AuditRecordTitle.EMPTY;
+        }
+        try {
+            // 鈹�鈹� 涓昏〃 + 鍏ㄩ儴瀛愯〃琛�**涓�娆¤В鏋�**锛�2026-08-03锛孋odex 缁堝 Low锛氭煡璇㈡斁澶э級鈹�鈹�鈹�鈹�鈹�鈹�鈹�鈹�
+            // 鍘熷厛姣忚鍚勮皟涓�娆� displayValues锛岃�岃В鏋愰摼閲岀殑 JnpfUserFieldValueResolver 姣忔涓�鏉�
+            // SQL鈥斺��200 涓瓙琛ㄨ灏辨槸 201 娆℃煡璇紝鍏ㄥ帇鍦� MQ 娑堣垂绾跨▼涓娿�傝В鏋愬櫒鏈韩鏃╁氨鏄壒閲忕殑
+            // 锛堟暣鎵逛竴娆� IN锛夛紝鏀惧ぇ鏉ヨ嚜銆屾寜琛屽垏纰庝簡鍠傜粰瀹冦�嶃�傝繖閲屽厛鎶婃墍鏈夊緟娓叉煋鐨勮鏀掗綈锛�
+            // 鍚堟垚**涓�浠�** diff 浜ょ粰鍚屼竴鏉¤В鏋愯矾寰勶紝鍐嶆寜浣嶇疆鍒嗗彂鍥炲悇琛屻��
+            List<List<String>> lineFields = new ArrayList<List<String>>();
+            List<Map<String, Object>> lineRows = new ArrayList<Map<String, Object>>();
+            lineFields.add(spec.mainFields());
+            lineRows.add(row);                     // 涓嬫爣 0 鎭掍负涓昏〃琛�
+
+            String subTable = null;
+            boolean unavailable = false;
+            // 杈撳叆渚ф病鐪嬪叏锛堥搴﹁�楀敖锛夈��**蹇呴』鐣欑棔**锛�2026-08-05 Codex 澶嶅 Low锛夛細棰濆害鑰楀敖鍚�
+            // 鏁村紶瀛愯〃閮戒笉纰帮紝杩炪�屼簨浠堕噷娌″甫杩欐壒鏁版嵁銆嶉兘娴嬩笉鍑烘潵锛寀navailable 鎭掍负 false鈥斺��
+            // 銆屽垽涓嶄簡銆嶅氨琚吉瑁呮垚浜嗐�屾病鍙戠敓銆嶏紙L075锛夈�傝ˉ鐪嬬殑浠d环姝f槸璧勬簮涓婄晫鍒氭媶鎺夌殑閭g瑪锛�
+            // 鎵�浠ラ�夋嫨濡傚疄璇淬�屾垜娌$湅鍏ㄣ�嶏紝鑰屼笉鏄姳閽辩湅鍏ㄣ��
+            boolean scanTruncated = false;
+            for (AuditTitleSpec.SubTableSpec sub : spec.subTables()) {
+                if (sub.fields().isEmpty()) {
+                    continue;
+                }
+                // 馃敶 涓婇檺**涓嬫帹鍒板彇琛岄偅涓�姝�**锛�2026-08-05 Codex 澶嶅 Medium鈶o級锛氬師鍏堣繖閬撻椄鍙尅浣�
+                // 銆岃繘鍏ユ覆鏌撶殑琛屾暟銆嶏紝鑰� subRows() 宸茬粡鎶婃暣涓瓙琛ㄥ弽搴忓垪鍖栧苟澶嶅埗瀹屼簡锛屽灞傝揪鍒�
+                // 涓婇檺涔嬪悗杩樹細缁х画鎶婂悗闈㈡瘡涓�寮犲瓙琛ㄩ兘瑙f瀽涓�閬嶁�斺�旂湡姝g殑璧勬簮涓婄晫浠庢潵娌″缓绔嬭捣鏉ャ��
+                // 鐜板湪鍚嶉鐢ㄥ敖灏� break锛屽墿涓嬬殑瀛愯〃杩炵閮戒笉纰帮紱鍚嶉娌$敤灏界殑浼犲墿浣欓搴﹁繘鍘伙紝
+                // 鐢� subRows 鍦ㄧ墿鍖栨椂鎴柇銆�
+                int remaining = MAX_SUB_SCAN - (lineRows.size() - 1);
+                if (remaining <= 0) {
+                    scanTruncated = true;
+                    break;
+                }
+                subTable = sub.tableName() != null ? sub.tableName() : sub.vModel();
+                // **澶氳涓�琛屽綋鍝ㄥ叺**锛氭嬁鍥� remaining+1 琛屽氨璇存槑杩欏紶瀛愯〃杩樻湁娌¤鐨勩��
+                // 鍙寜 remaining 瑕佸垯 rows.size()==remaining 鏃跺垎涓嶆竻銆屾伆濂借瀹屻�嶄笌銆岃鎴柇銆嶏紝
+                // 鑰岃繖涓よ�呭璇荤殑浜烘槸瀹屽叏涓嶅悓鐨勪袱浠朵簨銆傚鐗╁寲鐨勯偅涓�琛屾槸 O(1)锛屾崲鏉ョ簿纭垽瀹氥��
+                List<Map<String, Object>> rows = subRows(row, sub.vModel(), remaining + 1);
+                if (rows != null && rows.size() > remaining) {
+                    scanTruncated = true;
+                    rows = rows.subList(0, remaining);
+                }
+                if (rows == null) {
+                    // 琛ㄥ崟閰嶄簡瀛愯〃鏍囬瀛楁锛屼簨浠堕噷鍗存病鏈夎繖鎵规暟鎹��**濡傚疄鐣欑棔**鈥斺��
+                    // 闈欓粯褰撴垚"娌℃湁瀛愯〃"浼氭妸"鍒や笉浜�"浼鎴�"鏈潵灏辨病鏈�"锛堝悓 subTableUndecidable 鎯緥锛夈��
+                    unavailable = true;
+                    continue;
+                }
+                for (Map<String, Object> subRow : rows) {
+                    lineFields.add(sub.fields());
+                    lineRows.add(subRow);
+                }
+            }
+            // 馃敶 涓ら亾涓婇檺锛屽彛寰勪笉鍚岋紝缂轰竴涓嶅彲锛�2026-08-03 Codex 缁堝缂洪櫡 6锛夛細
+            //   路 MAX_SUB_SCAN 闄�**鍙備笌瑙f瀽鐨勫師濮嬭鏁�**鈥斺�斿悎鎵瑰悗琛屾暟鐩存帴鍐冲畾鍚堟垚 diff 鐨勫ぇ灏忥紝
+            //     娌℃湁瀹冨氨寰楁妸鎵�鏈夎閮藉杩涘幓鎵嶇煡閬撳涓嶅锛岄偅姝f槸瑕侀槻鐨勮啫鑳�锛�
+            //   路 MAX_SUB_ROWS 闄�**浜у嚭鐨勯潪绌烘爣棰樻潯鏁�**鈥斺�旇繖鏄敼鍔ㄥ墠鐨勬棦鏈夊彛寰勶紝蹇呴』淇濅綇銆�
+            // 鍙暀鍓嶈�呬細閫�鍖栵細瀛愯〃鍓� 200 琛屾爣棰樺瓧娈垫伆濂藉叏绌烘椂锛岀 201 琛岃捣鐨勬湁鏁堟爣棰樺叏琚涪鎺夛紝
+            // recordSubTitles 鐩存帴绌烘帀銆傛墍浠ユ壂鎻忕獥鍙e紑澶т竴妗o紝浜у嚭浠嶆寜鑰佸彛寰勬埅鏂��
+            List<String> lines = renderLines(lineFields, lineRows, valueContext, labels,
+                    excludedFields, frontendDisplayValues);
+            List<String> subTitles = new ArrayList<String>();
+            // 浜у嚭渚ф埅鏂悓鏍风暀鐥曪細杩欓亾涓婇檺浠庢敼鍔ㄥ墠灏辨槸**鎮勬倓**鎴殑锛岃鐨勪汉鐪嬪埌 200 鏉�
+            // 浼氫互涓洪偅灏辨槸鍏ㄩ儴锛�2026-08-05 Codex 澶嶅 Low 椤哄甫瑕嗙洊锛夈��
+            boolean rowsTruncated = false;
+            // 鈿狅笍 鍏堣烦绌鸿鍐嶅垽涓婇檺锛岄『搴忎笉鑳藉弽锛氬弽杩囨潵鐨勮瘽銆屽墿涓嬬殑琛屾伆濂藉叏娓叉煋涓嶅嚭鏍囬銆�
+            // 涔熶細琚姤鎴� truncated鈥斺�旈偅鏄妸銆岀湅鍏ㄤ簡銆佸彧鏄病鍐呭銆嶈鎴愪簡銆屾病鐪嬪叏銆嶏紝
+            // 涓� L075 楠傜殑銆屼袱绉嶇己甯叡鐢ㄤ竴涓〃绀恒�嶆槸鍚屼竴涓敊锛屽彧鏄柟鍚戠浉鍙嶃��
+            for (int i = 1; i < lines.size(); i++) {
+                if (lines.get(i) == null) {
+                    continue;
+                }
+                if (subTitles.size() >= MAX_SUB_ROWS) {
+                    rowsTruncated = true;
+                    break;
+                }
+                subTitles.add(lines.get(i));
+            }
+            // scanTruncated 浼樺厛锛氳緭鍏ヤ晶灏辨病鐪嬪叏鏃讹紝浜у嚭渚ф埅娌℃埅宸蹭笉鏄渶閲嶈鐨勪俊鎭��
+            String subScope = scanTruncated ? AuditRecordTitle.SCOPE_SCAN_TRUNCATED
+                    : (rowsTruncated ? AuditRecordTitle.SCOPE_ROWS_TRUNCATED : null);
+            return new AuditRecordTitle(lines.get(0), subTitles, subTable, unavailable, subScope);
+        } catch (Throwable t) {
+            // 鏍囬娓叉煋澶辫触鍙璁╂爣棰樼己甯紝涓嶈璁╂暣鏉′簨浠剁己甯紙L002 鐨勫悓娆惧彇鑸嶏紝浣嗕綔鐢ㄥ煙鏇村皬锛�
+            log.warn("[AUDIT-L0] 璁板綍鏍囬娓叉煋澶辫触锛屾湰娆′簨浠朵笉甯︽爣棰橈細{}", t.toString());
+            return AuditRecordTitle.EMPTY;
+        }
+    }
+
+    /**
+     * 琛ㄥ崟鏈惎鐢ㄤ换浣曞璁℃爣棰樺瓧娈垫椂锛屼互褰撳墠涓氬姟璁板綍鐨� {@code f_id} 浣滀负鏍囬銆�
+     * 浜嬩欢閲岀殑 {@code dataId} 鏄垱寤哄満鏅殑鍙潬鍥為��锛屽洜涓烘彁浜ゅ師濮嬫暟鎹笉涓�瀹氬洖濉富閿��
+     */
+    public String resolveMainTitle(AuditTitleSpec spec, AuditRecordTitle title,
+                                   Map<String, Object> row, String dataId) {
+        if (spec != null && !spec.isEmpty()) {
+            return title == null ? null : title.mainTitle();
+        }
+        Object rowId = valueOf(row, "f_id");
+        String recordId = rowId == null ? null : String.valueOf(rowId).trim();
+        if (recordId == null || recordId.isEmpty()) {
+            recordId = dataId == null ? null : dataId.trim();
+        }
+        if (recordId == null || recordId.isEmpty()) {
+            return null;
+        }
+        return recordId.length() <= MAX_LINE ? recordId : recordId.substring(0, MAX_LINE);
+    }
+
+    /**
+     * 澶氳**涓�娆¤В鏋�**锛氭墍鏈夎鐨勫瓧娈靛悎鎴愪竴浠� diff 浜ょ粰 {@link AuditFieldDiffViewService#build}锛�
+     * 鍐嶆寜浣嶇疆鍒囧洖鍚勮銆傝繑鍥炲�间笌鍏ュ弬閫愪綅瀵瑰簲锛屾覆鏌撲笉鍑哄唴瀹圭殑琛屼负 {@code null}銆�
+     *
+     * <p><b>涓轰粈涔堟寜浣嶇疆鍒嗗彂鑰屼笉鏄寜瀛楁鍚�</b>锛氬瓧娈靛悕鍦ㄨ法琛屾椂浼氶噸澶嶏紙姣忎釜瀛愯〃琛岄兘鏈夊悓涓�鎵瑰垪锛夛紝
+     * 鎸夊悕瀛楀缓 Map 浼氫簰鐩歌鐩栥�倇@code build} 閫愰」绛夐暱杩斿洖涓斾繚搴忊�斺�斿畠鍙湪 {@code field} 缂哄腑鏃�
+     * 涓㈤」锛岃�岃繖閲屾瘡涓�椤归兘甯﹂潪绌� field锛屾墍浠ヤ綅缃槸鍙潬鐨勯敭銆�
+     *
+     * <p><b>绛夐暱鍓嶆彁涓�鏃︿笉鎴愮珛灏遍��鍥為�愯瑙f瀽</b>锛氬鏌ュ嚑娆″彧鏄參锛岃�岄敊浣嶄細鎶� A 琛岀殑鍊兼樉绀哄埌
+     * B 琛岀殑鏍囬涓娾�斺�攞@code record_title} 鏄� append-only 瀛樺崄骞寸殑鏄庢枃鍒楋紝鍐欓敊娲椾笉鎺夈��
+     */
+    private List<String> renderLines(List<List<String>> lineFields,
+                                     List<Map<String, Object>> lineRows,
+                                     AuditEventEntity valueContext,
+                                     Map<String, AuditFieldDiff.FieldMeta> labels,
+                                     Set<String> excludedFields,
+                                     Map<String, String> frontendDisplayValues) {
+        List<Map<String, Object>> items = new ArrayList<Map<String, Object>>();
+        List<Integer> starts = new ArrayList<Integer>();
+        for (int i = 0; i < lineRows.size(); i++) {
+            starts.add(items.size());
+            appendItems(items, lineFields.get(i), lineRows.get(i), excludedFields);
+        }
+        List<String> out = new ArrayList<String>(lineRows.size());
+        if (items.isEmpty()) {
+            for (int i = 0; i < lineRows.size(); i++) {
+                out.add(null);
+            }
+            return out;
+        }
+        List<AuditFieldDiffViewVO> views = fieldDiffViewService.build(
+                valueContext, JsonUtil.getObjectToString(items), labels);
+        if (views == null || views.size() != items.size()) {
+            log.warn("[AUDIT-L0] 鏍囬鍚堟壒瑙f瀽杩斿洖 {} 椤广�佹湡鏈� {} 椤癸紝閫�鍥為�愯瑙f瀽",
+                    views == null ? -1 : views.size(), items.size());
+            for (int i = 0; i < lineRows.size(); i++) {
+                out.add(renderLine(lineFields.get(i), lineRows.get(i), valueContext,
+                        labels, excludedFields, i == 0 ? frontendDisplayValues : null));
+            }
+            return out;
+        }
+        for (int i = 0; i < lineRows.size(); i++) {
+            int from = starts.get(i);
+            int to = i + 1 < starts.size() ? starts.get(i + 1) : items.size();
+            Map<String, String> displays = toDisplays(views.subList(from, to));
+            if (i == 0) {
+                mergeFrontendDisplays(displays, lineFields.get(i), excludedFields,
+                        frontendDisplayValues);
+            }
+            out.add(displays.isEmpty() ? null : assembleLine(lineFields.get(i), displays));
+        }
+        return out;
+    }
+
+    /** 娓叉煋涓�琛岋紱鍏ㄩ儴瀛楁涓虹┖鎴栧叏琚帓闄ゆ椂杩斿洖 null锛堜笉浜у嚭绌鸿锛夈�傚悎鎵瑰墠鎻愪笉鎴愮珛鏃剁殑閫�璺�� */
+    private String renderLine(List<String> fields, Map<String, Object> row,
+                              AuditEventEntity valueContext,
+                              Map<String, AuditFieldDiff.FieldMeta> labels,
+                              Set<String> excludedFields,
+                              Map<String, String> frontendDisplayValues) {
+        Map<String, String> displays = displayValues(fields, row, valueContext, labels, excludedFields);
+        mergeFrontendDisplays(displays, fields, excludedFields, frontendDisplayValues);
+        if (displays.isEmpty()) {
+            return null;
+        }
+        return assembleLine(fields, displays);
+    }
+
+    private void mergeFrontendDisplays(Map<String, String> displays,
+                                       List<String> configuredFields,
+                                       Set<String> excludedFields,
+                                       Map<String, String> frontendDisplayValues) {
+        if (frontendDisplayValues == null || frontendDisplayValues.isEmpty()) {
+            return;
+        }
+        for (String field : configuredFields) {
+            String normalized = field.toLowerCase(Locale.ROOT);
+            if (containsIgnoreCase(excludedFields, normalized)) {
+                continue;
+            }
+            String value = frontendDisplayValues.get(normalized);
+            if (value == null || value.trim().isEmpty()) {
+                continue;
+            }
+            displays.put(normalized, truncate(value.trim(), MAX_LINE));
+        }
+    }
+
+    /** 鎶婅В鏋愬ソ鐨勫睍绀哄�兼寜瀛楁椤哄簭鎷兼垚 a/b/c锛涘叏绌鸿繑鍥� null銆� */
+    private String assembleLine(List<String> fields, Map<String, String> displays) {
+        StringBuilder sb = new StringBuilder();
+        for (String field : fields) {
+            String display = displays.get(field.toLowerCase(Locale.ROOT));
+            if (display == null || display.isEmpty()) {
+                continue;   // 绌哄�艰烦杩囷紝涓嶄骇鐢� a//c
+            }
+            if (sb.length() > 0) {
+                sb.append(SEPARATOR);
+            }
+            sb.append(display);
+            if (sb.length() >= MAX_LINE) {
+                return sb.substring(0, MAX_LINE);
+            }
+        }
+        return sb.length() == 0 ? null : sb.toString();
+    }
+
+    /**
+     * 璧� {@code field_diffs} 鐨勫悓涓�鏉″睍绀鸿В鏋愶細鎶婂緟娓叉煋瀛楁鍖呮垚涓�浠藉悎鎴� diff锛�
+     * 浜ょ粰 {@link AuditFieldDiffViewService#build} 瑙f瀽鍑� {@code newDisplay}銆�
+     *
+     * <p>{@code "type": 0} 鏄� field_diffs 濂戠害閲岀殑"鏂板"锛岃瑙f瀽渚у彧鐪� newData 閭d竴渚с��
+     */
+    private Map<String, String> displayValues(List<String> fields, Map<String, Object> row,
+                                              AuditEventEntity valueContext,
+                                              Map<String, AuditFieldDiff.FieldMeta> labels,
+                                              Set<String> excludedFields) {
+        List<Map<String, Object>> items = new ArrayList<Map<String, Object>>();
+        appendItems(items, fields, row, excludedFields);
+        if (items.isEmpty()) {
+            return Collections.emptyMap();
+        }
+        return toDisplays(fieldDiffViewService.build(
+                valueContext, JsonUtil.getObjectToString(items), labels));
+    }
+
+    /**
+     * 鎶婁竴琛岄噷寰呮覆鏌撶殑瀛楁杩藉姞鎴愬悎鎴� diff 鐨勯」銆�**姣忎竴椤圭殑 field 蹇呯劧闈炵┖**鈥斺��
+     * 杩欐槸 {@link #renderLines} 鎸変綅缃垎鍙戠殑鍓嶆彁锛坽@code build} 鍙湪 field 缂哄腑鏃朵涪椤癸級銆�
+     */
+    private static void appendItems(List<Map<String, Object>> items, List<String> fields,
+                                    Map<String, Object> row, Set<String> excludedFields) {
+        if (fields == null || row == null) {
+            return;
+        }
+        for (String field : fields) {
+            if (field == null) {
+                continue;
+            }
+            String lower = field.toLowerCase(Locale.ROOT);
+            if (lower.isEmpty()) {
+                continue;
+            }
+            if (excludedFields != null && excludedFields.contains(lower)) {
+                continue;
+            }
+            String raw = AuditFieldDiff.stringify(valueOf(row, lower));
+            if (raw.trim().isEmpty()) {
+                continue;
+            }
+            Map<String, Object> item = new LinkedHashMap<String, Object>();
+            item.put("field", lower);
+            item.put("newData", raw);
+            item.put("type", 0);
+            items.add(item);
+        }
+    }
+
+    /** 瑙f瀽缁撴灉 鈫� 瀛楁鍚嶅埌灞曠ず鍊肩殑鏄犲皠銆�**璋冪敤鏂归』淇濊瘉杩欐壒 view 鏉ヨ嚜鍚屼竴琛�**锛堝瓧娈靛悕浼氳法琛岄噸澶嶏級銆� */
+    private static Map<String, String> toDisplays(List<AuditFieldDiffViewVO> views) {
+        Map<String, String> out = new LinkedHashMap<String, String>();
+        if (views == null) {
+            return out;
+        }
+        for (AuditFieldDiffViewVO view : views) {
+            if (view.getField() == null || Boolean.TRUE.equals(view.getMasked())) {
+                continue;   // 鍙屼繚闄╋細瑙f瀽渚у垽瀹氫负鍑瘉鐨勶紝鍗充娇璋冪敤鏂规紡鎺掗櫎涔熶笉杩涙爣棰�
+            }
+            String display = view.getNewDisplay() != null && !view.getNewDisplay().isEmpty()
+                    ? view.getNewDisplay() : view.getNewData();
+            if (display != null && !display.trim().isEmpty()) {
+                out.put(view.getField().toLowerCase(Locale.ROOT), display.trim());
+            }
+        }
+        return out;
+    }
+
+    /**
+     * 鍙栧瓙琛ㄨ锛涢敭缂哄腑鎴栧舰鎬佷笉瀵硅繑鍥� null锛�= 涓嶅彲寰楋級锛岀┖鏁扮粍杩斿洖绌哄垪琛紙= 纭疄娌℃湁瀛愯褰曪級銆�
+     *
+     * <p><b>{@code limit} 鏄湰鏂规硶鐨勮祫婧愪笂鐣�</b>锛�2026-08-05 Codex 澶嶅 Medium鈶o級锛�**鐗╁寲鏃跺氨鎴柇**锛�
+     * 涓嶆槸鍏堝叏閲忓鍒跺啀浜ょ粰璋冪敤鏂规寫銆傝皟鐢ㄦ柟閭i亾 {@link #MAX_SUB_SCAN} 鍙寰椾綇銆屾覆鏌撳嚑琛屻�嶏紝
+     * 绠′笉浣忚繖閲屽鍒跺嚑琛屸�斺�斾竴寮� 5000 琛岀殑瀛愯〃鐓ф牱浼氳鏁翠唤鎼繘鍐呭瓨锛岃�� MQ 娑堣垂绾跨▼涓�
+     * 姣忔潯浜嬩欢閮借璧颁竴閬嶃��
+     *
+     * <p><b>娌¤ limit 瑕嗙洊鐨勯偅涓�鍗�</b>锛氬�兼槸 JSON 瀛楃涓叉椂 Jackson 浠嶈鎶婃暣涓暟缁勮В鏋愬畬
+     * 锛堟祦寮忚В鏋愯鎹㈡帀 {@link JsonUtil} 鐨勫叆鍙o紝浠d环涓嶆垚姣斾緥锛夈�備絾閭d唤鍘熷鏂囨湰鏈潵灏遍殢浜嬩欢
+     * 鎶ユ枃涓�璧峰湪鍐呭瓨閲屼簡锛岀湡姝d細浜屾鏀惧ぇ鐨勬槸杩欓噷鐨勫璞″鍒朵笌鍚庣画鐨勫悎鎴� diff锛屼袱鑰呴兘宸叉寜 limit 鏀朵綇銆�
+     */
+    @SuppressWarnings("unchecked")
+    private List<Map<String, Object>> subRows(Map<String, Object> row, String vModel, int limit) {
+        Object raw = valueOf(row, vModel == null ? null : vModel.toLowerCase(Locale.ROOT));
+        if (raw == null) {
+            return null;
+        }
+        Object parsed = raw;
+        if (raw instanceof String) {
+            String text = ((String) raw).trim();
+            if (text.isEmpty()) {
+                return Collections.emptyList();
+            }
+            try {
+                parsed = JsonUtil.getJsonToList(text, Map.class);
+            } catch (Throwable t) {
+                return null;
+            }
+        }
+        if (!(parsed instanceof List)) {
+            return null;
+        }
+        List<Map<String, Object>> out = new ArrayList<Map<String, Object>>();
+        for (Object item : (List<?>) parsed) {
+            if (out.size() >= limit) {
+                break;   // 鍚嶉鐢ㄥ敖锛氱墿鍖栧嵆姝紝涓嶆妸鏁村紶瀛愯〃鎼繘鍐呭瓨
+            }
+            if (item instanceof Map) {
+                out.add((Map<String, Object>) item);
+            }
+        }
+        return out;
+    }
+
+    private static Object valueOf(Map<String, Object> row, String lowerField) {
+        if (row == null || lowerField == null) {
+            return null;
+        }
+        for (Map.Entry<String, Object> entry : row.entrySet()) {
+            if (entry.getKey() != null
+                    && lowerField.equals(entry.getKey().toLowerCase(Locale.ROOT))) {
+                return entry.getValue();
+            }
+        }
+        return null;
+    }
+
+    private static boolean containsIgnoreCase(Set<String> values, String expected) {
+        if (values == null || expected == null) {
+            return false;
+        }
+        for (String value : values) {
+            if (value != null && expected.equals(value.toLowerCase(Locale.ROOT))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static String truncate(String value, int maxLength) {
+        return value.length() <= maxLength ? value : value.substring(0, maxLength);
+    }
+}

--
Gitblit v1.8.0