ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Spring Boot医院门诊系统:从架构设计到并发控制实战

Spring Boot医院门诊系统:从架构设计到并发控制实战

1. 项目概述与核心需求拆解

看到“springboot医院门诊管理信息系统”这个标题,做后台开发的朋友应该第一反应就是一套典型的业务管理系统。这类项目在Java开发圈里出现频率极高,但真正能把它做得严谨、可落地、代码能直接拿去用的,其实并不多见。我最初接到这个需求时,客户方的核心诉求就一句话:把门诊大厅那一套纸质流程搬到线上,让挂号、候诊、就诊、开方、收费、取药这些环节在系统里串成一条线,每个环节的数据要能追溯、能统计、能对账。

先说说这套系统到底解决什么问题。传统门诊流程里,患者到院后要排队挂号、手工填病历本,医生写完处方后患者拿着纸到处跑,药房凭感觉发药,收费处靠人工核对项目。整个过程不仅慢,而且极易出错,尤其是处方、收费、发药三个环节对不上账的时候,全靠人工来回扯皮。这个项目要做的事情,就是把整条链路数字化:患者在线建档、按科室排班挂号;医生工作站接诊、写病历、开电子处方;收费处按处方生成收费单;药房接单后自动扣减库存并发药。每一单都有状态机流转,任何环节出问题都能顺着记录倒查。

系统面向的角色也很清晰,我把它分成四类:门诊管理员(负责科室、医生排班、药品目录、统计报表)、医生(接诊、病历、处方)、收费员(收费、退费)、药房药师(发药、库存盘点),外加一个患者自助端(注册、挂号、查询记录)。用户体系是典型的RBAC模型,后边在权限设计部分我会细说。

适合谁来参考这套东西?如果你正在做Java Web方向的技术练习,或者你在中小型诊所、社区医院做信息化改造,又或者你只是想把Spring Boot的业务开发流程完整走一遍,这个项目都是很好的参照。它麻雀虽小,但五脏俱全,牵涉到用户权限、流程状态机、并发控制、事务一致性、文件上传(病历附件)、报表统计这些后端开发的常规硬骨头。

多说一句,我拿到的源码版本是Spring Boot 2.x + MyBatis Plus + MySQL这套组合,这也是目前中小型项目最主流的方案,学习成本低,招人也容易。

2. 技术栈选型与架构设计思路

2.1 后端框架与ORM选型逻辑

技术选型上,这个项目没有去追新,核心原因就一条:医院门诊系统是典型的强事务、高一致性业务,稳比新重要。

后端基础框架用了Spring Boot 2.7.x,理由很简单。Spring Boot 3.0虽然已经发布,但javax到jakarta的命名空间迁移对老项目不友好,而且大量第三方组件(尤其是MyBatis、Shiro这类)对Spring Boot 3的兼容都经历过一段混乱期。我自己在实际开发中踩过这个坑,某天手痒把一个小项目升到Spring Boot 3.2,结果MyBatis Plus找不到坐标,整个项目被迫回滚。所以这个门诊系统老老实实留在2.7,稳定、组件全、参考资料多,团队上手也快。

ORM层面选了MyBatis Plus而不是原生MyBatis或者JPA,主要是从开发效率考虑。门诊系统的业务表少说也有十二三张,如果全用原生MyBatis写XML,光是CRUD就能写到手软。MyBatis Plus的BaseMapper直接提供了单表操作的通用方法,分页插件 PageHelper 集成也很顺手。复杂的多表联查(比如挂号记录关联医生排班、处方关联药品)再用自定义SQL补齐,这样既不牺牲灵活性,又能把开发周期压下来。

这里要补充说明一个设计原则:单表操作能省则省,多表查询必须显式写SQL。我在代码评审时经常看到有人把MyBatis Plus的QueryWrapper玩出花来,一次关联查询嵌套五六层lambda条件,最后生成的SQL慢得离谱还不好调优。这个项目里我做了一个约束,凡是涉及JOIN的查询,一律进Mapper XML写原生SQL,方便DBA直接拿去分析执行计划。

2.2 前端方案与前后端交互约定

前端部分用了Vue 2 + Element UI管理后台模板,这是国内Java后端项目最常见的前后端分离搭档。为什么不用Vue 3?倒不是Vue 3不好,而是Element UI对Vue 2的生态最成熟,表格、表单、弹窗、日期选择器这些后台高频组件都是开箱即用。如果用Vue 3 + Element Plus,遇到版本兼容问题还得自己踩一轮坑,对于以业务功能为主的项目来说性价比不高。

前后端交互统一走JSON,RESTful风格接口。登录后前端拿到JWT令牌存到localStorage,每次请求在请求头里带Authorization字段。后端网关层做一个拦截器统一解析令牌,把当前登录用户的ID、角色信息塞到ThreadLocal上下文里,业务代码里直接用上下文取操作者信息,不用每个接口都重复解析令牌。这套做法在门诊系统里的好处很明显:收费单要记录收费员ID,处方单要记录开方医生ID,都直接从上下文取,不需要前端传,也避免了一种越权操作——前端传一个操作者ID参数,后端不做校验直接信任,那权限体系就形同虚设了。

2.3 核心技术点全景图

按业务价值和技术复杂度两个维度,我把这个系统的核心技术点排了个优先级:

技术点所属模块技术关键点优先级
RBAC权限控制全系统用户-角色-菜单三级模型,接口鉴权拦截器高
号源并发控制预约挂号乐观锁+库存预扣,防超号高
处方状态机处方/收费/药房状态字段流转,事务边界划分高
药品库存扣减药房管理发药事务扣减,禁止负库存高
乐观锁/悲观锁选型挂号与库存根据业务峰值流量选型中
数据统计报表管理后台时间维度聚合,慢查询优化中

优先级高的这几个点,不是我刻意拔高,而是它们直接决定了系统能不能真实运转。一个门诊系统如果挂号和库存控制不住,数据就乱了,别说上线,测试阶段就会被业务方怼死。

3. 数据库设计要点与核心表拆解

3.1 表结构总览与业务链路对应

数据库我用的是MySQL 8.0,InnoDB引擎,utf8mb4字符集。整套库一共14张核心表,先列出来,后面详细拆几张最关键的。

  1. sys_user(系统用户表,存管理员/收费员/药师的账号)
  2. sys_role(角色表)
  3. sys_menu(菜单权限表)
  4. sys_user_role(用户角色关联表)
  5. sys_role_menu(角色菜单关联表)
  6. patient(患者档案表)
  7. doctor(医生表,含科室、职称、简介)
  8. department(科室表)
  9. schedule(医生排班表)
  10. registration(挂号记录表)
  11. medical_record(病历记录表)
  12. prescription(处方主表)
  13. prescription_item(处方明细表)
  14. drug(药品表)
  15. drug_stock_log(药品库存流水表)

有15张表,不过没关系,多一张库存流水表很必要。后面第5章会具体讲。

数据库设计上,我特别坚持的一张表是drug_stock_log,也就是药品库存流水表。很多初学Spring Boot的人做管理系统,库存就一个字段,发药就减数量,收了货就加数量,看起来没问题,但一旦出现退货、报损、盘点调整,到底谁在什么时候动过库存就完全说不清了。加一张流水表,每次库存变更都写一条记录,既支持对账,也能在出问题时回溯操作人、操作时间和变更前后值。这是真实业务系统的必修课,宁可多存点数据,也不能缺审计线索。

3.2 核心表设计逐行拆解

下面挑三张最有代表性的表详细说,这三张表的设计直接影响业务链路能不能跑通。

第一张是schedule排班表。门诊挂号的前提是先有医生排班,排班决定哪个医生在哪个时间段出诊、放多少个号。表结构核心字段包括:doctor_id(关联医生)、department_id(所在科室,冗余存储避免频繁联查)、work_date(出诊日期)、period(上下午时段,用tinyint 1表示上午 2表示下午)、total_number(总号源数)、remain_number(剩余号源数)、version(乐观锁版本号,防并发超号)。排班表加version字段是这个项目的一个亮点操作,后面讲并发控制的时候会细说。

第二张是registration挂号记录表。这张表是整个流程的起点和追溯源头。核心字段有:patient_id、schedule_id(关联的是号源批次)、doctor_id、visit_time(就诊时间,从排班时间带过来)、status(1待就诊 2就诊中 3已完成 4已退号)、fee(挂号费)、operator_id(操作员,记录是谁挂的号)、order_no(流水号)。其中order_no的生成规则是日期+科室编号+四位自增序号,比如2024051501010032,从业务角度讲,这个流水号打印在挂号单上,患者报号就能快速定位记录,比自增主键好用得多。

第三张是prescription处方表。主表字段包括:patient_id、doctor_id、medical_record_id(关联病历)、total_amount(处方总金额)、status(1待收费 2已收费 3已发药 4已退费 5已作废)。处方明细表prescription_item则记录每一项药品:drug_id、drug_name(冗余,防止药品目录修改导致历史记录变味)、quantity、price、amount、usage_method(用法用量)。处方状态从待收费到已发药的流转,是整个系统事务一致性的核心链条,后续讲解。

3.3 数据库索引与查询性能设计

这个系统上线半年后数据量大概会在几十万到上百万级,虽然不算很大,但如果没有索引,查询一样会卡死。我在设计阶段就明确了几条索引策略:

一是registration表针对patient_id和create_time建联合索引,支持“患者按时间段查自己的挂号记录”这种高频查询。二是registration表的schedule_id字段建普通索引,因为退号、改号时要反查排班剩余号源。三是prescription_item表的prescription_id建索引,处方详情查询避免全表扫。四是在drug表的drug_name字段上建前缀索引,药房发药按药品名模糊搜索频率极高,前缀索引会让like 'keyword%'的查询走索引而不是扫全表。

时间字段和状态字段我一般不建议单独加索引,区分度太低,索引命中率堪忧,加了反而是优化器负担。更靠谱的方案是联合索引,把时间、状态、业务主键组合起来。门诊系统的统计报表大多按“时间范围+科室+状态”三个维度分组,所以我在registration表上建了(create_time, department_id, status)的联合索引,报表查询直接跑这个索引。

4. 关键业务模块的实战实现

4.1 登录认证与RBAC权限控制落地

登录这块我建议用JWT + 拦截器方案。Spring Security功能强大但学习曲线陡,配置也复杂;Shiro虽然轻一点,但和Spring Boot整合也要写不少配置。对于门诊系统这种内部管理系统,一个拦截器加JWT工具类完全够用,代码也更透明可控。

具体实现思路是:登录接口接收用户名密码,校验通过后用用户的id、角色编码、过期时间生成JWT令牌返回给前端。拦截器注册时配置好放行路径——登录接口、患者注册接口、验证码获取接口这三类路径不拦截,其余接口全部走鉴权。拦截器里先从Header取令牌,解析失败直接返回401,解析成功把用户信息放入ThreadLocal,供后续业务代码使用。

角色权限控制用注解加拦截器实现,我自定义了@RequireRole注解,标注在Controller方法上。例如收费接口标注@RequireRole("cashier"),发药接口标注@RequireRole("pharmacist"),拦截器在完成JWT校验后再查一次当前用户角色,不匹配就返回403。这套方案比在代码里if (user.getRole().equals("xxx"))清晰太多了,可读性和维护性都好了不少。

这里我踩过一个坑,也算经验教训:ThreadLocal用完一定要remove,否则在高并发下Tomcat线程池复用会串数据,A用户的登录信息可能被B用户的请求读到。我当时排查一个“莫名其妙越权”的线上问题排查了两天,最后发现就是ThreadLocal没有清理导致的。

4.2 排班与挂号的并发控制

处理医生排班时,业务场景是这样的:某医生周二上午出诊,放30个号,多个患者同时抢号就会并发更新排班表的remain_number——如果不用并发控制,很可能会超号。

我在这个系统里用了两种手段结合的方式。数量少、概率高的场景用乐观锁,给schedule表加version字段,更新时使用条件“where id=? and version=?”,更新成功后version加1。如果影响行数为0,说明这个号已被别人抢走,要么重试要么提示号源已满。门诊系统的挂号和机票抢票不一样,高峰流量相对平缓,乐观锁完全够用,而且避免了数据库连接长时间占用。

如果业务峰值流量更大的场景,比如义诊、免费号源发放,乐观锁的重试会造成大量的无效请求打到数据库。此时可以切换为预扣号源方案:在Redis里维护号源剩余数,前端先调用预占接口扣减Redis计数,再异步生成挂号记录。这个方案复杂度上来了,但对码量不大的项目来说,使用Redisson提供的RLock会简单很多,在挂号接口上直接加锁,配合等待重试时间设置,能控制并发压力。

需要强调一个细节:乐观锁的version字段必须放在更新SQL的条件里,且不能用MyBatis Plus的updateById直接传实体类。MyBatis Plus的乐观锁插件是支持的,记得在配置里开启@Version注解,否则自动填充version的逻辑不会生效。

4.3 医生工作站:电子病历与处方开立

医生端是这个系统逻辑最重的一块。医生登录后看到当日自己的排队患者列表,按顺序接诊;进入接诊界面,可以看到患者基本信息、历史病历、历史处方,然后录入本次问诊信息。

电子病历我分了几个字段:主诉、现病史、既往史、体格检查、初步诊断、处理意见。这些字段设计成TEXT类型。这里有个坑要提醒一下:TEXT字段虽然能存大文本,但在列表页查询时如果不做排除,MySQL会把TEXT内容一起读到内存,造成不必要的IO开销。我的做法是列表查询时不查TEXT字段,只有进入病历详情页时才带出全文。MyBatis Plus里可以用@TableField(select = false)把TEXT字段排除在基础查询之外,然后在自定义SQL里再查出来。

处方开立是整个系统事务性的核心环节。医生提交一张处方时,操作链路是:写入prescription主表、批量写入prescription_item明细表、更新medical_record状态为已开方。这三步必须在一个事务里,任何一个失败都要全部回滚。我直接用了@Transactional(rollbackFor = Exception.class)标注服务方法,同时要注意rollbackFor一定不能省略——Spring默认只在RuntimeException时回滚,如果代码里抛了业务异常(比如自定义的BusinessException继承RuntimeException就没事,但如果是继承Exception就会留下半截数据)。

4.4 收费与退费的账务一致性

收费模块的逻辑相对简单:根据处方ID查出明细,计算总金额,调用支付接口,更新处方状态为已收费。门诊系统一般对接支付宝、微信支付或者院内储值卡,这里不做支付渠道的深挖,重点在于退费流程。

退费场景是这样的:患者缴费后发现药品有过敏情况,或者医生说不用再吃这个药了,需要退费。退费时必须校验处方当前状态,只有状态是“已收费”的处方才能退费,且退费金额不能超过实收金额。退费完成后,处方状态更新为“已退费”,药房那边如果还没发药就保持原状,如果已经发药了就要联动触发退药动作。这个联动我第一次做的时候漏掉了,结果出现“钱退了药还在患者手里”的情况,被业务方狠狠教育了一轮。后来加了状态校验,退费前必须检查发药状态,已发药的处方退费时会自动生成退药单进入药房任务队列。这才是完整的闭环。

4.5 药房发药与药品库存流水

药房模块拆开看有两件事:发药和备药。

发药的动作是:药师选择待发药的处方,核对患者信息和药品明细,点击确认发药。这个操作会在事务里同时做三件事:更新处方状态为已发药、扣减药品库存、写一条库存流水记录。扣减库存的SQL用了条件更新,在事务SQL层面加上库存数量限制,防止负库存。至于库存流水,记录药品ID、变更前数量、变更后数量、变更原因(发药/入库/报损/退货)、操作人ID和操作时间。账实不符时,靠这张表就能查出是哪笔操作出了问题。

备药的功能处理的是医生开方后、患者还没取药这段时间,药房先做准备。药品有库存不足风险时系统会在医生开方界面给出预警,提示药品库存不足。这个预警在开方接口里做了实时库存查询,虽然会拖慢一点响应速度,但能避免开出一张药房根本拿不出药的处方。

5. 权限控制与系统安全设计细节

5.1 数据权限控制

门诊系统的权限设计不能只停留在功能权限层面,还要考虑数据权限。比如医生登录后只能看到自己名下的患者记录,收费员只能看到自己操作过的收费单——如果把全院的数据都暴露出来,越权和信息泄露就是分分钟的事。

数据权限的实现方法是:在SQL查询中强制拼接操作者条件。具体来说,所有涉及医生、收费员业务数据的查询Mapper参数里都增加userId,在对应的Mapper XML里统一加上and operator_id = #{userId}或者and doctor_id = #{userId}。这样做还有个好处是查询条件清晰,不会因为忘记加where条件就把全表数据拉出去。

当然了,管理员角色需要全院数据,这时候单独走另一个查询入口。针对管理员角色,我们提供一个业务层方法,不带用户ID过滤条件,但在Controller层做了角色鉴权校验,只允许管理员角色的请求到达这个方法。功能权限和数据权限分开处理,逻辑更清晰,也减少误伤。

5.2 密码安全与防越权设计

用户密码存储使用BCrypt加密,这是Spring Security推荐的算法,每加密一次会自动生成随机盐,即使是相同的明文密码加密后结果也不同,能有效对抗彩虹表攻击。项目里直接用Spring自带的BCryptPasswordEncoder,不用自己写加密工具类,避免自研算法里埋下安全漏洞。

接口层的防越权设计,我在前面多次提到了,这里专门把做法说透。除角色注解外,还要防止水平越权——比如一个患者登录后,把挂号单ID换成另一个患者的,系统就不该返回数据。这个问题是在数据查询前校验数据的归属者,拿当前登录人的ID与数据记录里的patient_id做比对。这个校验拦截器做不了,只能写在业务方法里,虽然啰嗦,但却是必须写的数据安全底线。

5.3 审计与日志体系

医院门诊系统的每笔诊疗、收费操作都涉及一段时间的保存要求,完整审计日志是刚需。我用AOP做了一个操作日志切面,注解@OpLog标注在需要记录的方法上,记录操作人、操作时间、操作类型、业务单号、请求参数摘要。切面统一处理日志,不污染业务代码。

关键业务表都加了create_time、update_time、create_by、update_by四个审计字段,MyBatis Plus的MetaObjectHandler做一个全局字段自动填充。这个习惯能解锁很多问题排查能力。比如退费额度错误时,靠create_by和update_by就能定位是谁改的数据;两张表对不上账时,凭update_time就能判断谁先谁后。对于小团队来说,审计字段是成本最低的高收益设计。

6. 避坑清单与性能优化心得

6.1 开发环境与时间处理的坑

Spring Boot项目的时区坑我必须专门讲一下,太容易踩了。MySQL驱动的URL连接参数里要显式加上serverTimezone=Asia/Shanghai,Jackson处理时间时要设置UTC+8或者直接返回时间戳。否则经常出现数据库里存的带时间格式的数据,返回给前端的时候差了8个小时。我司某次上线前的联调,医生端看到的预约时间是下午2点,患者端显示的却是早上6点,排查到最后就是时区配置不一致。

日期值的处理,后端时间字段我建议用LocalDateTime而不是Date,前端统一用时间戳字符串传递。LocalDateTime在Jackson序列化时要配一个全局的序列化配置,否则默认输出的是数组格式,前端根本没法用。

6.2 药品库存的并发扣减问题

并发扣减是库存系统的经典问题。我在发药事务里的扣减SQL是:

UPDATE drug SET stock = stock - #{quantity} WHERE id = #{drugId} AND stock >= #{quantity}

这个条件更新是原子操作,数据库行锁机制能保证同一行数据同时被修改时不超卖。然后配合库存流水表的插入,共同构成发药事务。这里必须用原生SQL,不能先查再改,否则就是经典的并发安全问题。

关于库存阈值预警,需要注意的细节很多。预警阈值并非一个全局值就能适配所有药品:有的药销量大、采购周期短,预警阈值可以定得高一些;有的药销量低、采购周期长,阈值定高了会导致频繁误报。我把这个参数定义在药品表的alert_stock字段,允许管理员按药品单独配置。医院门诊库存管理的实质是“常备药品不能断、慢药不积压”,这里的小改动能让使用体验舒服很多。

6.3 前端联调与跨域配置

前后端分离项目必然遇到跨域问题。开发环境的解决方法是配置CorsFilter或使用@CrossOrigin注解。上线后的方案是对外暴露一个统一域名,用Nginx把/api路径转到后端服务,这样前端只需请求同源地址。我在交付给客户时直接配好了Nginx的反向代理示例配置,避免客户那边部署人员绞尽脑汁研究CORS。

一个小坑提醒一下:前端上传患者证件、病历附件时,请求头要带Content-Type: multipart/form-data,这个Header不能手动设置成application/json,否则后端MultipartResolver解析不到文件流,一直报文件为空。这类前后端联调问题占了我调试时间的很大比重。

6.4 慢查询优化案例分析

项目上线没多久,有一次报表查询卡了二十多秒,我定位到是一条按科室统计患者数据的SQL,没有走索引。后来在explain里看执行计划,发现是create_time的索引失效了——因为我查询时用了DATE_FORMAT(create_time, '%Y-%m-%d')作为条件,MySQL没办法对函数包住的索引列做范围匹配。

优化办法是把查询条件改为范围查询:

WHERE create_time >= #{startTime} AND create_time < DATE_ADD(#{endTime}, INTERVAL 1 DAY)

改完之后查询秒回。这个经验也写进项目约束里:查询条件的索引列一律不允许包函数,如果想要按天分组统计,建一个冗余的date字段或者在创建表时规划一个时间分区字段,都比在查询时做函数转换靠谱。

7. 前端关键页面与交互细节

7.1 登录页与工作台设计

登录页不是技术重点,但有个体验细节值得说。门诊系统首次登录强制修改密码功能要做成前端判断+后端校验双重机制。前端根据后端返回的isFirstLogin字段跳转改密页,后端在拦截器里对首次登录用户做限制——即使前端不跳改密页,请求也会被拦截。技术上很简单,但对医护人员的账号安全习惯很重要。

工作台是各角色登录后的首页。医生工作台展示当日排班和候诊患者数量;收费员工作台展示待收费处方数和当日收费汇总;药师工作台展示待发药处方和库存预警列表。这几块都是实时数据,前端用了定时轮询接口,每30秒刷新一次,确保医生的候诊列表能及时看到新挂的号。后端针对这种高频但轻量的轮询接口做优化时注意控制SQL复杂度,避免每次轮询都跑大查询。

7.2 挂号页与排班可视化

挂号页面的核心交互是排班日历。患者(或前台人员)选择科室后,前端展示近7天的可预约排班,区分上午下午,显示剩余号源。这个界面做的时候注意一个细节:后端接口返回排班数据的时候只返回还有剩余号源的排班,全满的排班直接过滤掉。前端不用做太多空态判断,整体交互干净很多。

号源的实时性方面,前端每次点击一个排班会重新请求剩余号源数量,防止同一排班被多个人同时下单。挂号请求提交后,后端返回挂号结果,成功就展示挂号单详情,失败就提示号源不足,需要引导用户重新选择排班。这些逻辑在患者端小程序也复用了一套接口。

7.3 医生门诊工作台交互要点

医生门诊工作台的布局是:左侧候诊列表,中间病历录入区,右侧历史病历与处方参考。这几块区域的业务被拆成了多个接口:候诊列表、病历详情、历史处方,每个接口的数据量都不大,但组合起来能有效提高医生看诊效率。

病历录入区做了自动保存功能,防的是医生录入到一半意外退出导致内容丢失。前端监听input事件加防抖,每30秒调一次保存草稿接口。后端在病历表里加了is_draft字段,草稿状态和正式提交状态用状态机隔离开来。这个小功能在医院场景里非常受欢迎,尤其是医生诊室内电脑死机率高发的环境下。

8. 常见问题排查与上线部署杂谈

8.1 问题速查表

我整理了项目开发过程中最常遇到的问题,做成一张速查表,后续你接手这类系统时可以直接对照排查:

现象原因分析解决方案
跨域请求失败浏览器CORS策略拦截配置CorsFilter/Nginx反向代理
上传图片报空指针multipart请求头设置错误前端用FormData,不手动设置Content-Type
查询结果与数据库不一致MyBatis缓存未清理关闭二级缓存,或调用clearCache方法
动态表名字段失效@TableField注解不存在或映射错误检查实体类字段与表字段映射
时间字段差8小时MySQL连接时区与JVM时区不一致URL参数加serverTimezone=Asia/Shanghai
号源超卖乐观锁version冲突处理遗漏更新条件加version=#{version}并处理影响行数
退费失败但状态已变事务边界设置错误@Transactional加rollbackFor=Exception.class
报表统计慢索引列包函数导致失效索引列不包函数,改用范围查询

8.2 典型Bug复盘:一个超号的惨痛教训

这个Bug我记忆特别深。某次测试环境压测挂号接口,并发两百个请求抢同一个医生的号,结果系统只放了30个号源,最终却成功挂了50多个号。排查后发现就是乐观锁字段版本号在复用MyBatis Plus的updateById方法时没有生效,version字段根本没有作为更新条件拼进SQL。

后来我翻看MP的官方文档,发现乐观锁插件需要专门的两个配置步骤:一是实体字段上加@Version注解,二是配置类里注册乐观锁插件。我漏了第二步,所以整个乐观锁机制形同虚设。那位测试同事说,选择并发场景时要先核对插件是否真的启用,而不是凭印象认为MyBatis Plus就默认支持乐观锁。这个教训值得记一辈子。

8.3 部署上线与维护建议

打包部署上,这个项目是标准的Spring Boot应用,mvn package打jar包,配合Java 8运行时就能跑起来。服务器配置最低2核4G,数据库和Redis都单独部署在另一台机器上,生产环境切到阿里云或者自建机房都可以。

把环境区分开的建议多说一句:开发环境、测试环境、生产环境的三套application.yml配置文件,通过spring.profiles.active切换。生产环境的数据库密码不要写在配置里,用环境变量注入,避免源码泄露导致直接暴露数据库信息。

上线前的数据准备面比较大:药品目录初始化、科室信息导入、医生账号创建、角色权限分配。如果是从旧系统迁移,还需要处理历史数据。这里单独列一条经验:历史数据导入不要用程序run一次性搞定,最好用SQL脚本分批次导入,每批次加事务,导入完一批就核对总数,防止数据量大时中途失败导致重复导入或部分导入。

部署完成后的日常维护,核心是每天定时备份数据库。我在项目里加了定时任务,每天凌晨3点执行mysqldump备份,保留最近15天的备份文件。虽然不是云厂商级别的高可用方案,但对于中小型门诊系统来说,这套方案简单可靠,至少不会在数据丢失时毫无补救办法。

9. 源码学习建议与个人体会

如果你拿到这个项目的源码用来学习,我的建议是按这条路线来读:先看数据库初始化脚本,把每张表和每条关联关系理清楚;再看后端项目的整体目录结构,划分controller、service、mapper三层;然后顺着一条业务链路完整走一遍,比如挂号的创建排班、患者注册、提交挂号、扣减号源,把涉及到的Controller、Service、Mapper逐个看完。

读代码时注意看事务边界和状态流转。处方从待收费到已收费到已发药,数据库里的状态字段如何一步步变化,每次变化前有没有校验前置状态——这些都是真实业务系统最值钱的经验。纸上谈兵说状态机没有用,看这种真实项目代码里的状态流转实现,能学到的细节比理论多得多。

我个人实际操作这个项目最大的体会是:业务细节的严谨程度直接决定系统的可用性,而技术框架只是解决前端体验的基础设施。这个系统的所有功能拆开看都不算高深,难点在于把每个细节想清楚。比如退费时已发药怎么办、库存不足时医生端怎么提示、医生账号新建后首登要不要强制改密,这些不是“技术难点”,而是“业务基本功”。写代码容易,把业务逻辑掰扯得严丝合缝才是后端工程师真正的职场护城河。

最后分享一个实用技巧:开发这种业务管理系统时随手建一个接口文档模板。我用的是SpringDoc集成到项目里,启动项目后访问/doc.html就能看到所有接口的Swagger文档。联调阶段这个文档能减少无数无效沟通,测试也能拿着文档去写用例。代码写得好,接口文档更要写得清楚,两样都做好才算是把一个项目真正交付了出去。

RELATED READING

延伸阅读

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