@ -71,6 +71,32 @@ public final class RuleDiffUtil {
@@ -71,6 +71,32 @@ public final class RuleDiffUtil {
* /
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 ( ) {
}
@ -95,12 +121,14 @@ public final class RuleDiffUtil {
@@ -95,12 +121,14 @@ public final class RuleDiffUtil {
* @param toSnapshot 目标版本快照 。
* @param entrySections 需要对比的子表分区名 , 按顺序输出 。
* @param entryKeyFields 各子表分区对应的业务主键字段名 。
* @param entryFallbackFields 各子表分区对应的兜底配对字段名 ( 业务主键字段被改动时用 ) 。
* @return 差异结果 ( 不含版本号等元信息 , 由调用方填充 ) 。
* /
public static RuleVersionDiffDto diff ( Map < String , Object > fromSnapshot ,
Map < String , Object > toSnapshot ,
List < String > entrySections ,
Map < String , String [ ] > entryKeyFields ) {
Map < String , String [ ] > entryKeyFields ,
Map < String , String [ ] > entryFallbackFields ) {
RuleVersionDiffDto result = new RuleVersionDiffDto ( ) ;
List < FieldDiff > 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 {
@@ -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 {
@@ -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 newEntries 目标版本的分录行 。
* @param keyFields 业务主键字段名 , 为空时所有行归为一组按顺序配对 。
* @param fallbackFields 业务主键改动的兜底配对字段 , 为空时不做第二轮配对 。
* @return 分录差异 。
* /
public static EntryDiff diffEntries ( List < Map < String , Object > > oldEntries ,
List < Map < String , Object > > newEntries ,
String [ ] keyFields ) {
String [ ] keyFields ,
String [ ] fallbackFields ) {
EntryDiff entryDiff = new EntryDiff ( ) ;
Map < String , Deque < Map < String , Object > > > oldGroups = groupByKey ( oldEntries , 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 < > ( ) ;
keys . addAll ( oldGroups . keySet ( ) ) ;
keys . addAll ( newGroups . keySet ( ) ) ;
@ -187,10 +228,131 @@ public final class RuleDiffUtil {
@@ -187,10 +228,131 @@ public final class RuleDiffUtil {
Deque < Map < String , Object > > oldQueue = oldGroups . getOrDefault ( key , new ArrayDeque < > ( ) ) ;
Deque < Map < String , Object > > newQueue = newGroups . getOrDefault ( key , new ArrayDeque < > ( ) ) ;
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 > 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 ) ;
if ( ! changedFields . isEmpty ( ) ) {
if ( changedFields . isEmpty ( ) ) {
return ;
}
EntryChange change = new EntryChange ( ) ;
change . setKey ( key ) ;
change . setChangedFields ( changedFields ) ;
@ -198,16 +360,6 @@ public final class RuleDiffUtil {
@@ -198,16 +360,6 @@ public final class RuleDiffUtil {
change . setNewEntry ( newEntry ) ;
entryDiff . getModified ( ) . add ( change ) ;
}
}
while ( ! oldQueue . isEmpty ( ) ) {
entryDiff . getRemoved ( ) . add ( oldQueue . poll ( ) ) ;
}
while ( ! newQueue . isEmpty ( ) ) {
entryDiff . getAdded ( ) . add ( newQueue . poll ( ) ) ;
}
}
return entryDiff ;
}
/ * *
* 按业务主键把分录行分组 , 组内保持原始顺序 。