Browse Source

feat(botp): 保存转换规则时校验取值方式与分单策略

新增 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
dev-lml
lml 2 weeks ago
parent
commit
038b52c64d
  1. 99
      common/common-botp/src/main/java/apelet/common/botp/model/constant/SplitMergeStrategyEnum.java
  2. 105
      common/common-botp/src/main/java/apelet/common/botp/model/constant/ValueMethodEnum.java
  3. 107
      common/common-botp/src/main/java/apelet/common/botp/service/impl/ConvertRuleServiceImpl.java

99
common/common-botp/src/main/java/apelet/common/botp/model/constant/SplitMergeStrategyEnum.java

@ -0,0 +1,99 @@ @@ -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;
/**
* 分单/合并策略枚举。
*
* <p>对应 convert_rule.split_merge_strategy,落库为 varchar 的 "1"/"2"/"3",默认按「一对一」处理。</p>
*
* @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<String, SplitMergeStrategyEnum> 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(不兜底、不抛异常)。
*
* <p>供保存校验等场景使用:需要区分「未配置」与「配置了非法值」时,用本方法。</p>
*
* @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(" "));
}
}

105
common/common-botp/src/main/java/apelet/common/botp/model/constant/ValueMethodEnum.java

@ -0,0 +1,105 @@ @@ -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;
/**
* 字段取值方式枚举。
*
* <p>对应 convert_fieldmapping_entry.value_method,落库为 varchar 的 "1"/"2"/"3"/"4"。</p>
*
* @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<String, ValueMethodEnum> 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(不兜底、不抛异常)。
*
* <p>供保存校验等场景使用:需要区分「未配置」与「配置了非法值」时,用本方法。</p>
*
* @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(" "));
}
}

107
common/common-botp/src/main/java/apelet/common/botp/service/impl/ConvertRuleServiceImpl.java

@ -5,6 +5,8 @@ import apelet.common.botp.dao.ConvertPluginEntryMapper; @@ -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; @@ -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; @@ -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<ConvertRule,Long> implem @@ -99,11 +103,14 @@ public class ConvertRuleServiceImpl extends BaseService<ConvertRule,Long> 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<ConvertRule,Long> implem @@ -114,7 +121,10 @@ public class ConvertRuleServiceImpl extends BaseService<ConvertRule,Long> 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<ConvertRule,Long> implem @@ -126,6 +136,100 @@ public class ConvertRuleServiceImpl extends BaseService<ConvertRule,Long> implem
return addChild(convertRule,id);
}
/**
* 保存前的规则级校验。
*
* <p>集中校验转换规则本身与其字段映射分录,任一不通过即抛出 {@link MyRuntimeException} 阻止保存。</p>
*
* @param convertRule 待保存的转换规则。
*/
private void validateBeforeSave(ConvertRule convertRule) {
validateSplitMergeStrategy(convertRule);
validateFieldmappingValueMethod(convertRule);
}
/**
* 校验分单策略配置。
*
* <p>分单策略为空时按「一对一」处理(与字段默认值一致);配置了非法值时阻止保存;
* 选择「按规则分组合并」时必须配置分单字段。</p>
*
* @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());
}
}
/**
* 校验字段映射分录的取值方式。
*
* <p>取值方式为空或不在 {@link ValueMethodEnum} 允许范围内时,直接抛出异常阻止保存,
* 并在异常信息中定位到具体的分录行(第几行、源字段、目标字段),便于前端定位问题。</p>
*
* @param convertRule 待保存的转换规则,含字段映射分录。
*/
private void validateFieldmappingValueMethod(ConvertRule convertRule) {
List<ConvertFieldmappingEntry> 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<ConvertRule> getConvertRuleListWithRelation(ConvertRule convertRuleFilter, String buildOrderBy) {
LambdaQueryWrapper<ConvertRule> queryWrapper = new LambdaQueryWrapper<>(convertRuleFilter);
@ -198,7 +302,8 @@ public class ConvertRuleServiceImpl extends BaseService<ConvertRule,Long> implem @@ -198,7 +302,8 @@ public class ConvertRuleServiceImpl extends BaseService<ConvertRule,Long> implem
}
private void removeChild(Long id){
@Transactional(rollbackFor = Exception.class)
public void removeChild(Long id){
convertPluginEntryService.remove(new LambdaQueryWrapper<ConvertPluginEntry>().eq(ConvertPluginEntry::getParentId,id));
convertFieldmappingEntryService.remove(new LambdaQueryWrapper<ConvertFieldmappingEntry>().eq(ConvertFieldmappingEntry::getParentId,id));
}

Loading…
Cancel
Save