Commit 4ee2d0f6edc5051a4d587e57d2367013a4fcec97

Authored by YunaiV
1 parent 09aa9ad9

fix(oa): 修复报销明细日期 JSON 处理器初始化

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;
... ...