ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot + Vue高校门诊管理系统:从需求梳理到部署交付的完整实践

Spring Boot + Vue高校门诊管理系统:从需求梳理到部署交付的完整实践 高校的医务室或者门诊部这几年一直处于一种“看上去什么都有、实际上全靠手填”的状态。学生来了先在登记本上写姓名学号医生用手写处方单看完病拿着处方去窗口缴费收费员在小本子上记一笔月底想统计一下这个月感冒药消耗了多少、各学院就诊人数怎么分布基本只能靠翻本子。这种场景下做一套基于Spring Boot Vue的高校门诊管理系统把挂号、接诊、处方、收费、药房发药、统计报表全部串起来是很有实际意义的。前后端分离、带完整源码、数据库脚本和部署文档真正能跑起来而不是停留在课堂demo层面。这篇内容适合几种人来看准备做课程设计或毕业设计的学生想找一套能直接改改用的前后端分离项目的人学校信息中心想轻量化改造门诊流程的同事再或者单纯想看看Spring Boot Vue项目从设计到交付全流程怎么落地的人。我会把系统边界、技术选型、数据库设计、核心业务闭环、部署踩坑、交付物组织这几部分都拆开讲尽量把我实际操作过程中的判断逻辑和教训都写出来而不是只贴一个“能跑”的结果。1. 高校门诊和医院HIS的差距这个系统的边界划在哪1.1 高校门诊的业务场景拆解高校门诊不是“缩小版的医院”。我当时梳理系统功能前先蹲点观察了几天门诊的真实流程发现它和综合医院HIS有非常明显的差异这些差异直接决定了系统该做什么、不该做什么。第一服务对象是固定人群。来看病的主要是本校学生还有少量教职工。每个人有学号、工号身份可查不存在“随机散客”这个概念。所以学生档案、历史病历天然可以沉淀下来系统做出来以后医生可以随时调出某个学生过去的就诊记录这对判断常见病和复诊情况帮助很大。第二流程短且集中。高校门诊主要处理常见病、多发病比如感冒发烧、肠胃炎、运动外伤清创换药。大多数患者走的是“挂号→候诊→看诊→开药→缴费→取药”这条线很少涉及住院、手术、大型检查检验。业务流程短意味着系统不需要HIS那么庞大的模块矩阵但是每个环节的闭环必须完整。第三费用规模小但是笔数多。挂号费便宜药品也基本是基础医保目录内的常用药。但高峰时段比如新学期开学体检、流感季一天几百号人冲进来纯手动登记就会崩在窗口。收费员一边收钱一边记账漏记错记的情况很容易出现月底对账更是噩梦。第四对统计有刚需。校医院和门诊部要对上级汇报就诊人次、药品消耗、费用情况也要做内部核算。手写记录没法做即时统计往往月底加班补表。这些在系统里就是几张统计SQL的事这也是引入系统最直接的价值。1.2 为什么现成的HIS方案不适合直接拿来用市场上不是没有校医院信息化的产品但问题很现实。商用HIS功能太全光模块目录就能翻三页绝大多数高校门诊用不到采购和部署成本还不低后续维护也依赖厂商。更关键的是很多系统的操作习惯是面向专业医疗信息化的学校门诊的医生护士上手成本很高最后买回来还是当摆设。而如果完全不用系统又回到了手写处方、月底对账的原始状态。所以这个轻量化系统的定位就变成只做校门诊真正用得上的几个核心域其余一律不做。模块少不代表简单因为要覆盖到真实出诊、发药、收费三个角色业务流程的闭环测试反而比做“大而全”的壳子更花时间。这个边界想清楚了后面所有设计都不会跑偏。1.3 系统功能范围定义实际做的过程中我把功能拆成了四个端加一个公共认证学生端在线预挂号、取消挂号、查看挂号记录、查看处方和收费明细。医生端查看当日号源队列、接诊录入主诉和诊断、开处方、查看学生历史病历。药房端待发药列表、发药确认、药品库存维护、入库出库记录。管理端学生档案管理、医生排班管理、收费记录管理、挂号和药品消耗统计报表。四端加认证构成了整个系统的闭环。这里没有做复杂的检查检验模块、没有住院管理、没有排班调度的精细化算法因为这些在高校门诊场景里不是刚需。边界的价值在于开发工作量可控演示效果也集中。2. Spring Boot Vue这套组合在这个场景下为什么好用2.1 选型逻辑不是追新是对位需求先说结论如果今天重新做一遍我还是选Spring Boot做后端、Vue做前端不使用任何特别冷门或者“看起来很厉害”的框架。原因有三条。第一团队或个人的技术栈惯性。做高校门诊这类项目要么是毕业设计要么是学校信息中心带学生团队做。团队成员熟悉的往往就是Java体系从JavaEE课一路学到SSM、Spring Boot曲线上比较顺。Vue也是如此前端课程大部分以Vue为入口生态成熟遇到问题搜索引擎一抓一大把学习成本低。第二前后端分离让团队协作和后续维护都更清晰。后端只出JSON接口前端只管页面渲染和交互。门诊部以后想加一个自助缴费机或者对接校园一卡通后端接口可以复用很大一部分不需要重写业务逻辑。第三部署成本足够低。Spring Boot内嵌Tomcat打一个jar包就能跑Vue构建完是一堆静态文件扔给Nginx就行。整个系统不依赖独立的应用服务器也不需要复杂的中间件对一台2核4G的服务器来说绰绰有余这在学校环境里非常务实。2.2 后端接口层统一返回和通用处理的约定接口设计上第一件事就是把返回体统一。我定义了一个Result类结构固定为{ code, msg, data }code为200表示成功其他为业务错误码或者异常码。这样前后端联调时不需要对每个接口单独约定格式前端Axios响应拦截器里统一处理code省掉大量重复判断。异常处理也要提前约定。后端加了RestControllerAdvice全局异常处理业务异常比如库存不足、号源已满抛BusinessException校验异常抛参数异常都由全局处理器统一包装成Result返回。这样做的好处是前端拿到非200的code后弹出错误提示逻辑可以收敛得很干净。实际开发中最怕的就是每个Controller里自己try-catch然后返回不同结构后来的人维护一个接口就要看一遍逻辑。权限方面我用的方案是JWT 拦截器。学生、医生、药房、管理员不同角色能访问的接口不同拦截器负责校验token是否存在、是否有效具体业务接口再用注解控制角色。没有引入Spring Security是因为这个项目角色简单第三方的安全框架反而增加配置复杂度。如果后续要开放大量外部接口再考虑Spring Security现阶段拦截器足够。2.3 前端工程化从Axios封装到路由守卫前端用的是Vue 2.7 Element-UIVue CLI构建这套组合在管理类系统里非常成熟组件能满足大部分需求。这里有几个约定需要提前做好不然越写到后面越乱。Axios要统一封装。我在src/utils/request.js里创建了一个Axios实例设定baseURL并从localStorage里读token塞到请求头Authorization里。响应拦截器统一处理后端返回的结构code为200就返回data不是200就弹出错误提示遇到401token过期就清除本地登录态、跳转登录页。这样每个页面里调用接口只需要关心业务数据不用重复处理异常分支。路由守卫是另一个必须落地的点。系统里有四种角色学生、医生、药房、管理员的页面差异很大所以路由配置里对每个页面标记了meta.roles在beforeEach守卫里判断登录状态和角色不匹配就重定向到对应首页。这里容易踩的细节是“学生直接输入URL跳到管理页”的问题单纯靠前端隐藏菜单是不够的路由守卫必须拦住后端接口也要按角色做鉴权两边都要做。前端管体验后端管安全这句话在这个项目里体现得很直接。3. 数据库设计从挂号流水出发把门诊业务完整映射到表3.1 核心表结构和业务关系数据库是这个系统最值得花时间的部分。我建了13张表这里挑几张关键的说表结构如下表名用途关键字段student学生档案student_no、name、college、major、grade、phonedoctor医生档案doctor_no、name、department、title、introsys_user登录账号username、password、user_type、statusdrug药品目录drug_code、name、spec、manufacturer、stock、purchase_price、sale_pricedoctor_schedule医生排班doctor_id、schedule_date、period、total_count、remaining_count、statusregistration挂号记录reg_no、student_id、doctor_id、schedule_id、reg_time、fee、statusmedical_record诊断记录reg_id、chief_complaint、diagnosis、advice、diagnosis_timeprescription处方主表prescription_no、reg_id、doctor_id、create_time、total_amount、statusprescription_item处方明细prescription_id、drug_id、quantity、price、subtotalcharge_record收费记录charge_no、biz_type、biz_id、amount、pay_type、charge_time、operator_id这张表结构里最核心的关联逻辑是一次挂号对应一份医疗记录一份医疗记录可以开一张或多张处方一张处方对应多条药品明细收费记录可以关联处方也可以关联挂号用于退号退费。把这条链路理顺后面的业务代码就顺了。3.2 几个值得留意的字段设计细节第一金额一定要用decimal。药品单价、处方金额、收费金额全部decimal(10,2)不能用float或double否则累加出现0.30000000000000004这种结果是迟早的事。处方明细的单价读的是drug表里的售价快照而不是前端传值这点在下一章开处方时会说目的就是避免价格被篡改。第二状态字段用tinyint并用注释说明不要用意义不明的字符串。比如挂号状态0-待就诊、1-已就诊、2-已退号处方状态0-未收费、1-已收费、2-已发药排班状态0-停诊、1-出诊。每个状态常量在后端用枚举类统一定义前端页面也维护同一套常量映射保证两端不会各写一套数字意思否则联调的时候特别容易出“前端传1、后端以为是2”的乌龙。第三业务单号不要用自增ID直接展示。挂号单号、收费单号、处方编号我用时间戳加随机数生成比如REG202405200830001234这样既不会被看出业务量又方便跨系统对账。自增ID可以作为主键和关联外键但展示给用户看的单号一定要是独立生成的业务单号。第四所有时间字段统一datetime类型。创建时间和更新时间分别用insert_time和update_timeMyBatis-Plus的自动填充功能在插入和更新时自动维护不用每处代码手动set。这里特别提醒一下时区问题后面踩坑部分会详细讲先记住一个原则后端、数据库、前端三处的时区必须统一。3.3 初始化数据怎么造才合理一个空库跑不起来演示初始化数据直接决定别人拿到项目后能不能顺利看到效果。我准备的data.sql里除了建表还插了三类基础数据。第一类是系统账号。管理员admin/admin123还有几个演示医生账号、学生账号账号密码在README里写清楚。演示账号的密码我用MD5加盐存储登录认证时对比密文。如果这是生产项目我建议换成BCrypt但演示项目里MD5加盐可以接受关键是不要在数据库里存明文密码这个底线不能破。第二类是字典类数据。学校的基本院系、专业、科室字典这些不涉及业务逻辑数量稳定直接在脚本里写死。注意院系专业要和演示学生账号对应上避免出现“学生是计算机学院但学院字典里没有计算机学院”这种细节硬伤。第三类是药品目录。我放了15种高校门诊常用的药品覆盖感冒类、消炎类、外伤类、肠胃类每种药品带编码、规格、厂家、进价、售价、初始库存。售价、库存要尽量接近合理值否则演示到发药环节发现库存为零就很尴尬。初始库存可以适当多给一点比如每样100免得演示几次就发完了。4. 核心业务实现挂号、接诊开方、收费、发药这条链路怎么闭环4.1 挂号模块并发场景下的号源防超挂挂号是整条业务链的入口也是并发问题最集中的地方。学生选好医生和时段点确认挂号后端要做两件事扣减号源、插入挂号记录。如果直接先查排班表再update两个请求同时查到剩余号数为1就会超挂一个号卖出两个人后面接诊、收费、发药全乱。我的处理方式是用一条带条件的update原子操作UPDATE doctor_schedule SET remaining_count remaining_count - 1 WHERE id #{scheduleId} AND remaining_count 0这条SQL执行后如果受影响行数为1说明扣号成功再插入挂号记录如果为0说明号源已满直接返回“号源不足”。MySQL的行锁会保证同一时刻只有一个请求能真正扣减成功比在Java层面加同步锁稳妥得多也不存在分布式锁的复杂度。这个思路在很多库存类业务里都通用属于核心经验。取消挂号则是反向操作先校验挂号状态必须是待就诊然后将状态改为已退号再恢复排班的剩余号源。注意恢复号源时也要先检查原状态避免重复退号导致号源被加了两次。这里我额外加了一个限制已就诊的挂号不允许退号只有待就诊状态能退这样能防止学生看完病再退号找后账。4.2 诊断开方医生工作台的数据流转医生登录后台看到的是当日未接诊队列点击“接诊”后进入诊断页面。这个页面包含三块学生基本信息、历史病历记录、本次诊断表单。历史病历的展示很有用医生能很快看到学生之前是不是反复因为同一个问题来就诊对常见病判断有实际帮助。这里的数据就是从medical_record表按student_id倒序查出来的加一个分页就够了。开方这个动作本质上是对处方明细的增删改。前端提供药品搜索下拉框选择药品后自动带出单价填写数量按行计算小计处方总金额实时汇总。这里有个容易忽略的细节开完处方保存时前端传的是明细列表后端必须校验药品是否还存在、库存是否足够、单价是否被篡改。药品单价不能信任前端传的值合法做法是后端根据drug_id从库里查真实售价重新计算前端填写数量只是辅助交互。保存处方时我用了一个事务方法先插入处方主表拿到主键ID再批量插入明细表然后更新处方总金额。注意处方总金额也是后端重算的保证和明细的累加一致。这个事务方法在Controller里要加Transactional否则明细插入一半失败了主表还在就会产生垃圾数据。4.3 收费、退费与药房发药状态机触发点处方开完学生去收费窗口缴费也做了在线缴费的模拟接口收费操作核心做两件事校验处方状态必须为未收费并且未被作废创建收费记录并把处方状态改为已收费。这里要注意事务收费记录和处方状态更新要在同一个事务里任何一个失败整体回滚。收费方式我支持现金和校园卡两种在charge_record的pay_type字段里区分对账的时候可以按支付方式汇总。退费的流程比收费更考验状态判定。我的实现里已发药的处方不允许退费只能由药房做退药处理后再退回未发药的处方允许直接退费。每次退费都生成一条退费记录保留操作人和原收费单号保证对账时有据可查。退费后处方状态回到未收费学生如果还想重新缴费再走一遍收费流程就可以状态机是允许从已退费回到待收费的。药房发药是业务链的终点。药房人员看到的是所有已收费未发药的处方列表点击发药时后端校验库存并扣减同时更新处方状态为已发药。库存扣减同样用带条件的update防超卖和号源扣减是同一个套路UPDATE drug SET stock stock - #{quantity} WHERE id #{drugId} AND stock #{quantity}如果受影响行数是0说明库存不足返回具体的药品名称让药房人员补货。这样整个闭环就完整了号源、挂号、接诊、处方、收费、扣库存、发药每一步都有状态记录统计报表的数据自然就准确。5. 踩坑记录开发到部署最值得记录的几个问题5.1 跨域配置前端能访问后端了结果凭证又丢了开发阶段最大的拦路虎就是跨域。Vue dev server跑在8080后端8081前端页面发请求时浏览器直接报跨域错误。很多人第一反应是全部设成*但后面带cookie、带Authorization头的时候就会出问题。我当时的排查链路是这样的浏览器F12看到预检请求返回200但实际请求仍然报错再看Response Headers发现缺少Access-Control-Allow-Headers字段确认是后端没配置允许的自定义请求头于是写了CorsFilter把allowedOrigin设为指定前端地址而不是*allowedHeaders包含Content-Type和AuthorizationallowCredentials设为true。问题解决。这里提醒一句如果项目里真的需要允许任意来源就一定不要再用allowCredentials(true)。这是浏览器规范的限制Access-Control-Allow-Origin: *配合credentials: include会导致请求直接被浏览器拦截而且前端控制台报的错误很误导人看着像网络问题实际是配置冲突。5.2 Vue history路由部署后刷新404前端构建后扔到Nginx访问首页没问题但一到详情页按F5就404。开始还以为是路由写错排查了很久后来发现是Vue Router的history模式需要服务器把所有路径回退到index.html。Nginx配置里补上这一行就正常了location / { try_files $uri $uri/ /index.html; }这个坑几乎所有用history模式的前端项目都会踩。如果不想配置服务器也可以改用hash模式URL上带个#号但看上去不够正式。我选择的是保留history模式然后在部署文档里把Nginx配置原样写清楚后面接手的人不会在这里卡住。5.3 前后端日期差8小时从接口返回值追到MySQL时区测试过程中发现一个很隐蔽的问题挂号记录里显示的就诊时间总是比实际时间慢8小时。这个问题的排查链路很长但是很典型值得完整记录。第一步先怀疑前端格式化问题打开浏览器Network面板看接口返回的原始数据发现JSON里时间本身就少了8小时说明问题不在前端。第二步查后端Spring Boot的Jackson配置发现没有设置时间时区默认用了UTC。第三步查数据库连接串发现也没有指定serverTimezoneMySQL拿到时间后又按系统时区处理了一次双重叠加就出现了8小时偏差。修复方案是统一三处时区在application.yml里配置spring.jackson.time-zoneGMT8在数据库连接串后面加serverTimezoneAsia/Shanghai前端的日期组件显示时明确指定格式化时区。这里也建议所有时间相关的字段在后端统一用LocalDateTime避免java.util.Date的时区转换问题。5.4 导出报表时的中文乱码和文件名问题导出挂号统计和药品消耗的Excel时用EasyExcel导出没有乱码但用CSV导出时中文就乱。排查发现CSV用Excel打开时默认按ANSI解码而文件是UTF-8编码。解决方案是导出CSV时在文件内容最前面写入UTF-8的BOM头也就是\uFEFF这样Excel就能自动识别编码response.setContentType(text/csv;charsetutf-8); StringBuilder sb new StringBuilder(); sb.append(\uFEFF); sb.append(药品名称,出库数量,金额\n);还有一个小坑是设置Content-Disposition的时候文件名含中文直接用filenamexxx.csv会乱码甚至被截断。正确做法是用URLEncoder.encode(fileName, UTF-8)编码后再放到响应头里前端下载时才能拿到正常的文件名。6. 源码数据库文档的交付形式怎么让一个陌生人快速跑起来6.1 目录结构代码、脚本、文档各放一边项目交付给别人的“可用性”很大程度上取决于目录结构是否清爽。我做成了这样school-clinic/ ├── backend/ # Spring Boot 后端源码 │ ├── src/main/java/ │ ├── src/main/resources/ │ ├── pom.xml │ └── target/ ├── frontend/ # Vue 前端源码 │ ├── src/ │ ├── package.json │ └── vue.config.js ├── database/ │ ├── init.sql # 建库建表脚本 │ ├── data.sql # 初始数据脚本 │ └── README.md ├── docs/ │ ├── 需求文档.md │ ├── 设计文档.md │ ├── 部署文档.md │ └── 使用手册.md └── README.md这样划分的最大好处是接手的人能在几分钟内定位到任何一类文件。README是门面不能随便写必须包含项目能干什么、技术栈是什么、从零到启动需要哪几步、遇到端口冲突/数据库连不上怎么办、默认账号是什么。很多项目源码写得不错但README只有一句“基于Spring Boot和Vue”这种交付体验是很劝退的。6.2 SQL脚本的组织建库、建表、数据三层分离database目录下我把脚本拆成两个文件。init.sql只负责建库建表和索引包含CREATE DATABASE schooldb DEFAULT CHARACTER SET utf8mb4然后USE schooldb接着建表。data.sql只负责插入初始数据不允许包含任何建表语句。这样设计的原因是别人拿到项目后如果已经有库了就可以只执行data.sql而不动表结构如果改了表结构重新执行init.sql也不会被历史数据干扰。所有表统一用utf8mb4字符集InnoDB引擎主键用bigint AUTO_INCREMENT。索引方面挂号记录表的reg_no、业务单号和时间字段都要建索引否则演示数据量一大统计报表的SQL会变慢。如果以后想要扩展还可以加一个upgrade目录专门放版本升级脚本但现阶段作为课程设计或小型项目交付两个SQL文件足够了不要过度设计。6.3 文档的价值部署文档里的常见问题最能体现诚意很多人交付项目只给一个“安装步骤”了事但真正有价值的部署文档应该把容易出问题的点写进去。我在部署文档里专门列了一节常见问题Tomcat端口被占用怎么换MySQL 8和MySQL 5.7的驱动类名不同连接串写法也有差异Node版本太高导致Vue CLI构建失败时怎么降级Nginx静态资源配置了history回退之后为什么刷新就404。这些都是我在实际部署过程中遇到并且解决过的写下来对后来的人非常实用。功能使用手册也不能少至少要写明白每个角色登录进去应该看到什么、能做什么以及演示账号分别是哪几个角色。一个原则文档不是给评委看的是给下一个要跑起这套系统的人看的能少让他踩一个坑文档的价值就多一分。最后再多说一句个人体会。做完这套高校门诊管理系统的源码、数据库和文档我最大的感受是业务梳理的优先级永远排在写代码前面。把门诊的挂号、接诊、收费、发药这条闭环走明白表结构设计出来后面写业务代码只是按图索骥。如果你拿到这套系统想要继续扩展我觉得两个方向很值得做一是接微信小程序让学生真正在手机上挂号缴费二是对接校园一卡通把收费环节彻底无卡化。数据表结构和后端接口我已经按这个思路预留了扩展位真要做的时候不会伤筋动骨。
返回列表