|
|
@ -5,8 +5,10 @@ import com.zdxt.enforcementcode.businessdatasyn.config.SyncLogicalDeleteConfig; |
|
|
import com.zdxt.enforcementcode.businessdatasyn.config.SyncTableConfig; |
|
|
import com.zdxt.enforcementcode.businessdatasyn.config.SyncTableConfig; |
|
|
import com.zdxt.enforcementcode.businessdatasyn.config.SyncTableSideConfig; |
|
|
import com.zdxt.enforcementcode.businessdatasyn.config.SyncTableSideConfig; |
|
|
import com.zdxt.enforcementcode.businessdatasyn.domain.dto.RowSnapshot; |
|
|
import com.zdxt.enforcementcode.businessdatasyn.domain.dto.RowSnapshot; |
|
|
|
|
|
import com.zdxt.enforcementcode.businessdatasyn.exception.UniqueKeyConflictException; |
|
|
import lombok.RequiredArgsConstructor; |
|
|
import lombok.RequiredArgsConstructor; |
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
|
|
import org.springframework.dao.DuplicateKeyException; |
|
|
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; |
|
|
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; |
|
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; |
|
|
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; |
|
|
import org.springframework.stereotype.Service; |
|
|
import org.springframework.stereotype.Service; |
|
|
@ -67,31 +69,71 @@ public class V1SinkWriter implements SinkWriter { |
|
|
throw new IllegalStateException("Upsert 没有有效列: " + side.getTable()); |
|
|
throw new IllegalStateException("Upsert 没有有效列: " + side.getTable()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// INSERT INTO table (col1, col2, ...) VALUES (:col1, :col2, ...)
|
|
|
String pkCol = side.getPrimaryKey().get(0); |
|
|
// ON DUPLICATE KEY UPDATE col1 = VALUES(col1), col2 = VALUES(col2), ...
|
|
|
Object pkVal = columns.get(pkCol); |
|
|
StringJoiner colJoiner = new StringJoiner(", "); |
|
|
|
|
|
StringJoiner valJoiner = new StringJoiner(", "); |
|
|
|
|
|
StringJoiner updateJoiner = new StringJoiner(", "); |
|
|
|
|
|
|
|
|
|
|
|
MapSqlParameterSource params = new MapSqlParameterSource(); |
|
|
MapSqlParameterSource params = new MapSqlParameterSource(); |
|
|
|
|
|
allCols.forEach(col -> params.addValue(col, columns.get(col))); |
|
|
for (String col : allCols) { |
|
|
params.addValue("__pk", pkVal); |
|
|
colJoiner.add(col); |
|
|
|
|
|
valJoiner.add(":" + col); |
|
|
// 严禁使用 ON DUPLICATE KEY UPDATE:
|
|
|
// 主键不参与 UPDATE 部分
|
|
|
// 若表上存在除主键外的唯一索引(如 enterprise_information.unique(enterprise_number)),
|
|
|
if (!side.getPrimaryKey().contains(col)) { |
|
|
// 冲突时 MySQL 会把"另一行"整行改写,造成静默数据覆盖且无法从同步日志回溯。
|
|
|
updateJoiner.add(col + " = VALUES(" + col + ")"); |
|
|
// 改为按主键判存在 → UPDATE ... WHERE pk / INSERT 分离,唯一索引冲突时直接抛 DuplicateKeyException,
|
|
|
|
|
|
// 由 TableSyncExecutor 记入 biz_sync_log 并最终置 DEAD,交人工裁决。
|
|
|
|
|
|
Integer exists = namedJdbcTemplate.queryForObject( |
|
|
|
|
|
"SELECT COUNT(1) FROM " + side.getTable() + " WHERE " + pkCol + " = :__pk", |
|
|
|
|
|
new MapSqlParameterSource("__pk", pkVal), Integer.class); |
|
|
|
|
|
|
|
|
|
|
|
if (exists != null && exists > 0) { |
|
|
|
|
|
StringJoiner setJoiner = new StringJoiner(", "); |
|
|
|
|
|
for (String col : allCols) { |
|
|
|
|
|
if (!side.getPrimaryKey().contains(col)) { |
|
|
|
|
|
setJoiner.add(col + " = :" + col); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
if (setJoiner.length() == 0) { |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
String sql = "UPDATE " + side.getTable() + " SET " + setJoiner + " WHERE " + pkCol + " = :__pk"; |
|
|
|
|
|
log.debug("[BizSync] V1 update SQL: {}", sql); |
|
|
|
|
|
namedJdbcTemplate.update(sql, params); |
|
|
|
|
|
} else { |
|
|
|
|
|
StringJoiner colJoiner = new StringJoiner(", "); |
|
|
|
|
|
StringJoiner valJoiner = new StringJoiner(", "); |
|
|
|
|
|
for (String col : allCols) { |
|
|
|
|
|
colJoiner.add(col); |
|
|
|
|
|
valJoiner.add(":" + col); |
|
|
|
|
|
} |
|
|
|
|
|
String sql = "INSERT INTO " + side.getTable() + " (" + colJoiner + ") VALUES (" + valJoiner + ")"; |
|
|
|
|
|
log.debug("[BizSync] V1 insert SQL: {}", sql); |
|
|
|
|
|
try { |
|
|
|
|
|
namedJdbcTemplate.update(sql, params); |
|
|
|
|
|
} catch (DuplicateKeyException e) { |
|
|
|
|
|
handleDuplicateKey(side, pkCol, pkVal, e); |
|
|
} |
|
|
} |
|
|
params.addValue(col, columns.get(col)); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
String sql = "INSERT INTO " + side.getTable() + " (" + colJoiner + ") VALUES (" + valJoiner + ")"; |
|
|
/** |
|
|
if (updateJoiner.length() > 0) { |
|
|
* 区分主键重复(幂等)与唯一键冲突(业务键撞车) |
|
|
sql += " ON DUPLICATE KEY UPDATE " + updateJoiner; |
|
|
* <p>主键重复:说明行已存在,视为幂等成功,忽略; |
|
|
|
|
|
* 唯一键冲突:说明撞到了表上其他唯一索引(如 enterprise_information.unique(enterprise_number)), |
|
|
|
|
|
* 抛 UniqueKeyConflictException 交由 Executor 层记入 biz_sync_log 并跳过,不重试。</p> |
|
|
|
|
|
*/ |
|
|
|
|
|
private void handleDuplicateKey(SyncTableSideConfig side, String pkCol, Object pkVal, DuplicateKeyException e) { |
|
|
|
|
|
Integer recheck = namedJdbcTemplate.queryForObject( |
|
|
|
|
|
"SELECT COUNT(1) FROM " + side.getTable() + " WHERE " + pkCol + " = :__pk", |
|
|
|
|
|
new MapSqlParameterSource("__pk", pkVal), Integer.class); |
|
|
|
|
|
if (recheck != null && recheck > 0) { |
|
|
|
|
|
log.debug("[BizSync] V1 INSERT 命中幂等重复(主键已存在),忽略: table={}, pk={}", |
|
|
|
|
|
side.getTable(), pkVal); |
|
|
|
|
|
return; |
|
|
} |
|
|
} |
|
|
|
|
|
String msg = String.format("唯一键冲突: table=%s, pk=%s, 原因=%s", |
|
|
log.debug("[BizSync] V1 upsert SQL: {}", sql); |
|
|
side.getTable(), pkVal, e.getMessage()); |
|
|
namedJdbcTemplate.update(sql, params); |
|
|
log.warn("[BizSync] V1 {}", msg); |
|
|
|
|
|
throw new UniqueKeyConflictException(side.getTable(), String.valueOf(pkVal), msg, e); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
private void doDelete(SyncTableSideConfig side, String pkValue) { |
|
|
private void doDelete(SyncTableSideConfig side, String pkValue) { |
|
|
|