From 1c48cc51f89edfb6568c088ed1733355cada6cb0 Mon Sep 17 00:00:00 2001 From: zhuibobo Date: Sun, 26 Jul 2026 22:32:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E6=95=B0=E6=8D=AE=E5=90=8C?= =?UTF-8?q?=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../002-二期数据库更新.sql | 3 + .../executor/TableSyncExecutor.java | 14 +++- .../resolver/RowMergeResolver.java | 64 +++++++++---------- .../businessdatasyn/writer/V2SinkWriter.java | 12 +++- 4 files changed, 55 insertions(+), 38 deletions(-) diff --git a/script/sql/businessdatasyn/002-二期数据库更新.sql b/script/sql/businessdatasyn/002-二期数据库更新.sql index f2ebcfaa..89569644 100644 --- a/script/sql/businessdatasyn/002-二期数据库更新.sql +++ b/script/sql/businessdatasyn/002-二期数据库更新.sql @@ -174,6 +174,9 @@ truncate table qr_code_generate_history; truncate table enterprise_pub_qrcode; -- 自动设置更新时间 +ALTER TABLE enforcement_registration + MODIFY COLUMN update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; + ALTER TABLE enterprise_law_enforcement MODIFY COLUMN update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/executor/TableSyncExecutor.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/executor/TableSyncExecutor.java index 92091c8b..90c9ac17 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/executor/TableSyncExecutor.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/executor/TableSyncExecutor.java @@ -234,8 +234,18 @@ public class TableSyncExecutor { record.setStatus("NORMAL"); } record.setOwnerSide(owner.getCode()); - record.setContentHashV1(v1Row != null ? v1Row.getHash() : null); - record.setContentHashV2(v2Row != null ? v2Row.getHash() : null); + // 基线哈希必须对齐到"写入后"的收敛状态,否则下一轮会把本次写入误判为对侧变更(伪冲突→反向覆盖) + String hashV1 = v1Row != null ? v1Row.getHash() : null; + String hashV2 = v2Row != null ? v2Row.getHash() : null; + switch (decision.getAction()) { + // 写入V2后,V2内容 = V1内容,两侧基线都取V1哈希 + case WRITE_V2, CONFLICT_V1_WIN -> hashV2 = hashV1; + // 写入V1后,V1内容 = V2内容,两侧基线都取V2哈希 + case WRITE_V1, CONFLICT_V2_WIN -> hashV1 = hashV2; + default -> { /* 删除/恢复等场景保持各自哈希 */ } + } + record.setContentHashV1(hashV1); + record.setContentHashV2(hashV2); record.setSnapshotV1Json(toJson(v1Row)); record.setSnapshotV2Json(toJson(v2Row)); record.setLastSyncTime(LocalDateTime.now()); diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/resolver/RowMergeResolver.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/resolver/RowMergeResolver.java index 671aa7b6..cf3f2d9a 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/resolver/RowMergeResolver.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/resolver/RowMergeResolver.java @@ -56,6 +56,15 @@ public class RowMergeResolver { // 场景 7/8:一侧删除 if (v1Row == null || v1Deleted) { if (v2Exists) { + // 影子表无历史记录 → V2 新增行(首次进入同步系统),不是 V1 侧删除 + if (lastHashV1 == null && lastHashV2 == null && v1Row == null) { + return SyncDecision.builder() + .action(SyncActionEnum.WRITE_V1) + .ownerSide(ownerSide) + .reason("V2新增数据(影子表无记录),首次同步到V1") + .targetV1Snapshot(v2Row) + .build(); + } if (ownerSide == OwnerSide.V1) { // 场景 7:归属方(V1)删除 → 删除对侧(V2) if (!deleteAllowed(config)) { @@ -89,6 +98,15 @@ public class RowMergeResolver { if (v2Row == null || v2Deleted) { if (v1Exists) { + // 影子表无历史记录 → V1 新增行(首次进入同步系统),不是 V2 侧删除 + if (lastHashV1 == null && lastHashV2 == null && v2Row == null) { + return SyncDecision.builder() + .action(SyncActionEnum.WRITE_V2) + .ownerSide(ownerSide) + .reason("V1新增数据(影子表无记录),首次同步到V2") + .targetV2Snapshot(v1Row) + .build(); + } if (ownerSide == OwnerSide.V2) { // 场景 8:归属方(V2)删除 → 删除对侧(V1) if (!deleteAllowed(config)) { @@ -151,41 +169,21 @@ public class RowMergeResolver { .build(); } - // 两侧同时变更 → 冲突,按归属方决定 + // 两侧同时变更 → 冲突,归属方数据胜出,单向覆盖对侧 if (ownerSide == OwnerSide.V1) { - if (strictOwner) { - // 严格模式:V1 覆盖 V2,并记录冲突 - return SyncDecision.builder() - .action(SyncActionEnum.CONFLICT_V1_WIN) - .ownerSide(ownerSide) - .reason("两侧同时变更,严格模式V1归属方胜出,覆盖V2") - .targetV2Snapshot(v1Row) - .build(); - } else { - // 非严格模式:V2 改动保留,把 V2 新内容写回 V1(视为补充) - return SyncDecision.builder() - .action(SyncActionEnum.WRITE_V1) - .ownerSide(ownerSide) - .reason("两侧同时变更,非严格模式,V2变更视为补充写回V1") - .targetV1Snapshot(v2Row) - .build(); - } + return SyncDecision.builder() + .action(SyncActionEnum.CONFLICT_V1_WIN) + .ownerSide(ownerSide) + .reason("两侧同时变更,归属方V1胜出,覆盖V2") + .targetV2Snapshot(v1Row) + .build(); } else { - if (strictOwner) { - return SyncDecision.builder() - .action(SyncActionEnum.CONFLICT_V2_WIN) - .ownerSide(ownerSide) - .reason("两侧同时变更,严格模式V2归属方胜出,覆盖V1") - .targetV1Snapshot(v2Row) - .build(); - } else { - return SyncDecision.builder() - .action(SyncActionEnum.WRITE_V2) - .ownerSide(ownerSide) - .reason("两侧同时变更,非严格模式,V1变更视为补充写回V2") - .targetV2Snapshot(v1Row) - .build(); - } + return SyncDecision.builder() + .action(SyncActionEnum.CONFLICT_V2_WIN) + .ownerSide(ownerSide) + .reason("两侧同时变更,归属方V2胜出,覆盖V1") + .targetV1Snapshot(v2Row) + .build(); } } } diff --git a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/writer/V2SinkWriter.java b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/writer/V2SinkWriter.java index 12ea06a5..fa74f565 100644 --- a/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/writer/V2SinkWriter.java +++ b/zdxt-modules/zdxt-enforcement-code/src/main/java/com/zdxt/enforcementcode/businessdatasyn/writer/V2SinkWriter.java @@ -11,8 +11,10 @@ import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Service; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.StringJoiner; /** @@ -31,7 +33,10 @@ public class V2SinkWriter implements SinkWriter { @Override public void upsert(SyncTableConfig config, RowSnapshot snapshot) { SyncTableSideConfig side = config.getV2(); - doUpsert(side, snapshot); + // v2独有列的默认值仅在 INSERT 时生效,UPDATE 时不触碰,防止 V1→V2 回写清空 V2 侧已有值 + Map v2Defaults = config.getV2OnlyColumnsDefaults(); + Set insertOnlyCols = (v2Defaults != null) ? v2Defaults.keySet() : Collections.emptySet(); + doUpsert(side, snapshot, insertOnlyCols); } @DS("master") @@ -54,7 +59,7 @@ public class V2SinkWriter implements SinkWriter { doLogicalDelete(side, ldConfig, pkValue); } - private void doUpsert(SyncTableSideConfig side, RowSnapshot snapshot) { + private void doUpsert(SyncTableSideConfig side, RowSnapshot snapshot, Set insertOnlyCols) { Map columns = snapshot.getColumns(); List allCols = side.getColumns().stream() .filter(col -> columns.containsKey(col) && col.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) @@ -73,7 +78,8 @@ public class V2SinkWriter implements SinkWriter { for (String col : allCols) { colJoiner.add(col); valJoiner.add(":" + col); - if (!side.getPrimaryKey().contains(col)) { + // 主键和 v2 独有默认值列不进入 UPDATE 子句:默认值只用于首次插入,更新时保留 V2 已有值 + if (!side.getPrimaryKey().contains(col) && !insertOnlyCols.contains(col)) { updateJoiner.add(col + " = VALUES(" + col + ")"); } params.addValue(col, columns.get(col));