做中医类的管理系统,最麻烦的地方从来不是代码,而是怎么把"辨证论治"这套中医思维,落成一套让医生愿意用的结构化流程。最近刚好手头在搞一个基于Spring Boot的中医医嘱管理系统,整个项目从零到能跑大概花了两周,源码也整理出来了,编号16586。这套系统的核心价值,就是把开方、审方、用药叮嘱、复诊跟踪这一整条链子,从手写处方签变成线上闭环,既给中医馆省了事,也让患者拿到医嘱之后不再是一张纸看完就扔。我尽量把项目里的设计思路、表结构、核心接口和一些实战中踩过的坑,按能直接复现的标准写出来。不管你是刚学Spring Boot想做课设,还是诊所里想搞信息化,这篇东西应该都能让你少走不少弯路。
1. 系统整体设计与模块拆解
1.1 中医医嘱到底"特"在哪
很多人一听到"医嘱管理",第一反应就是照搬西医的处方系统。真做下来你会发现,这两者的核心逻辑差别非常大。西医处方基本是"诊断→开药→用法用量"一条直线,药品名称、剂量、频次都是标准化的。但中医医嘱的链条是"辨证→立法→选方→用药→调护",每一步都带着医生个人的辨证思路。
举个例子,同一个患者来看失眠,一个医生可能辨证为心脾两虚,开归脾汤加减;另一个医生可能看到的是肝郁化火,直接上龙胆泻肝汤。方剂不是固定的药品ID组合,而是围绕"证型"展开的动态组合。所以我在设计这个系统的时候,没有把"医嘱"简单做成一张药品明细表,而是把"辨证记录"单独拆出来,让每一条医嘱都能追溯到当时的辨证过程。如果以后要复诊对比,医生一眼就能看到上次是什么证、用了什么法、开了什么方,这样整个病情演变就有迹可循。
另外还有一层特殊性:中医医嘱里的"用药指导"不是简单的"一日三次",而是包含煎法、服法、忌口、情志调摄、生活作息这些调护内容。这些信息在传统的HIS系统里通常没有专门字段,容易被医生手写在备注里,最后患者也看不清。所以我在系统里专门设计了"医嘱扩展信息",把煎煮方法、服用时间、禁忌事项结构化存起来,打印出来的医嘱单才算真正完整。
1.2 功能模块划分与业务流程串讲
整个系统按角色和业务链路,我把它拆成了七个核心模块:
- 系统管理:用户、角色、菜单权限,用的是经典的RBAC模型
- 患者管理:患者基本信息、过敏史、既往病史、体质档案
- 辨证论治:望闻问切信息记录、证型判定、治法选择
- 方剂管理:经典方剂库、药材组成、剂量与加减法参考
- 医嘱管理:开立医嘱、审核医嘱、执行记录、状态流转
- 用药提醒:按医嘱生成服药计划,对接小程序或短信通道
- 统计报表:复诊率、用药频次、证型分布等基础统计
这里我想重点说下业务流转的设计。医嘱不是开完就完了,它是有状态的。我把医嘱状态定义成了五个:
- 待审核:医生开完方,还没经过科室负责人审核
- 已确认:审核通过,准备进入发药/执行环节
- 执行中:患者已取药,按计划开始用药
- 已归档:整个疗程结束,医嘱归档可查
- 已作废:开方有误或被医生主动撤销
这个状态机看起来简单,但实际用起来很舒服。因为中医馆经常有老医生坐诊、年轻医生跟诊的情况,年轻医生开的方子需要老医生把关。有了审核流,系统里就明确划分了"开方权"和"审核权",这在合规层面非常重要。而且后面做统计报表的时候,直接按状态过滤,不会再出现"这个医嘱到底算没算数"的争议。
2. 技术选型:为什么是Spring Boot + MyBatis-Plus
2.1 核心后端技术栈
这套系统的后端技术栈非常主流,没有引入任何"为了显得高级而用"的组件。我在选型的时候有一个原则:凡是单体应用能解决的,绝对不上分布式;凡是成熟稳定的老版本,绝不盲目追新。
核心依赖清单如下:
- Spring Boot 2.7.x:稳定、生态成熟,遇到问题一搜就有答案
- MyBatis-Plus 3.5.x:单表CRUD不需要手写SQL,复杂查询再自己写XML
- MySQL 8.0:医疗场景数据量不大,单库单表完全够用,加了索引查询依然毫秒级
- Redis:用来做验证码缓存和Token刷新,不存核心业务数据
- JWT:无状态登录,避免传统Session在集群环境下的同步问题
- Knife4j:接口文档,前后端联调的时候特别省事
- Vue 3 + Element Plus:前端独立工程,后端只提供JSON接口
我特意选了Spring Boot 2.7而不是3.x,原因很简单:2.7用的人多、资料多、踩坑的分享也多,而且很多老项目的依赖兼容性更好。对于这个体量的项目,升级到3.x带来的性能提升其实体感不明显,反而可能被Jakarta命名空间迁移折腾半天。
2.2 为什么不用更重的微服务方案
有些朋友一上来就问:要不要拆成用户服务、医嘱服务、药品服务几个微服务?我的回答是不需要。中医医嘱管理系统的核心业务就发生在一家中型中医馆或者一个小型中医院内部,日均医嘱量撑死几百条,数据量远没到需要分库分表的程度。微服务的优势在独立部署和弹性伸缩,但代价是分布式事务、服务发现、链路追踪这一整套复杂度。用单体应用,一台普通服务器跑跟玩一样,维护起来也省心。
当然,如果以后真要做集团化的连锁中医馆,系统肯定要改造成多租户模式,那时候再按"患者域""处方域""库存域"切分微服务也不迟。但那是业务规模逼出来的重构,不是前期设计就应该背上的包袱。
2.3 关键依赖配置与注意事项
这套系统里有几个配置细节要特别注意,我在搭建的时候踩过不少坑。
第一个是MyBatis-Plus的分页插件。如果只引入依赖而不注册PaginationInnerInterceptor,分页查询会直接查出全表数据,在数据量上来之后非常危险。配置方式很简单,在配置类里加一个MybatisPlusInterceptor的Bean就行。
第二个是Redis序列化方式。默认的JDK序列化存中文会变成乱码,调试时看着非常难受。我建议统一用GenericJackson2JsonRedisSerializer,可读性好,跨语言也没有兼容问题。
第三个是时间的全局序列化格式。后端返回的时间格式如果不统一,前端会收到一串时间戳,显示还得自己处理。我在application.yml里配了全局的日期格式,并且把时区固定为GMT+8,避免服务器部署在海外时时间错乱。
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+83. 数据库设计:把"辨证论治"落成表结构
3.1 核心表设计与关系说明
数据库是整个系统的地基,我花的时间最多。总共设计了十一张核心表,这里挑最重要的几张说一下设计思路。
患者表patient:除了姓名、性别、年龄、手机号这些基础字段,我专门加了constitution_type(体质类型)和allergy_history(过敏史)。体质类型在中医里非常重要,同样是感冒,气虚体质和湿热体质的用药思路完全不一样,这个字段能帮医生在开方前快速做参考。
辨证记录表diagnosis_record:字段包括syndrome_type(证型)、treatment_principle(治法)、tongue_image(舌象)、pulse_condition(脉象)、clinical_manifestation(临床表现)。舌象和脉象这种信息在传统系统里基本是文本备注,但在中医系统里它们恰恰是辨证的核心证据。我把它们独立成字段,就是为了方便以后做统计分析,比如"舌红少苔"和"阴虚证"的相关性。
方剂表formula和药材表herb:它们之间是多对多关系,通过中间表formula_item关联,中间表里带dosage字段。药材库里除了名字和功效分类,我建议一定要加unit和default_dosage,因为在医嘱明细里最终要生成完整的用药剂量。
医嘱主表medical_order:这是整个系统的核心表。字段包括order_no(单号)、patient_id(患者ID)、doctor_id(开方医生)、diagnosis_id(关联辨证记录)、formula_id(关联方剂)、total_days(用药天数)、status(状态)、review_remark(审核意见)。这里最需要注意的关系是:医嘱不是直接挂患者,而是先挂诊断记录,再通过诊断记录挂到患者。这样设计以后,查"这个患者这半年看过几次病、每次是什么证"就非常方便。
3.2 医嘱状态机与数据流转
设计表结构的时候还有一个点特别重要:状态字段别用tinyint存魔法数字,然后代码里到处写if (status == 2)。我在项目里用了一个枚举类OrderStatusEnum,把状态码、描述、操作角色、下一步状态全部枚举化。比如WAIT_REVIEW只能由"审核人"角色操作,可以流转到CONFIRMED或CANCELLED。这样做的好处,一是前端下拉框直接绑定枚举,二是后端的权限校验有据可依,三是后期如果要加流程引擎,枚举可以直接对接。
数据流转的整个过程是这样的:医生创建医嘱时状态为待审核,审核人通过后自动生成用药提醒计划,计划推送到患者端。患者在用药过程中,系统根据医嘱的total_days自动判断是否需要生成复诊提醒,等复诊完成之后,医生会再开一条新医嘱,老医嘱自动进入归档状态。整个数据链路中没有"孤儿数据",每一条记录都能查到它的上下游。
4. 核心实现细节与实操步骤
4.1 开立医嘱的完整接口流程
开医嘱这个接口是整个系统里最核心的功能,我前前后后重构了三版。第一版图省事,一个接口里塞了一百多行代码,又是查患者、又是查方剂、又是算剂量,结果改一个需求就得动一大片代码。后来我按领域逻辑把开医嘱拆成了两个接口:一个是"保存草稿",一个是"正式提交"。
保存草稿接口只做一件事:把前端传来的医嘱数据完整存进数据库,状态置为待审核,不做任何业务校验。这就解决了医生在诊疗过程中断断续续录入的问题,比如一边问诊一边录入四诊信息,十几分钟后才正式开方。
正式提交接口才是核心,它的逻辑顺序是这样的:
- 校验患者存在且状态正常
- 校验医生拥有开方权限
- 校验辨证记录、方剂记录一致性
- 校验方剂药材库存是否满足疗程用量
- 生成医嘱单号(格式:
ZY+ yyyyMMdd + 4位流水号) - 保存医嘱主表、医嘱明细、调护信息
- 发布一条消息给消息中心,触发用药提醒计划生成
@Transactional(rollbackFor = Exception.class) public SubmitResult submitOrder(OrderSubmitDTO dto) { Patient patient = patientMapper.selectById(dto.getPatientId()); if (patient == null || patient.getStatus() == 0) { throw new BizException("患者不存在或已停诊"); } Formula formula = formulaMapper.selectById(dto.getFormulaId()); if (formula == null) { throw new BizException("方剂不存在"); } List<OrderItemDTO> items = dto.getItems(); checkStock(items); MedicalOrder order = buildOrder(dto, patient, formula); medicalOrderMapper.insert(order); items.forEach(item -> orderItemMapper.insert(buildItem(item, order))); publishReminderPlan(order.getId()); return SubmitResult.success(order.getOrderNo()); }这里有一个非常重要的实操经验:整个开方过程必须加@Transactional,而且事务边界要覆盖到"库存扣减"这一步。我见过很多项目把库存扣减放到异步消息里去处理,结果并发一高,库存就超卖。虽然医嘱系统不像电商那样有恐怖的并发量,但严谨一点总没错。
4.2 方剂与药材的关联处理
方剂管理模块里藏着一个新手特别容易忽略的坑:方剂里面的药材用量,到底是"方剂级别"的还是"医嘱级别"的?举个例子,一个医生经常开四君子汤,默认剂量是党参15克、白术15克、茯苓15克、甘草6克。但这次这个患者脾虚得厉害,党参想加到20克。如果用量存在方剂表里,那每次开方都要复制一份方剂出来,非常麻烦。
我的处理方法是:方剂表里的formula_item保存的是参考剂量,医嘱明细里的order_item保存的是实际用量。开方的时候先带出参考剂量,医生可以在前端自由调整,提交时按实际剂量保存。这样既保留了医生个人的开方习惯,又给了个体化调整的空间。两套数据互不干扰,统计"某方剂平均用药量"的时候两种数据都能算,看你想从哪个维度分析。
这里还涉及一个经典问题:同一方剂里如果出现相同的药材怎么处理。比如很多时方里既有生姜又有大枣,而这两个也可能作为单味药出现在方子里。所以我在order_item里加了item_seq字段做区分,避免前端遍历时用药材ID作为唯一键导致渲染错乱。
4.3 权限与登录鉴权实操
权限这块我用了Spring Security + JWT的组合。很多教程喜欢甩一长串配置类,真正搞清楚每一步干什么的人不多。我这里给一个简化但能跑的思路。
登录接口校验用户名密码,通过之后生成JWT,Token里塞三个关键信息:用户ID、角色编码、吊销版本号。这里有个细节:Token过期时间我设成8小时,跟一个诊次的时长匹配,既不会让医生频繁登录,也不会因为Token太长生出安全隐患。
权限控制通过自定义注解@RequireRole实现,配合Spring的HandlerInterceptor。比如审核医嘱接口上直接标@RequireRole("AUDITOR"),没有审核角色的人请求直接返回403。这种硬编码的方式看着不够灵活,但对这个规模的项目来说足够清晰,也比引入复杂的权限框架更好维护。Redis在这里用来存Token黑名单,如果用户主动退出或者管理员禁用账号,就把Token加进黑名单,过期时间正好等于Token剩余有效时间。
4.4 复诊提醒与定时任务
复诊提醒是这个系统一个比较亮眼的功能。在传统流程里,医嘱开完,医生提醒患者"吃完这七副再来复诊",但患者经常忘。系统则是在医嘱确认之后,自动生成一条提醒计划,在疗程结束前一天触发提醒。
实现起来用的是Spring自带的@Scheduled定时任务,配合一个medication_reminder表。每天晚上九点,定时任务扫描明天的用药计划,把需要提醒的记录捞出来,通过消息平台推送。这个方案用不上消息队列,更用不上重型的分布式任务调度框架,因为数据量小、任务简单,一个定时任务加一张表完全够用。
@Scheduled(cron = "0 0 21 * * ?") public void sendTomorrowReminder() { List<ReminderPlan> plans = reminderPlanMapper.selectTomorrowPlans(); plans.stream() .filter(plan -> plan.getRemindStatus() == 0) .forEach(plan -> { messageSender.sendRemind(plan.getPatientId(), plan.getOrderId()); plan.setRemindStatus(1); reminderPlanMapper.updateById(plan); }); }这里其实还隐含了一个提升体验的小设计:不是只提醒一次,而是分两次。第一次在疗程开始前提醒"明天开始服药,记得按时煎药",第二次在疗程最后一天提醒"疗程即将结束,请预约复诊"。两次提醒的内容完全不一样,第一次解决的是"别忘吃药",第二次解决的是"别忘复诊"。角度不同,才能督促患者真正完成一个完整疗程。
5. 部署与调试实录
5.1 本地环境准备
先把环境说清楚。我本地用的组合是:JDK 1.8、Maven 3.6.3、MySQL 8.0、Redis 6.2。这个组合非常经典,如果你用IDEA开发,装好Lombok插件基本不会再遇到什么环境层面的问题。
拿到源码之后,第一步不是急着跑起来,而是先看配置文件。重点检查application.yml里的数据源地址、Redis地址和端口有没有改成自己本地的值。如果数据库里还没有初始化数据,脚本目录下有一个init.sql,直接执行就行。这个脚本里包含了建库、建表、初始化管理员账号以及一些演示数据,执行完系统里就有一个可用的管理员账号和两条测试医嘱记录。
启动顺序也有讲究:先启动Redis,再启动Spring Boot后端。如果你先把后端跑起来,Redis没启动,系统启动过程会报连接异常。有些项目会做Redis延迟连接,但我的实现里没有做这层容错,因为我觉得开发阶段把问题暴露出来比掩盖问题更利于排查。
5.2 打包部署与常见环境问题
生产环境部署我用了最保守的方案:打包成可执行JAR,然后扔到服务器上用nohup跑。前后端分离,前端用Nginx托管静态文件,后端跑在8080端口,Nginx配置一段反向代理/api路径到后端服务。
server { listen 80; location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有两个常见问题值得提醒。第一个是跨域。如果前端直接调用后端地址,而不是通过Nginx代理,浏览器会报CORS错误。我前期的解法是在后端加一个全局CORS配置类,后期还是改成了Nginx反向代理的方式,因为生产环境用同域请求最省事,CORS配置就当开发环境调试用的临时通道。
第二个问题是MySQL连接时区。如果你在application.yml里只配了jdbc:mysql://localhost:3306/zy_order不带serverTimezone,大概率会报The server time zone value异常。解决方法是连接串后面加?serverTimezone=Asia/Shanghai&characterEncoding=utf8,两个参数一个管时区,一个管字符集,缺了后面查中文数据会出现乱码。
6. 踩坑记录与排查技巧实录
6.1 数据库层面的典型问题
先说我印象最深的一个坑:MyBatis-Plus的字段自动填充。我在很多表里加了create_time和update_time字段,希望通过@TableField(fill = FieldFill.INSERT)自动填充。但配置了自动填充之后,发现更新操作时update_time始终不变化。排查半天,原因是实体类字段名用的是updateTime,但数据库字段名是update_time,数据库下划线转驼峰的配置没生效。检查一下map-underscore-to-camel-case是否设置为true,以及实体类的@TableName注解是否正确即可。这个问题虽然低级,但特别容易在新项目里翻车。
第二个坑是分页查询丢数据。某天测试发现,查看医嘱列表第二页时数据重复了,原来是排序字段没有加唯一性。order_no虽然是唯一的,但如果不显式指定,MySQL会按主键排序,而主键是自增ID,正常来说不会重复。问题出在我用create_time排序,而很多记录是同秒创建的,排序不稳定就会翻页错乱。解决方案是排序语句里加一个次要排序字段,比如order by create_time desc, id desc。
6.2 业务逻辑层面的隐藏陷阱
开医嘱接口在压测环境下遇到过一次并发问题:同一个患者几乎同时提交两条一模一样的医嘱,结果两条都成功了。这在实际场景中不太可能发生,但如果是连续点击"提交"按钮,前端没有做防重复处理,就很容易出现重复数据。我的解法是在数据库层面给源单号字段加唯一索引,在业务层面做幂等校验。前端也做了按钮防抖,三重保障基本上杜绝了这个问题。
方剂库存换算也是一个容易出错的点。库存表里存的是克数,但药材有的按克抓,有的按副算(比如大枣三枚)。前端界面如果直接显示"库存剩余"数字,容易产生误导。我的处理是封装了一个StockConverter工具类,按药材的unit字段自动换算展示单位,让库存语义更加清晰。这个类看着不复杂,但如果不做,后面药品入库、盘点、医嘱发药三个环节的数据口径都会打架。
6.3 前后端联调与并发处理经验
联调阶段最影响效率的事情是接口文档维护。一开始我用Word写接口说明,前端每次都要来问"这个字段到底是什么含义",后来换成Knife4j自动生成接口文档,每次后端改完接口,前端刷新页面就能看到最新的参数说明。效率提升不是一点半点。
还有一次是权限拦截放行问题。后端的登录接口、验证码接口、患者查询接口都要区分是否需要鉴权。我一开始把所有接口都拦了,导致前端登录页面都进不来。后来用白名单配置拦截规则,把认证相关的接口放行,其他的统一走JWT校验。这里要特别注意:接口路径匹配用AntPathMatcher比直接用String.equals靠谱得多,能处理路径通配符和参数占位符。
7. 调试技巧与日志分析思路
7.1 日志分级与关键日志埋点
项目里的日志我用Logback做了分级处理。开发环境打印DEBUG级别,生产环境只保留INFO和ERROR。但是业务日志不能只分级别,更要分场景。比如开医嘱这种核心操作,我在接口入口、库存校验、提交成功三个位置都打了日志,日志内容包括单号、患者ID、医生ID这些关键信息。这样一旦患者反馈"我明明开过方但系统找不到",我就能通过单号把整个操作过程复盘出来。
7.2 慢SQL定位与优化
某次测试发现医嘱列表接口在数据量达到五千条时响应时间超过三秒,用日志分析工具定位到是一条多表关联查询没有命中索引。原SQL是这样的:医嘱表left join患者表left join辨证记录表,但辨证表的patient_id字段没有建索引。通过查看执行计划发现这里走了全表扫描,加上索引之后响应时间降到一百毫秒以内。这里也建议大家遇到性能问题先看执行计划,大部分慢查询都是索引缺失导致的,不是代码本身写得差。
7.3 MyBatis-Plus的SQL日志打印
MyBatis-Plus有个非常好用的调试功能:在配置文件中开启SQL日志打印。开启之后控制台会输出每条SQL语句及执行结果,分析问题时可以看到MyBatis-Plus实际生成的SQL长什么样,特别适合排查那些"奇怪的查询结果"是由什么逻辑造成的。
mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl顺便提一个很实用的技巧:在开发环境把SQL日志配上格式化输出,假设你在调试一个多表查询,就能用同一个日志窗口看到三到四条SQL依次执行,整个数据请求过程一目了然。生产环境一定不要开这个,日志量太大会影响性能,而且SQL语句中包含敏感业务信息,存在泄露风险。
8. 合规与安全注意事项
这里必须多说一句。医疗数据不是普通业务数据,个人信息保护法实施之后,患者信息属于敏感个人信息,处理的时候必须有合规意识。在这个项目里我做了几件事:一是患者手机号在列表接口里做了脱敏处理,只显示前三位后四位;二是患者的完整信息查询需要独立的权限,不是所有角色都能看;三是在导出患者数据的时候做了操作日志留痕,哪个人在什么时间导出了哪些数据都记录在案。
另外要说的是,这个项目做的是"医嘱管理",不是"医疗诊断"。系统里的方剂库、证型库是给医生提供参考和记录的,绝对不能让系统自动给患者推荐方剂,更不能输出任何"诊断结论"。我在系统设计里把"方剂参考"和"实际开方"明确区分开,系统只记录医生已确认的辨证和方剂数据,不做任何自主决策。这个底线一定要守住,不管以后怎么加功能,都不能越过这条线。
如果未来要把这个系统真正部署到医疗机构生产环境,建议这三件事必须做:等保测评、数据加密存储、定期备份。这几个点不是代码层面的功能,而是运营层面的合规要求,千万不要等到监管检查才临时补。
9. 如果让我重做一次,我会调整什么
最后说点个人体会。这个系统做完之后,我复盘了一下,觉得有几个地方如果重做一遍会改掉。
第一,前端技术栈我会考虑用更轻的方案。因为系统核心用户是诊所里的医生和前台,他们用的设备往往比较老旧。Vue 3 + Element Plus打包出来的体积不小,加载速度在普通办公网络上并不快。如果换成服务端渲染或者更轻的前端方案,体验会好很多。
第二,会在项目初期就引入接口参数校验框架,而不是手动写一堆if判断。我后来把核心接口的DTO全部加上了校验注解,@NotBlank、@NotNull、@Size这些,代码干净很多,参数问题也不会再渗透到Service层。
第三,也是最重要的一点:我会更早让真实的医生参与使用反馈。第一版做出来的时候,我觉得功能已经很完整了,但医生用完说"这个界面太复杂了,我就想开个方,你让我填那么多必填项"。后来我把必填项砍到最少,很多信息做成选填和智能默认值,医生反倒愿意认真去补全了。这个教训说明,业务流程的系统化必须和一线操作的便捷性反复磨合,技术实现能力再强,脱离使用者习惯的产品也走不远。做管理系统这事儿,一半是技术,另一半是对业务场景的理解。希望这份分享能把这两半都帮到你一些。