From 5698a8b72f2c51a5726d83c7b8c5229522b04e23 Mon Sep 17 00:00:00 2001
From: lml <1297555865@qq.com>
Date: Tue, 20 Oct 2026 13:55:09 +0800
Subject: [PATCH] =?UTF-8?q?feat(botp):=20=E8=BD=AC=E6=8D=A2/=E5=8F=8D?=
=?UTF-8?q?=E5=86=99=E8=A7=84=E5=88=99=E7=89=88=E6=9C=AC=E5=8F=B7=E9=80=92?=
=?UTF-8?q?=E5=A2=9E=E4=B8=8E=E5=85=A8=E9=87=8F=E5=BF=AB=E7=85=A7=EF=BC=8C?=
=?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=89=88=E6=9C=AC=E5=8E=86=E5=8F=B2=E4=B8=8E?=
=?UTF-8?q?=E5=AF=B9=E6=AF=94=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 保存即版本 +1 并落全量快照:新增 convert_rule_ver / reverse_writing_rules_ver,
快照以 JSON 内联存储规则主表 + 全部子表分录(字段映射、插件分录 / 反写公式分录、业务规则分录)
- 版本号唯一且单调:两张快照表建唯一索引 (rule_id, rule_version);实体 ruleVersion 加
@Version 并注册 OptimisticLockerInnerInterceptor,用「页面打开时的版本号」做 CAS,
影响行数为 0 即提示「已被他人修改,请刷新后重试」,避免并发保存互相覆盖或写坏版本号
- 新增版本历史接口:getVersionList(版本号/创建时间/创建人,不返回 JSON 正文)、
getVersionSnapshot(只读查看指定版本全量快照)、compareVersion(与当前版本或指定版本对比)
- 版本对比只产出新增/删除/修改三类结论,不做逐字 diff;规则主表按字段比对并忽略审计字段,
子表分录按业务主键配对(分录为「先删后插」,主键每版都变,按主键配会把所有行显示成删改)
- 快照 JSON 分区名对齐任务分派文档附录 A.2:fieldMappings / pluginEntries / formulas / businesses
- 反写规则保存逻辑从 ReverseWritingRulesController 下沉到 Service.saveAll,
使版本号递增与快照写入同主表、子表处于同一事务
- 新增幂等 DDL 脚本 botp-rule-version-script.sql:缺列时补 rule_version(information_schema 守卫)、
历史空版本号兜底为 1、建两张快照表,可重复执行
---
.../common/botp/config/MybatisPlusConfig.java | 27 ++
.../botp/controller/ConvertRuleController.java | 21 ++
.../controller/ReverseWritingRulesController.java | 60 ++--
.../common/botp/dao/ConvertRuleVerMapper.java | 11 +
.../botp/dao/ReverseWritingRulesVerMapper.java | 11 +
.../apelet/common/botp/dto/RuleVersionDiffDto.java | 147 +++++++++
.../java/apelet/common/botp/model/ConvertRule.java | 1 +
.../apelet/common/botp/model/ConvertRuleVer.java | 54 ++++
.../common/botp/model/ReverseWritingRules.java | 2 +
.../common/botp/model/ReverseWritingRulesVer.java | 54 ++++
.../common/botp/service/IConvertRuleService.java | 28 ++
.../botp/service/IReverseWritingRulesService.java | 40 ++-
.../botp/service/impl/ConvertRuleServiceImpl.java | 171 ++++++++++-
.../impl/ReverseWritingRulesServiceImpl.java | 212 +++++++++++++
.../java/apelet/common/botp/util/RuleDiffUtil.java | 329 +++++++++++++++++++++
.../apelet/common/botp/util/RuleSnapshotUtil.java | 160 ++++++++++
.../apelet/common/botp/util/RuleVersionUtil.java | 50 ++++
.../db-scripts/botp-rule-version-script.sql | 75 +++++
18 files changed, 1412 insertions(+), 41 deletions(-)
create mode 100644 common/common-botp/src/main/java/apelet/common/botp/config/MybatisPlusConfig.java
create mode 100644 common/common-botp/src/main/java/apelet/common/botp/dao/ConvertRuleVerMapper.java
create mode 100644 common/common-botp/src/main/java/apelet/common/botp/dao/ReverseWritingRulesVerMapper.java
create mode 100644 common/common-botp/src/main/java/apelet/common/botp/dto/RuleVersionDiffDto.java
create mode 100644 common/common-botp/src/main/java/apelet/common/botp/model/ConvertRuleVer.java
create mode 100644 common/common-botp/src/main/java/apelet/common/botp/model/ReverseWritingRulesVer.java
create mode 100644 common/common-botp/src/main/java/apelet/common/botp/util/RuleDiffUtil.java
create mode 100644 common/common-botp/src/main/java/apelet/common/botp/util/RuleSnapshotUtil.java
create mode 100644 common/common-botp/src/main/java/apelet/common/botp/util/RuleVersionUtil.java
create mode 100644 zz-resource/db-scripts/botp-rule-version-script.sql
diff --git a/common/common-botp/src/main/java/apelet/common/botp/config/MybatisPlusConfig.java b/common/common-botp/src/main/java/apelet/common/botp/config/MybatisPlusConfig.java
new file mode 100644
index 0000000..17b6ecc
--- /dev/null
+++ b/common/common-botp/src/main/java/apelet/common/botp/config/MybatisPlusConfig.java
@@ -0,0 +1,27 @@
+package apelet.common.botp.config;
+
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * BOTP 模块的 MyBatis-Plus 插件配置。
+ *
+ *
注册乐观锁插件:实体上标注了 {@code @Version} 的字段(转换规则 / 反写规则的 ruleVersion)
+ * 在执行 {@code updateById} 时会自动带上 {@code WHERE version = 旧值} 并写入 {@code version + 1},
+ * 更新影响行数为 0 即代表版本已被别人改过。
+ *
+ * 该插件只对带 {@code @Version} 字段的实体生效,因此不会改变其它模块 / 其它实体的更新行为。
+ *
+ */
+@Configuration
+public class MybatisPlusConfig {
+
+ @Bean
+ public MybatisPlusInterceptor mybatisPlusInterceptor() {
+ MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+ interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
+ return interceptor;
+ }
+}
diff --git a/common/common-botp/src/main/java/apelet/common/botp/controller/ConvertRuleController.java b/common/common-botp/src/main/java/apelet/common/botp/controller/ConvertRuleController.java
index 33db53c..2138c30 100644
--- a/common/common-botp/src/main/java/apelet/common/botp/controller/ConvertRuleController.java
+++ b/common/common-botp/src/main/java/apelet/common/botp/controller/ConvertRuleController.java
@@ -149,6 +149,27 @@ public class ConvertRuleController {
return ResponseResult.success(convertRuleService.getConvertFieldmappingEntry(id));
}
+ @GetMapping(value = "/getVersionList")
+ @Schema(description = "转换规则版本快照列表")
+ public ResponseResult> getVersionList(@RequestParam Long id) {
+ return ResponseResult.success(convertRuleService.getVersionList(id));
+ }
+
+ @GetMapping(value = "/getVersionSnapshot")
+ @Schema(description = "转换规则指定版本的全量快照")
+ public ResponseResult> getVersionSnapshot(@RequestParam Long id,
+ @RequestParam(required = false) Integer ruleVersion) {
+ return ResponseResult.success(convertRuleService.getVersionSnapshot(id, ruleVersion));
+ }
+
+ @GetMapping(value = "/compareVersion")
+ @Schema(description = "转换规则版本对比(新增/删除/修改,不做逐字 diff)")
+ public ResponseResult> compareVersion(@RequestParam Long id,
+ @RequestParam Integer fromVersion,
+ @RequestParam(required = false) Integer toVersion) {
+ return ResponseResult.success(convertRuleService.compareVersion(id, fromVersion, toVersion));
+ }
+
/**
* 根据源单据查询关联关系
*
diff --git a/common/common-botp/src/main/java/apelet/common/botp/controller/ReverseWritingRulesController.java b/common/common-botp/src/main/java/apelet/common/botp/controller/ReverseWritingRulesController.java
index 103c8aa..902ecbd 100644
--- a/common/common-botp/src/main/java/apelet/common/botp/controller/ReverseWritingRulesController.java
+++ b/common/common-botp/src/main/java/apelet/common/botp/controller/ReverseWritingRulesController.java
@@ -8,7 +8,6 @@ import apelet.common.botp.service.IReverseWritingFormulaService;
import apelet.common.botp.service.IReverseWritingRulesService;
import apelet.common.core.annotation.MyRequestBody;
import apelet.common.core.constant.ErrorCodeEnum;
-import apelet.common.core.exception.MyRuntimeException;
import apelet.common.core.object.MyPageData;
import apelet.common.core.object.MyPageParam;
import apelet.common.core.object.ResponseResult;
@@ -88,46 +87,12 @@ public class ReverseWritingRulesController {
@PostMapping(value = "/saveOrUpdate")
@Schema(description = "添加主数据和分录数据")
- @Transactional(rollbackFor = Exception.class)
public ResponseResult> addAll(@RequestBody ReverseWritingRulesDto dto) {
if (dto == null) {
return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED);
}
- ReverseWritingRules reverseWritingRules = new ReverseWritingRules();
- BeanUtils.copyProperties(dto, reverseWritingRules);
- if (reverseWritingRules.getId() != null) {
- // 更新走按主键就地更新,避免删除重建导致创建人等审计字段丢失
- ReverseWritingRules original = rulesService.getById(reverseWritingRules.getId());
- if (original != null) {
- reverseWritingRules.setCreateUserId(original.getCreateUserId());
- reverseWritingRules.setCreateTime(original.getCreateTime());
- }
- reverseWritingRules.setUpdateTime(new Date());
- if (!rulesService.updateById(reverseWritingRules)) {
- throw new MyRuntimeException("反写规则配置数据保存失败!");
- }
- // 分录按规则id全量覆盖
- formulaService.remove(new LambdaQueryWrapper().eq(ReverseWritingFormula::getRulesId, reverseWritingRules.getId()));
- businessService.remove(new LambdaQueryWrapper().eq(ReverseWritingBusiness::getRulesId, reverseWritingRules.getId()));
- } else {
- reverseWritingRules.setCreateTime(new Date());
- boolean save = rulesService.save(reverseWritingRules);
- if (!save) {
- throw new MyRuntimeException("反写规则配置数据保存失败!");
- }
- }
- //反写公式
- List formulaList = dto.getFormulaList();
- if (ObjectUtil.isNotEmpty(formulaList)) {
- formulaList.forEach(f -> f.setRulesId(reverseWritingRules.getId()));
- formulaService.saveBatch(formulaList);
- }
- // 业务规则
- List businessList = dto.getBusinessList();
- if (ObjectUtil.isNotEmpty(businessList)) {
- businessList.forEach(f -> f.setRulesId(reverseWritingRules.getId()));
- businessService.saveBatch(businessList);
- }
+ // 版本号 +1 与全量快照均在 Service 内完成,与主表/子表写入处于同一事务
+ rulesService.saveAll(dto);
return ResponseResult.success("保存成功");
}
@@ -156,4 +121,25 @@ public class ReverseWritingRulesController {
return ResponseResult.success(businessList);
}
+ @GetMapping(value = "/getVersionList")
+ @Schema(description = "反写规则版本快照列表")
+ public ResponseResult> getVersionList(@RequestParam Long id) {
+ return ResponseResult.success(rulesService.getVersionList(id));
+ }
+
+ @GetMapping(value = "/getVersionSnapshot")
+ @Schema(description = "反写规则指定版本的全量快照")
+ public ResponseResult> getVersionSnapshot(@RequestParam Long id,
+ @RequestParam(required = false) Integer ruleVersion) {
+ return ResponseResult.success(rulesService.getVersionSnapshot(id, ruleVersion));
+ }
+
+ @GetMapping(value = "/compareVersion")
+ @Schema(description = "反写规则版本对比(新增/删除/修改,不做逐字 diff)")
+ public ResponseResult> compareVersion(@RequestParam Long id,
+ @RequestParam Integer fromVersion,
+ @RequestParam(required = false) Integer toVersion) {
+ return ResponseResult.success(rulesService.compareVersion(id, fromVersion, toVersion));
+ }
+
}
diff --git a/common/common-botp/src/main/java/apelet/common/botp/dao/ConvertRuleVerMapper.java b/common/common-botp/src/main/java/apelet/common/botp/dao/ConvertRuleVerMapper.java
new file mode 100644
index 0000000..b4be72b
--- /dev/null
+++ b/common/common-botp/src/main/java/apelet/common/botp/dao/ConvertRuleVerMapper.java
@@ -0,0 +1,11 @@
+package apelet.common.botp.dao;
+
+import apelet.common.botp.model.ConvertRuleVer;
+import apelet.common.core.base.dao.BaseDaoMapper;
+
+/**
+ * 转换规则版本快照 Mapper。
+ *
+ */
+public interface ConvertRuleVerMapper extends BaseDaoMapper {
+}
diff --git a/common/common-botp/src/main/java/apelet/common/botp/dao/ReverseWritingRulesVerMapper.java b/common/common-botp/src/main/java/apelet/common/botp/dao/ReverseWritingRulesVerMapper.java
new file mode 100644
index 0000000..876579c
--- /dev/null
+++ b/common/common-botp/src/main/java/apelet/common/botp/dao/ReverseWritingRulesVerMapper.java
@@ -0,0 +1,11 @@
+package apelet.common.botp.dao;
+
+import apelet.common.botp.model.ReverseWritingRulesVer;
+import apelet.common.core.base.dao.BaseDaoMapper;
+
+/**
+ * 反写规则版本快照 Mapper。
+ *
+ */
+public interface ReverseWritingRulesVerMapper extends BaseDaoMapper {
+}
diff --git a/common/common-botp/src/main/java/apelet/common/botp/dto/RuleVersionDiffDto.java b/common/common-botp/src/main/java/apelet/common/botp/dto/RuleVersionDiffDto.java
new file mode 100644
index 0000000..5b598ed
--- /dev/null
+++ b/common/common-botp/src/main/java/apelet/common/botp/dto/RuleVersionDiffDto.java
@@ -0,0 +1,147 @@
+package apelet.common.botp.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 规则版本对比结果。
+ *
+ * 对比粒度是「字段 / 分录行」,只给出新增、删除、修改三类结论,不做逐字 diff:
+ * 规则主表按字段比对;子表分录按业务主键配对后按字段比对,配对不上的分别算新增或删除。
+ *
+ * @author admin
+ * @since 2024-05-17
+ */
+@Data
+@Schema(description = "规则版本对比结果")
+public class RuleVersionDiffDto implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 变化类型:新增。
+ */
+ public static final String CHANGE_ADDED = "ADDED";
+
+ /**
+ * 变化类型:删除。
+ */
+ public static final String CHANGE_REMOVED = "REMOVED";
+
+ /**
+ * 变化类型:修改。
+ */
+ public static final String CHANGE_MODIFIED = "MODIFIED";
+
+ @Schema(description = "规则Id")
+ private Long ruleId;
+
+ @Schema(description = "对比基准版本号")
+ private Integer fromVersion;
+
+ @Schema(description = "对比目标版本号")
+ private Integer toVersion;
+
+ @Schema(description = "基准版本的快照时间")
+ private Date fromCreateTime;
+
+ @Schema(description = "目标版本的快照时间")
+ private Date toCreateTime;
+
+ @Schema(description = "规则主表字段差异")
+ private List ruleDiff = new ArrayList<>();
+
+ /**
+ * 子表分录差异,key 为快照分区名(fieldMappings / pluginEntries / formulas / businesses)。
+ */
+ @Schema(description = "子表分录差异")
+ private Map entries = new LinkedHashMap<>();
+
+ @Schema(description = "新增条目数(主表字段 + 各子表分录)")
+ private int addedCount;
+
+ @Schema(description = "删除条目数")
+ private int removedCount;
+
+ @Schema(description = "修改条目数")
+ private int modifiedCount;
+
+ /**
+ * 单个字段的差异。
+ */
+ @Data
+ @Schema(description = "字段差异")
+ public static class FieldDiff implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "字段名")
+ private String field;
+
+ @Schema(description = "变化类型:ADDED / REMOVED / MODIFIED")
+ private String changeType;
+
+ @Schema(description = "基准版本的值")
+ private Object oldValue;
+
+ @Schema(description = "目标版本的值")
+ private Object newValue;
+
+ public static FieldDiff of(String field, String changeType, Object oldValue, Object newValue) {
+ FieldDiff diff = new FieldDiff();
+ diff.setField(field);
+ diff.setChangeType(changeType);
+ diff.setOldValue(oldValue);
+ diff.setNewValue(newValue);
+ return diff;
+ }
+ }
+
+ /**
+ * 一类子表分录的差异。
+ */
+ @Data
+ @Schema(description = "子表分录差异")
+ public static class EntryDiff implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ @Schema(description = "新增的分录行")
+ private List