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