|
|
|
@ -71,6 +71,32 @@ public final class RuleDiffUtil { |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public static final String[] KEY_BUSINESS = {"preconditionsField", "establishedField", "notEstablishedField"}; |
|
|
|
public static final String[] KEY_BUSINESS = {"preconditionsField", "establishedField", "notEstablishedField"}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 字段映射的兜底配对字段。 |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <p>业务主键里的 {@code oldfieldNumber}(源字段)正是用户最常改动的对象。精确配对失败后, |
|
|
|
|
|
|
|
* 只在「目标字段相同」的行之间两两对认,从而把「换了源字段」识别成修改。 |
|
|
|
|
|
|
|
* 注意不把 {@code entryTarget} 放进兜底:它经常为空,等于放宽成「同组随便配」, |
|
|
|
|
|
|
|
* 会把「真新增 + 真删除」硬凑成「修改」。{@code entryTarget} 放在参照字段里, |
|
|
|
|
|
|
|
* 要求配对的两行目标分录一致。</p> |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
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() { |
|
|
|
private RuleDiffUtil() { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@ -95,12 +121,14 @@ public final class RuleDiffUtil { |
|
|
|
* @param toSnapshot 目标版本快照。 |
|
|
|
* @param toSnapshot 目标版本快照。 |
|
|
|
* @param entrySections 需要对比的子表分区名,按顺序输出。 |
|
|
|
* @param entrySections 需要对比的子表分区名,按顺序输出。 |
|
|
|
* @param entryKeyFields 各子表分区对应的业务主键字段名。 |
|
|
|
* @param entryKeyFields 各子表分区对应的业务主键字段名。 |
|
|
|
|
|
|
|
* @param entryFallbackFields 各子表分区对应的兜底配对字段名(业务主键字段被改动时用)。 |
|
|
|
* @return 差异结果(不含版本号等元信息,由调用方填充)。 |
|
|
|
* @return 差异结果(不含版本号等元信息,由调用方填充)。 |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public static RuleVersionDiffDto diff(Map<String, Object> fromSnapshot, |
|
|
|
public static RuleVersionDiffDto diff(Map<String, Object> fromSnapshot, |
|
|
|
Map<String, Object> toSnapshot, |
|
|
|
Map<String, Object> toSnapshot, |
|
|
|
List<String> entrySections, |
|
|
|
List<String> entrySections, |
|
|
|
Map<String, String[]> entryKeyFields) { |
|
|
|
Map<String, String[]> entryKeyFields, |
|
|
|
|
|
|
|
Map<String, String[]> entryFallbackFields) { |
|
|
|
RuleVersionDiffDto result = new RuleVersionDiffDto(); |
|
|
|
RuleVersionDiffDto result = new RuleVersionDiffDto(); |
|
|
|
List<FieldDiff> ruleDiff = diffFields(asMap(fromSnapshot.get(RuleSnapshotUtil.SECTION_RULE)), |
|
|
|
List<FieldDiff> ruleDiff = diffFields(asMap(fromSnapshot.get(RuleSnapshotUtil.SECTION_RULE)), |
|
|
|
asMap(toSnapshot.get(RuleSnapshotUtil.SECTION_RULE)), RULE_IGNORE_FIELDS); |
|
|
|
asMap(toSnapshot.get(RuleSnapshotUtil.SECTION_RULE)), RULE_IGNORE_FIELDS); |
|
|
|
@ -112,9 +140,11 @@ public final class RuleDiffUtil { |
|
|
|
if (entrySections != null) { |
|
|
|
if (entrySections != null) { |
|
|
|
for (String section : entrySections) { |
|
|
|
for (String section : entrySections) { |
|
|
|
String[] keyFields = entryKeyFields == null ? null : entryKeyFields.get(section); |
|
|
|
String[] keyFields = entryKeyFields == null ? null : entryKeyFields.get(section); |
|
|
|
|
|
|
|
String[] fallbackFields = entryFallbackFields == null ? null : entryFallbackFields.get(section); |
|
|
|
EntryDiff entryDiff = diffEntries(asMapList(fromSnapshot.get(section)), |
|
|
|
EntryDiff entryDiff = diffEntries(asMapList(fromSnapshot.get(section)), |
|
|
|
asMapList(toSnapshot.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.getEntries().put(section, entryDiff); |
|
|
|
result.setAddedCount(result.getAddedCount() + entryDiff.getAdded().size()); |
|
|
|
result.setAddedCount(result.getAddedCount() + entryDiff.getAdded().size()); |
|
|
|
result.setRemovedCount(result.getRemovedCount() + entryDiff.getRemoved().size()); |
|
|
|
result.setRemovedCount(result.getRemovedCount() + entryDiff.getRemoved().size()); |
|
|
|
@ -165,21 +195,32 @@ public final class RuleDiffUtil { |
|
|
|
/** |
|
|
|
/** |
|
|
|
* 按业务主键比对两组分录行。 |
|
|
|
* 按业务主键比对两组分录行。 |
|
|
|
* |
|
|
|
* |
|
|
|
* <p>业务主键相同的行按出现顺序两两配对,配上的比字段(有差异即「修改」); |
|
|
|
* <p>分两轮配对:</p> |
|
|
|
* 多出来的分别记「新增」或「删除」。这样即使分录主键每版都变,也能识别出「同一行被改了」。</p> |
|
|
|
* <ol> |
|
|
|
|
|
|
|
* <li>第一轮按完整业务主键精确配对,配上的比字段,有差异即「修改」。</li> |
|
|
|
|
|
|
|
* <li>第二轮处理剩下的行:业务主键字段本身被用户改动时(例如字段映射改了源字段), |
|
|
|
|
|
|
|
* 第一轮必然配对不上,会直接落到「删除 + 新增」。因此按 {@code fallbackFields} |
|
|
|
|
|
|
|
* (例如目标字段)再配一次,配上的一律记「修改」。</li> |
|
|
|
|
|
|
|
* </ol> |
|
|
|
|
|
|
|
* <p>两轮都配不上的,才分别记「新增」或「删除」。</p> |
|
|
|
* |
|
|
|
* |
|
|
|
* @param oldEntries 基准版本的分录行。 |
|
|
|
* @param oldEntries 基准版本的分录行。 |
|
|
|
* @param newEntries 目标版本的分录行。 |
|
|
|
* @param newEntries 目标版本的分录行。 |
|
|
|
* @param keyFields 业务主键字段名,为空时所有行归为一组按顺序配对。 |
|
|
|
* @param keyFields 业务主键字段名,为空时所有行归为一组按顺序配对。 |
|
|
|
|
|
|
|
* @param fallbackFields 业务主键改动的兜底配对字段,为空时不做第二轮配对。 |
|
|
|
* @return 分录差异。 |
|
|
|
* @return 分录差异。 |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
public static EntryDiff diffEntries(List<Map<String, Object>> oldEntries, |
|
|
|
public static EntryDiff diffEntries(List<Map<String, Object>> oldEntries, |
|
|
|
List<Map<String, Object>> newEntries, |
|
|
|
List<Map<String, Object>> newEntries, |
|
|
|
String[] keyFields) { |
|
|
|
String[] keyFields, |
|
|
|
|
|
|
|
String[] fallbackFields) { |
|
|
|
EntryDiff entryDiff = new EntryDiff(); |
|
|
|
EntryDiff entryDiff = new EntryDiff(); |
|
|
|
Map<String, Deque<Map<String, Object>>> oldGroups = groupByKey(oldEntries, keyFields); |
|
|
|
Map<String, Deque<Map<String, Object>>> oldGroups = groupByKey(oldEntries, keyFields); |
|
|
|
Map<String, Deque<Map<String, Object>>> newGroups = groupByKey(newEntries, keyFields); |
|
|
|
Map<String, Deque<Map<String, Object>>> newGroups = groupByKey(newEntries, keyFields); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 第一轮:精确配对
|
|
|
|
|
|
|
|
List<Map<String, Object>> restOld = new ArrayList<>(); |
|
|
|
|
|
|
|
List<Map<String, Object>> restNew = new ArrayList<>(); |
|
|
|
Set<String> keys = new LinkedHashSet<>(); |
|
|
|
Set<String> keys = new LinkedHashSet<>(); |
|
|
|
keys.addAll(oldGroups.keySet()); |
|
|
|
keys.addAll(oldGroups.keySet()); |
|
|
|
keys.addAll(newGroups.keySet()); |
|
|
|
keys.addAll(newGroups.keySet()); |
|
|
|
@ -187,10 +228,131 @@ public final class RuleDiffUtil { |
|
|
|
Deque<Map<String, Object>> oldQueue = oldGroups.getOrDefault(key, new ArrayDeque<>()); |
|
|
|
Deque<Map<String, Object>> oldQueue = oldGroups.getOrDefault(key, new ArrayDeque<>()); |
|
|
|
Deque<Map<String, Object>> newQueue = newGroups.getOrDefault(key, new ArrayDeque<>()); |
|
|
|
Deque<Map<String, Object>> newQueue = newGroups.getOrDefault(key, new ArrayDeque<>()); |
|
|
|
while (!oldQueue.isEmpty() && !newQueue.isEmpty()) { |
|
|
|
while (!oldQueue.isEmpty() && !newQueue.isEmpty()) { |
|
|
|
|
|
|
|
addModifiedIfChanged(entryDiff, key, oldQueue.poll(), newQueue.poll()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
restOld.addAll(oldQueue); |
|
|
|
|
|
|
|
restNew.addAll(newQueue); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 第二轮:业务主键字段被改动的兜底配对
|
|
|
|
|
|
|
|
if (fallbackFields != null && fallbackFields.length > 0) { |
|
|
|
|
|
|
|
restNew = pairByFallback(entryDiff, restOld, restNew, fallbackFields); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
entryDiff.getRemoved().addAll(restOld); |
|
|
|
|
|
|
|
entryDiff.getAdded().addAll(restNew); |
|
|
|
|
|
|
|
return entryDiff; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 第二轮配对:按兜底字段把剩余的旧行与新行配上,配上的记「修改」。 |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <p>两道约束,宁可把一次改动漏判成「删除 + 新增」,也不要把「真的新增一条」和 |
|
|
|
|
|
|
|
* 「真的删除一条」硬凑成一条修改:</p> |
|
|
|
|
|
|
|
* <ol> |
|
|
|
|
|
|
|
* <li>同一兜底 key 下旧行与新行数量必须相等;</li> |
|
|
|
|
|
|
|
* <li>配对成功后同一兜底 key 下的行数不能由多变少或由少变多之外的其它结构变化—— |
|
|
|
|
|
|
|
* 即要求配上的这一对,除业务主键字段外**至少还有一处相同、且不只是兜底字段相同**, |
|
|
|
|
|
|
|
* 保证确实是「同一行只改了业务主键」。</li> |
|
|
|
|
|
|
|
* </ol> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param entryDiff 分录差异(累积修改项)。 |
|
|
|
|
|
|
|
* @param restOld 第一轮剩下的旧行。 |
|
|
|
|
|
|
|
* @param restNew 第一轮剩下的新行。 |
|
|
|
|
|
|
|
* @param fallbackFields 兜底配对字段。 |
|
|
|
|
|
|
|
* @return 仍未配上对的新行剩余项(旧行剩余项由调用方统一记为删除)。 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private static List<Map<String, Object>> pairByFallback(EntryDiff entryDiff, |
|
|
|
|
|
|
|
List<Map<String, Object>> restOld, |
|
|
|
|
|
|
|
List<Map<String, Object>> restNew, |
|
|
|
|
|
|
|
String[] fallbackFields) { |
|
|
|
|
|
|
|
Map<String, Deque<Map<String, Object>>> oldGroups = groupByKey(restOld, fallbackFields); |
|
|
|
|
|
|
|
Map<String, Deque<Map<String, Object>>> newGroups = groupByKey(restNew, fallbackFields); |
|
|
|
|
|
|
|
Set<String> allKeys = new LinkedHashSet<>(oldGroups.keySet()); |
|
|
|
|
|
|
|
allKeys.addAll(newGroups.keySet()); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (String key : allKeys) { |
|
|
|
|
|
|
|
Deque<Map<String, Object>> oldQueue = oldGroups.get(key); |
|
|
|
|
|
|
|
Deque<Map<String, Object>> newQueue = newGroups.get(key); |
|
|
|
|
|
|
|
if (oldQueue == null || newQueue == null || oldQueue.size() != newQueue.size()) { |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
while (!oldQueue.isEmpty()) { |
|
|
|
Map<String, Object> oldEntry = oldQueue.poll(); |
|
|
|
Map<String, Object> oldEntry = oldQueue.poll(); |
|
|
|
Map<String, Object> newEntry = newQueue.poll(); |
|
|
|
Map<String, Object> newEntry = newQueue.poll(); |
|
|
|
|
|
|
|
// 兜底字段必须都非空且相等,才认作同一行
|
|
|
|
|
|
|
|
if (!shareIdentity(oldEntry, newEntry, fallbackFields)) { |
|
|
|
|
|
|
|
continue; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
addModifiedIfChanged(entryDiff, key, oldEntry, newEntry); |
|
|
|
|
|
|
|
restOld.remove(oldEntry); |
|
|
|
|
|
|
|
restNew.remove(newEntry); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return restNew; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 判断两行是否可以认作「同一行只改了被兜底的那部分业务主键」。 |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <p>唯一的判据是:两行在 {@code fallbackFields}(兜底字段)上**都非空且相等**。 |
|
|
|
|
|
|
|
* 为什么不再检查业务主键里剩下的字段:剩下的字段正是「可能被用户改掉」的那部分 |
|
|
|
|
|
|
|
* (例如字段映射的源字段),要求它相同等于把兜底配对又退化成精确配对,兜底就失去意义。</p> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* <p>「真新增 + 真删除」被误判成修改的风险,由调用方的两道约束控制: |
|
|
|
|
|
|
|
* 同一兜底 key 下旧行与新行数量必须相等;且兜底字段不能是容易为空、导致大范围匹配的字段。</p> |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param oldEntry 旧行。 |
|
|
|
|
|
|
|
* @param newEntry 新行。 |
|
|
|
|
|
|
|
* @param fallbackFields 兜底字段(配对依据)。 |
|
|
|
|
|
|
|
* @return 认作同一行返回 true。 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private static boolean shareIdentity(Map<String, Object> oldEntry, |
|
|
|
|
|
|
|
Map<String, Object> newEntry, |
|
|
|
|
|
|
|
String[] fallbackFields) { |
|
|
|
|
|
|
|
return equalAndNonNull(oldEntry, newEntry, fallbackFields); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 判断两行在给定字段上是否都非空且相等。 |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param oldEntry 旧行。 |
|
|
|
|
|
|
|
* @param newEntry 新行。 |
|
|
|
|
|
|
|
* @param fields 字段名。 |
|
|
|
|
|
|
|
* @return 全部字段都非空且相等返回 true。 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private static boolean equalAndNonNull(Map<String, Object> oldEntry, |
|
|
|
|
|
|
|
Map<String, Object> newEntry, |
|
|
|
|
|
|
|
String[] fields) { |
|
|
|
|
|
|
|
if (fields == null || fields.length == 0) { |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
for (String field : fields) { |
|
|
|
|
|
|
|
Object oldValue = oldEntry.get(field); |
|
|
|
|
|
|
|
Object newValue = newEntry.get(field); |
|
|
|
|
|
|
|
if (oldValue == null || newValue == null || !valueEquals(oldValue, newValue)) { |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* 比对一对已配对的分录行,字段有差异则记一条「修改」。 |
|
|
|
|
|
|
|
* |
|
|
|
|
|
|
|
* @param entryDiff 分录差异。 |
|
|
|
|
|
|
|
* @param key 业务主键。 |
|
|
|
|
|
|
|
* @param oldEntry 基准版本的行。 |
|
|
|
|
|
|
|
* @param newEntry 目标版本的行。 |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
private static void addModifiedIfChanged(EntryDiff entryDiff, |
|
|
|
|
|
|
|
String key, |
|
|
|
|
|
|
|
Map<String, Object> oldEntry, |
|
|
|
|
|
|
|
Map<String, Object> newEntry) { |
|
|
|
List<FieldDiff> changedFields = diffFields(oldEntry, newEntry, ENTRY_IGNORE_FIELDS); |
|
|
|
List<FieldDiff> changedFields = diffFields(oldEntry, newEntry, ENTRY_IGNORE_FIELDS); |
|
|
|
if (!changedFields.isEmpty()) { |
|
|
|
if (changedFields.isEmpty()) { |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
EntryChange change = new EntryChange(); |
|
|
|
EntryChange change = new EntryChange(); |
|
|
|
change.setKey(key); |
|
|
|
change.setKey(key); |
|
|
|
change.setChangedFields(changedFields); |
|
|
|
change.setChangedFields(changedFields); |
|
|
|
@ -198,16 +360,6 @@ public final class RuleDiffUtil { |
|
|
|
change.setNewEntry(newEntry); |
|
|
|
change.setNewEntry(newEntry); |
|
|
|
entryDiff.getModified().add(change); |
|
|
|
entryDiff.getModified().add(change); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
while (!oldQueue.isEmpty()) { |
|
|
|
|
|
|
|
entryDiff.getRemoved().add(oldQueue.poll()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
while (!newQueue.isEmpty()) { |
|
|
|
|
|
|
|
entryDiff.getAdded().add(newQueue.poll()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return entryDiff; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* 按业务主键把分录行分组,组内保持原始顺序。 |
|
|
|
* 按业务主键把分录行分组,组内保持原始顺序。 |
|
|
|
|