Spring Boot 中医医嘱管理系统这种项目,第一眼看上去可能觉得就是个常规的 CRUD 后台,最多加了个中医的壳。但真正动手做过之后才会发现,医嘱管理远不止是简单的“病历表 + 药品表”做两个增删改查。它牵扯到中医特有的证候诊断、处方动态生成、医嘱类型拆分、药品剂量单位换算,甚至还有填报合规、漏诊防御这些业务边界问题。
这篇就围绕我实际开发这个项目的全过程来聊:从模块设计到数据库建模,从登录鉴权到核心业务代码的写法,再到部署交付会踩的坑。我会把关键代码、建表思路和排查经验一起放出来,方便你直接参照修改。无论你是拿它做毕业设计、接外包,还是想自己从头搭一个中医门诊系统,这篇的内容都值得仔细看完。
1. 先想清楚:中医医嘱管理系统到底管的什么
1.1 中医门诊场景里的真实痛点
我之前去一家中医馆调研过,发现他们日常流程大概是这样的:老中医坐诊,手写病历和处方单,护士再把处方录入电脑,药房照方抓药。问题很快就暴露出来了:
- 同一个患者多次复诊,纸质病历难查历史,医生自己都容易记混上次开的方子。
- 处方录入环节靠人工,药品名、剂量、煎服方法经常录错字。
- “医嘱”不只是开药,还包括饮食禁忌、针灸推拿、复诊时间提醒、煎药方法等等,这些分散在不同人的笔记本里,根本无法统计。
- 开药缺乏历史参考,患者对某味药过敏,医生可能没注意到,因为翻不到旧的记录。
所以我做这个系统时,把核心定位落在“以患者为中心,以医嘱为核心”八个字上。所谓医嘱,不是一张简单的处方表,而是一条由诊断结果、中药处方、辅助疗法、生活建议、复诊计划共同构成的结构化记录。一句话总结:这个系统的本质,是把老中医脑子里那套灵活的、个性化的诊疗思路,转译成一套可检索、可追溯、可统计的数据模型。
1.2 技术选型不追新,够用且稳
技术栈我选的是 Spring Boot 2.x + MyBatis Plus + MySQL 5.7 + Redis(可选),前端用的是 Bootstrap 版后台模板,整体属于经典的后端渲染方案。你可能会问,现在前后端分离这么流行,为什么不上 Vue 或 React?
我在实际接手这个项目时想得很清楚:在真实的小型门诊场景里,用户往往就是医生助理和药房管理员,他们对系统的核心诉求是:打开页面就能用,录入要快,打印要方便。前后端分离架构除了增加接口联调成本之外,对这类内部系统的体验提升其实很有限。反而是传统模板渲染加简单 jQuery 操作,开发效率高、部署简单、出问题好排查。
如果后面需要扩展上门诊大屏、患者小程序预约,再单独抽一层 API 模块做前后端分离也不迟。这种渐进式扩展比一上来就上微服务要现实得多。
提示:如果你是用这个项目做毕业设计或课程项目,技术栈的亮点可以重点放在 Spring Security 的权限设计、MyBatis Plus 的动态 SQL、MySQL 事务一致性上,而不是堆砌分布式组件,评审老师一眼就能看出你是不是真懂的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 系统功能拆解:从“开药单”到“医嘱闭环”
2.1 六大核心业务模块的划分
按业务边界,我把整个系统拆成六个模块,每个模块之间尽量低耦合,但数据流是闭环的:
- 患者管理模块:负责建档、维护基础信息、查询历史就诊记录。
- 医生排班与坐诊模块:记录每位医生的坐诊时段,用于挂号分诊。
- 病历管理模块:结构化的中医病历,含主诉、现病史、既往史、舌苔脉象、中西医诊断结果。
- 中医医嘱模块:这是核心中的核心,向下拆成中药处方(含药味、剂量、煎法、服法)、中医适宜技术(针灸、拔罐、推拿)、健康指导(饮食起居情绪调节)、复诊提醒。
- 收费与药房联动模块:医嘱提交后自动生成费用清单,药房可看到待抓药明细,并做发药确认。
- 统计报表模块:按病种、药品使用频次、医生工作量等维度输出统计图表。
有一点我在设计时反复考虑过:到底要不要把药品库存管理并入系统?仔细权衡之后,我决定不并入。理由很实际,中医处方里的药味组合太灵活,常用药可能就几百种,库存管理通常是交给独立的中药房系统或 ERP 处理的。我在这个系统里只做“处方校验”和“发药状态”,不碰库存流水。边界划分清晰了,后面维护和交接都省心。
2.2 用户角色权限的设计逻辑
权限这块,我使用了基于 RBAC 的模型,定义了三种角色:系统管理员、医生、护士/药房操作员。
- 系统管理员:负责医生账号管理、基础数据维护、系统配置。
- 医生:负责病历书写、医嘱开立、复诊计划设置。医生的权限范围限定在“自己接诊的患者”,不能跨医生查询其他医生的详细病历,除非管理员授权会诊。
- 护士/药房操作员:只能查看已提交的医嘱、执行发药确认、录入煎药信息,没有修改处方内容的权限。
这个权限模型的落地其实不复杂,核心就是在每次查询时带上医生id 或者 deptId 的过滤条件。Spring Security 里我做了自定义的 UserDetailsService 和数据级权限切面,这样业务接口里就不需要反复写“判断是不是本人”的代码了。
2.3 业务流程的闭环设计
医嘱系统的流程如果画出来,大概是这样一条链:
code复制患者建档 → 医生接诊 → 书写病历 → 辨证论治 → 开立医嘱 → 提交审核 → 收费联动 → 药房抓药确认 → 复诊回访
这里面容易出问题的环节有两个:
第一是医嘱的“提交审核”,我设计了一个状态机。医嘱状态从“草稿”到“已提交”再到“审核通过”“已发药”“已完成”,每一步都记录操作时间和操作人,避免事后扯皮。
第二是“复诊回访”,这个词看着简单,实际门槛很高。患者三副药吃完之后,到底有没有改善?之前开的针灸疗程有没有按时做完?系统要能自动列出“今天该回访的患者”,并且由护士补充回访结果,这个结果会回填到后续医生接诊时的参考信息中。
3. 数据库建模中的核心权衡
3.1 一份可落地的表结构设计方案
建表是整个项目里最吃经验和耐心的一步。我这里直接给出核心表的字段设计,和字段说明一起列出来,你建表的时候可以直接参考:
patient(患者表)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint(20) | 主键 |
| name | varchar(64) | 姓名 |
| gender | tinyint(1) | 性别,1男2女 |
| birth_date | date | 出生年月 |
| phone | varchar(20) | 联系电话 |
| address | varchar(255) | 住址 |
| allergic_history | text | 过敏史,这个字段在中医场景里很重要 |
| medical_record_no | varchar(32) | 病历号,门诊编号,打印病历封面用 |
diagnosis_record(病历表)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint(20) | 主键 |
| patient_id | bigint(20) | 患者id |
| doctor_id | bigint(20) | 接诊医生id |
| visit_time | datetime | 就诊时间 |
| chief_complaint | text | 主诉 |
| present_illness | text | 现病史 |
| past_history | text | 既往史 |
| tongue | varchar(255) | 舌象描述 |
| pulse | varchar(255) | 脉象描述 |
| tcm_syndrome | varchar(255) | 中医证候诊断 |
| western_diagnosis | varchar(255) | 西医诊断 |
medical_order(医嘱主表)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint(20) | 主键 |
| record_id | bigint(20) | 关联病历id |
| order_no | varchar(32) | 医嘱编号,可带日期前缀 |
| order_type | tinyint(1) | 医嘱类型:1中药处方,2针灸,3推拿,4健康指导 |
| status | tinyint(1) | 状态:0草稿,1已提交,2审核通过,3已发药,4已完成 |
| create_time | datetime | 开立时间 |
| doctor_id | bigint(20) | 开立医生id |
medical_order_detail(医嘱明细表)
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint(20) | 主键 |
| order_id | bigint(20) | 医嘱主表id |
| item_name | varchar(128) | 药品名或项目名 |
| dosage | varchar(64) | 剂量,比如“10g”“15g” |
| usage_method | varchar(255) | 用法,比如“水煎服” |
| frequency | varchar(32) | 频次,比如“每日一剂,早晚分服” |
| days | int(11) | 天数 |
我特意把 dosage 字段设计成 varchar,而不是数字,这是很多人容易踩的坑。中药处方里有些药是按“克”“钱”“片”“条”计算,还有“先煎”“后下”这种特殊要求,比如“附子 6g(先煎)”,用纯数字很容易丢语义。既然这里有编码的健壮性要求,我宁可在查询统计时再单独抽一层药品分析接口,也不让录入时因为类型限制而导致信息丢失。
3.2 为什么把医嘱拆成主表和明细表
这是整个数据库设计里最关键的一个决策。一开始我也有过偷懒的想法:把一副药方的所有药味塞在一个字段里,用逗号分隔,查出来直接展示,方便是挺方便,但后续如果要统计“某某药材在三个月内被开过多少次”“哪些药经常搭配在一起”,单独拆分字段就会让你做大量的字符串处理,又慢又容易错。
拆成主表和明细表之后,每一味药都是一条独立的数据记录。统计药材频次一个 group by 就搞定了:
sql复制SELECT item_name, COUNT(*) AS cnt
FROM medical_order_detail
GROUP BY item_name
ORDER BY cnt DESC;
一目了然。而且以后如果需要和药品库存系统对接,药品主数据是唯一索引,关联过去非常干净。
3.3 索引设计的实际经验
这套系统里的数据量不大,但索引设计还是不能马虎。我比较关注的是这几个:
- diagnosis_record 表上建了 (patient_id, visit_time) 联合索引,查询患者历史病历时直接走索引,不需要回表过滤。
- medical_order 表上建了 (doctor_id, status) 联合索引,医生待办列表的查询很频繁。
- medical_order_detail 表上建了 (order_id) 普通索引,这个不用多说,主表关联明细表必用。
我确实发现了一个实际问题:最初我在 medical_order_detail 表里漏建了 order_id 索引,当医嘱数量超过两万条之后,打开一条旧医嘱的详情竟然要花两三秒。加完索引之后,基本是毫秒级返回。这个真实经历说明,数据量小的时候确实感觉不到,但设计提前做好索引,未来能少折腾很多。
4. 核心实现解析:从登录鉴权到处方生成
4.1 Spring Security 鉴权流程的落地
这个系统的鉴权我没用太复杂的 JWT 方案,内部后台管理系统用的是 Session + 自定义过滤器。一个原因是为了保证实现简洁可维护,另一个原因是这套系统几乎没有移动端,不需要考虑跨域 Token 刷新的问题。
我需要说明一下,为什么很多 Spring Boot 教程都推荐 JWT,而我在这种场景里偏偏选择 Session。这是因为 JWT 的优势在无状态、跨域共享的场景里比较突出,但如果只有单一服务端,Session 的“服务端可强制注销”和“简化权限变更”反而是更实用的。医生账号如果被盗用,管理员一键踢人下线,用 JWT 就很难做到不配合黑名单机制。
登录的流程我写成这样:
java复制@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserService.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
List<SimpleGrantedAuthority> authorities = authorityMapper
.selectByUserId(user.getId())
.stream()
.map(auth -> new SimpleGrantedAuthority(auth.getPermCode()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getPassword(), authorities);
}
关键点在于权限编码,比如 doctor:order:add、pharmacist:order:dispatch,然后通过 @PreAuthorize("hasAuthority('doctor:order:add')") 直接挂在 Controller 方法上,可读性很好,新人接手也能看懂。
4.2 医嘱生成的业务逻辑与事务管理
开医嘱是整个系统里业务逻辑最重的一环。一个规范的开嘱动作,需要同时做这几件事:
- 校验患者存在且状态正常。
- 作废当前诊断下旧版草稿医嘱。
- 插入医嘱主表,状态为草稿。
- 批量插入医嘱明细,保证至少有一味药或一个项目。
- 记录费用清单草稿。
这几步必须在一个事务里,否则会出现主表成功、明细失败的数据残缺情况。我这里用 @Transactional 做事务控制,并且设置了 rollback 规则:
java复制@Transactional(rollbackFor = Exception.class)
public Long createOrder(CreateOrderRequest request) {
MedicalOrder order = new MedicalOrder();
order.setRecordId(request.getRecordId());
order.setDoctorId(CurrentUserHolder.getUserId());
order.setOrderType(request.getOrderType());
order.setStatus(OrderStatus.DRAFT.getCode());
order.setCreateTime(new Date());
medicalOrderMapper.insert(order);
List<MedicalOrderDetail> details = request.getDetails().stream()
.map(d -> {
MedicalOrderDetail detail = new MedicalOrderDetail();
detail.setOrderId(order.getId());
detail.setItemName(d.getItemName());
detail.setDosage(d.getDosage());
detail.setUsageMethod(d.getUsageMethod());
detail.setFrequency(d.getFrequency());
detail.setDays(d.getDays());
return detail;
}).collect(Collectors.toList());
if (details.isEmpty()) {
throw new BusinessException("医嘱明细不能为空");
}
medicalOrderDetailMapper.batchInsert(details);
return order.getId();
}
提示一下,这里特别值得注意一个细节:Spring 的事务默认只在抛出 RuntimeException 时才回滚。如果你在业务代码里 catch 了异常然后吞掉,事务是不会感知到的。所以务必要把 rollbackFor = Exception.class 写清楚,这是一个很容易在项目评审时被追问的点。
4.3 中药处方与“证候—禁忌”的关联逻辑
开中药处方时,一个很有中医特色的点是药味之间的“十八反、十九畏”配伍禁忌。这部分逻辑如果靠医生自觉,那系统就不够专业了。我搞了一个 herb_conflict 表,存了常用的禁忌配伍对:
| herb_a | herb_b | reason |
|---|---|---|
| 甘草 | 甘遂 | 十八反 |
| 乌头 | 半夏 | 十八反 |
| 藜芦 | 人参 | 十八反 |
开处方提交的时候,后端有一段校验逻辑,逐行检测同一张处方里有没有出现冲突药对:
java复制private void validateHerbConflict(List<OrderDetailRequest> details) {
List<String> herbNames = details.stream()
.map(OrderDetailRequest::getItemName)
.collect(Collectors.toList());
List<HerbConflict> conflicts = herbConflictMapper
.selectByHerbNames(herbNames);
if (!conflicts.isEmpty()) {
throw new BusinessException("处方中存在配伍禁忌: " +
conflicts.get(0).getHerbA() + " 与 " +
conflicts.get(0).getHerbB() + ",请重新核对");
}
}
这个列表不可能覆盖临床全部情况,中国药典收录的禁忌组合很多,但在系统里可以先将常规常见的几百组内置进去,后台允许管理员增删。这就解决了“系统阻碍灵活用药”和“盲目开药存在隐患”之间的平衡问题。
4.4 收费用药与药房联动的实现思路
医嘱提交审核通过以后,系统会自动生成一条收费记录,包含药品费或治疗费。收费状态的流转是:待收费 → 已收费 → 已发药。
这里我做了一个比较特殊的设计:药房操作员能在“已收费”状态下看到处方明细,但发药之前系统会先校验收费状态是否是“已完成”。这个逻辑避免了“药发出去了钱还没收”的管理漏洞。
技术上用一张独立的 charge_record 表关联 medical_order_id,保留收费操作人、收费时间、费用金额。如果某条医嘱因为患者退费,还需要回滚医嘱状态,让医生能重新修改处方。这种双向状态同步,就是我在状态机设计里提到的比较关键的部分。
5. 实操过程实录:从启动到跑通全流程
5.1 本地环境准备与初始化
开发环境我这边用的是 JDK 1.8、Maven 3.8、MySQL 5.7,Redis 部分功能我做了降级设计,没 Redis 也能跑,只是登录验证码和缓存部分自动切换为本地内存模式。启动步骤非常简单:
- 创建数据库
tcm_order_db,字符集选 utf8mb4。 - 执行项目里附带
sql目录下的初始化脚本,包括建表语句和默认管理员账号数据。 - 修改
application.yml里的数据源账号密码。 - 启动
SpringBootApplication,访问http://localhost:8080/。 - 默认管理员账号:admin / admin123,首次登录后强制改密。
步骤本身不复杂,但我还是要提醒你,如果你拿到的是别人的开源源码,第一步一定要看它的 application.yml,确认数据库名和端口有没有被改过。如果你没注意环境配置差异,经常会花半小时去排查一个“明明代码没问题但就是连不上库”的问题。
5.2 快速生成一套演示数据的技巧
能展示完整流程意味着要有一套能支撑业务闭环的演示数据。我自己写了一个 DataInitializer,在系统启动时检测患者表数据量,如果等于 0 就自动插入 10 位虚拟患者、3 位医生、若干历史病历和医嘱。
这样你演示的时候,从患者列表点进去,直接就能看到历史病历时间线,而不是空荡荡的界面。如果你接手这套代码,这一步强烈建议保留,在真实交付阶段把它关掉就行,因为有一个开关控制是否启用,非常方便。
5.3 页面交互与打印单据的实现
中医门诊有一个使用频率极高的功能:打印病历封面和处方笺。我这边直接用浏览器的 window.print(),配合专门的打印样式表 print.css 来实现。样式表里设置 @media print,把打印区域外的一切元素隐藏,并用 @page { margin: 10mm; } 调整页边距。打印模板就是普通 HTML 排版,医生点一下“打印处方”按钮,浏览器弹窗直接出内容,省去了对接后端生成 PDF 的复杂流程。
这个方案的优点是简单、零依赖。缺点是不同浏览器的打印效果略有差异,Chrome 是我试下来最好的选择,Firefox 的表格线粗一像素问题在打印时候容易显现。
6. 常见问题与排查技巧实录
6.1 高频问题整理
我整理了一下这个项目里比较容易翻车的高频问题,做成速查表给你:
| 问题描述 | 根源方向 | 排查建议 |
|---|---|---|
| 登录后列表页打不开,提示 500 | 多半是 MyBatis Plus 的 tenant_id 或者逻辑删除字段配置有误 |
查看控制台完整 SQL,直接复制到 Navicat 里执行 |
| 保存医嘱时报“明细不能为空” | 前端提交的 JSON 字段名与后端不一致 | 仔细对比前端 layui table 或 form 的字段绑定名 |
| 发药状态一直无法流转 | 状态机校验不过,比如先改了发药状态但收费状态没置为已完成 | 到数据库查看 charge_record 和 medical_order 的状态值 |
| 同样是查询,先查快后查慢 | 索引缺失或者 SQL 没走索引 | 对慢 SQL 执行 EXPLAIN,看扫描行数 |
| 验证码图片不显示 | 项目禁用了 Redis 导致缓存配置读不到 | 查看启动日志确认是否走默认本地缓存策略 |
6.2 一次典型的“事务不回滚”排查经历
我在联调阶段遇到过一次比较典型的问题:同事拿我的代码测试,连续开三张医嘱,前两张成功,第三张故意提交空明细,按说应该抛异常回滚。结果他打开数据库一看,前两张订单全部写入成功,第三张也插了一条没有明细的主记录。
我第一反应是事务没生效,排查了三个地方:
- 类上有没有加
@Service注解?加了。 - 启动类有没有加
@EnableTransactionManagement?2.x 之后其实默认支持。 - 异常有没有被 Controller 层的 try-catch 吃掉?排查后发现确实是某一个 service 内部用了
try-catch (Exception e) { e.printStackTrace(); },把异常吞掉后还返回了一个错误对象,事务当然不会回滚。
这里可以直接给结论:事务管理与异常传播的关系相当密切,任何被吞掉的异常都会让事务感知不到真正的失败。 我后来统一改成了用一个全局异常处理器 @RestControllerAdvice,所有业务异常都在 Controller 边界统一捕获,service 层全部直接抛出去,问题彻底解决。
6.3 数据初始化脚本的版本管理经验
项目里的 SQL 脚本我推荐放到 db/migration 目录里,文件名带版本号,比如 V1__init_schema.sql、V2__add_herb_conflict.sql。每一版改动都用增量 SQL,不直接改旧脚本。这样无论在本地还是服务器上执行,环境一致性都有保障。
可别小看这一点。我见过很多源码下载包里只有一份全量 SQL,你拿到手在本地一跑没问题,但上线之后发现要新增一个字段,直接手动去生产库执行 ALTER TABLE,时间一长,根本没人知道线上库和项目代码的版本差异在哪。用版本化迁移管理,能把这个问题从机制上消除。
7. 部署上线与交付经验
7.1 服务器端部署三步走
项目最终部署在 Linux 服务器上,我用的是最经典的方式:打包成可执行 Jar,然后用 systemd 做成服务,日志写到指定目录。步骤大致如下:
bash复制mvn clean package -DskipTests
scp target/tcm-order-system.jar root@server:/opt/tcm/
然后新建一个 tcm.service 文件,核心内容:
ini复制[Service]
WorkingDirectory=/opt/tcm
ExecStart=/usr/bin/java -Xms256m -Xmx512m -jar /opt/tcm/tcm-order-system.jar
Restart=always
RestartSec=10
启动服务并设为开机自启:
bash复制systemctl daemon-reload
systemctl enable tcm
systemctl start tcm
这种方式比直接用 java -jar 启动挂在前台要稳得多,服务意外退出会自动重启,运维省心。如果需求更正式一点,前端页面还能用 Nginx 做反向代理,同一端口下路径转发,不过那属于额外优化了,我这边暂时没有做到这个程度。
7.2 交付源码时的清单检查
每次把源码给出去之前,我建议你做一轮系统自查,我总结了七个检查点:
- 是否删掉了自己机器上的本地绝对路径配置。
application.yml里的密码是否改成了环境变量占位符。- README 里是否写清楚了启动步骤、账号、数据库版本。
- 初始化 SQL 是否能在干净库里一次执行通过。
- 是否清空了敏感受试患者的真实信息。
- 是否有不必要的调试输出,比如
System.out.println、printStackTrace。 - 静态资源里是否残留无用的 demo 文件,比如临时测试上传的图片。
这套清单做完,交付整体都会顺畅很多,少一次“哎呀这个忘了删”的尴尬。
8. 后续扩展方向与我的几点体会
做完这个命令系统之后,我明显感觉到这类垂直领域业务系统比通用后台管理系统更有意思,因为你需要去理解一个行业的运行逻辑。
我接下来还打算做这样几个扩展方向:
- 患者微信小程序:允许患者在线查看自己的病历和医嘱,复诊预约提醒,不用到店排队。
- 中药处方拍照识别:通过 OCR 识别纸质处方,自动转为结构化明细,减少录入成本。
- 中医体质辨识问卷:在患者建档时引入“九种体质”问卷,辅助医生诊断和养生建议生成。
- 引入更细粒度的数据统计:按证候、方剂、季节做多维分析,给门诊运营提供参考。
我个人印象最深的一点是:在开发过程中,我和一位中医朋友反复对了很多遍业务概念,例如“辨证论治”“随症加减”到底该怎么结构化,这其实是跨学科协作的典型体验。技术不难,难的是你对业务的理解深度。中医医嘱系统看起来是软件项目,但如果你不懂中医的临证思路,做出来的就只是套着中医皮的空壳。
最后再分享一个小技巧:如果你是拿这个项目去面试,重点是讲清楚“医嘱类型为什么用状态机来管理”“处方明细为什么要拆表存”,而不是背框架API。能把这些为什么讲明白的人,通常就是面试官真正想找的那种有真实项目经验的人。
