From 038b52c64d1ff53ca8580e34c99988bc630bef36 Mon Sep 17 00:00:00 2001 From: lml <1297555865@qq.com> Date: Mon, 19 Oct 2026 15:43:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(botp):=20=E4=BF=9D=E5=AD=98=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2=E8=A7=84=E5=88=99=E6=97=B6=E6=A0=A1=E9=AA=8C=E5=8F=96?= =?UTF-8?q?=E5=80=BC=E6=96=B9=E5=BC=8F=E4=B8=8E=E5=88=86=E5=8D=95=E7=AD=96?= =?UTF-8?q?=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 ValueMethodEnum 与 SplitMergeStrategyEnum 承载字段可选值,消除魔法值, 并为二者补充 parse/validOptions 方法供保存校验使用。 保存入口(addAll/editAll)统一增加 validateBeforeSave 前置校验, 校验不通过抛 MyRuntimeException 阻止保存,且早于任何写库操作,避免脏数据。 - 取值方式:为空或未知时阻止保存,错误信息定位到分录行号、源字段、 目标字段、非法值、合法选项及分录id - 分单策略:未配置按默认「一对一」放行;非法值阻止保存;选择「按规则分组合并」 但未配置分单字段时阻止保存 - 事务:addAll/editAll 涉及主表与分录的多次独立写库(editAll 为先删后插), 此前无事务边界,中途失败会造成主表已改、分录残缺或产生无分录的空规则。 现补充 @Transactional(rollbackFor = Exception.class),removeChild 一并补充 files: common/common-botp/src/main/java/apelet/common/botp/model/constant/ValueMethodEnum.java common/common-botp/src/main/java/apelet/common/botp/model/constant/SplitMergeStrategyEnum.java common/common-botp/src/main/java/apelet/common/botp/service/impl/ConvertRuleServiceImpl.java --- .../model/constant/SplitMergeStrategyEnum.java | 99 +++++++++++++++++++ .../botp/model/constant/ValueMethodEnum.java | 105 ++++++++++++++++++++ .../botp/service/impl/ConvertRuleServiceImpl.java | 107 ++++++++++++++++++++- 3 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 common/common-botp/src/main/java/apelet/common/botp/model/constant/SplitMergeStrategyEnum.java create mode 100644 common/common-botp/src/main/java/apelet/common/botp/model/constant/ValueMethodEnum.java diff --git a/common/common-botp/src/main/java/apelet/common/botp/model/constant/SplitMergeStrategyEnum.java b/common/common-botp/src/main/java/apelet/common/botp/model/constant/SplitMergeStrategyEnum.java new file mode 100644 index 0000000..2fc86d9 --- /dev/null +++ b/common/common-botp/src/main/java/apelet/common/botp/model/constant/SplitMergeStrategyEnum.java @@ -0,0 +1,99 @@ +package apelet.common.botp.model.constant; + +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 分单/合并策略枚举。 + * + *

对应 convert_rule.split_merge_strategy,落库为 varchar 的 "1"/"2"/"3",默认按「一对一」处理。

+ * + * @author admin + * @since 2024-01-18 + */ +public enum SplitMergeStrategyEnum { + + /** + * 一对一:一条源单对应一条目标单。 + */ + ONE_TO_ONE("1", "一对一"), + + /** + * 多对一:多条源单合并为一条目标单。 + */ + MANY_TO_ONE("2", "多对一"), + + /** + * 按规则分组合并:按分单字段分组后分别合并。 + */ + GROUP_BY_RULE("3", "按规则分组合并"); + + @Getter + private final String code; + + @Getter + private final String description; + + private static final Map ENUM_MAP; + + SplitMergeStrategyEnum(String code, String description) { + this.code = code; + this.description = description; + } + + static { + ENUM_MAP = Arrays.stream(values()).collect(Collectors.toMap(SplitMergeStrategyEnum::getCode, e -> e)); + } + + /** + * 根据 code 获取枚举对象。 + * + * @param code 分单策略编码。 + * @return 枚举对象。 + */ + public static SplitMergeStrategyEnum of(String code) { + return Optional.ofNullable(ENUM_MAP.get(code)) + .orElseThrow(() -> new IllegalArgumentException("无法根据 code 获取分单策略:" + code)); + } + + /** + * 根据 code 获取枚举对象,空值按「一对一」处理(与字段默认值一致)。 + * + * @param code 分单策略编码。 + * @return 枚举对象。 + */ + public static SplitMergeStrategyEnum ofNullable(String code) { + if (StringUtils.isBlank(code)) { + return ONE_TO_ONE; + } + return of(code.trim()); + } + + /** + * 根据 code 获取枚举对象,空值或未知 code 均返回 null(不兜底、不抛异常)。 + * + *

供保存校验等场景使用:需要区分「未配置」与「配置了非法值」时,用本方法。

+ * + * @param code 分单策略编码。 + * @return 枚举对象;不是已知编码时返回 null。 + */ + public static SplitMergeStrategyEnum parse(String code) { + return StringUtils.isBlank(code) ? null : ENUM_MAP.get(code.trim()); + } + + /** + * 生成可选值说明,用于错误提示。 + * + * @return 形如「1-一对一 2-多对一 3-按规则分组合并」。 + */ + public static String validOptions() { + return Arrays.stream(values()) + .map(e -> e.getCode() + "-" + e.getDescription()) + .collect(Collectors.joining(" ")); + } +} diff --git a/common/common-botp/src/main/java/apelet/common/botp/model/constant/ValueMethodEnum.java b/common/common-botp/src/main/java/apelet/common/botp/model/constant/ValueMethodEnum.java new file mode 100644 index 0000000..2695141 --- /dev/null +++ b/common/common-botp/src/main/java/apelet/common/botp/model/constant/ValueMethodEnum.java @@ -0,0 +1,105 @@ +package apelet.common.botp.model.constant; + +import lombok.Getter; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 字段取值方式枚举。 + * + *

对应 convert_fieldmapping_entry.value_method,落库为 varchar 的 "1"/"2"/"3"/"4"。

+ * + * @author admin + * @since 2024-01-18 + */ +public enum ValueMethodEnum { + + /** + * 映射:取源单同名字段的值。 + */ + MAPPING("1", "映射"), + + /** + * 常量:直接使用配置的固定值。 + */ + CONSTANT("2", "常量"), + + /** + * 计算公式:按配置的公式表达式计算。 + */ + EXPRESSION("3", "计算公式"), + + /** + * 按条件取值 + */ + CONDITION("4", "按条件取值") + ; + + @Getter + private final String code; + + @Getter + private final String description; + + private static final Map ENUM_MAP; + + ValueMethodEnum(String code, String description) { + this.code = code; + this.description = description; + } + + static { + ENUM_MAP = Arrays.stream(values()).collect(Collectors.toMap(ValueMethodEnum::getCode, e -> e)); + } + + /** + * 根据 code 获取枚举对象。 + * + * @param code 取值方式编码。 + * @return 枚举对象。 + */ + public static ValueMethodEnum of(String code) { + return Optional.ofNullable(ENUM_MAP.get(code)) + .orElseThrow(() -> new IllegalArgumentException("无法根据 code 获取字段取值方式:" + code)); + } + + /** + * 根据 code 获取枚举对象,空值按「映射」处理(与字段默认值一致)。 + * + * @param code 取值方式编码。 + * @return 枚举对象。 + */ + public static ValueMethodEnum ofNullable(String code) { + if (StringUtils.isBlank(code)) { + return MAPPING; + } + return of(code.trim()); + } + + /** + * 根据 code 获取枚举对象,空值或未知 code 均返回 null(不兜底、不抛异常)。 + * + *

供保存校验等场景使用:需要区分「未配置」与「配置了非法值」时,用本方法。

+ * + * @param code 取值方式编码。 + * @return 枚举对象;不是已知编码时返回 null。 + */ + public static ValueMethodEnum parse(String code) { + return StringUtils.isBlank(code) ? null : ENUM_MAP.get(code.trim()); + } + + /** + * 生成可选值说明,用于错误提示。 + * + * @return 形如「1-映射 2-常量 3-计算公式 4-按条件取值」。 + */ + public static String validOptions() { + return Arrays.stream(values()) + .map(e -> e.getCode() + "-" + e.getDescription()) + .collect(Collectors.joining(" ")); + } +} diff --git a/common/common-botp/src/main/java/apelet/common/botp/service/impl/ConvertRuleServiceImpl.java b/common/common-botp/src/main/java/apelet/common/botp/service/impl/ConvertRuleServiceImpl.java index 9deb952..a1c93b4 100644 --- a/common/common-botp/src/main/java/apelet/common/botp/service/impl/ConvertRuleServiceImpl.java +++ b/common/common-botp/src/main/java/apelet/common/botp/service/impl/ConvertRuleServiceImpl.java @@ -5,6 +5,8 @@ import apelet.common.botp.dao.ConvertPluginEntryMapper; import apelet.common.botp.dao.ConvertRuleMapper; import apelet.common.botp.dto.*; import apelet.common.botp.model.*; +import apelet.common.botp.model.constant.SplitMergeStrategyEnum; +import apelet.common.botp.model.constant.ValueMethodEnum; import apelet.common.botp.service.*; import apelet.common.botp.service.execute.BotpService; import apelet.common.botp.service.execute.entity.BillData; @@ -13,6 +15,7 @@ import apelet.common.botp.util.Constant; import apelet.common.core.base.dao.BaseDaoMapper; import apelet.common.core.base.service.BaseService; import apelet.common.core.constant.GlobalDeletedFlag; +import apelet.common.core.exception.MyRuntimeException; import apelet.common.core.object.*; import apelet.common.generator.dao.OnlFormHeadMapper; import apelet.common.generator.model.OnlFormField; @@ -31,6 +34,7 @@ import org.jetbrains.annotations.NotNull; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.stream.Collectors; @@ -99,11 +103,14 @@ public class ConvertRuleServiceImpl extends BaseService implem } @Override + @Transactional(rollbackFor = Exception.class) public Boolean editAll(ConvertRule convertRule) { Long id = convertRule.getId(); if (null==id){ return false; } + //保存前校验(分单策略、字段映射取值方式),不通过直接阻止保存 + validateBeforeSave(convertRule); //设置主数据基本信息 addOrUpdateBaseData(convertRule,null); boolean update = this.updateById(convertRule); @@ -114,7 +121,10 @@ public class ConvertRuleServiceImpl extends BaseService implem } @Override + @Transactional(rollbackFor = Exception.class) public Boolean addAll(ConvertRule convertRule) { + //保存前校验(分单策略、字段映射取值方式),不通过直接阻止保存 + validateBeforeSave(convertRule); //设置主数据基本信息 addOrUpdateBaseData(convertRule,null); boolean save = this.save(convertRule); @@ -126,6 +136,100 @@ public class ConvertRuleServiceImpl extends BaseService implem return addChild(convertRule,id); } + /** + * 保存前的规则级校验。 + * + *

集中校验转换规则本身与其字段映射分录,任一不通过即抛出 {@link MyRuntimeException} 阻止保存。

+ * + * @param convertRule 待保存的转换规则。 + */ + private void validateBeforeSave(ConvertRule convertRule) { + validateSplitMergeStrategy(convertRule); + validateFieldmappingValueMethod(convertRule); + } + + /** + * 校验分单策略配置。 + * + *

分单策略为空时按「一对一」处理(与字段默认值一致);配置了非法值时阻止保存; + * 选择「按规则分组合并」时必须配置分单字段。

+ * + * @param convertRule 待保存的转换规则。 + */ + private void validateSplitMergeStrategy(ConvertRule convertRule) { + String splitMergeStrategy = convertRule.getSplitMergeStrategy(); + if (StringUtils.isBlank(splitMergeStrategy)) { + // 未配置按默认值「一对一」处理,与 SplitMergeStrategyEnum.ofNullable 保持一致 + return; + } + SplitMergeStrategyEnum strategy = SplitMergeStrategyEnum.parse(splitMergeStrategy); + if (strategy == null) { + throw new MyRuntimeException("分单策略未知:" + splitMergeStrategy + + ",可选值为 " + SplitMergeStrategyEnum.validOptions()); + } + if (strategy == SplitMergeStrategyEnum.GROUP_BY_RULE + && StringUtils.isBlank(convertRule.getGroupByField())) { + throw new MyRuntimeException("分单策略为「" + SplitMergeStrategyEnum.GROUP_BY_RULE.getDescription() + + "」时必须配置分单字段,可选值为 " + SplitMergeStrategyEnum.validOptions()); + } + } + + /** + * 校验字段映射分录的取值方式。 + * + *

取值方式为空或不在 {@link ValueMethodEnum} 允许范围内时,直接抛出异常阻止保存, + * 并在异常信息中定位到具体的分录行(第几行、源字段、目标字段),便于前端定位问题。

+ * + * @param convertRule 待保存的转换规则,含字段映射分录。 + */ + private void validateFieldmappingValueMethod(ConvertRule convertRule) { + List fieldmappingEntries = convertRule.getConvertFieldmappingEntry(); + if (ObjectUtil.isEmpty(fieldmappingEntries)) { + return; + } + for (int i = 0; i < fieldmappingEntries.size(); i++) { + ConvertFieldmappingEntry entry = fieldmappingEntries.get(i); + if (entry == null) { + continue; + } + String valueMethod = entry.getValueMethod(); + if (StringUtils.isBlank(valueMethod)) { + throw new MyRuntimeException(buildValueMethodErrorMsg(i, entry, "未配置取值方式")); + } + if (ValueMethodEnum.parse(valueMethod) == null) { + throw new MyRuntimeException(buildValueMethodErrorMsg(i, entry, "取值方式未知:" + valueMethod)); + } + } + } + + /** + * 构建取值方式校验失败的定位信息。 + * + * @param index 分录行下标(从 0 开始)。 + * @param entry 出错的分录。 + * @param reason 具体原因。 + * @return 带行号与字段定位的错误信息。 + */ + private String buildValueMethodErrorMsg(int index, ConvertFieldmappingEntry entry, String reason) { + StringBuilder msg = new StringBuilder("字段映射第 ").append(index + 1).append(" 行"); + if (StringUtils.isNotBlank(entry.getOldfieldName()) || StringUtils.isNotBlank(entry.getOldfieldNumber())) { + msg.append("(源字段:") + .append(StringUtils.defaultIfBlank(entry.getOldfieldName(), entry.getOldfieldNumber())) + .append(")"); + } + if (StringUtils.isNotBlank(entry.getNewfieldName()) || StringUtils.isNotBlank(entry.getNewfieldNumber())) { + msg.append("(目标字段:") + .append(StringUtils.defaultIfBlank(entry.getNewfieldName(), entry.getNewfieldNumber())) + .append(")"); + } + msg.append(" ").append(reason) + .append(",可选值为 ").append(ValueMethodEnum.validOptions()); + if (entry.getId() != null) { + msg.append(",分录id=").append(entry.getId()); + } + return msg.toString(); + } + @Override public List getConvertRuleListWithRelation(ConvertRule convertRuleFilter, String buildOrderBy) { LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(convertRuleFilter); @@ -198,7 +302,8 @@ public class ConvertRuleServiceImpl extends BaseService implem } - private void removeChild(Long id){ + @Transactional(rollbackFor = Exception.class) + public void removeChild(Long id){ convertPluginEntryService.remove(new LambdaQueryWrapper().eq(ConvertPluginEntry::getParentId,id)); convertFieldmappingEntryService.remove(new LambdaQueryWrapper().eq(ConvertFieldmappingEntry::getParentId,id)); }