Commit 3dcbf38249ced4dffd13b7c4fa787a3a6b45ddc6
Committed by
GitHub
Merge pull request #1254 from dadiyang/fix/a45-autocode-rule-prefix-unique
fix(mes): 编码规则固定字符(编码前缀)跨规则重复时保存拒绝,避免不同规则生成无法区分的编码
Showing
4 changed files
with
220 additions
and
0 deletions
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 | +} | ... | ... |