diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/autocode/MesMdAutoCodePartMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/autocode/MesMdAutoCodePartMapper.java index 9b7a5df..ec9107d 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/autocode/MesMdAutoCodePartMapper.java +++ b/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; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO; +import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodePartTypeEnum; import org.apache.ibatis.annotations.Mapper; import java.util.List; @@ -21,6 +22,10 @@ public interface MesMdAutoCodePartMapper extends BaseMapperX selectFixedCharPartList() { + return selectList(MesMdAutoCodePartDO::getType, MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType()); + } + default void deleteByRuleId(Long ruleId) { delete(MesMdAutoCodePartDO::getRuleId, ruleId); } diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java index 25bbb04..bc19c6d 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java @@ -121,6 +121,7 @@ public interface ErrorCodeConstants { ErrorCode AUTO_CODE_REDIS_ERROR = new ErrorCode(1_040_110_003, "编码生成服务不可用,请稍后重试"); ErrorCode AUTO_CODE_GENERATE_FAILED = new ErrorCode(1_040_110_004, "编码生成失败"); ErrorCode AUTO_CODE_PART_SERIAL_NUMBER_DUPLICATE = new ErrorCode(1_040_110_005, "流水号分段只能存在一个"); + ErrorCode AUTO_CODE_PART_FIXED_CHAR_DUPLICATE = new ErrorCode(1_040_110_006, "固定字符与其他编码规则相同,请修改固定字符"); // ========== MES 日历排班-计划班次(1-040-200-000) ========== ErrorCode CAL_PLAN_SHIFT_NOT_EXISTS = new ErrorCode(1_040_200_000, "计划班次不存在"); diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodePartServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodePartServiceImpl.java index fb9e459..bf5388d 100644 --- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodePartServiceImpl.java +++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodePartServiceImpl.java @@ -1,18 +1,27 @@ package cn.iocoder.yudao.module.mes.service.md.autocode; import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.mes.controller.admin.md.autocode.vo.part.MesMdAutoCodePartSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO; import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodePartMapper; +import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodeRuleMapper; import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodePartTypeEnum; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_FIXED_CHAR_DUPLICATE; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_NOT_EXISTS; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_SERIAL_NUMBER_DUPLICATE; @@ -29,6 +38,9 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService { private MesMdAutoCodePartMapper partMapper; @Resource + private MesMdAutoCodeRuleMapper ruleMapper; + + @Resource private MesMdAutoCodeRuleService ruleService; @Override @@ -37,6 +49,8 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService { ruleService.validateAutoCodeRuleExists(createReqVO.getRuleId()); // 校验流水号分段唯一性 validateSerialNumberPartUnique(null, createReqVO.getRuleId(), createReqVO.getType()); + // 校验固定字符(编码前缀)与其他规则不重复 + validateFixedCharUnique(null, createReqVO); // 插入 MesMdAutoCodePartDO part = BeanUtils.toBean(createReqVO, MesMdAutoCodePartDO.class); @@ -52,6 +66,8 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService { ruleService.validateAutoCodeRuleExists(updateReqVO.getRuleId()); // 校验流水号分段唯一性 validateSerialNumberPartUnique(updateReqVO.getId(), updateReqVO.getRuleId(), updateReqVO.getType()); + // 校验固定字符(编码前缀)与其他规则不重复 + validateFixedCharUnique(updateReqVO.getId(), updateReqVO); // 更新 MesMdAutoCodePartDO updateObj = BeanUtils.toBean(updateReqVO, MesMdAutoCodePartDO.class); @@ -101,4 +117,45 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService { } } + /** + * 校验固定字符(编码前缀)与其他编码规则不重复 + * + * 规则的前缀=该规则全部固定字符分段(按分段排序、截取到分段长度内的有效值)拼接。 + * 两条规则前缀相同(含均无固定字符分段)时,生成的编码无法区分归属业务,保存时拒绝, + * 从源头避免不同规则生成相同编码;生成编码时的全局查重仍保留,兜底存量配置的冲突。 + */ + private void validateFixedCharUnique(Long id, MesMdAutoCodePartSaveReqVO reqVO) { + // 1. 全租户固定字符分段,按规则分组 + Map> fixedPartsByRuleId = partMapper.selectFixedCharPartList().stream() + .collect(Collectors.groupingBy(MesMdAutoCodePartDO::getRuleId)); + // 2. 本次保存后,本规则的前缀指纹(排除正在修改的分段,加入本次保存的分段) + List myFixedParts = new ArrayList<>( + fixedPartsByRuleId.getOrDefault(reqVO.getRuleId(), Collections.emptyList())); + myFixedParts.removeIf(part -> ObjUtil.equal(id, part.getId())); + if (MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType().equals(reqVO.getType())) { + myFixedParts.add(BeanUtils.toBean(reqVO, MesMdAutoCodePartDO.class)); + } + String myFingerprint = buildFixedCharFingerprint(myFixedParts); + // 3. 与租户内其他未删除规则的前缀指纹逐一比对 + for (MesMdAutoCodeRuleDO rule : ruleMapper.selectList()) { + if (ObjUtil.equal(rule.getId(), reqVO.getRuleId())) { + continue; + } + List otherFixedParts = fixedPartsByRuleId.getOrDefault(rule.getId(), Collections.emptyList()); + if (myFingerprint.equals(buildFixedCharFingerprint(otherFixedParts))) { + throw exception(AUTO_CODE_PART_FIXED_CHAR_DUPLICATE); + } + } + } + + /** + * 构建前缀指纹:固定字符分段按分段排序,拼接截取到分段长度内的固定字符(空值按空串) + */ + private String buildFixedCharFingerprint(List fixedParts) { + return fixedParts.stream() + .sorted(Comparator.comparing(MesMdAutoCodePartDO::getSort)) + .map(part -> StrUtil.sub(StrUtil.emptyToDefault(part.getFixCharacter(), ""), 0, part.getLength())) + .collect(Collectors.joining()); + } + } diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodePartServiceImplTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodePartServiceImplTest.java new file mode 100644 index 0000000..032d19a --- /dev/null +++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodePartServiceImplTest.java @@ -0,0 +1,157 @@ +package cn.iocoder.yudao.module.mes.service.md.autocode; + +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.module.mes.controller.admin.md.autocode.vo.part.MesMdAutoCodePartSaveReqVO; +import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO; +import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO; +import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodePartMapper; +import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodeRuleMapper; +import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodePartTypeEnum; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link MesMdAutoCodePartServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +@ExtendWith(MockitoExtension.class) +public class MesMdAutoCodePartServiceImplTest { + + @InjectMocks + private MesMdAutoCodePartServiceImpl partService; + + @Mock + private MesMdAutoCodePartMapper partMapper; + @Mock + private MesMdAutoCodeRuleMapper ruleMapper; + @Mock + private MesMdAutoCodeRuleService ruleService; + + private static MesMdAutoCodePartDO fixedPart(Long id, Long ruleId, Integer sort, String fixCharacter, Integer length) { + return MesMdAutoCodePartDO.builder() + .id(id).ruleId(ruleId).sort(sort) + .type(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType()) + .length(length).fixCharacter(fixCharacter).build(); + } + + private static MesMdAutoCodePartSaveReqVO fixedReqVO(Long ruleId, Integer sort, String fixCharacter, Integer length) { + MesMdAutoCodePartSaveReqVO reqVO = new MesMdAutoCodePartSaveReqVO(); + reqVO.setRuleId(ruleId); + reqVO.setSort(sort); + reqVO.setType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType()); + reqVO.setLength(length); + reqVO.setFixCharacter(fixCharacter); + return reqVO; + } + + @Test + public void testCreateAutoCodePart_fixedCharDuplicate() { + // 准备:其他规则已占用相同前缀 "ITEM_" + when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList( + fixedPart(1L, 1L, 1, "ITEM_", 5))); + when(ruleMapper.selectList()).thenReturn(Arrays.asList( + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); + + // 断言:保存被拒,且未插入 + ServiceException ex = assertThrows(ServiceException.class, + () -> partService.createAutoCodePart(fixedReqVO(10L, 1, "ITEM_", 5))); + assertEquals(1040110006, ex.getCode()); + verify(partMapper, never()).insert(org.mockito.ArgumentMatchers.any()); + } + + @Test + public void testCreateAutoCodePart_fixedCharUnique() { + // 准备:其他规则前缀 "ITEM_",本次保存 "SN_" + when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList( + fixedPart(1L, 1L, 1, "ITEM_", 5))); + when(ruleMapper.selectList()).thenReturn(Arrays.asList( + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); + + // 断言:保存成功 + partService.createAutoCodePart(fixedReqVO(10L, 1, "SN_", 3)); + verify(partMapper).insert(org.mockito.ArgumentMatchers.any()); + } + + @Test + public void testCreateAutoCodePart_fixedCharTruncatedDuplicate() { + // 准备:其他规则固定字符 "IT_X" 长度 2(生成时截断为 "IT") + when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList( + fixedPart(1L, 1L, 1, "IT_X", 2))); + when(ruleMapper.selectList()).thenReturn(Arrays.asList( + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); + + // 断言:本次保存 "IT" 截断后同为 "IT",保存被拒 + ServiceException ex = assertThrows(ServiceException.class, + () -> partService.createAutoCodePart(fixedReqVO(10L, 1, "IT", 4))); + assertEquals(1040110006, ex.getCode()); + verify(partMapper, never()).insert(org.mockito.ArgumentMatchers.any()); + } + + @Test + public void testCreateAutoCodePart_bothRulesWithoutFixedChar() { + // 准备:租户内另一规则无任何固定字符分段(前缀为空串),本次保存流水号分段(本规则也无固定字符) + when(partMapper.selectFixedCharPartList()).thenReturn(Collections.emptyList()); + when(ruleMapper.selectList()).thenReturn(Arrays.asList( + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); + + MesMdAutoCodePartSaveReqVO reqVO = new MesMdAutoCodePartSaveReqVO(); + reqVO.setRuleId(10L); + reqVO.setSort(1); + reqVO.setType(MesMdAutoCodePartTypeEnum.SERIAL_NUMBER.getType()); + reqVO.setLength(4); + reqVO.setSerialStartNo(1); + + // 断言:两条规则前缀均为空(生成的编码无法区分业务),保存被拒 + ServiceException ex = assertThrows(ServiceException.class, + () -> partService.createAutoCodePart(reqVO)); + assertEquals(1040110006, ex.getCode()); + verify(partMapper, never()).insert(org.mockito.ArgumentMatchers.any()); + } + + @Test + public void testUpdateAutoCodePart_fixedCharBecomesDuplicate() { + // 准备:本规则已有固定字符 "OLD_",修改为与其他规则相同的 "ITEM_" + MesMdAutoCodePartDO myPart = fixedPart(100L, 10L, 1, "OLD_", 4); + when(partMapper.selectById(100L)).thenReturn(myPart); + when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList( + myPart, fixedPart(1L, 1L, 1, "ITEM_", 5))); + when(ruleMapper.selectList()).thenReturn(Arrays.asList( + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); + + // 断言:保存被拒,且未更新 + ServiceException ex = assertThrows(ServiceException.class, + () -> partService.updateAutoCodePart(fixedReqVO(10L, 1, "ITEM_", 5).setId(100L))); + assertEquals(1040110006, ex.getCode()); + verify(partMapper, never()).updateById(org.mockito.ArgumentMatchers.any()); + } + + @Test + public void testUpdateAutoCodePart_fixedCharBecomesUnique() { + // 准备:本规则已有固定字符 "ITEM_"(与其他规则重复),修改为 "SN_" + MesMdAutoCodePartDO myPart = fixedPart(100L, 10L, 1, "ITEM_", 5); + when(partMapper.selectById(100L)).thenReturn(myPart); + when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList( + myPart, fixedPart(1L, 1L, 1, "WO_", 3))); + when(ruleMapper.selectList()).thenReturn(Arrays.asList( + new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); + + // 断言:修改后前缀不再重复,保存成功 + partService.updateAutoCodePart(fixedReqVO(10L, 1, "SN_", 3).setId(100L)); + verify(partMapper).updateById(org.mockito.ArgumentMatchers.any()); + } + +}