Commit 4ee2d0f6edc5051a4d587e57d2367013a4fcec97
1 parent
09aa9ad9
fix(oa): 修复报销明细日期 JSON 处理器初始化
Showing
3 changed files
with
74 additions
and
14 deletions
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java
| 1 | 1 | package cn.iocoder.yudao.framework.mybatis.config; |
| 2 | 2 | |
| 3 | -import cn.hutool.core.collection.CollUtil; | |
| 4 | 3 | import cn.hutool.core.util.StrUtil; |
| 5 | 4 | import cn.iocoder.yudao.framework.common.util.json.JsonUtils; |
| 6 | 5 | import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler; |
| ... | ... | @@ -23,6 +22,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; |
| 23 | 22 | import org.apache.ibatis.annotations.Mapper; |
| 24 | 23 | import org.apache.ibatis.mapping.ResultMap; |
| 25 | 24 | import org.mybatis.spring.annotation.MapperScan; |
| 25 | +import org.springframework.beans.factory.ObjectProvider; | |
| 26 | +import org.springframework.beans.factory.SmartInitializingSingleton; | |
| 26 | 27 | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| 27 | 28 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| 28 | 29 | import org.springframework.context.annotation.Bean; |
| ... | ... | @@ -30,7 +31,6 @@ import org.springframework.context.annotation.Primary; |
| 30 | 31 | import org.springframework.core.env.ConfigurableEnvironment; |
| 31 | 32 | |
| 32 | 33 | import java.util.ArrayList; |
| 33 | -import java.util.List; | |
| 34 | 34 | import java.util.concurrent.TimeUnit; |
| 35 | 35 | |
| 36 | 36 | /** |
| ... | ... | @@ -49,6 +49,8 @@ public class YudaoMybatisAutoConfiguration { |
| 49 | 49 | (cache) -> cache.maximumSize(1024) |
| 50 | 50 | .expireAfterWrite(5, TimeUnit.SECONDS)) |
| 51 | 51 | ); |
| 52 | + // 特殊:JacksonTypeHandler 先使用 JsonUtils 的 ObjectMapper(支持 LocalDateTime),避免使用 MyBatis Plus 默认的 ObjectMapper | |
| 53 | + JacksonTypeHandler.setObjectMapper(JsonUtils.getObjectMapper()); | |
| 52 | 54 | } |
| 53 | 55 | |
| 54 | 56 | @Bean |
| ... | ... | @@ -101,15 +103,11 @@ public class YudaoMybatisAutoConfiguration { |
| 101 | 103 | throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType)); |
| 102 | 104 | } |
| 103 | 105 | |
| 104 | - @Bean // 特殊:返回结果使用 Object 而不用 JacksonTypeHandler 的原因,避免因为 JacksonTypeHandler 被 mybatis 全局使用! | |
| 105 | - public Object jacksonTypeHandler(List<ObjectMapper> objectMappers) { | |
| 106 | - // 特殊:设置 JacksonTypeHandler 的 ObjectMapper! | |
| 107 | - ObjectMapper objectMapper = CollUtil.getFirst(objectMappers); | |
| 108 | - if (objectMapper == null) { | |
| 109 | - objectMapper = JsonUtils.getObjectMapper(); | |
| 110 | - } | |
| 111 | - JacksonTypeHandler.setObjectMapper(objectMapper); | |
| 112 | - return new JacksonTypeHandler(Object.class); | |
| 106 | + @Bean | |
| 107 | + public SmartInitializingSingleton jacksonTypeHandlerInitializer(ObjectProvider<ObjectMapper> objectMappers) { | |
| 108 | + // 特殊:所有单例初始化后,设置 JacksonTypeHandler 的 ObjectMapper,优先唯一或 @Primary 的 ObjectMapper,否则使用 JsonUtils 的 | |
| 109 | + // 原因:避免延迟初始化未设置、多个 ObjectMapper 时取到不支持 LocalDateTime 的,且不在创建 SqlSessionFactory 时获取 ObjectMapper。https://t.zsxq.com/KmPNU | |
| 110 | + return () -> JacksonTypeHandler.setObjectMapper(objectMappers.getIfUnique(JsonUtils::getObjectMapper)); | |
| 113 | 111 | } |
| 114 | 112 | |
| 115 | 113 | } | ... | ... |
yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/reimbursement/OaReimbursementServiceImplTest.java
| 1 | 1 | package cn.iocoder.yudao.module.oa.service.reimbursement; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | +import cn.hutool.core.date.LocalDateTimeUtil; | |
| 4 | 5 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | 6 | import cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration; |
| 6 | 7 | import cn.iocoder.yudao.framework.security.core.LoginUser; |
| ... | ... | @@ -14,6 +15,8 @@ import cn.iocoder.yudao.module.oa.controller.admin.reimbursement.vo.OaReimbursem |
| 14 | 15 | import cn.iocoder.yudao.module.oa.dal.dataobject.reimbursement.OaReimbursementDO; |
| 15 | 16 | import cn.iocoder.yudao.module.oa.dal.mysql.reimbursement.OaReimbursementMapper; |
| 16 | 17 | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| 18 | +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | |
| 19 | +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; | |
| 17 | 20 | import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; |
| 18 | 21 | import com.fasterxml.jackson.databind.ObjectMapper; |
| 19 | 22 | import jakarta.annotation.Resource; |
| ... | ... | @@ -30,6 +33,7 @@ import java.math.BigDecimal; |
| 30 | 33 | import java.time.LocalDateTime; |
| 31 | 34 | import java.util.Arrays; |
| 32 | 35 | import java.util.Collections; |
| 36 | +import java.util.List; | |
| 33 | 37 | |
| 34 | 38 | import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; |
| 35 | 39 | import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; |
| ... | ... | @@ -61,8 +65,6 @@ public class OaReimbursementServiceImplTest extends BaseDbUnitTest { |
| 61 | 65 | |
| 62 | 66 | @BeforeEach |
| 63 | 67 | public void before() { |
| 64 | - // JSON 处理器为静态共享配置,每个用例恢复当前上下文的生产日期序列化配置 | |
| 65 | - JacksonTypeHandler.setObjectMapper(objectMapper); | |
| 66 | 68 | SecurityFrameworkUtils.setLoginUser(new LoginUser().setId(10L).setUserType(2), new MockHttpServletRequest()); |
| 67 | 69 | } |
| 68 | 70 | |
| ... | ... | @@ -126,6 +128,7 @@ public class OaReimbursementServiceImplTest extends BaseDbUnitTest { |
| 126 | 128 | assertEquals(new BigDecimal("12.34"), application.getTotalPrice()); |
| 127 | 129 | assertEquals(2, application.getInvoiceCount()); |
| 128 | 130 | assertEquals(1, CollUtil.getFirst(application.getItems()).getExpenseType()); |
| 131 | + assertEquals(LocalDateTime.of(2026, 9, 13, 8, 0), CollUtil.getFirst(application.getItems()).getExpenseTime()); | |
| 129 | 132 | verify(processInstanceApi).createProcessInstance(eq(10L), argThat(dto -> |
| 130 | 133 | "oa_reimbursement".equals(dto.getProcessDefinitionKey()) && id.toString().equals(dto.getBusinessKey()) |
| 131 | 134 | && !dto.getVariables().containsKey("title"))); |
| ... | ... | @@ -214,6 +217,42 @@ public class OaReimbursementServiceImplTest extends BaseDbUnitTest { |
| 214 | 217 | } |
| 215 | 218 | |
| 216 | 219 | @Test |
| 220 | + public void testGetReimbursementAndPage_itemsExpenseTime() { | |
| 221 | + // mock 数据 | |
| 222 | + Long id = reimbursementService.createReimbursement(randomReimbursementSaveReqVO()); | |
| 223 | + LocalDateTime expenseTime = LocalDateTime.of(2026, 9, 13, 8, 0); | |
| 224 | + | |
| 225 | + // 调用 | |
| 226 | + OaReimbursementDO reimbursement = reimbursementService.getReimbursement(id); | |
| 227 | + PageResult<OaReimbursementDO> page = reimbursementService.getReimbursementPage(10L, new OaReimbursementPageReqVO()); | |
| 228 | + | |
| 229 | + // 断言:使用 Spring 主 ObjectMapper,详情、列表读取明细时间,库中保持毫秒时间戳 | |
| 230 | + assertSame(objectMapper, JacksonTypeHandler.getObjectMapper()); | |
| 231 | + assertEquals(expenseTime, CollUtil.getFirst(reimbursement.getItems()).getExpenseTime()); | |
| 232 | + assertEquals(1L, page.getTotal()); | |
| 233 | + assertEquals(expenseTime, CollUtil.getFirst(CollUtil.getFirst(page.getList()).getItems()).getExpenseTime()); | |
| 234 | + List<String> items = reimbursementMapper.selectObjs(new LambdaQueryWrapper<OaReimbursementDO>() | |
| 235 | + .select(OaReimbursementDO::getItems).eq(OaReimbursementDO::getId, id)); | |
| 236 | + assertTrue(CollUtil.getFirst(items).contains("\"expenseTime\":" + LocalDateTimeUtil.toEpochMilli(expenseTime))); | |
| 237 | + } | |
| 238 | + | |
| 239 | + @Test | |
| 240 | + public void testGetReimbursementPage_itemsExpenseTimeString() { | |
| 241 | + // mock 数据:库中明细时间为可读字符串 | |
| 242 | + Long id = reimbursementService.createReimbursement(randomReimbursementSaveReqVO()); | |
| 243 | + reimbursementMapper.update(new LambdaUpdateWrapper<OaReimbursementDO>().eq(OaReimbursementDO::getId, id) | |
| 244 | + .setSql("items = {0}", "[{\"expenseTime\":\"2026-09-13 08:00:00\",\"expenseType\":1,\"price\":12.34}]")); | |
| 245 | + | |
| 246 | + // 调用 | |
| 247 | + PageResult<OaReimbursementDO> page = reimbursementService.getReimbursementPage(10L, new OaReimbursementPageReqVO()); | |
| 248 | + | |
| 249 | + // 断言 | |
| 250 | + assertEquals(1L, page.getTotal()); | |
| 251 | + assertEquals(LocalDateTime.of(2026, 9, 13, 8, 0), | |
| 252 | + CollUtil.getFirst(CollUtil.getFirst(page.getList()).getItems()).getExpenseTime()); | |
| 253 | + } | |
| 254 | + | |
| 255 | + @Test | |
| 217 | 256 | public void testUpdateReimbursementStatus_successAndRepeated() { |
| 218 | 257 | // mock 数据 |
| 219 | 258 | OaReimbursementDO application = randomReimbursementDO(); | ... | ... |
yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/travel/OaTravelReimbursementServiceImplTest.java
| ... | ... | @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.oa.service.travel; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | |
| 5 | 6 | import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; |
| 6 | 7 | import cn.iocoder.yudao.framework.security.core.LoginUser; |
| 7 | 8 | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| ... | ... | @@ -16,6 +17,7 @@ import cn.iocoder.yudao.module.oa.dal.redis.no.OaNoRedisDAO; |
| 16 | 17 | import cn.iocoder.yudao.module.system.api.dict.DictDataApi; |
| 17 | 18 | import cn.iocoder.yudao.module.system.api.user.AdminUserApi; |
| 18 | 19 | import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; |
| 20 | +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; | |
| 19 | 21 | import jakarta.annotation.Resource; |
| 20 | 22 | import jakarta.validation.ConstraintViolationException; |
| 21 | 23 | import org.junit.jupiter.api.AfterEach; |
| ... | ... | @@ -150,6 +152,7 @@ public class OaTravelReimbursementServiceImplTest extends BaseDbUnitTest { |
| 150 | 152 | OaTravelReimbursementDO record = travelReimbursementMapper.selectById(id); |
| 151 | 153 | assertEquals(1, record.getDays()); |
| 152 | 154 | assertEquals(reqVO.getItems().size(), record.getItems().size()); |
| 155 | + assertEquals(LocalDateTime.of(2026, 9, 14, 10, 0), CollUtil.getFirst(record.getItems()).getExpenseTime()); | |
| 153 | 156 | assertEquals(reqVO.getFileUrls(), record.getFileUrls()); |
| 154 | 157 | assertEquals(new BigDecimal("0.00"), record.getTotalPrice()); |
| 155 | 158 | assertEquals("TRAVEL-NEW", record.getNo()); |
| ... | ... | @@ -423,6 +426,25 @@ public class OaTravelReimbursementServiceImplTest extends BaseDbUnitTest { |
| 423 | 426 | } |
| 424 | 427 | |
| 425 | 428 | @Test |
| 429 | + public void testGetTravelReimbursementAndPage_itemsExpenseTime() { | |
| 430 | + // mock 数据 | |
| 431 | + OaTravelReimbursementDO record = randomTravelReimbursementDO(); | |
| 432 | + travelReimbursementMapper.insert(record); | |
| 433 | + LocalDateTime expenseTime = LocalDateTime.of(2026, 9, 14, 10, 0); | |
| 434 | + | |
| 435 | + // 调用 | |
| 436 | + OaTravelReimbursementDO detail = travelReimbursementService.getTravelReimbursement(record.getId()); | |
| 437 | + PageResult<OaTravelReimbursementDO> page = travelReimbursementService.getTravelReimbursementPage(10L, | |
| 438 | + new OaTravelReimbursementPageReqVO()); | |
| 439 | + | |
| 440 | + // 断言:未声明 ObjectMapper Bean 时使用 JsonUtils 的 ObjectMapper,详情、列表读取明细时间 | |
| 441 | + assertSame(JsonUtils.getObjectMapper(), JacksonTypeHandler.getObjectMapper()); | |
| 442 | + assertEquals(expenseTime, CollUtil.getFirst(detail.getItems()).getExpenseTime()); | |
| 443 | + assertEquals(1L, page.getTotal()); | |
| 444 | + assertEquals(expenseTime, CollUtil.getFirst(CollUtil.getFirst(page.getList()).getItems()).getExpenseTime()); | |
| 445 | + } | |
| 446 | + | |
| 447 | + @Test | |
| 426 | 448 | public void testUpdateTravelReimbursementStatus_notExists() { |
| 427 | 449 | |
| 428 | 450 | // 调用,并断言异常 |
| ... | ... | @@ -593,7 +615,8 @@ public class OaTravelReimbursementServiceImplTest extends BaseDbUnitTest { |
| 593 | 615 | .setStartTime(LocalDateTime.of(2026, 9, 14, 0, 0)).setEndTime(LocalDateTime.of(2026, 9, 15, 0, 0)) |
| 594 | 616 | .setStatus(-1).setProcessInstanceId(null).setPayStatus(false) |
| 595 | 617 | .setTravelApplyId(null).setTotalPrice(BigDecimal.ZERO) |
| 596 | - .setItems(Collections.singletonList(new OaTravelReimbursementDO.Item().setPrice(BigDecimal.ZERO))) | |
| 618 | + .setItems(Collections.singletonList(new OaTravelReimbursementDO.Item().setPrice(BigDecimal.ZERO) | |
| 619 | + .setExpenseTime(LocalDateTime.of(2026, 9, 14, 10, 0)))) | |
| 597 | 620 | .setFileUrls(Collections.singletonList("https://example.com/receipt.pdf"))); |
| 598 | 621 | record.setCreator("10"); |
| 599 | 622 | return record; | ... | ... |