From 4ee2d0f6edc5051a4d587e57d2367013a4fcec97 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Thu, 22 Oct 2026 23:40:45 +0800 Subject: [PATCH] fix(oa): 修复报销明细日期 JSON 处理器初始化 --- yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java | 20 +++++++++----------- yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/reimbursement/OaReimbursementServiceImplTest.java | 43 +++++++++++++++++++++++++++++++++++++++++-- yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/travel/OaTravelReimbursementServiceImplTest.java | 25 ++++++++++++++++++++++++- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java index 72a10e0..4dd6718 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.framework.mybatis.config; -import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.mybatis.core.handler.DefaultDBFieldHandler; @@ -23,6 +22,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.mapping.ResultMap; import org.mybatis.spring.annotation.MapperScan; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.SmartInitializingSingleton; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -30,7 +31,6 @@ import org.springframework.context.annotation.Primary; import org.springframework.core.env.ConfigurableEnvironment; import java.util.ArrayList; -import java.util.List; import java.util.concurrent.TimeUnit; /** @@ -49,6 +49,8 @@ public class YudaoMybatisAutoConfiguration { (cache) -> cache.maximumSize(1024) .expireAfterWrite(5, TimeUnit.SECONDS)) ); + // 特殊:JacksonTypeHandler 先使用 JsonUtils 的 ObjectMapper(支持 LocalDateTime),避免使用 MyBatis Plus 默认的 ObjectMapper + JacksonTypeHandler.setObjectMapper(JsonUtils.getObjectMapper()); } @Bean @@ -101,15 +103,11 @@ public class YudaoMybatisAutoConfiguration { throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType)); } - @Bean // 特殊:返回结果使用 Object 而不用 JacksonTypeHandler 的原因,避免因为 JacksonTypeHandler 被 mybatis 全局使用! - public Object jacksonTypeHandler(List objectMappers) { - // 特殊:设置 JacksonTypeHandler 的 ObjectMapper! - ObjectMapper objectMapper = CollUtil.getFirst(objectMappers); - if (objectMapper == null) { - objectMapper = JsonUtils.getObjectMapper(); - } - JacksonTypeHandler.setObjectMapper(objectMapper); - return new JacksonTypeHandler(Object.class); + @Bean + public SmartInitializingSingleton jacksonTypeHandlerInitializer(ObjectProvider objectMappers) { + // 特殊:所有单例初始化后,设置 JacksonTypeHandler 的 ObjectMapper,优先唯一或 @Primary 的 ObjectMapper,否则使用 JsonUtils 的 + // 原因:避免延迟初始化未设置、多个 ObjectMapper 时取到不支持 LocalDateTime 的,且不在创建 SqlSessionFactory 时获取 ObjectMapper。https://t.zsxq.com/KmPNU + return () -> JacksonTypeHandler.setObjectMapper(objectMappers.getIfUnique(JsonUtils::getObjectMapper)); } } diff --git a/yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/reimbursement/OaReimbursementServiceImplTest.java b/yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/reimbursement/OaReimbursementServiceImplTest.java index 06c337b..2457da5 100644 --- a/yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/reimbursement/OaReimbursementServiceImplTest.java +++ b/yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/reimbursement/OaReimbursementServiceImplTest.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.oa.service.reimbursement; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.jackson.config.YudaoJacksonAutoConfiguration; import cn.iocoder.yudao.framework.security.core.LoginUser; @@ -14,6 +15,8 @@ import cn.iocoder.yudao.module.oa.controller.admin.reimbursement.vo.OaReimbursem import cn.iocoder.yudao.module.oa.dal.dataobject.reimbursement.OaReimbursementDO; import cn.iocoder.yudao.module.oa.dal.mysql.reimbursement.OaReimbursementMapper; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.Resource; @@ -30,6 +33,7 @@ import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; +import java.util.List; import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; @@ -61,8 +65,6 @@ public class OaReimbursementServiceImplTest extends BaseDbUnitTest { @BeforeEach public void before() { - // JSON 处理器为静态共享配置,每个用例恢复当前上下文的生产日期序列化配置 - JacksonTypeHandler.setObjectMapper(objectMapper); SecurityFrameworkUtils.setLoginUser(new LoginUser().setId(10L).setUserType(2), new MockHttpServletRequest()); } @@ -126,6 +128,7 @@ public class OaReimbursementServiceImplTest extends BaseDbUnitTest { assertEquals(new BigDecimal("12.34"), application.getTotalPrice()); assertEquals(2, application.getInvoiceCount()); assertEquals(1, CollUtil.getFirst(application.getItems()).getExpenseType()); + assertEquals(LocalDateTime.of(2026, 9, 13, 8, 0), CollUtil.getFirst(application.getItems()).getExpenseTime()); verify(processInstanceApi).createProcessInstance(eq(10L), argThat(dto -> "oa_reimbursement".equals(dto.getProcessDefinitionKey()) && id.toString().equals(dto.getBusinessKey()) && !dto.getVariables().containsKey("title"))); @@ -214,6 +217,42 @@ public class OaReimbursementServiceImplTest extends BaseDbUnitTest { } @Test + public void testGetReimbursementAndPage_itemsExpenseTime() { + // mock 数据 + Long id = reimbursementService.createReimbursement(randomReimbursementSaveReqVO()); + LocalDateTime expenseTime = LocalDateTime.of(2026, 9, 13, 8, 0); + + // 调用 + OaReimbursementDO reimbursement = reimbursementService.getReimbursement(id); + PageResult page = reimbursementService.getReimbursementPage(10L, new OaReimbursementPageReqVO()); + + // 断言:使用 Spring 主 ObjectMapper,详情、列表读取明细时间,库中保持毫秒时间戳 + assertSame(objectMapper, JacksonTypeHandler.getObjectMapper()); + assertEquals(expenseTime, CollUtil.getFirst(reimbursement.getItems()).getExpenseTime()); + assertEquals(1L, page.getTotal()); + assertEquals(expenseTime, CollUtil.getFirst(CollUtil.getFirst(page.getList()).getItems()).getExpenseTime()); + List items = reimbursementMapper.selectObjs(new LambdaQueryWrapper() + .select(OaReimbursementDO::getItems).eq(OaReimbursementDO::getId, id)); + assertTrue(CollUtil.getFirst(items).contains("\"expenseTime\":" + LocalDateTimeUtil.toEpochMilli(expenseTime))); + } + + @Test + public void testGetReimbursementPage_itemsExpenseTimeString() { + // mock 数据:库中明细时间为可读字符串 + Long id = reimbursementService.createReimbursement(randomReimbursementSaveReqVO()); + reimbursementMapper.update(new LambdaUpdateWrapper().eq(OaReimbursementDO::getId, id) + .setSql("items = {0}", "[{\"expenseTime\":\"2026-09-13 08:00:00\",\"expenseType\":1,\"price\":12.34}]")); + + // 调用 + PageResult page = reimbursementService.getReimbursementPage(10L, new OaReimbursementPageReqVO()); + + // 断言 + assertEquals(1L, page.getTotal()); + assertEquals(LocalDateTime.of(2026, 9, 13, 8, 0), + CollUtil.getFirst(CollUtil.getFirst(page.getList()).getItems()).getExpenseTime()); + } + + @Test public void testUpdateReimbursementStatus_successAndRepeated() { // mock 数据 OaReimbursementDO application = randomReimbursementDO(); diff --git a/yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/travel/OaTravelReimbursementServiceImplTest.java b/yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/travel/OaTravelReimbursementServiceImplTest.java index deb9743..f7edd80 100644 --- a/yudao-module-oa/src/test/java/cn/iocoder/yudao/module/oa/service/travel/OaTravelReimbursementServiceImplTest.java +++ b/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; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; @@ -16,6 +17,7 @@ import cn.iocoder.yudao.module.oa.dal.redis.no.OaNoRedisDAO; import cn.iocoder.yudao.module.system.api.dict.DictDataApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import jakarta.annotation.Resource; import jakarta.validation.ConstraintViolationException; import org.junit.jupiter.api.AfterEach; @@ -150,6 +152,7 @@ public class OaTravelReimbursementServiceImplTest extends BaseDbUnitTest { OaTravelReimbursementDO record = travelReimbursementMapper.selectById(id); assertEquals(1, record.getDays()); assertEquals(reqVO.getItems().size(), record.getItems().size()); + assertEquals(LocalDateTime.of(2026, 9, 14, 10, 0), CollUtil.getFirst(record.getItems()).getExpenseTime()); assertEquals(reqVO.getFileUrls(), record.getFileUrls()); assertEquals(new BigDecimal("0.00"), record.getTotalPrice()); assertEquals("TRAVEL-NEW", record.getNo()); @@ -423,6 +426,25 @@ public class OaTravelReimbursementServiceImplTest extends BaseDbUnitTest { } @Test + public void testGetTravelReimbursementAndPage_itemsExpenseTime() { + // mock 数据 + OaTravelReimbursementDO record = randomTravelReimbursementDO(); + travelReimbursementMapper.insert(record); + LocalDateTime expenseTime = LocalDateTime.of(2026, 9, 14, 10, 0); + + // 调用 + OaTravelReimbursementDO detail = travelReimbursementService.getTravelReimbursement(record.getId()); + PageResult page = travelReimbursementService.getTravelReimbursementPage(10L, + new OaTravelReimbursementPageReqVO()); + + // 断言:未声明 ObjectMapper Bean 时使用 JsonUtils 的 ObjectMapper,详情、列表读取明细时间 + assertSame(JsonUtils.getObjectMapper(), JacksonTypeHandler.getObjectMapper()); + assertEquals(expenseTime, CollUtil.getFirst(detail.getItems()).getExpenseTime()); + assertEquals(1L, page.getTotal()); + assertEquals(expenseTime, CollUtil.getFirst(CollUtil.getFirst(page.getList()).getItems()).getExpenseTime()); + } + + @Test public void testUpdateTravelReimbursementStatus_notExists() { // 调用,并断言异常 @@ -593,7 +615,8 @@ public class OaTravelReimbursementServiceImplTest extends BaseDbUnitTest { .setStartTime(LocalDateTime.of(2026, 9, 14, 0, 0)).setEndTime(LocalDateTime.of(2026, 9, 15, 0, 0)) .setStatus(-1).setProcessInstanceId(null).setPayStatus(false) .setTravelApplyId(null).setTotalPrice(BigDecimal.ZERO) - .setItems(Collections.singletonList(new OaTravelReimbursementDO.Item().setPrice(BigDecimal.ZERO))) + .setItems(Collections.singletonList(new OaTravelReimbursementDO.Item().setPrice(BigDecimal.ZERO) + .setExpenseTime(LocalDateTime.of(2026, 9, 14, 10, 0)))) .setFileUrls(Collections.singletonList("https://example.com/receipt.pdf"))); record.setCreator("10"); return record; -- libgit2 0.21.4