Commit 6734be4f8a1092c5bd68dc97275e4a4508186fa7
1 parent
f938ee11
refactor: 统一日期解析、优惠券和 MES 修复代码风格
Showing
8 changed files
with
77 additions
and
68 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializer.java
| @@ -11,6 +11,7 @@ import java.io.IOException; | @@ -11,6 +11,7 @@ import java.io.IOException; | ||
| 11 | import java.time.Instant; | 11 | import java.time.Instant; |
| 12 | import java.time.LocalDateTime; | 12 | import java.time.LocalDateTime; |
| 13 | import java.time.format.DateTimeFormatter; | 13 | import java.time.format.DateTimeFormatter; |
| 14 | +import java.util.regex.Pattern; | ||
| 14 | 15 | ||
| 15 | /** | 16 | /** |
| 16 | * LocalDateTime 反序列化器,支持毫秒时间戳、常见日期时间字符串和 ISO-8601 日期。 | 17 | * LocalDateTime 反序列化器,支持毫秒时间戳、常见日期时间字符串和 ISO-8601 日期。 |
| @@ -21,6 +22,8 @@ public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDa | @@ -21,6 +22,8 @@ public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDa | ||
| 21 | 22 | ||
| 22 | public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer(); | 23 | public static final TimestampLocalDateTimeDeserializer INSTANCE = new TimestampLocalDateTimeDeserializer(); |
| 23 | 24 | ||
| 25 | + private static final Pattern TIMESTAMP_PATTERN = Pattern.compile("-?\\d+"); | ||
| 26 | + | ||
| 24 | @Override | 27 | @Override |
| 25 | public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { | 28 | public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { |
| 26 | String text = p.getText(); | 29 | String text = p.getText(); |
| @@ -28,14 +31,16 @@ public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDa | @@ -28,14 +31,16 @@ public class TimestampLocalDateTimeDeserializer extends JsonDeserializer<LocalDa | ||
| 28 | return null; | 31 | return null; |
| 29 | } | 32 | } |
| 30 | text = text.trim(); | 33 | text = text.trim(); |
| 31 | - if (text.matches("-?\\d+")) { | 34 | + // 情况一:毫秒时间戳 |
| 35 | + if (TIMESTAMP_PATTERN.matcher(text).matches()) { | ||
| 32 | return LocalDateTimeUtil.of(Long.parseLong(text)); | 36 | return LocalDateTimeUtil.of(Long.parseLong(text)); |
| 33 | } | 37 | } |
| 34 | - // Hutool 5.x 的自动解析会忽略部分负时区偏移,并截断纳秒。 | 38 | + // 情况二:ISO-8601 日期;标准解析保留时区偏移与纳秒精度 |
| 35 | if (text.contains("T")) { | 39 | if (text.contains("T")) { |
| 36 | return LocalDateTimeUtil.of(DateTimeFormatter.ISO_DATE_TIME.parseBest( | 40 | return LocalDateTimeUtil.of(DateTimeFormatter.ISO_DATE_TIME.parseBest( |
| 37 | text, Instant::from, LocalDateTime::from)); | 41 | text, Instant::from, LocalDateTime::from)); |
| 38 | } | 42 | } |
| 43 | + // 情况三:常见日期时间字符串 | ||
| 39 | return LocalDateTimeUtil.of(DateUtil.parse(text).toInstant()); | 44 | return LocalDateTimeUtil.of(DateUtil.parse(text).toInstant()); |
| 40 | } | 45 | } |
| 41 | 46 |
yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/json/databind/TimestampLocalDateTimeDeserializerTest.java
| @@ -4,30 +4,28 @@ import com.fasterxml.jackson.databind.ObjectMapper; | @@ -4,30 +4,28 @@ import com.fasterxml.jackson.databind.ObjectMapper; | ||
| 4 | import com.fasterxml.jackson.databind.module.SimpleModule; | 4 | import com.fasterxml.jackson.databind.module.SimpleModule; |
| 5 | import org.junit.jupiter.api.Test; | 5 | import org.junit.jupiter.api.Test; |
| 6 | 6 | ||
| 7 | +import java.time.Instant; | ||
| 7 | import java.time.LocalDateTime; | 8 | import java.time.LocalDateTime; |
| 9 | +import java.time.OffsetDateTime; | ||
| 10 | +import java.time.ZoneId; | ||
| 8 | 11 | ||
| 9 | import static org.junit.jupiter.api.Assertions.assertEquals; | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; |
| 10 | -import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| 11 | import static org.junit.jupiter.api.Assertions.assertNull; | 13 | import static org.junit.jupiter.api.Assertions.assertNull; |
| 14 | +import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| 12 | 15 | ||
| 13 | /** | 16 | /** |
| 14 | * {@link TimestampLocalDateTimeDeserializer} 的单元测试 | 17 | * {@link TimestampLocalDateTimeDeserializer} 的单元测试 |
| 15 | * | 18 | * |
| 16 | - * 覆盖全部在用格式(epoch 毫秒 / 常见字符串 / ISO / 纯日期), | ||
| 17 | - * 以及非法输入抛清晰错误(不再静默落 1970-01-01)。 | ||
| 18 | - * | ||
| 19 | * @author 芋道源码 | 19 | * @author 芋道源码 |
| 20 | */ | 20 | */ |
| 21 | public class TimestampLocalDateTimeDeserializerTest { | 21 | public class TimestampLocalDateTimeDeserializerTest { |
| 22 | 22 | ||
| 23 | - private static final ObjectMapper MAPPER; | 23 | + private static final ObjectMapper MAPPER = new ObjectMapper(); |
| 24 | 24 | ||
| 25 | static { | 25 | static { |
| 26 | - ObjectMapper mapper = new ObjectMapper(); | ||
| 27 | SimpleModule module = new SimpleModule(); | 26 | SimpleModule module = new SimpleModule(); |
| 28 | module.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); | 27 | module.addDeserializer(LocalDateTime.class, TimestampLocalDateTimeDeserializer.INSTANCE); |
| 29 | - mapper.registerModule(module); | ||
| 30 | - MAPPER = mapper; | 28 | + MAPPER.registerModule(module); |
| 31 | } | 29 | } |
| 32 | 30 | ||
| 33 | private LocalDateTime parse(String json) throws Exception { | 31 | private LocalDateTime parse(String json) throws Exception { |
| @@ -37,13 +35,15 @@ public class TimestampLocalDateTimeDeserializerTest { | @@ -37,13 +35,15 @@ public class TimestampLocalDateTimeDeserializerTest { | ||
| 37 | @Test | 35 | @Test |
| 38 | public void testEpochMilliNumber() throws Exception { | 36 | public void testEpochMilliNumber() throws Exception { |
| 39 | // 1700000000000 = 2023-11-14T22:13:20Z(本地时区换算后) | 37 | // 1700000000000 = 2023-11-14T22:13:20Z(本地时区换算后) |
| 40 | - LocalDateTime expected = LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(1700000000000L), java.time.ZoneId.systemDefault()); | 38 | + LocalDateTime expected = LocalDateTime.ofInstant(Instant.ofEpochMilli(1700000000000L), |
| 39 | + ZoneId.systemDefault()); | ||
| 41 | assertEquals(expected, parse("1700000000000")); | 40 | assertEquals(expected, parse("1700000000000")); |
| 42 | } | 41 | } |
| 43 | 42 | ||
| 44 | @Test | 43 | @Test |
| 45 | public void testEpochMilliString() throws Exception { | 44 | public void testEpochMilliString() throws Exception { |
| 46 | - LocalDateTime expected = LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(1700000000000L), java.time.ZoneId.systemDefault()); | 45 | + LocalDateTime expected = LocalDateTime.ofInstant(Instant.ofEpochMilli(1700000000000L), |
| 46 | + ZoneId.systemDefault()); | ||
| 47 | assertEquals(expected, parse("\"1700000000000\"")); | 47 | assertEquals(expected, parse("\"1700000000000\"")); |
| 48 | } | 48 | } |
| 49 | 49 | ||
| @@ -71,24 +71,24 @@ public class TimestampLocalDateTimeDeserializerTest { | @@ -71,24 +71,24 @@ public class TimestampLocalDateTimeDeserializerTest { | ||
| 71 | @Test | 71 | @Test |
| 72 | public void testIsoWithOffset() throws Exception { | 72 | public void testIsoWithOffset() throws Exception { |
| 73 | // 前端 Date 对象序列化:带 Z(UTC)。2026-09-11T00:00:00Z 换算到系统时区 | 73 | // 前端 Date 对象序列化:带 Z(UTC)。2026-09-11T00:00:00Z 换算到系统时区 |
| 74 | - LocalDateTime expected = java.time.OffsetDateTime.parse("2026-09-11T00:00:00Z") | ||
| 75 | - .atZoneSameInstant(java.time.ZoneId.systemDefault()).toLocalDateTime(); | 74 | + LocalDateTime expected = OffsetDateTime.parse("2026-09-11T00:00:00Z") |
| 75 | + .atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); | ||
| 76 | assertEquals(expected, parse("\"2026-09-11T00:00:00Z\"")); | 76 | assertEquals(expected, parse("\"2026-09-11T00:00:00Z\"")); |
| 77 | } | 77 | } |
| 78 | 78 | ||
| 79 | @Test | 79 | @Test |
| 80 | public void testIsoWithOffsetMillis() throws Exception { | 80 | public void testIsoWithOffsetMillis() throws Exception { |
| 81 | // JSON.stringify(Date) 实际产物:带毫秒 + Z(无 value-format 的 Date 对象控件提交格式) | 81 | // JSON.stringify(Date) 实际产物:带毫秒 + Z(无 value-format 的 Date 对象控件提交格式) |
| 82 | - LocalDateTime expected = java.time.OffsetDateTime.parse("2026-09-11T00:00:00.000Z") | ||
| 83 | - .atZoneSameInstant(java.time.ZoneId.systemDefault()).toLocalDateTime(); | 82 | + LocalDateTime expected = OffsetDateTime.parse("2026-09-11T00:00:00.000Z") |
| 83 | + .atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); | ||
| 84 | assertEquals(expected, parse("\"2026-09-11T00:00:00.000Z\"")); | 84 | assertEquals(expected, parse("\"2026-09-11T00:00:00.000Z\"")); |
| 85 | } | 85 | } |
| 86 | 86 | ||
| 87 | @Test | 87 | @Test |
| 88 | public void testIsoWithOffsetPlus() throws Exception { | 88 | public void testIsoWithOffsetPlus() throws Exception { |
| 89 | // 带 +08:00 时区偏移的形式 | 89 | // 带 +08:00 时区偏移的形式 |
| 90 | - LocalDateTime expected = java.time.OffsetDateTime.parse("2026-09-11T08:00:00.000+08:00") | ||
| 91 | - .atZoneSameInstant(java.time.ZoneId.systemDefault()).toLocalDateTime(); | 90 | + LocalDateTime expected = OffsetDateTime.parse("2026-09-11T08:00:00.000+08:00") |
| 91 | + .atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); | ||
| 92 | assertEquals(expected, parse("\"2026-09-11T08:00:00.000+08:00\"")); | 92 | assertEquals(expected, parse("\"2026-09-11T08:00:00.000+08:00\"")); |
| 93 | } | 93 | } |
| 94 | 94 | ||
| @@ -99,16 +99,16 @@ public class TimestampLocalDateTimeDeserializerTest { | @@ -99,16 +99,16 @@ public class TimestampLocalDateTimeDeserializerTest { | ||
| 99 | 99 | ||
| 100 | @Test | 100 | @Test |
| 101 | public void testIsoWithNegativeOffsetAndMillis() throws Exception { | 101 | public void testIsoWithNegativeOffsetAndMillis() throws Exception { |
| 102 | - LocalDateTime expected = java.time.OffsetDateTime.parse("2026-09-11T23:30:00.123-05:30") | ||
| 103 | - .atZoneSameInstant(java.time.ZoneId.systemDefault()).toLocalDateTime(); | 102 | + LocalDateTime expected = OffsetDateTime.parse("2026-09-11T23:30:00.123-05:30") |
| 103 | + .atZoneSameInstant(ZoneId.systemDefault()).toLocalDateTime(); | ||
| 104 | assertEquals(expected, parse("\"2026-09-11T23:30:00.123-05:30\"")); | 104 | assertEquals(expected, parse("\"2026-09-11T23:30:00.123-05:30\"")); |
| 105 | } | 105 | } |
| 106 | 106 | ||
| 107 | @Test | 107 | @Test |
| 108 | public void testZeroAndNegativeEpochMilli() throws Exception { | 108 | public void testZeroAndNegativeEpochMilli() throws Exception { |
| 109 | for (long millis : new long[]{0L, -1L}) { | 109 | for (long millis : new long[]{0L, -1L}) { |
| 110 | - LocalDateTime expected = LocalDateTime.ofInstant(java.time.Instant.ofEpochMilli(millis), | ||
| 111 | - java.time.ZoneId.systemDefault()); | 110 | + LocalDateTime expected = LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), |
| 111 | + ZoneId.systemDefault()); | ||
| 112 | assertEquals(expected, parse(Long.toString(millis))); | 112 | assertEquals(expected, parse(Long.toString(millis))); |
| 113 | assertEquals(expected, parse("\"" + millis + "\"")); | 113 | assertEquals(expected, parse("\"" + millis + "\"")); |
| 114 | } | 114 | } |
| @@ -134,9 +134,8 @@ public class TimestampLocalDateTimeDeserializerTest { | @@ -134,9 +134,8 @@ public class TimestampLocalDateTimeDeserializerTest { | ||
| 134 | 134 | ||
| 135 | @Test | 135 | @Test |
| 136 | public void testInvalidThrows() { | 136 | public void testInvalidThrows() { |
| 137 | - // 非法输入抛清晰错误,不再静默落 1970 | 137 | + // 调用,并断言异常 |
| 138 | assertThrows(Exception.class, () -> parse("\"not-a-date\"")); | 138 | assertThrows(Exception.class, () -> parse("\"not-a-date\"")); |
| 139 | - | ||
| 140 | } | 139 | } |
| 141 | 140 | ||
| 142 | } | 141 | } |
yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
| @@ -216,13 +216,16 @@ public class CouponServiceImpl implements CouponService { | @@ -216,13 +216,16 @@ public class CouponServiceImpl implements CouponService { | ||
| 216 | public void takeCouponByRegister(Long userId) { | 216 | public void takeCouponByRegister(Long userId) { |
| 217 | List<CouponTemplateDO> templates = couponTemplateService.getCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER); | 217 | List<CouponTemplateDO> templates = couponTemplateService.getCouponTemplateListByTakeType(CouponTakeTypeEnum.REGISTER); |
| 218 | // 1. 过滤不可领取(如已过期)的模板,避免单张异常中断整批 | 218 | // 1. 过滤不可领取(如已过期)的模板,避免单张异常中断整批 |
| 219 | - List<CouponTemplateDO> takeableTemplates = templates.stream() | ||
| 220 | - .filter(this::isCouponTemplateNotExpired) | ||
| 221 | - .collect(Collectors.toList()); | ||
| 222 | - if (takeableTemplates.size() < templates.size()) { | ||
| 223 | - log.info("[takeCouponByRegister][用户({}) 过滤 {} 张不可领取的模板]", userId, templates.size() - takeableTemplates.size()); | ||
| 224 | - } | ||
| 225 | - // 2. 逐个发放,单张失败不影响其余 | 219 | + List<CouponTemplateDO> takeableTemplates = filterList(templates, template -> { |
| 220 | + boolean expired = CouponTemplateValidityTypeEnum.DATE.getType().equals(template.getValidityType()) | ||
| 221 | + && LocalDateTimeUtils.beforeNow(template.getValidEndTime()); | ||
| 222 | + if (expired) { | ||
| 223 | + log.warn("[takeCouponByRegister][用户({}) 模板({}) 已过期,跳过发放]", userId, template.getId()); | ||
| 224 | + } | ||
| 225 | + return !expired; | ||
| 226 | + }); | ||
| 227 | + | ||
| 228 | + // 2.1 逐个发放,单张失败不影响其余 | ||
| 226 | int successCount = 0; | 229 | int successCount = 0; |
| 227 | for (CouponTemplateDO template : takeableTemplates) { | 230 | for (CouponTemplateDO template : takeableTemplates) { |
| 228 | try { | 231 | try { |
| @@ -232,17 +235,12 @@ public class CouponServiceImpl implements CouponService { | @@ -232,17 +235,12 @@ public class CouponServiceImpl implements CouponService { | ||
| 232 | log.error("[takeCouponByRegister][用户({}) 模板({}) 发放失败]", userId, template.getId(), e); | 235 | log.error("[takeCouponByRegister][用户({}) 模板({}) 发放失败]", userId, template.getId(), e); |
| 233 | } | 236 | } |
| 234 | } | 237 | } |
| 235 | - // 3. 应发全失败时留一条 ERROR 标记,供监控告警 | 238 | + // 2.2 应发全失败时留一条 ERROR 标记,供监控告警 |
| 236 | if (CollUtil.isNotEmpty(takeableTemplates) && successCount == 0) { | 239 | if (CollUtil.isNotEmpty(takeableTemplates) && successCount == 0) { |
| 237 | log.error("[takeCouponByRegister][用户({}) 全部 {} 张新人券发放失败]", userId, takeableTemplates.size()); | 240 | log.error("[takeCouponByRegister][用户({}) 全部 {} 张新人券发放失败]", userId, takeableTemplates.size()); |
| 238 | } | 241 | } |
| 239 | } | 242 | } |
| 240 | 243 | ||
| 241 | - private boolean isCouponTemplateNotExpired(CouponTemplateDO template) { | ||
| 242 | - return !CouponTemplateValidityTypeEnum.DATE.getType().equals(template.getValidityType()) | ||
| 243 | - || !LocalDateTimeUtils.beforeNow(template.getValidEndTime()); | ||
| 244 | - } | ||
| 245 | - | ||
| 246 | @Override | 244 | @Override |
| 247 | public int expireCoupon() { | 245 | public int expireCoupon() { |
| 248 | // 1. 查询待过期的优惠券 | 246 | // 1. 查询待过期的优惠券 |
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/autocode/MesMdAutoCodePartMapper.java
| @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.mes.dal.mysql.md.autocode; | @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.mes.dal.mysql.md.autocode; | ||
| 3 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; | 3 | import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; |
| 4 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; | 4 | import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; |
| 5 | import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO; | 5 | import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO; |
| 6 | -import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodePartTypeEnum; | ||
| 7 | import org.apache.ibatis.annotations.Mapper; | 6 | import org.apache.ibatis.annotations.Mapper; |
| 8 | 7 | ||
| 9 | import java.util.List; | 8 | import java.util.List; |
| @@ -22,8 +21,8 @@ public interface MesMdAutoCodePartMapper extends BaseMapperX<MesMdAutoCodePartDO | @@ -22,8 +21,8 @@ public interface MesMdAutoCodePartMapper extends BaseMapperX<MesMdAutoCodePartDO | ||
| 22 | .orderByAsc(MesMdAutoCodePartDO::getSort)); | 21 | .orderByAsc(MesMdAutoCodePartDO::getSort)); |
| 23 | } | 22 | } |
| 24 | 23 | ||
| 25 | - default List<MesMdAutoCodePartDO> selectFixedCharPartList() { | ||
| 26 | - return selectList(MesMdAutoCodePartDO::getType, MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType()); | 24 | + default List<MesMdAutoCodePartDO> selectByType(Integer type) { |
| 25 | + return selectList(MesMdAutoCodePartDO::getType, type); | ||
| 27 | } | 26 | } |
| 28 | 27 | ||
| 29 | default void deleteByRuleId(Long ruleId) { | 28 | default void deleteByRuleId(Long ruleId) { |
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodePartServiceImpl.java
| @@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.mes.controller.admin.md.autocode.vo.part.MesMdAut | @@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.mes.controller.admin.md.autocode.vo.part.MesMdAut | ||
| 7 | import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO; | 7 | import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO; |
| 8 | import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO; | 8 | import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO; |
| 9 | import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodePartMapper; | 9 | import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodePartMapper; |
| 10 | -import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodeRuleMapper; | ||
| 11 | import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodePartTypeEnum; | 10 | import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodePartTypeEnum; |
| 12 | import jakarta.annotation.Resource; | 11 | import jakarta.annotation.Resource; |
| 13 | import org.springframework.stereotype.Service; | 12 | import org.springframework.stereotype.Service; |
| @@ -21,6 +20,7 @@ import java.util.Map; | @@ -21,6 +20,7 @@ import java.util.Map; | ||
| 21 | import java.util.stream.Collectors; | 20 | import java.util.stream.Collectors; |
| 22 | 21 | ||
| 23 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 22 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 23 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMultiMap; | ||
| 24 | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_FIXED_CHAR_DUPLICATE; | 24 | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_FIXED_CHAR_DUPLICATE; |
| 25 | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_NOT_EXISTS; | 25 | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_NOT_EXISTS; |
| 26 | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_SERIAL_NUMBER_DUPLICATE; | 26 | import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.AUTO_CODE_PART_SERIAL_NUMBER_DUPLICATE; |
| @@ -36,10 +36,6 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService { | @@ -36,10 +36,6 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService { | ||
| 36 | 36 | ||
| 37 | @Resource | 37 | @Resource |
| 38 | private MesMdAutoCodePartMapper partMapper; | 38 | private MesMdAutoCodePartMapper partMapper; |
| 39 | - | ||
| 40 | - @Resource | ||
| 41 | - private MesMdAutoCodeRuleMapper ruleMapper; | ||
| 42 | - | ||
| 43 | @Resource | 39 | @Resource |
| 44 | private MesMdAutoCodeRuleService ruleService; | 40 | private MesMdAutoCodeRuleService ruleService; |
| 45 | 41 | ||
| @@ -125,10 +121,10 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService { | @@ -125,10 +121,10 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService { | ||
| 125 | * 从源头避免不同规则生成相同编码;生成编码时的全局查重仍保留,兜底存量配置的冲突。 | 121 | * 从源头避免不同规则生成相同编码;生成编码时的全局查重仍保留,兜底存量配置的冲突。 |
| 126 | */ | 122 | */ |
| 127 | private void validateFixedCharUnique(Long id, MesMdAutoCodePartSaveReqVO reqVO) { | 123 | 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. 本次保存后,本规则的前缀指纹(排除正在修改的分段,加入本次保存的分段) | 124 | + // 1.1 所有固定字符分段,按规则分组 |
| 125 | + Map<Long, List<MesMdAutoCodePartDO>> fixedPartsByRuleId = convertMultiMap( | ||
| 126 | + partMapper.selectByType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType()), MesMdAutoCodePartDO::getRuleId); | ||
| 127 | + // 1.2 本次保存后,本规则的前缀指纹(排除正在修改的分段,加入本次保存的分段) | ||
| 132 | List<MesMdAutoCodePartDO> myFixedParts = new ArrayList<>( | 128 | List<MesMdAutoCodePartDO> myFixedParts = new ArrayList<>( |
| 133 | fixedPartsByRuleId.getOrDefault(reqVO.getRuleId(), Collections.emptyList())); | 129 | fixedPartsByRuleId.getOrDefault(reqVO.getRuleId(), Collections.emptyList())); |
| 134 | myFixedParts.removeIf(part -> ObjUtil.equal(id, part.getId())); | 130 | myFixedParts.removeIf(part -> ObjUtil.equal(id, part.getId())); |
| @@ -136,20 +132,23 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService { | @@ -136,20 +132,23 @@ public class MesMdAutoCodePartServiceImpl implements MesMdAutoCodePartService { | ||
| 136 | myFixedParts.add(BeanUtils.toBean(reqVO, MesMdAutoCodePartDO.class)); | 132 | myFixedParts.add(BeanUtils.toBean(reqVO, MesMdAutoCodePartDO.class)); |
| 137 | } | 133 | } |
| 138 | String myFingerprint = buildFixedCharFingerprint(myFixedParts); | 134 | String myFingerprint = buildFixedCharFingerprint(myFixedParts); |
| 139 | - // 3. 与租户内其他未删除规则的前缀指纹逐一比对 | ||
| 140 | - for (MesMdAutoCodeRuleDO rule : ruleMapper.selectList()) { | 135 | + |
| 136 | + // 2. 与租户内其他未删除规则的前缀指纹逐一比对 | ||
| 137 | + List<MesMdAutoCodeRuleDO> rules = ruleService.getAutoCodeRuleList(); | ||
| 138 | + for (MesMdAutoCodeRuleDO rule : rules) { | ||
| 141 | if (ObjUtil.equal(rule.getId(), reqVO.getRuleId())) { | 139 | if (ObjUtil.equal(rule.getId(), reqVO.getRuleId())) { |
| 142 | continue; | 140 | continue; |
| 143 | } | 141 | } |
| 144 | List<MesMdAutoCodePartDO> otherFixedParts = fixedPartsByRuleId.getOrDefault(rule.getId(), Collections.emptyList()); | 142 | List<MesMdAutoCodePartDO> otherFixedParts = fixedPartsByRuleId.getOrDefault(rule.getId(), Collections.emptyList()); |
| 145 | - if (myFingerprint.equals(buildFixedCharFingerprint(otherFixedParts))) { | 143 | + String otherFingerprint = buildFixedCharFingerprint(otherFixedParts); |
| 144 | + if (myFingerprint.equals(otherFingerprint)) { | ||
| 146 | throw exception(AUTO_CODE_PART_FIXED_CHAR_DUPLICATE); | 145 | throw exception(AUTO_CODE_PART_FIXED_CHAR_DUPLICATE); |
| 147 | } | 146 | } |
| 148 | } | 147 | } |
| 149 | } | 148 | } |
| 150 | 149 | ||
| 151 | /** | 150 | /** |
| 152 | - * 构建前缀指纹:固定字符分段按分段排序,拼接截取到分段长度内的固定字符(空值按空串) | 151 | + * 构建固定字符分段的前缀指纹:按分段排序,拼接截取到分段长度内的固定字符 |
| 153 | */ | 152 | */ |
| 154 | private String buildFixedCharFingerprint(List<MesMdAutoCodePartDO> fixedParts) { | 153 | private String buildFixedCharFingerprint(List<MesMdAutoCodePartDO> fixedParts) { |
| 155 | return fixedParts.stream() | 154 | return fixedParts.stream() |
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodeRuleService.java
| @@ -46,6 +46,13 @@ public interface MesMdAutoCodeRuleService { | @@ -46,6 +46,13 @@ public interface MesMdAutoCodeRuleService { | ||
| 46 | MesMdAutoCodeRuleDO getAutoCodeRule(Long id); | 46 | MesMdAutoCodeRuleDO getAutoCodeRule(Long id); |
| 47 | 47 | ||
| 48 | /** | 48 | /** |
| 49 | + * 获得编码规则列表 | ||
| 50 | + * | ||
| 51 | + * @return 编码规则列表 | ||
| 52 | + */ | ||
| 53 | + List<MesMdAutoCodeRuleDO> getAutoCodeRuleList(); | ||
| 54 | + | ||
| 55 | + /** | ||
| 49 | * 获得编码规则分页 | 56 | * 获得编码规则分页 |
| 50 | * | 57 | * |
| 51 | * @param pageReqVO 分页查询 | 58 | * @param pageReqVO 分页查询 |
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodeRuleServiceImpl.java
| @@ -73,6 +73,11 @@ public class MesMdAutoCodeRuleServiceImpl implements MesMdAutoCodeRuleService { | @@ -73,6 +73,11 @@ public class MesMdAutoCodeRuleServiceImpl implements MesMdAutoCodeRuleService { | ||
| 73 | } | 73 | } |
| 74 | 74 | ||
| 75 | @Override | 75 | @Override |
| 76 | + public List<MesMdAutoCodeRuleDO> getAutoCodeRuleList() { | ||
| 77 | + return ruleMapper.selectList(); | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + @Override | ||
| 76 | public PageResult<MesMdAutoCodeRuleDO> getAutoCodeRulePage(MesMdAutoCodeRulePageReqVO pageReqVO) { | 81 | public PageResult<MesMdAutoCodeRuleDO> getAutoCodeRulePage(MesMdAutoCodeRulePageReqVO pageReqVO) { |
| 77 | return ruleMapper.selectPage(pageReqVO); | 82 | return ruleMapper.selectPage(pageReqVO); |
| 78 | } | 83 | } |
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/md/autocode/MesMdAutoCodePartServiceImplTest.java
| @@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.mes.controller.admin.md.autocode.vo.part.MesMdAut | @@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.mes.controller.admin.md.autocode.vo.part.MesMdAut | ||
| 5 | import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO; | 5 | import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodePartDO; |
| 6 | import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO; | 6 | import cn.iocoder.yudao.module.mes.dal.dataobject.md.autocode.MesMdAutoCodeRuleDO; |
| 7 | import cn.iocoder.yudao.module.mes.dal.mysql.md.autocode.MesMdAutoCodePartMapper; | 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; | 8 | import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodePartTypeEnum; |
| 10 | import org.junit.jupiter.api.Test; | 9 | import org.junit.jupiter.api.Test; |
| 11 | import org.junit.jupiter.api.extension.ExtendWith; | 10 | import org.junit.jupiter.api.extension.ExtendWith; |
| @@ -37,8 +36,6 @@ public class MesMdAutoCodePartServiceImplTest { | @@ -37,8 +36,6 @@ public class MesMdAutoCodePartServiceImplTest { | ||
| 37 | @Mock | 36 | @Mock |
| 38 | private MesMdAutoCodePartMapper partMapper; | 37 | private MesMdAutoCodePartMapper partMapper; |
| 39 | @Mock | 38 | @Mock |
| 40 | - private MesMdAutoCodeRuleMapper ruleMapper; | ||
| 41 | - @Mock | ||
| 42 | private MesMdAutoCodeRuleService ruleService; | 39 | private MesMdAutoCodeRuleService ruleService; |
| 43 | 40 | ||
| 44 | private static MesMdAutoCodePartDO fixedPart(Long id, Long ruleId, Integer sort, String fixCharacter, Integer length) { | 41 | private static MesMdAutoCodePartDO fixedPart(Long id, Long ruleId, Integer sort, String fixCharacter, Integer length) { |
| @@ -61,9 +58,9 @@ public class MesMdAutoCodePartServiceImplTest { | @@ -61,9 +58,9 @@ public class MesMdAutoCodePartServiceImplTest { | ||
| 61 | @Test | 58 | @Test |
| 62 | public void testCreateAutoCodePart_fixedCharDuplicate() { | 59 | public void testCreateAutoCodePart_fixedCharDuplicate() { |
| 63 | // 准备:其他规则已占用相同前缀 "ITEM_" | 60 | // 准备:其他规则已占用相同前缀 "ITEM_" |
| 64 | - when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList( | 61 | + when(partMapper.selectByType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType())).thenReturn(Arrays.asList( |
| 65 | fixedPart(1L, 1L, 1, "ITEM_", 5))); | 62 | fixedPart(1L, 1L, 1, "ITEM_", 5))); |
| 66 | - when(ruleMapper.selectList()).thenReturn(Arrays.asList( | 63 | + when(ruleService.getAutoCodeRuleList()).thenReturn(Arrays.asList( |
| 67 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); | 64 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); |
| 68 | 65 | ||
| 69 | // 断言:保存被拒,且未插入 | 66 | // 断言:保存被拒,且未插入 |
| @@ -76,9 +73,9 @@ public class MesMdAutoCodePartServiceImplTest { | @@ -76,9 +73,9 @@ public class MesMdAutoCodePartServiceImplTest { | ||
| 76 | @Test | 73 | @Test |
| 77 | public void testCreateAutoCodePart_fixedCharUnique() { | 74 | public void testCreateAutoCodePart_fixedCharUnique() { |
| 78 | // 准备:其他规则前缀 "ITEM_",本次保存 "SN_" | 75 | // 准备:其他规则前缀 "ITEM_",本次保存 "SN_" |
| 79 | - when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList( | 76 | + when(partMapper.selectByType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType())).thenReturn(Arrays.asList( |
| 80 | fixedPart(1L, 1L, 1, "ITEM_", 5))); | 77 | fixedPart(1L, 1L, 1, "ITEM_", 5))); |
| 81 | - when(ruleMapper.selectList()).thenReturn(Arrays.asList( | 78 | + when(ruleService.getAutoCodeRuleList()).thenReturn(Arrays.asList( |
| 82 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); | 79 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); |
| 83 | 80 | ||
| 84 | // 断言:保存成功 | 81 | // 断言:保存成功 |
| @@ -89,9 +86,9 @@ public class MesMdAutoCodePartServiceImplTest { | @@ -89,9 +86,9 @@ public class MesMdAutoCodePartServiceImplTest { | ||
| 89 | @Test | 86 | @Test |
| 90 | public void testCreateAutoCodePart_fixedCharTruncatedDuplicate() { | 87 | public void testCreateAutoCodePart_fixedCharTruncatedDuplicate() { |
| 91 | // 准备:其他规则固定字符 "IT_X" 长度 2(生成时截断为 "IT") | 88 | // 准备:其他规则固定字符 "IT_X" 长度 2(生成时截断为 "IT") |
| 92 | - when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList( | 89 | + when(partMapper.selectByType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType())).thenReturn(Arrays.asList( |
| 93 | fixedPart(1L, 1L, 1, "IT_X", 2))); | 90 | fixedPart(1L, 1L, 1, "IT_X", 2))); |
| 94 | - when(ruleMapper.selectList()).thenReturn(Arrays.asList( | 91 | + when(ruleService.getAutoCodeRuleList()).thenReturn(Arrays.asList( |
| 95 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); | 92 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); |
| 96 | 93 | ||
| 97 | // 断言:本次保存 "IT" 截断后同为 "IT",保存被拒 | 94 | // 断言:本次保存 "IT" 截断后同为 "IT",保存被拒 |
| @@ -104,8 +101,8 @@ public class MesMdAutoCodePartServiceImplTest { | @@ -104,8 +101,8 @@ public class MesMdAutoCodePartServiceImplTest { | ||
| 104 | @Test | 101 | @Test |
| 105 | public void testCreateAutoCodePart_bothRulesWithoutFixedChar() { | 102 | public void testCreateAutoCodePart_bothRulesWithoutFixedChar() { |
| 106 | // 准备:租户内另一规则无任何固定字符分段(前缀为空串),本次保存流水号分段(本规则也无固定字符) | 103 | // 准备:租户内另一规则无任何固定字符分段(前缀为空串),本次保存流水号分段(本规则也无固定字符) |
| 107 | - when(partMapper.selectFixedCharPartList()).thenReturn(Collections.emptyList()); | ||
| 108 | - when(ruleMapper.selectList()).thenReturn(Arrays.asList( | 104 | + when(partMapper.selectByType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType())).thenReturn(Collections.emptyList()); |
| 105 | + when(ruleService.getAutoCodeRuleList()).thenReturn(Arrays.asList( | ||
| 109 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); | 106 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); |
| 110 | 107 | ||
| 111 | MesMdAutoCodePartSaveReqVO reqVO = new MesMdAutoCodePartSaveReqVO(); | 108 | MesMdAutoCodePartSaveReqVO reqVO = new MesMdAutoCodePartSaveReqVO(); |
| @@ -127,9 +124,9 @@ public class MesMdAutoCodePartServiceImplTest { | @@ -127,9 +124,9 @@ public class MesMdAutoCodePartServiceImplTest { | ||
| 127 | // 准备:本规则已有固定字符 "OLD_",修改为与其他规则相同的 "ITEM_" | 124 | // 准备:本规则已有固定字符 "OLD_",修改为与其他规则相同的 "ITEM_" |
| 128 | MesMdAutoCodePartDO myPart = fixedPart(100L, 10L, 1, "OLD_", 4); | 125 | MesMdAutoCodePartDO myPart = fixedPart(100L, 10L, 1, "OLD_", 4); |
| 129 | when(partMapper.selectById(100L)).thenReturn(myPart); | 126 | when(partMapper.selectById(100L)).thenReturn(myPart); |
| 130 | - when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList( | 127 | + when(partMapper.selectByType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType())).thenReturn(Arrays.asList( |
| 131 | myPart, fixedPart(1L, 1L, 1, "ITEM_", 5))); | 128 | myPart, fixedPart(1L, 1L, 1, "ITEM_", 5))); |
| 132 | - when(ruleMapper.selectList()).thenReturn(Arrays.asList( | 129 | + when(ruleService.getAutoCodeRuleList()).thenReturn(Arrays.asList( |
| 133 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); | 130 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); |
| 134 | 131 | ||
| 135 | // 断言:保存被拒,且未更新 | 132 | // 断言:保存被拒,且未更新 |
| @@ -144,9 +141,9 @@ public class MesMdAutoCodePartServiceImplTest { | @@ -144,9 +141,9 @@ public class MesMdAutoCodePartServiceImplTest { | ||
| 144 | // 准备:本规则已有固定字符 "ITEM_"(与其他规则重复),修改为 "SN_" | 141 | // 准备:本规则已有固定字符 "ITEM_"(与其他规则重复),修改为 "SN_" |
| 145 | MesMdAutoCodePartDO myPart = fixedPart(100L, 10L, 1, "ITEM_", 5); | 142 | MesMdAutoCodePartDO myPart = fixedPart(100L, 10L, 1, "ITEM_", 5); |
| 146 | when(partMapper.selectById(100L)).thenReturn(myPart); | 143 | when(partMapper.selectById(100L)).thenReturn(myPart); |
| 147 | - when(partMapper.selectFixedCharPartList()).thenReturn(Arrays.asList( | 144 | + when(partMapper.selectByType(MesMdAutoCodePartTypeEnum.FIXED_CHAR.getType())).thenReturn(Arrays.asList( |
| 148 | myPart, fixedPart(1L, 1L, 1, "WO_", 3))); | 145 | myPart, fixedPart(1L, 1L, 1, "WO_", 3))); |
| 149 | - when(ruleMapper.selectList()).thenReturn(Arrays.asList( | 146 | + when(ruleService.getAutoCodeRuleList()).thenReturn(Arrays.asList( |
| 150 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); | 147 | new MesMdAutoCodeRuleDO().setId(1L), new MesMdAutoCodeRuleDO().setId(10L))); |
| 151 | 148 | ||
| 152 | // 断言:修改后前缀不再重复,保存成功 | 149 | // 断言:修改后前缀不再重复,保存成功 |