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)); }