Commit 3dcbf38249ced4dffd13b7c4fa787a3a6b45ddc6

Authored by 芋道源码
Committed by GitHub
2 parents d68047bb 262f7d1e

Merge pull request #1254 from dadiyang/fix/a45-autocode-rule-prefix-unique

fix(mes): 编码规则固定字符(编码前缀)跨规则重复时保存拒绝,避免不同规则生成无法区分的编码
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/autocode/MesMdAutoCodePartMapper.java
... ... @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.mes.dal.mysql.md.autocode;
3 3 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
4 4 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
5 5 import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO;
  6 +import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodePartTypeEnum;
6 7 import org.apache.ibatis.annotations.Mapper;
7 8  
8 9 import java.util.List;
... ... @@ -21,6 +22,10 @@ public interface MesMdAutoCodePartMapper extends BaseMapperX<MesMdAutoCodePartDO
21 22 .orderByAsc(MesMdAutoCodePartDO::getSort));
22 23 }
23 24  
  25 + default List<MesMdAutoCodePartDO> selectFixedCharPartList() {
  26 + return selectList(MesMdAutoCodePartDO::getType, MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType());
  27 + }
  28 +
24 29 default void deleteByRuleId(Long ruleId) {
25 30 delete(MesMdAutoCodePartDO::getRuleId, ruleId);
26 31 }
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
... ... @@ -121,6 +121,7 @@ public interface ErrorCodeConstants {
121 121 ErrorCode AUTO_CODE_REDIS_ERROR = new ErrorCode(1_040_110_003, "编码生成服务不可用,请稍后重试");
122 122 ErrorCode AUTO_CODE_GENERATE_FAILED = new ErrorCode(1_040_110_004, "编码生成失败");
123 123 ErrorCode AUTO_CODE_PART_SERIAL_NUMBER_DUPLICATE = new ErrorCode(1_040_110_005, "流水号分段只能存在一个");
  124 + ErrorCode AUTO_CODE_PART_FIXED_CHAR_DUPLICATE = new ErrorCode(1_040_110_006, "固定字符与其他编码规则相同,请修改固定字符");
124 125  
125 126 // ========== MES 日历排班-计划班次(1-040-200-000) ==========
126 127 ErrorCode CAL_PLAN_SHIFT_NOT_EXISTS = new ErrorCode(1_040_200_000, "计划班次不存在");
... ...
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodePartServiceImpl.java
1 1 package cn.iocoder.yudao.module.mes.service.md.autocode;
2 2  
3 3 import cn.hutool.core.util.ObjUtil;
  4 +import cn.hutool.core.util.StrUtil;
4 5 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
5 6 import cn.iocoder.yudao.module.mes.controller.admin.md.autocode.vo.part.MesMdAutoCodePartSaveReqVO;
6 7 import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO;
  8 +import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO;
7 9 import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodePartMapper;
  10 +import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodeRuleMapper;
8 11 import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodePartTypeEnum;
9 12 import jakarta.annotation.Resource;
10 13 import org.springframework.stereotype.Service;
11 14 import org.springframework.validation.annotation.Validated;
12 15  
  16 +import java.util.ArrayList;
  17 +import java.util.Collections;
  18 +import java.util.Comparator;
13 19 import java.util.List;
  20 +import java.util.Map;
  21 +import java.util.stream.Collectors;
14 22  
15 23 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  24 +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_FIXED_CHAR_DUPLICATE;
16 25 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_NOT_EXISTS;
17 26 import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_SERIAL_NUMBER_DUPLICATE;
18 27  
... ... @@ -29,6 +38,9 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService {
29 38 private MesMdAutoCodePartMapper partMapper;
30 39  
31 40 @Resource
  41 + private MesMdAutoCodeRuleMapper ruleMapper;
  42 +
  43 + @Resource
32 44 private MesMdAutoCodeRuleService ruleService;
33 45  
34 46 @Override
... ... @@ -37,6 +49,8 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService {
37 49 ruleService.validateAutoCodeRuleExists(createReqVO.getRuleId());
38 50 // 校验流水号分段唯一性
39 51 validateSerialNumberPartUnique(null, createReqVO.getRuleId(), createReqVO.getType());
  52 + // 校验固定字符(编码前缀)与其他规则不重复
  53 + validateFixedCharUnique(null, createReqVO);
40 54  
41 55 // 插入
42 56 MesMdAutoCodePartDO part = BeanUtils.toBean(createReqVO, MesMdAutoCodePartDO.class);
... ... @@ -52,6 +66,8 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService {
52 66 ruleService.validateAutoCodeRuleExists(updateReqVO.getRuleId());
53 67 // 校验流水号分段唯一性
54 68 validateSerialNumberPartUnique(updateReqVO.getId(), updateReqVO.getRuleId(), updateReqVO.getType());
  69 + // 校验固定字符(编码前缀)与其他规则不重复
  70 + validateFixedCharUnique(updateReqVO.getId(), updateReqVO);
55 71  
56 72 // 更新
57 73 MesMdAutoCodePartDO updateObj = BeanUtils.toBean(updateReqVO, MesMdAutoCodePartDO.class);
... ... @@ -101,4 +117,45 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService {
101 117 }
102 118 }
103 119  
  120 + /**
  121 + * 校验固定字符(编码前缀)与其他编码规则不重复
  122 + *
  123 + * 规则的前缀=该规则全部固定字符分段(按分段排序、截取到分段长度内的有效值)拼接。
  124 + * 两条规则前缀相同(含均无固定字符分段)时,生成的编码无法区分归属业务,保存时拒绝,
  125 + * 从源头避免不同规则生成相同编码;生成编码时的全局查重仍保留,兜底存量配置的冲突。
  126 + */
  127 + private void validateFixedCharUnique(Long id, MesMdAutoCodePartSaveReqVO reqVO) {
  128 + // 1. 全租户固定字符分段,按规则分组
  129 + Map<Long, List<MesMdAutoCodePartDO>> fixedPartsByRuleId = partMapper.selectFixedCharPartList().stream()
  130 + .collect(Collectors.groupingBy(MesMdAutoCodePartDO::getRuleId));
  131 + // 2. 本次保存后,本规则的前缀指纹(排除正在修改的分段,加入本次保存的分段)
  132 + List<MesMdAutoCodePartDO> myFixedParts = new ArrayList<>(
  133 + fixedPartsByRuleId.getOrDefault(reqVO.getRuleId(), Collections.emptyList()));
  134 + myFixedParts.removeIf(part -> ObjUtil.equal(id, part.getId()));
  135 + if (MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType().equals(reqVO.getType())) {
  136 + myFixedParts.add(BeanUtils.toBean(reqVO, MesMdAutoCodePartDO.class));
  137 + }
  138 + String myFingerprint = buildFixedCharFingerprint(myFixedParts);
  139 + // 3. 与租户内其他未删除规则的前缀指纹逐一比对
  140 + for (MesMdAutoCodeRuleDO rule : ruleMapper.selectList()) {
  141 + if (ObjUtil.equal(rule.getId(), reqVO.getRuleId())) {
  142 + continue;
  143 + }
  144 + List<MesMdAutoCodePartDO> otherFixedParts = fixedPartsByRuleId.getOrDefault(rule.getId(), Collections.emptyList());
  145 + if (myFingerprint.equals(buildFixedCharFingerprint(otherFixedParts))) {
  146 + throw exception(AUTO_CODE_PART_FIXED_CHAR_DUPLICATE);
  147 + }
  148 + }
  149 + }
  150 +
  151 + /**
  152 + * 构建前缀指纹:固定字符分段按分段排序,拼接截取到分段长度内的固定字符(空值按空串)
  153 + */
  154 + private String buildFixedCharFingerprint(List<MesMdAutoCodePartDO> fixedParts) {
  155 + return fixedParts.stream()
  156 + .sorted(Comparator.comparing(MesMdAutoCodePartDO::getSort))
  157 + .map(part -> StrUtil.sub(StrUtil.emptyToDefault(part.getFixCharacter(), ""), 0, part.getLength()))
  158 + .collect(Collectors.joining());
  159 + }
  160 +
104 161 }
... ...
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodePartServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.mes.service.md.autocode;
  2 +
  3 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  4 +import cn.iocoder.yudao.module.mes.controller.admin.md.autocode.vo.part.MesMdAutoCodePartSaveReqVO;
  5 +import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO;
  6 +import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO;
  7 +import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodePartMapper;
  8 +import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodeRuleMapper;
  9 +import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodePartTypeEnum;
  10 +import org.junit.jupiter.api.Test;
  11 +import org.junit.jupiter.api.extension.ExtendWith;
  12 +import org.mockito.InjectMocks;
  13 +import org.mockito.Mock;
  14 +import org.mockito.junit.jupiter.MockitoExtension;
  15 +
  16 +import java.util.Arrays;
  17 +import java.util.Collections;
  18 +
  19 +import static org.junit.jupiter.api.Assertions.assertEquals;
  20 +import static org.junit.jupiter.api.Assertions.assertThrows;
  21 +import static org.mockito.ArgumentMatchers.any;
  22 +import static org.mockito.Mockito.never;
  23 +import static org.mockito.Mockito.verify;
  24 +import static org.mockito.Mockito.when;
  25 +
  26 +/**
  27 + * {@link MesMdAutoCodePartServiceImpl} 的单元测试
  28 + *
  29 + * @author 芋道源码
  30 + */
  31 +@ExtendWith(MockitoExtension.class)
  32 +public class MesMdAutoCodePartServiceImplTest {
  33 +
  34 + @InjectMocks
  35 + private MesMdAutoCodePartServiceImpl partService;
  36 +
  37 + @Mock
  38 + private MesMdAutoCodePartMapper partMapper;
  39 + @Mock
  40 + private MesMdAutoCodeRuleMapper ruleMapper;
  41 + @Mock
  42 + private MesMdAutoCodeRuleService ruleService;
  43 +
  44 + private static MesMdAutoCodePartDO fixedPart(Long id, Long ruleId, Integer sort, String fixCharacter, Integer length) {
  45 + return MesMdAutoCodePartDO.builder()
  46 + .id(id).ruleId(ruleId).sort(sort)
  47 + .type(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType())
  48 + .length(length).fixCharacter(fixCharacter).build();
  49 + }
  50 +
  51 + private static MesMdAutoCodePartSaveReqVO fixedReqVO(Long ruleId, Integer sort, String fixCharacter, Integer length) {
  52 + MesMdAutoCodePartSaveReqVO reqVO = new MesMdAutoCodePartSaveReqVO();
  53 + reqVO.setRuleId(ruleId);
  54 + reqVO.setSort(sort);
  55 + reqVO.setType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType());
  56 + reqVO.setLength(length);
  57 + reqVO.setFixCharacter(fixCharacter);
  58 + return reqVO;
  59 + }
  60 +
  61 + @Test
  62 + public void testCreateAutoCodePart_fixedCharDuplicate() {
  63 + // 准备:其他规则已占用相同前缀 "ITEM_"
  64 + when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList(
  65 + fixedPart(1L, 1L, 1, "ITEM_", 5)));
  66 + when(ruleMapper.selectList()).thenReturn(Arrays.asList(
  67 + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L)));
  68 +
  69 + // 断言:保存被拒,且未插入
  70 + ServiceException ex = assertThrows(ServiceException.class,
  71 + () -> partService.createAutoCodePart(fixedReqVO(10L, 1, "ITEM_", 5)));
  72 + assertEquals(1040110006, ex.getCode());
  73 + verify(partMapper, never()).insert(org.mockito.ArgumentMatchers.<MesMdAutoCodePartDO>any());
  74 + }
  75 +
  76 + @Test
  77 + public void testCreateAutoCodePart_fixedCharUnique() {
  78 + // 准备:其他规则前缀 "ITEM_",本次保存 "SN_"
  79 + when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList(
  80 + fixedPart(1L, 1L, 1, "ITEM_", 5)));
  81 + when(ruleMapper.selectList()).thenReturn(Arrays.asList(
  82 + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L)));
  83 +
  84 + // 断言:保存成功
  85 + partService.createAutoCodePart(fixedReqVO(10L, 1, "SN_", 3));
  86 + verify(partMapper).insert(org.mockito.ArgumentMatchers.<MesMdAutoCodePartDO>any());
  87 + }
  88 +
  89 + @Test
  90 + public void testCreateAutoCodePart_fixedCharTruncatedDuplicate() {
  91 + // 准备:其他规则固定字符 "IT_X" 长度 2(生成时截断为 "IT")
  92 + when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList(
  93 + fixedPart(1L, 1L, 1, "IT_X", 2)));
  94 + when(ruleMapper.selectList()).thenReturn(Arrays.asList(
  95 + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L)));
  96 +
  97 + // 断言:本次保存 "IT" 截断后同为 "IT",保存被拒
  98 + ServiceException ex = assertThrows(ServiceException.class,
  99 + () -> partService.createAutoCodePart(fixedReqVO(10L, 1, "IT", 4)));
  100 + assertEquals(1040110006, ex.getCode());
  101 + verify(partMapper, never()).insert(org.mockito.ArgumentMatchers.<MesMdAutoCodePartDO>any());
  102 + }
  103 +
  104 + @Test
  105 + public void testCreateAutoCodePart_bothRulesWithoutFixedChar() {
  106 + // 准备:租户内另一规则无任何固定字符分段(前缀为空串),本次保存流水号分段(本规则也无固定字符)
  107 + when(partMapper.selectFixedCharPartList()).thenReturn(Collections.emptyList());
  108 + when(ruleMapper.selectList()).thenReturn(Arrays.asList(
  109 + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L)));
  110 +
  111 + MesMdAutoCodePartSaveReqVO reqVO = new MesMdAutoCodePartSaveReqVO();
  112 + reqVO.setRuleId(10L);
  113 + reqVO.setSort(1);
  114 + reqVO.setType(MesMdAutoCodePartTypeEnum.SERIAL_NUMBER.getType());
  115 + reqVO.setLength(4);
  116 + reqVO.setSerialStartNo(1);
  117 +
  118 + // 断言:两条规则前缀均为空(生成的编码无法区分业务),保存被拒
  119 + ServiceException ex = assertThrows(ServiceException.class,
  120 + () -> partService.createAutoCodePart(reqVO));
  121 + assertEquals(1040110006, ex.getCode());
  122 + verify(partMapper, never()).insert(org.mockito.ArgumentMatchers.<MesMdAutoCodePartDO>any());
  123 + }
  124 +
  125 + @Test
  126 + public void testUpdateAutoCodePart_fixedCharBecomesDuplicate() {
  127 + // 准备:本规则已有固定字符 "OLD_",修改为与其他规则相同的 "ITEM_"
  128 + MesMdAutoCodePartDO myPart = fixedPart(100L, 10L, 1, "OLD_", 4);
  129 + when(partMapper.selectById(100L)).thenReturn(myPart);
  130 + when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList(
  131 + myPart, fixedPart(1L, 1L, 1, "ITEM_", 5)));
  132 + when(ruleMapper.selectList()).thenReturn(Arrays.asList(
  133 + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L)));
  134 +
  135 + // 断言:保存被拒,且未更新
  136 + ServiceException ex = assertThrows(ServiceException.class,
  137 + () -> partService.updateAutoCodePart(fixedReqVO(10L, 1, "ITEM_", 5).setId(100L)));
  138 + assertEquals(1040110006, ex.getCode());
  139 + verify(partMapper, never()).updateById(org.mockito.ArgumentMatchers.<MesMdAutoCodePartDO>any());
  140 + }
  141 +
  142 + @Test
  143 + public void testUpdateAutoCodePart_fixedCharBecomesUnique() {
  144 + // 准备:本规则已有固定字符 "ITEM_"(与其他规则重复),修改为 "SN_"
  145 + MesMdAutoCodePartDO myPart = fixedPart(100L, 10L, 1, "ITEM_", 5);
  146 + when(partMapper.selectById(100L)).thenReturn(myPart);
  147 + when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList(
  148 + myPart, fixedPart(1L, 1L, 1, "WO_", 3)));
  149 + when(ruleMapper.selectList()).thenReturn(Arrays.asList(
  150 + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L)));
  151 +
  152 + // 断言:修改后前缀不再重复,保存成功
  153 + partService.updateAutoCodePart(fixedReqVO(10L, 1, "SN_", 3).setId(100L));
  154 + verify(partMapper).updateById(org.mockito.ArgumentMatchers.<MesMdAutoCodePartDO>any());
  155 + }
  156 +
  157 +}
... ...