From 5d6fca24d69b387f219783ff8b295daa45c021c5 Mon Sep 17 00:00:00 2001
From: lml <1297555865@qq.com>
Date: Wed, 21 Oct 2026 14:32:52 +0800
Subject: [PATCH] =?UTF-8?q?fix(botp):=20=E4=BF=AE=E5=A4=8D=E7=89=88?=
=?UTF-8?q?=E6=9C=AC=E5=AF=B9=E6=AF=94=E6=8A=8A=E3=80=8C=E6=94=B9=E6=BA=90?=
=?UTF-8?q?=E5=AD=97=E6=AE=B5=E3=80=8D=E8=AF=AF=E5=88=A4=E6=88=90=E4=B8=80?=
=?UTF-8?q?=E5=A2=9E=E4=B8=80=E5=88=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 字段映射的业务主键含源字段(oldfieldNumber),用户改源字段会让精确配对失败,
同一行被拆成「删除 1 条 + 新增 1 条」,实际应识别为「修改 1 条」
- 版本对比改为两轮配对:第一轮按完整业务主键精确配对;第二轮对剩余行按兜底字段再配一次
(字段映射用目标字段、反写公式用反写字段;插件与业务规则主键本身唯一,不做兜底)
- 兜底配对加两道约束防止误合并:同一兜底 key 下旧行数须等于新行数、且兜底字段两行都非空且相等
真新增 + 真删除不会被硬凑成一条修改
- 修正 FALLBACK_FIELD_MAPPING 取值:不用 entryTarget(常为空,等于放宽成同组随便配),
改用 newfieldNumber
---
.../botp/service/impl/ConvertRuleServiceImpl.java | 18 +-
.../impl/ReverseWritingRulesServiceImpl.java | 18 +-
.../java/apelet/common/botp/util/RuleDiffUtil.java | 202 ++++++++++++++++++---
3 files changed, 211 insertions(+), 27 deletions(-)
diff --git a/common/common-botp/src/main/java/apelet/common/botp/service/impl/ConvertRuleServiceImpl.java b/common/common-botp/src/main/java/apelet/common/botp/service/impl/ConvertRuleServiceImpl.java
index 2dacb5a..9402d63 100644
--- a/common/common-botp/src/main/java/apelet/common/botp/service/impl/ConvertRuleServiceImpl.java
+++ b/common/common-botp/src/main/java/apelet/common/botp/service/impl/ConvertRuleServiceImpl.java
@@ -422,7 +422,8 @@ public class ConvertRuleServiceImpl extends BaseService implem
RuleDiffUtil.parseSnapshot(fromVer.getSnapshotJson()),
RuleDiffUtil.parseSnapshot(toVer.getSnapshotJson()),
Arrays.asList(RuleSnapshotUtil.SECTION_FIELD_MAPPINGS, RuleSnapshotUtil.SECTION_PLUGIN_ENTRIES),
- buildEntryKeyFields());
+ buildEntryKeyFields(),
+ buildEntryFallbackFields());
result.setRuleId(ruleId);
result.setFromVersion(fromVer.getRuleVersion());
result.setToVersion(toVer.getRuleVersion());
@@ -443,6 +444,21 @@ public class ConvertRuleServiceImpl extends BaseService implem
return keyFields;
}
+ /**
+ * 各子表分区在业务主键字段被改动时的兜底配对字段。
+ *
+ * 字段映射的业务主键里含「源字段」,用户改源字段会让该行精确配对失败,
+ * 这里按「目标字段 + 目标分录」再配一次,避免把一次修改显示成删除 + 新增。
+ *
+ * @return 分区名 -> 兜底配对字段名。
+ */
+ private Map buildEntryFallbackFields() {
+ Map fallbackFields = new LinkedHashMap<>(4);
+ fallbackFields.put(RuleSnapshotUtil.SECTION_FIELD_MAPPINGS, RuleDiffUtil.FALLBACK_FIELD_MAPPING);
+ fallbackFields.put(RuleSnapshotUtil.SECTION_PLUGIN_ENTRIES, RuleDiffUtil.FALLBACK_PLUGIN_ENTRY);
+ return fallbackFields;
+ }
+
/**
* 添加子数据方法
diff --git a/common/common-botp/src/main/java/apelet/common/botp/service/impl/ReverseWritingRulesServiceImpl.java b/common/common-botp/src/main/java/apelet/common/botp/service/impl/ReverseWritingRulesServiceImpl.java
index bd92ed9..32f9799 100644
--- a/common/common-botp/src/main/java/apelet/common/botp/service/impl/ReverseWritingRulesServiceImpl.java
+++ b/common/common-botp/src/main/java/apelet/common/botp/service/impl/ReverseWritingRulesServiceImpl.java
@@ -253,7 +253,8 @@ public class ReverseWritingRulesServiceImpl extends ServiceImpl反写公式的业务主键里含「取数的源单字段」,只调整取数来源时按反写字段再配一次;
+ * 业务规则没有稳定的标识字段,不做兜底。
+ *
+ * @return 分区名 -> 兜底配对字段名。
+ */
+ private Map buildEntryFallbackFields() {
+ Map fallbackFields = new LinkedHashMap<>(4);
+ fallbackFields.put(RuleSnapshotUtil.SECTION_FORMULAS, RuleDiffUtil.FALLBACK_FORMULA);
+ fallbackFields.put(RuleSnapshotUtil.SECTION_BUSINESSES, RuleDiffUtil.FALLBACK_BUSINESS);
+ return fallbackFields;
+ }
}
diff --git a/common/common-botp/src/main/java/apelet/common/botp/util/RuleDiffUtil.java b/common/common-botp/src/main/java/apelet/common/botp/util/RuleDiffUtil.java
index 855d2cb..713693d 100644
--- a/common/common-botp/src/main/java/apelet/common/botp/util/RuleDiffUtil.java
+++ b/common/common-botp/src/main/java/apelet/common/botp/util/RuleDiffUtil.java
@@ -71,6 +71,32 @@ public final class RuleDiffUtil {
*/
public static final String[] KEY_BUSINESS = {"preconditionsField", "establishedField", "notEstablishedField"};
+ /**
+ * 字段映射的兜底配对字段。
+ *
+ * 业务主键里的 {@code oldfieldNumber}(源字段)正是用户最常改动的对象。精确配对失败后,
+ * 只在「目标字段相同」的行之间两两对认,从而把「换了源字段」识别成修改。
+ * 注意不把 {@code entryTarget} 放进兜底:它经常为空,等于放宽成「同组随便配」,
+ * 会把「真新增 + 真删除」硬凑成「修改」。{@code entryTarget} 放在参照字段里,
+ * 要求配对的两行目标分录一致。
+ */
+ public static final String[] FALLBACK_FIELD_MAPPING = {"newfieldNumber"};
+
+ /**
+ * 插件分录的兜底配对字段:业务主键就是插件类名,类名一改通常确实是换插件,不做兜底。
+ */
+ public static final String[] FALLBACK_PLUGIN_ENTRY = {};
+
+ /**
+ * 反写公式的兜底配对字段:反写目标字段没变、只换了取数字段时,认作同一行。
+ */
+ public static final String[] FALLBACK_FORMULA = {"formulaField"};
+
+ /**
+ * 业务规则的兜底配对字段:业务规则本身没有稳定的标识字段,不做兜底。
+ */
+ public static final String[] FALLBACK_BUSINESS = {};
+
private RuleDiffUtil() {
}
@@ -91,16 +117,18 @@ public final class RuleDiffUtil {
/**
* 对比两个版本快照,产出新增 / 删除 / 修改三类差异。
*
- * @param fromSnapshot 基准版本快照。
- * @param toSnapshot 目标版本快照。
- * @param entrySections 需要对比的子表分区名,按顺序输出。
+ * @param fromSnapshot 基准版本快照。
+ * @param toSnapshot 目标版本快照。
+ * @param entrySections 需要对比的子表分区名,按顺序输出。
* @param entryKeyFields 各子表分区对应的业务主键字段名。
+ * @param entryFallbackFields 各子表分区对应的兜底配对字段名(业务主键字段被改动时用)。
* @return 差异结果(不含版本号等元信息,由调用方填充)。
*/
public static RuleVersionDiffDto diff(Map fromSnapshot,
Map toSnapshot,
List entrySections,
- Map entryKeyFields) {
+ Map entryKeyFields,
+ Map entryFallbackFields) {
RuleVersionDiffDto result = new RuleVersionDiffDto();
List ruleDiff = diffFields(asMap(fromSnapshot.get(RuleSnapshotUtil.SECTION_RULE)),
asMap(toSnapshot.get(RuleSnapshotUtil.SECTION_RULE)), RULE_IGNORE_FIELDS);
@@ -112,9 +140,11 @@ public final class RuleDiffUtil {
if (entrySections != null) {
for (String section : entrySections) {
String[] keyFields = entryKeyFields == null ? null : entryKeyFields.get(section);
+ String[] fallbackFields = entryFallbackFields == null ? null : entryFallbackFields.get(section);
EntryDiff entryDiff = diffEntries(asMapList(fromSnapshot.get(section)),
asMapList(toSnapshot.get(section)),
- keyFields == null ? new String[0] : keyFields);
+ keyFields == null ? new String[0] : keyFields,
+ fallbackFields == null ? new String[0] : fallbackFields);
result.getEntries().put(section, entryDiff);
result.setAddedCount(result.getAddedCount() + entryDiff.getAdded().size());
result.setRemovedCount(result.getRemovedCount() + entryDiff.getRemoved().size());
@@ -165,21 +195,32 @@ public final class RuleDiffUtil {
/**
* 按业务主键比对两组分录行。
*
- * 业务主键相同的行按出现顺序两两配对,配上的比字段(有差异即「修改」);
- * 多出来的分别记「新增」或「删除」。这样即使分录主键每版都变,也能识别出「同一行被改了」。
+ * 分两轮配对:
+ *
+ * - 第一轮按完整业务主键精确配对,配上的比字段,有差异即「修改」。
+ * - 第二轮处理剩下的行:业务主键字段本身被用户改动时(例如字段映射改了源字段),
+ * 第一轮必然配对不上,会直接落到「删除 + 新增」。因此按 {@code fallbackFields}
+ * (例如目标字段)再配一次,配上的一律记「修改」。
+ *
+ * 两轮都配不上的,才分别记「新增」或「删除」。
*
- * @param oldEntries 基准版本的分录行。
- * @param newEntries 目标版本的分录行。
- * @param keyFields 业务主键字段名,为空时所有行归为一组按顺序配对。
+ * @param oldEntries 基准版本的分录行。
+ * @param newEntries 目标版本的分录行。
+ * @param keyFields 业务主键字段名,为空时所有行归为一组按顺序配对。
+ * @param fallbackFields 业务主键改动的兜底配对字段,为空时不做第二轮配对。
* @return 分录差异。
*/
public static EntryDiff diffEntries(List