ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于Spring Boot的中医病案管理系统设计与实践

基于Spring Boot的中医病案管理系统设计与实践

去年帮一家中医门诊部做内部信息化改造,老板提的需求很朴素:把柜子里那十几摞纸质病案变成能查、能统计的电子档案。一开始我以为这就是个增删改查的常规管理系统,真正动手才发现,中医病案的水远比想象中深——光“舌象脉象怎么存”“复诊时方剂怎么调整”这两个问题,就够讨论好几天。后来我基于 Java 和 Spring Boot 搭了一套中医病案管理系统,从需求梳理到上线跑通用了大概两个月,中间踩了不少坑,也攒下一些还算通用的设计思路。如果你正准备做医疗信息管理系统,或者在为类似选题做技术方案选型,这篇文章应该能帮你少走弯路。这里我不讲那种只贴截图的项目流水账,而是把功能拆解、表结构设计、关键功能实现、部署排查这些真正要动手的部分,一条条摊开讲清楚。

1. 项目定位与核心需求拆解

1.1 中医病案管理和其他信息管理系统有什么不一样

普通的企业信息管理系统,核心往往是“单据流转”和“审批状态”,比如订单、工单、报销单。但中医病案管理系统面对的对象是“人”和“诊疗过程”,它的数据模型和业务逻辑天然更复杂。我接需求时先梳理了门诊部的实际工作流,大致是这样的:

  • 患者初诊,医生问诊,记录主诉、现病史、既往史。
  • 医生做望闻问切,重点记录舌象、脉象、面色等体征。
  • 医生给出辨证分型,比如“肝郁脾虚证”“风寒束表证”,并确定治则治法。
  • 开出中药处方,处方里包含多味中药,每味药有剂量、用法(先煎、后下、包煎等)。
  • 患者按疗程服药,过段时间回来复诊,医生需要看上次的病案和方剂,判断效果并调整处方。

这套流程决定了系统不能只做“病案登记”,还必须支持辨证分型管理、处方明细记录、复诊对比、疗程跟踪。而且中医有很多自由文本内容,比如“脉弦细”“舌淡红苔薄白”这类描述,既有一定的规范性,又允许医生按个人表达习惯书写。系统要在结构化和灵活性之间找到平衡,这是第一个设计难点。

1.2 核心模块划分与功能边界

我最终把系统切成了六个核心模块:

模块核心功能主要使用角色
患者管理建档、基本信息维护、历史病案查询医生、前台
病案管理初诊/复诊病案录入、编辑、查看、导出医生
辨证与方剂管理证型字典、中药字典、处方明细医生、管理员
复诊跟踪按患者维度查看诊疗时间线、疗效对比医生
统计报表疾病谱统计、方剂频率统计、就诊量分析管理员
系统管理用户、角色、菜单、操作日志管理员

这么划分的好处是边界清晰。患者管理管“谁来看病”,病案管理管“每次看病的完整记录”,复诊跟踪管“这个人的长期治疗脉络”,统计报表管“从数据里看出业务趋势”。模块之间通过患者ID和病案ID关联,不搞复杂的跨模块直接调用,后续维护成本低。

另外我坚持把“辨证分型”和“中药处方”从病案主表里拆出来,单独建表。原因很简单:一个患者可能多次就诊,同一次就诊可能有多张处方(比如先开汤剂再开中成药),如果全部塞进主表,表结构会非常臃肿,后续想统计分析也麻烦。宁可前期多两张表,也别等到上线了再拆。

1.3 技术选型:为什么是Spring Boot

技术选型时,我在几个方案里纠结过。第一个想法是用 Python + Flask,开发快,但考虑到门诊部后续可能要接医保接口、做复杂权限控制,而且团队里懂 Java 的人更多,最终选了 Spring Boot。选它的理由比较实际:

  • Spring Boot 的生态成熟,MyBatis-Plus、Spring Security、Redis 这些配套组件资料多,遇到问题容易找到解决方案。
  • 自动配置和内置容器让部署很省事,打一个 jar 包就能跑,不需要单独装 Tomcat。
  • JPA 虽然开发快,但复杂查询和动态SQL不如 MyBatis 灵活;MyBatis-Plus 既有基础的 CRUD,又能写自定义 XML,兼容性最好。

前端我选了 Vue + Element UI,后台管理页面的表单很多,Element UI 的表格、下拉、日期选择组件开箱即用,开发效率高。前后端分离后,后端只提供 JSON 接口,前端独立部署在 Nginx,后续如果要加小程序端,直接复用接口就行。

2. 数据库设计与核心表结构

2.1 数据建模从哪里入手

中医病案的数据建模,我建议别一上来就画ER图,而是先定义清楚“一次完整的诊疗记录”包含哪些信息。我们门诊部的医生每看一个患者,必须记录的内容有:就诊时间、主诉、现病史、既往史、舌象、脉象、辨证分型、治则治法、处方、医嘱。其中辨证分型可能有多重证型,处方包含多味药。

基于这个业务事实,我确定了一套“一主多从”的表结构方案:病案主表存一次就诊的概要信息,子表存辨证分型和处方明细。这样的设计有两个好处。第一,查询病案列表时不需要关联子表,速度快。第二,子表可以灵活扩展,比如以后要加“外治方案”“针灸取穴”,不需要改动主表。

表数量在合理范围内,我不追求过度范式化。比如“舌象描述”我直接存文本字段,同时保留一个可选的图片字段用来存放舌苔照片的URL。你让医生下拉选择舌色、苔色、苔质其实很难落实,因为临床上的组合太多了,文本录入加关键词统计反而是更务实的方式。

2.2 核心表结构详解

下面是几张关键表的 DDL,我在门诊部实际用的就是这套结构,跑了大半年没怎么改过。

-- 患者基本信息表 CREATE TABLE patient ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '患者ID', patient_no VARCHAR(32) UNIQUE COMMENT '病历号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender TINYINT COMMENT '性别 0未知 1男 2女', birth_date DATE COMMENT '出生日期', phone VARCHAR(20) COMMENT '联系电话', id_card VARCHAR(18) COMMENT '身份证号', address VARCHAR(200) COMMENT '联系地址', allergy_history TEXT COMMENT '过敏史', created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='患者基本信息表';
-- 病案主表 CREATE TABLE medical_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '病案ID', record_no VARCHAR(32) UNIQUE COMMENT '病案编号', patient_id BIGINT NOT NULL COMMENT '患者ID', visit_date DATETIME NOT NULL COMMENT '就诊时间', visit_type TINYINT DEFAULT 1 COMMENT '就诊类型 1初诊 2复诊', chief_complaint TEXT COMMENT '主诉', present_illness TEXT COMMENT '现病史', past_history TEXT COMMENT '既往史', tongue_desc TEXT COMMENT '舌象', pulse_desc TEXT COMMENT '脉象', syndrome TEXT COMMENT '辨证分型(冗余文本,便于列表展示)', treatment_principle VARCHAR(200) COMMENT '治则治法', doctor VARCHAR(50) COMMENT '就诊医生', status TINYINT DEFAULT 1 COMMENT '状态 1正常 2已作废', remark TEXT COMMENT '备注', created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_patient_id (patient_id), KEY idx_visit_date (visit_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='病案主表';
-- 辨证分型明细表 CREATE TABLE record_syndrome ( id BIGINT PRIMARY KEY AUTO_INCREMENT, record_id BIGINT NOT NULL COMMENT '病案ID', syndrome_name VARCHAR(100) NOT NULL COMMENT '证型名称', syndrome_desc TEXT COMMENT '辩证要点', KEY idx_record_id (record_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='辨证分型明细表';
-- 处方明细表 CREATE TABLE prescription_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, record_id BIGINT NOT NULL COMMENT '病案ID', patent_name VARCHAR(200) COMMENT '方剂名称', herb_name VARCHAR(100) NOT NULL COMMENT '中药名称', dosage DECIMAL(10,3) COMMENT '剂量', unit VARCHAR(20) COMMENT '单位 克/片/袋', decoction_method VARCHAR(100) COMMENT '煎服法 如先煎、后下、包煎', note VARCHAR(200) COMMENT '备注', sort_order INT DEFAULT 0 COMMENT '排序', KEY idx_record_id (record_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='处方明细表';

患者表和病案主表用 patient_id 关联,病案主表和辨证明细、处方明细用 record_id 关联。注意 patient_id 和 record_id 都建了索引,因为这两个字段是所有查询的入口。ID 用自增主键就够了,没必要用UUID做主键,但在“病历号”“病案编号”这种业务标识上,我单独生成了一个带日期前缀的编号,比如BR20250101001,方便人工识别和纸质档案对照。

还有一张表容易漏,就是复诊跟踪表。复诊不是简单新增一条病案记录,它需要记录“相对于上次的变化”,比如症状是否缓解、有没有出现不良反应、方剂做了哪些调整。我设计了 follow_up 表,字段包括 patient_id、base_record_id(对应的初诊病案ID)、visit_date、symptom_change、side_effect、adjust_note。通过它才能画出完整的“患者诊疗时间线”。

2.3 中医特色字段要怎么存

中医数据的存储有几个坑,我逐个说明,这些都是实际踩过的。

第一,所有文本字段的字符集一定要用 utf8mb4,不能用 utf8。中医病案里经常出现生僻字,我遇到过“㿠白”这样的字,utf8 字符集存不进去,直接报错。这个错误在开发初期环境看不出问题,一上线生产环境就暴露。

第二,舌象脉象建议“文本描述 + 图片附件”双轨存储。纯文本的好处是方便检索,图片的好处是保留临床原始信息。我当时的做法是舌象、脉象字段存文本,图片用独立的上传接口传至本地磁盘,数据库只保存访问路径。上传目录按月分文件夹,避免单目录文件过多。

第三,证型名称不要只存一个字符串。临床上一个患者常见复合证型,比如“脾虚湿盛兼肝郁”,如果只存一个字段,后期统计“哪个证型最多”时会很痛苦。我用了 record_syndrome 子表,一条病案可以存多个证型,统计时直接 group by syndrome_name。最初也担心这样会增加录入时间,但实际做下来,医生在系统里勾选常用证型,再补充自定义描述,30秒内可以完成。

第四,处方明细的剂量字段要小心。中药剂量有“3g”“6g”“15g”这样的整数,也有“0.5g”“1.5g”这样的小数,还有“3片”“5枚”“1袋”这样的特殊单位。所以我设计剂量为 DECIMAL(10,3),再配一个 unit 字段存放单位,这样就覆盖了所有类型。排序字段 sort_order 也必须有,否则抓药时药味顺序是乱的要命。

3. 核心功能模块的实现要点

3.1 患者建档与病案录入流程

患者建档逻辑不复杂,但容易忽略一个业务细节:同一个患者可能用不同手机号挂过号,系统里产生了两条患者记录。门诊部的要求是“一人一号”,所以建档时我做了重复检测。前端输入身份证号或手机号时,实时请求后端接口检查是否已有患者档案;如果已有,直接弹出提示,让前台确认是否复用旧档案。这样后面统计“患者总数”时不会虚高。

病案录入是医生用得最多的功能,也是要求最细的模块。表单拆成四个区块:基本信息、问诊信息、辨证信息、处方信息。问诊信息里主诉、现病史是必填的大文本框,辨证信息和处方信息通过动态表格实现。医生可以在辨证区块点“添加证型”,在处方区块点“添加药物”,每行药可以是下拉选择中药字典里的名称,也可以直接输入,剂量和单位单独填写。

这里有一个交互细节值得分享:处方明细里的药材名称,我用了“可搜索下拉”组件,输入中药名关键字时,自动匹配字典中所有包含该关键字的药名。比如输入“白术”,会匹配出“白术”“炒白术”“麸炒白术”。中医里炮制方法不同,药效不同,必须区分清楚。这个功能看起来小,但医生反馈是最实用的。

后端病案保存我用了事务控制。一次保存操作涉及三张表:medical_record 主表、record_syndrome 证型子表、prescription_item 处方子表。任何一个子表保存失败,整个事务回滚,不能出现病案主表存了但处方丢了的情况。具体实现如下:

@Service public class MedicalRecordServiceImpl implements MedicalRecordService { @Autowired private MedicalRecordMapper medicalRecordMapper; @Autowired private RecordSyndromeMapper syndromeMapper; @Autowired private PrescriptionItemMapper prescriptionItemMapper; @Override @Transactional(rollbackFor = Exception.class) public Long saveRecord(MedicalRecordSaveDTO dto) { // 1. 保存主表 MedicalRecord record = new MedicalRecord(); record.setRecordNo(createRecordNo()); record.setPatientId(dto.getPatientId()); record.setVisitDate(dto.getVisitDate()); record.setChiefComplaint(dto.getChiefComplaint()); record.setTongueDesc(dto.getTongueDesc()); record.setPulseDesc(dto.getPulseDesc()); // 冗余存储证型名称,便于列表展示不查子表 record.setSyndrome(String.join("、", dto.getSyndromeList())); medicalRecordMapper.insert(record); // 2. 保存证型子表 if (dto.getSyndromeList() != null) { for (String name : dto.getSyndromeList()) { RecordSyndrome syndrome = new RecordSyndrome(); syndrome.setRecordId(record.getId()); syndrome.setSyndromeName(name); syndromeMapper.insert(syndrome); } } // 3. 保存处方子表 if (dto.getPrescriptionItems() != null) { int sort = 0; for (PrescriptionItemDTO item : dto.getPrescriptionItems()) { PrescriptionItem prescription = new PrescriptionItem(); prescription.setRecordId(record.getId()); prescription.setHerbName(item.getHerbName()); prescription.setDosage(item.getDosage()); prescription.setUnit(item.getUnit()); prescription.setDecoctionMethod(item.getDecoctionMethod()); prescription.setSortOrder(sort++); prescriptionItemMapper.insert(prescription); } } return record.getId(); } }

注意@Transactional的 rollbackFor 一定要设成Exception.class,不要只默认捕获 RuntimeException。中医病案保存过程里如果有 JSON 解析失败、空指针这类异常,不该让部分数据落库。跑了几个月,能确认这个事务设计是稳妥的。

3.2 辨证论治与方剂管理的实现

辨证论治是中医的核心,在系统里对应的就是“证型字典”和“中药字典”两个基础数据管理模块。这两个模块我刚开始的时候没在意,觉得就是两张字典表,后来业务一跑起来才发现字典设计直接影响录入效率。

证型字典表我存证型名称、辩证要点、常用方剂参考,方便医生录入时参考。中药字典表则存药名、性味归经、功效分类、常用剂量范围、注意事项。有了这两个字典,前端录入时可以进行联想、校验,比如剂量填写超出常用范围时给一个提示。但注意,提示只能作为参考,不能强制拦截。有些特殊患者剂量就是要超出常规,强制校验会激怒医生。

处方管理这里有个好用的设计:把常用处方做成“方剂模板”。门诊部有几个老中医,治疗风寒感冒基本都在一个基础方上加减。我在药品录入区加了一个“载入常用方”按钮,选择模板后自动填入整张处方,医生只需要微调个别药的剂量或增减一二味药。这个功能大大提升了录入速度,我强烈建议做病案系统的朋友都加上。

模板的实现不复杂。建一张 prescription_template 表和 template_item 子表,存模板名、适用证型和药品明细。医生保存病案时,可以顺手把当前处方存为模板。注意模板属于个人还是共享,我这里做成了“个人模板”和“公共模板”两种,用 create_by 字段区分,可以避免某人维护的模板干扰到其他人的处方习惯。

3.3 复诊跟踪与疗程管理

复诊模块是这个系统区别于普通电子病历系统的一个亮点。做这个功能之前,医生复盘患者病情变化主要靠翻纸质病案,效率非常低。系统上线后,我做了患者诊疗时间线页面:进入某个患者的详情页,会按时间倒序展示他的所有病案记录。每次复诊,系统自动把这次和上次的关键指标并列展示:

  • 主诉变化(上次主诉 vs 本次主诉)
  • 舌象变化(上次舌象 vs 本次舌象)
  • 脉象变化(上次脉象 vs 本次脉象)
  • 原方剂 vs 调整后方剂
  • 医生填写的症状变化描述

这个对比视图在门诊场景里非常实用。医生开复诊方时,先看一眼对比页,再决定是“守方”还是“改方”。技术上就是按 patient_id 关联查询 medical_record 表,取出最近N条记录,在 Java 层做两两配对。不需要复杂SQL,关键是前端展示要清晰:左右分栏的对比卡片,高亮显示变化内容。这块用 Vue 的 v-if 判断字段是否有值变化,代码量不大,但业务价值很高。

复诊类型我用 visit_type 字段标记,1 是初诊,2 是复诊。初诊病案一旦保存,visit_type 就不能随便改,防止业务数据混乱。

3.4 统计报表与数据导出

统计报表模块是门诊部管理者的刚需。他们最关心的几个数字是:每天接诊量、病种分布、证型频率、高频中药排名。我报表部分没有引入太重的大数据组件,直接用 SQL 聚合 + ECharts 展示。

高频中药排名是这样一个思路:

SELECT herb_name, COUNT(*) AS freq FROM prescription_item WHERE record_id IN ( SELECT id FROM medical_record WHERE visit_date BETWEEN #{startTime} AND #{endTime} ) GROUP BY herb_name ORDER BY freq DESC LIMIT 20;

证型频率类似,只是把表换成 record_syndrome,按 syndrome_name 分组。疾病谱统计稍微复杂一点,中医没有标准ICD编码,我按主诉中的关键词维度做简化统计,对主要症状词频进行汇总,配合人工标注的病案标签,能满足门诊部的日常业务需求。

数据导出我用了两种方式。列表界面的“导出Excel”是常规操作,用 Apache POI 导出病案列表。另外单独做了一个“病案归档包导出”功能,把指定时间段的全量病案连同舌象图片打包成ZIP,方便离线存档。这个功能和纸质病案归档逻辑对应,门诊部的人很容易理解。

4. 权限控制与安全设计

4.1 基于RBAC的权限模型

医疗数据属于敏感数据,权限控制不可糊弄。我按照RBAC模型设计权限:用户表 sys_user、角色表 sys_role、菜单表 sys_menu,外加用户角色关联表和角色菜单关联表。核心角色有三种:

  • 管理员:全部功能,包括用户管理、字典管理、统计数据。
  • 医生:病案录入、修改、查询、复诊跟踪,不能改系统和字典。
  • 前台/护士:患者建档、查患者基本信息,不能看完整病案,更不能改处方。

权限控制落到前端和后端两层。前端根据用户角色动态生成菜单,没有权限的页面直接不显示。后端用 Spring Security + JWT 做接口权限校验,每个接口上标注所需角色,比如:

@PreAuthorize("hasRole('DOCTOR') or hasRole('ADMIN')") @PostMapping("/record/save") public Result saveRecord(@RequestBody MedicalRecordSaveDTO dto) { return Result.ok(medicalRecordService.saveRecord(dto)); }

这里有个实操提醒:在生病案修改接口的时候,权限不只是“医生能改”,还要考虑“这个医生能不能改别人的病案”。我加了一个数据范围控制,医生只能修改自己创建的病案,管理员可以改全部。实现方法是查询时加create_by条件,如果是管理员角色则不附加。这个控制逻辑不难,但很容易被忽略,等出了医疗纠纷再回头看权限就晚了。

4.2 数据隔离与操作审计

医疗数据还有一个特点是强审计需求。门诊部的管理规范要求:谁在什么时候改了什么数据,要有迹可查。我做了两件事。

第一,所有病案的更新操作不直接覆盖原记录的关键字段,而是把更新前后内容记录到 audit_log 表。字段包括操作人、操作时间、操作类型、业务类型、业务ID、变更内容(JSON格式,包含old和new)。轻量级,够用,不引入复杂的事件溯源框架。

第二,操作日志模块提供查询页面,管理员可以按时间、操作人、业务类型筛选记录。遇到过两次医生误改处方的投诉,通过这张表很快就定位到了操作人和操作内容。

4.3 敏感信息处理

患者信息涉及隐私,存储层面我做了一些处理。身份证号和手机号是敏感字段,数据库里我做了加密存储,用的 AES 对称加密。系统内展示时,列表页默认脱敏(中间四位打星号),只有进入详情页且具备权限的用户才能看到完整信息。

注意加密密钥不要硬编码在代码里,也不要提交到Git仓库。我用的是配置文件注入,部署时通过环境变量传入。这个操作虽然只花半小时,但可以避免密钥泄露风险。

另外,门诊部的内网环境相对封闭,我建议生产环境部署时不直接用 root 账号连接数据库,单独建一个最小权限的数据库账号。这个习惯适用于所有中小型管理系统的上线部署。

5. 实操过程与部署记录

5.1 环境准备与项目初始化

开发环境我用的是 JDK 1.8 + Maven 3.8 + MySQL 5.7,生产环境用的是 JDK 1.8 + MySQL 8.0。Spring Boot 版本选了 2.7.x,不要一上来就追最新大版本,2.7.x 的资料最多、踩坑经验也最容易搜到,生态上兼容性最好。

项目初始化我推荐走 Spring Initializr 生成基础结构,需要注意勾选的依赖有:Spring Web、MyBatis-Plus、MySQL Driver、Redis、Lombok、Validation。Redis 在系统里有两个用途:存登录 token、缓存证型和中药字典。字典数据基本不变,每次启动时加载到 Redis,前端查询时走缓存接口,性能改善非常明显。

application.yml 核心配置如下:

server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/tcm_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: tcm_app password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl jwt: secret: ${JWT_SECRET} expire-hours: 12

这里要专门提醒:characterEncoding=utf8mb4这个参数如果写成utf8,虽然JDBC驱动可能会自动修正,但我在调试连接池时遇到过中文乱码的奇葩问题,统一写utf8mb4最省心。另外连接参数里serverTimezone=Asia/Shanghai必不可少,否则 MySQL 8.0 和 Java 的时区不一致,查询日期时会出现几小时的漂移。

5.2 核心接口与页面实现实录

前端我用 Vue 2 + Element UI,项目通过 Vue CLI 创建,开发环境通过代理转发接口请求。核心页面包括:患者管理列表页、患者建档对话框、病案录入页、病案详情页、复诊时间线页、统计报表页。

病案录入页是最大的页面,交互复杂,踩的坑最多。我记录几个关键点:

第一,动态表格的 v-model 绑定不要用item.someField这种方式,因为动态行删除时索引会乱。我统一在添加行时给每行生成一个临时 uuid 作为行ID,表格操作时按行ID来定位。这是个很小的细节,但能省掉很多调试时间。

第二,药品下拉联想组件,刚实现时用本地过滤所有药名,结果字典两千多条,输入响应有明显的卡顿。后来改成远程搜索接口,输入关键词后请求后端接口,后端用 like 查询返回最多30条。响应速度从卡顿变成了毫秒级。经验就是:小数据集用本地过滤,中等以上数据集一定要远程搜索。

第三,病案保存成功后不要直接关闭页面,而是弹出一个提示框,问医生“是否需要立即新增复诊”。因为临床场景里,医生经常需要在一个患者身上连续记录几个时间点的病情,这个优化能减少医生点击次数。

5.3 部署上线与性能调优

部署架构很简单,但我要讲几个优化点。后端打 jar 包后用 systemd 托管,前端构建后放到 Nginx 的 HTML 目录。Nginx 配置了反向代理,把/api开头的请求转发到后端服务。

上线之后遇到的主要性能问题在病案列表页。当病案表累积到上万条后,直接查询medical_record关联patient表按患者姓名搜索,响应时间到了两三秒。排查后发现是因为我没做分页查询的 count 缓存,每次请求都执行全表 count。解决方法是使用 MyBatis-Plus 的分页插件,并给它配置了合理的 maxLimit 参数,限制单次最大查询数量,同时给经常组合查询的字段加了联合索引(patient_id+visit_date联合索引)。优化后接口响应稳定在 300ms 以内。

还有一个小优化:病案列表的证型展示,直接读主表的冗余字段 syndrome,不关联子表。这个冗余字段设计在性能优化时体现出了价值。

6. 常见问题与排查技巧实录

6.1 生僻字和特殊符号导致的乱码、保存失败

中医病案里生僻字出现频率不低,我第一次遇到“㿠”这个字时,数据库直接报错,一看日志是Incorrect string value。排查发现建表语句里用了utf8,MySQL 的 utf8 最多支持3字节,生僻字是4字节编码。解决办法:所有表统一改成utf8mb4,并修改数据库连接参数。手工执行的 SQL 是:

ALTER TABLE medical_record CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

注意修改完字符集后,已经存入的旧数据如果有乱码,需要先用备份恢复,所以这种修改最好在数据量还小的时候做。另外一个坑是前端表单传给后端时,JSON解析器可能不认识生僻字,需要在后端配置 UTF-8 编码过滤器,Spring Boot 默认已是 UTF-8,但如果用了其他编码的中间件要重点排查。

6.2 病案关联查询慢

病案列表页涉及患者姓名查询、就诊时间范围筛选、证型筛选。一开始数据量小感觉不到慢,上万条记录后页面开始转圈。排查手段是看 MySQL 的慢查询日志。我发现问题出在LEFT JOIN patient时患者表没有走索引,因为 patient_name 字段没建索引。添加索引后速度明显提升:

ALTER TABLE patient ADD INDEX idx_name (name);

同时给 medical_record 表的 visit_date 建了索引。记住一个原则:WHERE 条件和 JOIN 条件的字段尽量都建索引,但不要盲目索引太多列,否则写入性能会下降。

6.3 并发建档导致患者重复

门诊部高峰期,两个前台同时给同一个新患者建档,可能因为同时查询“患者不存在”而插入两条记录。我的解决方法是给 patient 表的身份证号字段加唯一索引(如果患者提供身份证的话),数据库层面拦截重复。没有身份证号的婴幼儿,则通过“姓名+出生日期+监护人手机号”做应用层重复校验。注意唯一索引在有空值的列上是允许多个空值的,所以身份证字段的可空性不影响应用层逻辑。

6.4 数据备份与恢复

医疗机构的数据是核心资产,绝不能只靠服务器上有数据就不备份。我上线前做了一个简单的 shell 脚本,每天凌晨通过 mysqldump 备份全库数据到另一台机器的磁盘,保留最近30天备份。脚本逻辑不复杂,但有两个关键点:备份时加上--single-transaction参数,保证 InnoDB 备份一致性;恢复前必须严格确认备份文件日期,避免把老数据覆盖新数据。

我给门诊部做过一次备份恢复演练,从备份文件恢复跑到服务正常启动大约花了20分钟。演练过程发现了一个坑:如果备份时没有把存储舌象图片的目录一起备份,数据库恢复了但历史图片全丢了。后来我把备份脚本改成“数据库导出 + 图片目录压缩”两步,这个问题才算真正闭环。

7. 再聊几句我这个项目的真实体会

可能有人看完觉得这个系统技术上不算复杂,确实,论并发、论数据规模,它和互联网大厂的后端系统没法比。但医疗信息管理系统真正的复杂度不在并发,而在业务语义的严谨性。中医病案里的“一人多方”“复诊调理”“证型变化”,每一块都需要产品化的思维去处理,技术是为业务逻辑服务的。

系统上线后我最开心的事,不是跑通了多少接口,而是门诊部一位老中医跟我说“现在开复诊方之前点一下患者,上次的方子就出来了,不用翻本子了”。这句话比任何技术指标都有说服力。

最后想对正在做类似系统的人说几句:不要一上来就想把系统做得大而全,先花一周时间去门诊现场观察真实工作流,把医生、护士、前台的实际动作记录下来,再开始建表。数据建模阶段多花时间,后面开发会省很多时间。如果你已经上了生产环境,遇到病案数据混乱不要太焦虑,先做数据清洗,加上操作日志和校验规则,系统会逐步稳定下来。

还是那句话,医疗类系统的核心是数据安全、责任清晰、操作高效。方向对了,技术实现只是时间问题。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进