fix(bug): 修复诊疗目录 SysDictData 反序列化错误

根因: commit 68cfa4882 将 Jackson 配置从 Jackson2ObjectMapperBuilderCustomizer
改为直接定义 ObjectMapper bean,导致 Spring Boot 自动配置失效。

修复: 改回 Jackson2ObjectMapperBuilderCustomizer,保留 Spring Boot 默认设置。

同时提交分析报告到 MD/bugs/
This commit is contained in:
2026-06-11 17:30:30 +08:00
parent 1f738c969a
commit babd8d0c04
11 changed files with 103 additions and 41 deletions

View File

@@ -0,0 +1,43 @@
# 诊疗目录 SysDictData 反序列化错误 诸葛亮分析报告
> **文档类型**: Bug分析
> **分析时间**: 2026-06-11
> **分析模型**: mimo-v2.5 (LLM深度分析)
---
## 基本信息
- **标题**: 诊疗目录 /system/catalog/diagnosistreatment 进入报错
- **错误**: Cannot deserialize value of type SysDictData from Array value
- **模块**: 诊疗目录管理
---
## 根因分析
**根本原因**: commit `68cfa4882` 将 Jackson 配置从 `Jackson2ObjectMapperBuilderCustomizer` 改为直接定义 `ObjectMapper` bean。
直接定义 `ObjectMapper` bean 会导致 Spring Boot 的 Jackson 自动配置完全失效:
1. Spring Boot 默认的 Jackson 模块(如 `jackson-datatype-jdk8``jackson-datatype-jsr310`)不会自动注册
2. 默认的序列化/反序列化特性设置丢失
3. `ObjectMapper` 的默认可见性设置可能不同
`DictAspect` 处理 `@Dict` 注解的 DTO 时Jackson 在序列化/反序列化过程中遇到 `SysDictData` 类型,由于缺少正确的模块配置,无法正确处理嵌套的数组/对象结构。
## 修复方案
`ApplicationConfig.java` 中的 `ObjectMapper` bean 改回 `Jackson2ObjectMapperBuilderCustomizer`,让 Spring Boot 自动配置保持生效。
### 修改文件
- `core-framework/src/main/java/com/core/framework/config/ApplicationConfig.java`
### 修改内容
- `public ObjectMapper objectMapper()``public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization()`
- 移除手动创建的 `ObjectMapper` 实例
- 使用 builder 模式配置,保留 Spring Boot 默认设置
---
## 路由决策
- **修复 Agent**: guanyu (后端)
- **原因**: Jackson 配置问题,纯后端修改

View File

@@ -3,17 +3,15 @@ package com.core.framework.config;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.jackson2.autoconfigure.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;
@@ -40,19 +38,14 @@ public class ApplicationConfig {
};
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
mapper.setDateFormat(sdf);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addDeserializer(LocalDateTime.class, LOCAL_DATE_TIME_DESERIALIZER);
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
mapper.registerModule(javaTimeModule);
return mapper;
public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization() {
return builder -> {
builder.timeZone(TimeZone.getDefault());
builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addDeserializer(LocalDateTime.class, LOCAL_DATE_TIME_DESERIALIZER);
builder.modules(javaTimeModule);
builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
};
}
}
}

View File

@@ -304,6 +304,9 @@ public class EmergencyController {
@PostMapping("/green-channel/activate")
@Transactional(rollbackFor = Exception.class)
public R<?> activateGreenChannel(@RequestBody EmergencyGreenChannel gc) {
if (gc.getPatientId() == null) {
return R.fail("患者ID不能为空请选择患者后再激活绿色通道");
}
gc.setActivateTime(new Date());
gc.setCreateTime(new Date());
greenChannelService.save(gc);

View File

@@ -7,6 +7,7 @@ import com.healthlink.his.web.inhospitalcharge.dto.*;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Date;
import java.util.List;
/**
@@ -30,11 +31,15 @@ public interface IInHospitalRegisterAppService {
* @param registeredFlag 已登记标识,已登记传 1 ,待登记传 0
* @param pageNo 当前页
* @param pageSize 每页多少条
* @param startTime 开始时间
* @param endTime 结束时间
* @param organizationId 入院科室ID
* @param request 请求
* @return 住院登记信息
*/
IPage<InHospitalRegisterQueryDto> getRegisterInfo(InHospitalRegisterQueryDto inHospitalRegisterQueryDto,
String searchKey, String registeredFlag, Integer pageNo, Integer pageSize, HttpServletRequest request);
String searchKey, String registeredFlag, Integer pageNo, Integer pageSize,
Date startTime, Date endTime, Long organizationId, HttpServletRequest request);
/**
* 查询患者基本信息

View File

@@ -173,7 +173,8 @@ public class InHospitalRegisterAppServiceImpl implements IInHospitalRegisterAppS
*/
@Override
public IPage<InHospitalRegisterQueryDto> getRegisterInfo(InHospitalRegisterQueryDto inHospitalRegisterQueryDto,
String searchKey, String registeredFlag, Integer pageNo, Integer pageSize, HttpServletRequest request) {
String searchKey, String registeredFlag, Integer pageNo, Integer pageSize,
Date startTime, Date endTime, Long organizationId, HttpServletRequest request) {
Integer encounterStatus = EncounterZyStatus.TO_BE_REGISTERED.getValue(); // 待登记
// 构建查询条件
QueryWrapper<InHospitalRegisterQueryDto> queryWrapper
@@ -182,9 +183,8 @@ public class InHospitalRegisterAppServiceImpl implements IInHospitalRegisterAppS
IPage<InHospitalRegisterQueryDto> inHospitalRegisterInfo = inHospitalRegisterAppMapper
.getInHospitalRegisterInfo(new Page<>(pageNo, pageSize), EncounterClass.IMP.getValue(), encounterStatus,
registeredFlag, LocationForm.WARD.getValue(), queryWrapper,
inHospitalRegisterQueryDto.getStartTime(), inHospitalRegisterQueryDto.getEndTime(),
inHospitalRegisterQueryDto.getOrganizationId());
registeredFlag, LocationForm.WARD.getValue(), startTime, endTime, organizationId,
queryWrapper);
inHospitalRegisterInfo.getRecords().forEach(e -> {
// 性别
e.setGenderEnum_enumText(EnumUtils.getInfoByValue(AdministrativeGender.class, e.getGenderEnum()));

View File

@@ -10,6 +10,8 @@ import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import jakarta.servlet.http.HttpServletRequest;
/**
@@ -53,6 +55,9 @@ public class InHospitalRegisterController {
* @param registeredFlag 已登记标识,已登记传 1 ,待登记传 0
* @param pageNo 当前页
* @param pageSize 每页多少条
* @param startTime 开始时间
* @param endTime 结束时间
* @param organizationId 入院科室ID
* @param request 请求
* @return 住院登记信息
*/
@@ -61,9 +66,13 @@ public class InHospitalRegisterController {
@RequestParam(value = "searchKey", defaultValue = "") String searchKey,
@RequestParam(value = "registeredFlag") String registeredFlag,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize, HttpServletRequest request) {
@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "startTime", required = false) Date startTime,
@RequestParam(value = "endTime", required = false) Date endTime,
@RequestParam(value = "organizationId", required = false) Long organizationId,
HttpServletRequest request) {
return R.ok(iInHospitalRegisterAppService.getRegisterInfo(inHospitalRegisterQueryDto, searchKey, registeredFlag,
pageNo, pageSize, request));
pageNo, pageSize, startTime, endTime, organizationId, request));
}
/**

View File

@@ -100,25 +100,24 @@ public class InHospitalRegisterQueryDto {
*/
private Integer statusEnum;
/**
* 开始时间
* 开始时间(查询条件)
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/**
* 结束时间
* 结束时间(查询条件)
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
/**
* 组织ID
* 组织ID(查询条件)
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long organizationId;
}
// PLACEHOLDER_FOR_NEW_FIELDS
}

View File

@@ -28,15 +28,18 @@ public interface InHospitalRegisterAppMapper {
* @param encounterStatus 登记状态
* @param registeredFlag 已登记标识,已登记传 1 ,待登记传 0
* @param formEnum 物理位置
* @param startTime 开始时间
* @param endTime 结束时间
* @param organizationId 入院科室ID
* @param queryWrapper 查询条件
* @return 住院登记信息
*/
IPage<InHospitalRegisterQueryDto> getInHospitalRegisterInfo(@Param("page") Page<InHospitalRegisterQueryDto> page,
@Param("encounterClass") Integer encounterClass, @Param("encounterStatus") Integer encounterStatus,
@Param("registeredFlag") String registeredFlag, @Param("formEnum") Integer formEnum,
@Param(Constants.WRAPPER) QueryWrapper<InHospitalRegisterQueryDto> queryWrapper,
@Param("startTime") Date startTime, @Param("endTime") Date endTime,
@Param("organizationId") Long organizationId);
@Param("organizationId") Long organizationId,
@Param(Constants.WRAPPER) QueryWrapper<InHospitalRegisterQueryDto> queryWrapper);
/**
* 查询患者基本信息

View File

@@ -512,7 +512,7 @@
personal_account.balance_amount,
personal_account.id AS account_id,
T2.category_code,
ao.name AS org_name
COALESCE(ao.name, al1."name") AS org_name
FROM med_medication_request AS T1
LEFT JOIN med_medication_definition AS T2
ON T2.id = T1.medication_id

View File

@@ -47,8 +47,8 @@
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total,prev,pager,next" @current-change="loadData"/>
<el-dialog v-model="addVisible" title="激活绿色通道" width="500px">
<el-form :model="addForm" label-width="120px">
<el-form-item label="患者ID"><el-input-number v-model="addForm.patientId" :min="1"/></el-form-item>
<el-form ref="addFormRef" :model="addForm" :rules="addFormRules" label-width="120px">
<el-form-item label="患者ID" prop="patientId"><el-input-number v-model="addForm.patientId" :min="1" style="width:100%"/></el-form-item>
<el-form-item label="疾病类型"><el-input v-model="addForm.diseaseType" placeholder="如: 急性心肌梗死、脑卒中"/></el-form-item>
<el-form-item label="目标时间(min)"><el-input-number v-model="addForm.targetTime" :min="1"/></el-form-item>
<el-form-item label="医生"><el-input v-model="addForm.doctor"/></el-form-item>
@@ -76,13 +76,15 @@ import {getPage,activate,complete,getStats,del} from './api'
const tableData=ref([]);const total=ref(0);const stats=ref({})
const addVisible=ref(false);const completeVisible=ref(false)
const addForm=ref({patientId:null,diseaseType:'',targetTime:90,doctor:''})
const addFormRef=ref(null)
const addFormRules={patientId:[{required:true,message:'请选择患者',trigger:'blur'}]}
const completeForm=ref({doorToTreatmentTime:60});let currentId=null
const q=ref({pageNo:1,pageSize:20,diseaseType:'',isAchieved:null})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
const refreshStats=async()=>{const r=await getStats({});stats.value=r.data||{}}
const showAdd=()=>{addForm.value={patientId:null,diseaseType:'',targetTime:90,doctor:''};addVisible.value=true}
const showComplete=(row)=>{currentId=row.id;completeForm.value={doorToTreatmentTime:60};completeVisible.value=true}
const submitAdd=async()=>{await activate(addForm.value);ElMessage.success('绿色通道已激活');addVisible.value=false;loadData();refreshStats()}
const submitAdd=async()=>{if(addFormRef.value){try{await addFormRef.value.validate()}catch{return}}await activate(addForm.value);ElMessage.success('绿色通道已激活');addVisible.value=false;loadData();refreshStats()}
const doComplete=async()=>{await complete(currentId,completeForm.value);ElMessage.success('评估完成');completeVisible.value=false;loadData();refreshStats()}
const delItem=async(id)=>{await del(id);ElMessage.success('已删除');loadData();refreshStats()}
onMounted(()=>{loadData();refreshStats()})

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div class="transfer-out-container">
<!-- 顶部搜索区域 -->
<div>
@@ -271,9 +271,14 @@
</span>
</template>
</vxe-column>
<vxe-column title="医嘱状态" align="center" width="120">
<template #default="scope">
{{ scope.row.requestStatus_enumText || '-' }}
</template>
</vxe-column>
<vxe-column title="执行科室" align="center" min-width="120">
<template #default="scope">
{{ scope.row.orgName || '-' }}
{{ scope.row.positionName || scope.row.orgName || '-' }}
</template>
</vxe-column>
<vxe-column title="开始时间" width="180">
@@ -1109,8 +1114,8 @@ defineExpose({ refreshTap });
width: 120px !important;
}
:deep(.vxe-table--header th:nth-child(5)),
:deep(.vxe-table--header th:nth-child(6)) {
:deep(.vxe-table--header th:nth-child(7)),
:deep(.vxe-table--header th:nth-child(8)) {
width: 180px !important;
}