ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue+MyBatis+MySQL:社区医院管理系统全栈项目拆解

SpringBoot+Vue+MyBatis+MySQL:社区医院管理系统全栈项目拆解 打开IDE之前我很清楚这个社区医院管理系统要做什么一套给社区卫生服务中心、小型诊所用的业务管理软件功能无非是挂号、门诊、开药、收费、库存这几大块但真正动手之后才发现这个小项目几乎把SpringBoot、Vue、MyBatis、MySQL这套组合的主流知识点全部串起来了。如果你正打算找一个能练手、能写进简历、也能直接跑起来的全栈项目或者想看看别人是怎么把前后端分离的代码组织成一门完整生意那这篇拆解应该对你有用。我会尽量站在“当时自己做的时候会怎么想、怎么选、怎么踩坑”的角度来讲而不是给你贴一堆官方文档。这套系统的技术栈并不新但正因为不新它才足够典型和稳定。SpringBoot负责后端接口服务Vue负责管理后台界面MyBatis管数据库访问MySQL存数据四个组件各管一段分工清楚。这种组合在中小型系统里的成熟度非常高遇到问题基本都能搜到解决方案这也是为什么很多教学项目和企业内部系统都长这样。1. 项目整体设计与技术选型思路1.1 需求拆解社区医院到底需要什么系统社区医院的业务和大型三甲医院有本质区别。三甲医院系统复杂在流程重、系统多、接口杂而社区医院更接近一个“小诊所的放大版”患者进门挂号医生接诊开方药房发药收费处结账住院部管理床位和护理记录。业务链路清晰角色分明数据量也不大所以完全不需要引入微服务、消息队列这类重型组件一个单体后端加上一个后台管理页面就足够了。我在设计之前把用户角色梳理了一遍系统管理员、挂号收费员、门诊医生、药师、护士。每一种角色对应的操作入口和权限边界都要清晰。比如医生只能看到排给自己的患者和开立处方收费员只能操作收费和退费管理员负责维护药品字典、用户账号和基础数据。这个角色权限模型早年做烂了就会变成一团乱麻所以我第一时间把菜单权限和接口权限的事想清楚了后面写代码就顺很多。功能模块拆出来就是系统管理、患者管理、挂号管理、门诊诊疗、处方管理、药房库存、收费退费、住院管理、统计报表。第一版我只做了前六个模块和一部分报表因为住院流程涉及护理记录和床位管理复杂度较高我放到二期去做。如果你是自己练手建议也按这个节奏来不要一上来就想覆盖全部业务先跑通“挂号-就诊-开药-收费-发药”这个核心闭环比堆功能有价值得多。1.2 为什么选SpringBootVueMyBatisMySQL这套组合先说后端。SpringBoot不用多解释它在Spring生态里属于“开箱即用”的类型内嵌Tomcat不用打war包部署一个java -jar就能跑起来这对小团队和单机部署特别友好。社区医院系统通常部署在医院内网的Windows或Linux服务器上运维能力有限SpringBoot这种低运维成本的框架就非常合适。MyBatis的选择稍微有点故事。JPA在单表CRUD上确实省事但是到了多表关联、复杂统计这种查询场景SQL在手写和自动生成之间来回横跳反而不如MyBatis直接写SQL来得痛快。社区医院系统的报表查询很多比如按科室统计接诊量、按药品统计消耗量、按时间区间统计收费金额这些SQL一旦用ORM自动生成不仅可读性差调优也无从下手。我选MyBatis就是图它能精确控制SQL配合动态SQL标签多条件组合查询也很好写。前端用Vue就不用纠结了。管理后台这种交互密集型的页面Vue的双向绑定和组件化开发效率非常高而且生态里有现成的Element UI或者Element Plus组件库表格、表单、弹窗、分页这些后台高频组件拿过来直接用开发速度能快三分之一。你甚至不需要追求复杂的状态管理方案Vue的响应式加上一个简单的Pina或者干脆只在组件内部管理状态就够用。MySQL是数据库里最稳妥的默认项社区医院的数据量用MySQL根本不存在性能瓶颈而且MySQL的InnoDB引擎在事务、行级锁、崩溃恢复这些方面的表现对这个体量的系统来说是过剩的。我用MySQL的另一个考虑是招人和查资料的便利性社区医院的系统后期维护可能外包给当地小IT公司用MySQL意味着接手的人门槛最低。1.3 前后端分离的项目结构规划项目采用标准的前后端分离结构后端一个SpringBoot工程前端一个Vue工程两边通过RESTful JSON接口通信。这种结构的好处是开发和部署互不干扰前端工程师不用装JDK后端工程师不用碰Node只要接口约定清楚就行。实际部署的时候我选择用Nginx托管前端静态文件并把 /api 路径反向代理到后端服务的8080端口这样就不存在跨域问题配置也简单。后端的包结构我按功能而非技术分层。网上很多教程喜欢建controller、service、mapper这种纯技术分包结果一个业务模块的代码被拆得七零八落找起来很痛苦。我这次用的是按模块分包com.hosp.admin ├── common统一返回结构、异常处理、工具类 ├── config配置类MyBatis、CORS、拦截器 ├── module │ ├── system用户、角色、菜单 │ ├── patient患者档案 │ ├── registration挂号 │ ├── clinic门诊医生工作台 │ ├── pharmacy药品字典、库存 │ └── finance收费、退费、对账 │ └── report统计报表每个模块内部再按controller、service、mapper、entity、vo分包。这样从包名就能看出类的职责改挂号就去registration目录改药房就去pharmacy目录开发的时候不用反复在几十个平级包之间翻来翻去。这个小习惯我强烈建议你养成它会让项目规模变大之后的维护成本明显下降。2. 数据库设计与核心表结构2.1 从业务链路推导表结构数据库设计是整个系统最值得花时间的地方。我是从业务链路反推表结构的患者建档、挂号、医生接诊、开处方、药房发药、收费结算每一个业务动作对应一张业务表再辅以字典表和维护表。核心表大概是这几张sys_user系统用户、patient患者档案、registration挂号记录、clinic_record诊疗记录、prescription处方主表、prescription_item处方明细、drug药品字典、drug_stock药品库存、charge_record收费记录。这几张表之间通过逻辑外键关联我用业务字段而不是物理外键。物理外键在数据一致性上有优势但在InnoDB下会影响写入性能而且社区医院系统删数据场景少主要靠代码保证一致性所以我把外键约束去掉只在查询时通过表关联和索引来保证效率。这个取舍在中小型系统领域是主流做法面试被问到也算一个说得出口的理由。处方主表只存患者ID、医生ID、接诊记录ID、总金额、状态这些汇总信息处方明细表存药品ID、数量、单价、用法用量。主表和明细表的设计是典型的父子表结构这个必须理解透因为几乎所有业务系统里都会遇到“一个订单拆多条明细”的模型。开方的接口会同时往两张表写数据必须用事务这一步后面讲到后端时再说。2.2 核心表字段设计要点我挑几张代表性的表说几个字段设计上的讲究。患者表patient我除了常规的name、gender、phone之外专门加了id_card身份证号、medical_insurance_no医保号、allergy_history过敏史、past_history既往病史。过敏史和既往病史放在患者主档而不是诊疗记录里是为了让医生接诊时第一眼就能看到风险信息这一点在真实医疗场景里非常重要。身份证号我做了唯一索引但允许为空因为社区医院有一部分自费患者不提供身份证。药品表drug字段里有drug_name、specification规格、unit单位、manufacturer、price、prescription_type处方类型处方药/OTC。规格字段很多人会忽略但社区医院发药必须精确到“0.25g*24片”这种粒度否则药房和收费都容易出问题。价格字段我用decimal(10,2)注意千万别用float或double存金额二进制浮点数在累加对账时会出精度的幺蛾子这是所有财务相关系统的铁律。挂号表registration字段有patient_id、doctor_id、visit_date、time_slot时段、registration_fee挂号费、status待就诊/就诊中/已完成/已取消。这里我想说一个查询设计列表页最常见的按日期和医生查当天排班所以visit_date和doctor_id建联合索引查询速度本来就没压力但索引设计是习惯问题提前建好总比上线后补强。2.3 状态字段与逻辑删除的坑所有业务表我都加了status字段用tinyint维护状态机用0和1表示无效和有效是误区业务状态应该用更有语义的编码。比如挂号记录的状态我定义成0待就诊、1就诊中、2已完成、3已取消、4已退号。状态机的流转在代码里控制不在数据库里控制因为数据库的约束表达能力有限而且改起来麻烦。另外就是删除逻辑。患者、药品、用户这些核心主档数据不能物理删一旦物理删掉历史业务数据就变成孤儿数据报表统计也会跟着出错。所以每张业务主档表都有deleted字段默认0删除操作改成UPDATE。但挂号、处方明细这类流水数据连逻辑删除都不要加流水就是流水错了走红冲和退费流程而不是删记录这也是医疗系统审计方面的基本要求。3. 后端核心实现接口设计、鉴权与MyBatis数据访问3.1 统一返回结构与全局异常处理前后端联调最容易出的问题就是接口返回格式不统一有的接口成功时返回data失败时返回一堆看不懂的error code。我从第一个接口开始就封装了统一的返回类public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(int code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }所有controller的返回值都包一层Result前端拿到以后先判断code再取data。这样在Axios拦截器里可以统一处理错误码不需要在每一个页面对响应做防御性判断。全局异常处理我用了RestControllerAdvice把所有异常分成三类处理业务异常自定义BizException、参数校验异常MethodArgumentNotValidException、系统异常Exception兜底。这里有个小细节参数校验异常要把具体的字段错误信息返回给前端我用BindingResult去提取第一条错误提示而不是只返回一个笼统的“参数错误”。用户体验差别很大。3.2 登录鉴权JWT的完整落地流程社区医院系统有多个角色接口权限必须做。我用的方案是JWT配合SpringBoot拦截器。用户登录成功后后端签发一个JWT令牌令牌里面放了用户ID、用户名、角色编码过期时间设置为12小时。前端把令牌存在localStorage里每次请求在Axios请求拦截器里加Authorization头。后端拦截器的核心逻辑是从请求头里解析出JWT验证签名和过期时间然后根据当前用户角色判断该接口是否在允许范围内。我把接口权限配置放在数据库的菜单表里菜单位表中维护了url_path和permission_code登录时在Redis里缓存用户权限列表这样比写在代码里的硬编码权限更灵活管理员可以在前端菜单管理页面动态调整权限。这里提醒两个坑。第一个是JWT在注销场景下本来是失效不了的除非把过期时间设很短或者引入黑名单机制但对内部管理系统来说12小时过期基本够用管理员可以通过修改用户状态让token失效没必要把方案复杂化。第二个是密码存储问题我一律用BCrypt加盐哈希绝不在数据库里存明文密码。我见过太多教学项目直接用明文存密码放在真实系统里就是事故。3.3 MyBatis的实践细节动态SQL、分页与缓存MyBatis在这个项目里最亮眼的地方是动态SQL。比如患者列表的多条件查询姓名、手机号、身份证、建档日期区间都是可选条件用动态SQL标签可以优雅地拼SQLselect idselectPatientPage resultTypecom.hosp.admin.module.patient.vo.PatientVO SELECT * FROM patient where if testparam.name ! null and param.name ! AND name LIKE CONCAT(%, #{param.name}, %) /if if testparam.phone ! null and param.phone ! AND phone #{param.phone} /if if testparam.startDate ! null AND create_time gt; #{param.startDate} /if if testparam.endDate ! null AND create_time lt; #{param.endDate} /if AND deleted 0 /where ORDER BY id DESC /select用 标签会自动去掉多余的AND用 控制查询条件拼不拼这套组合几乎可以应对所有检索场景。注意like查询我用CONCAT(%, #{param.name}, %)拼接而不是直接写%${param.name}%——后者会有SQL注入风险。这是MyBatis面试题里最经常被问的点也是实际开发的红线。分页我引入了PageHelper插件它在MyBatis执行过程中自动拦截SQL并生成count查询和limit语句。用法很简单PageHelper.startPage(pageNum, pageSize); ListPatientVO list patientMapper.selectPatientPage(param); PageInfoPatientVO pageInfo new PageInfo(list);返回值里有total、pages这些分页导航需要的数据。但要注意PageHelper的线程本地变量在异常情况下可能残留所以我习惯在finally里加PageHelper.clearPage()或者确保每次startPage后面紧跟查询语句。关于MyBatis缓存我在调试阶段被一级缓存坑过一次。MyBatis一级缓存是SqlSession级别的默认开启同一个SqlSession里查相同SQL会直接走缓存。看似省了数据库查询但在事务里如果两次查询之间数据被修改了第二次查到的是缓存里的旧值。社区医院系统里医生开药时先加载药品列表随后药师改库存同一个请求里如果刚好复用同一个SqlSession就可能读到过期数据。MyBatis默认每次请求都会创建新的SqlSession所以日常开发少遇到这种问题但如果你在循环里复用同一个Mapper去查询就要警惕。二级缓存是mapper级别的多个SqlSession共享我这个项目没开二级缓存原因是需求没到那个性能瓶颈而二级缓存一旦配合不当会出现脏读权衡之下不开反而省心。3.4 事务、锁与并发处理的取舍开处方、写处方明细、扣库存、登记收费这串操作必须放到一个事务里任何一个环节失败都要全部回滚。SpringBoot里我用Transactional注解搞定默认遇到RuntimeException回滚所以我在service层会把业务异常统一包装成BizException让它继承RuntimeException确保事务按预期回滚。库存并发是另一个值得说的点。社区医院的药房发药理论上存在两个窗口同时给不同患者发同一种药的情况。如果只靠“先查库存再update库存”并发下会超卖。我直接在扣库存的SQL里加条件UPDATE drug_stock SET stock_num stock_num - #{count} WHERE drug_id #{drugId} AND stock_num #{count}受影响行数为1说明扣减成功0说明库存不足。这就是乐观锁思想的SQL级实现不需要给数据库加锁也不需要分布式锁对这台系统的并发量来说绰绰有余。数据库的悲观锁、乐观锁、间隙锁这些知识这次都用上了吗其实没有用了就是牛刀杀鸡但理解它们才能判断什么时候该用什么时候不该用。4. 前端Vue实现与前后端联调注意事项4.1 Vue项目搭建与环境配置前端我用的Vue 3加Vite构建UI库选Element Plus。Vite的启动速度比Webpack快太多开发体验不是一个量级的。如果你本地环境还没弄好我建议按照官方文档把Node.js装成LTS版本然后npm config设置一下registry镜像不然依赖下载会卡到怀疑人生。组件库的使用上Element Plus的表格组件el-table配el-pagination分页组件几乎是后台系统的绝配。把后端返回的pageInfo里的total交给el-pagination翻页时触发事件重新请求接口这个套路写熟之后做任何管理列表都很快。表单验证我用Element Plus自带规则必填项、手机号格式、金额范围这些都在rules里配好。前端校验只是提升体验不能替代后端校验这个原则我在项目里反复给同事强调——前端校验可以通过改代码绕过后端校验才是真正的底线。4.2 路由设计与权限控制前端路由设计分了两个部分静态路由和动态路由。静态路由就是登录页、忘记密码页这些动态路由根据用户角色从后端拿回来以后用router.addRoute的方式动态注册。为什么这么做为了让用户根本看不到自己没权限的菜单。社区医院的收费员登录后只看到收费和退费菜单医生登录后只看到挂号查询、接诊工作台和处方开立管理员才看到完整的系统管理菜单。这些菜单数据在后端登录接口里一并返回前端拿到之后根据路径生成侧边栏。路由守卫里我也做了登录拦截router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); } else if (!token) { next(/login); } else { next(); } });页面刷新的时候动态路由会因为Vue重新初始化而丢失我处理的方式是在App.vue的onMounted里重新拉取用户信息并注册路由用Pinia存一份路由状态防止重复注册。4.3 Axios封装与接口联调实战前后端联调最烦的就是跨域和状态码处理。开发阶段我在Vite的vite.config.js里配置了代理server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api开头的路径就会被转发到后端绕过浏览器跨域限制。生产环境我用Nginx做同样的事配置里一段location /api匹配就把请求转发到后端服务。这个方案比在后端加CrossOrigin注解全局放行要规范得多也不会暴露后端真实端口。Axios拦截器我封装了两个关键逻辑请求拦截器里加token响应拦截器里统一处理业务错误码。后端返回code非200的时候拦截器直接弹出错误提示并且中断后续操作。还有一个细节响应拦截器里要处理token过期的情况发现401就清空localStorage并跳回登录页这个如果不处理页面会出现一堆报错并且一直卡在失效状态下。接口联调我踩过最经典的坑是日期格式。后端返回LocalDateTimeJackson默认序列化成数组或者带T的ISO字符串Element Plus的日期组件不认。我在后端的配置类里统一了全局JSON序列化规则把日期格式化成yyyy-MM-dd HH:mm:ss同时处理了LocalDateTime和Date的兼容。前端拿到的都是格式化好的字符串直接就能展示。4.4 前端打包与SpringBoot集成部署开发完成后前端需要打包构建。执行npm run build后Vite会把项目打包成dist目录里面是index.html和一堆static资源。部署方式有两种第一种是传统方案dist目录交给Nginx托管同一个Nginx里配置/api反向代理。第二种是把dist目录复制到SpringBoot的src/main/resources/static目录下重新打包成jar这样整个系统就只剩一个jar文件访问时直接打开后端端口SpringBoot会把index.html作为静态页面响应。第二种方案对社区医院这种一台服务器部署的场景特别友好不用额外装Nginx。我用第二种比较多但要注意一个坑前端路由如果用了history模式刷新页面的请求会打到后端后端没有对应的接口处理就会404。解决办法是在后端加一个路径转发让所有非/api路径都返回index.html或者直接把前端路由改成hash模式。我在项目里为了省事用了hash模式页面URL带#号样式上稍丑一点但稳定省事。如果你追求好看就改用history模式并在后端做转发。5. 部署上线、性能优化与常见问题排查5.1 生产环境部署的完整步骤社区医院的生产环境我选的是Windows Server因为医院现有的IT人员对Windows更熟。部署步骤其实不复杂服务器装好JDK 17MySQL 8.0我选择MySQL 8.0是因为SpringBoot 3.x对MySQL 8的兼容性更自然如果你用的是SpringBoot 2.7那MySQL 5.7也完全没问题关键看依赖版本对不对齐。后端打包前先改生产配置数据库连接池参数要调一下默认的HikariCP的话我设置maximumPoolSize为20minimumIdle为5对社区医院几十个并发窗口来说完全够用。数据库连接串里要加useSSLfalse和serverTimezoneAsia/Shanghai这两个参数不然SSL握手和时区问题会折腾你半小时。后端跑起来之后用Nginx托管前端dist目录反向代理/api请求到localhost:8080。Nginx配置里我加了几行location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }注意proxy_pass末尾反斜杠的写法带着斜杠会去掉匹配前缀中的/api不带斜杠会把完整路径透传。这里是老生常谈但每次都要提醒自己的点我吃过亏。5.2 性能优化与SQL优化实战系统上线后的性能优化我认为顺序应该是先看SQL再看缓存最后才考虑换组件或加机器。我用MySQL的慢查询日志找到了几个高频慢SQL最典型的是报表页面的一个分组统计在没有任何索引的情况下统计某月每天的挂号人数。优化方式很直接给visit_date和status建立联合索引然后改写SQL把GROUP BY的时间函数放在等值条件里。原来慢到三秒的查询优化后变成了几十毫秒。这就是索引对SQL的降维打击你甚至不需要懂什么高深的优化技巧把where条件和order by涉及的列加索引就够用了。前端性能方面我做了三件事生产包用Vite的默认资源压缩图片全部走本地静态资源而不是外链列表接口的返回字段做了精简不把大文本字段一次性返回比如既往病史这种长文本列表页只显示前50个字符详情页才请求完整内容前端对字典数据比如性别、科室、药品分类做了全局缓存首次加载后放进Pinia里后续页面切换不再重复请求。这三件事做完页面打开速度改善非常明显。5.3 常见问题排查速查表项目从开做到上线我们整理了内部的一份排错速查表这里挑几条最典型的分享给大家。现象原因排查思路前端请求/api一直404Nginx代理路径配置不对或后端没启动先curl后端接口确认后端正常再看Nginx日志看实际转发地址数据库连接报SSL错误MySQL连接串缺useSSL参数在jdbc url中加useSSLfalse时间字段存储偏8小时时区设置不一致数据库连接串加serverTimezoneAsia/Shanghai前端注意格式化JSON序列化后LocalDateTime格式奇怪没配Jackson时间格式在全局配置里统一设置LocalDateTime的序列化格式页面刷新后出现白屏前端history模式刷新404改用hash模式或后端做所有路径转发到index.html扣库存发现超卖查询和更新之间没有并发控制SQL更新语句里加stock_num count条件JWT失效后仍能访问接口拦截器没校验token过期拦截器中先解析JWT校验ExpirationSpringBoot版本太高的兼容性问题也提一下。如果你的JDK版本或MyBatis starter版本和SpringBoot版本不匹配大概率会启动报NoSuchMethodError或者Bean创建失败。我这次用的是SpringBoot 3.2配合mybatis-spring-boot-starter 3.0.3适配没有问题。如果你在网上找的是旧的教程配上新的SpringBoot依赖版本经常翻车这时候去Maven仓库里找对应starter的最新release版本就好。5.4 测试用例与上线前的自检清单上线前的自测环节我习惯跑一遍核心业务链路管理员登录创建医生账号收费员给患者建档并挂号医生接诊开处方药师确认发药扣库存收费员完成收费。这条链路每天要跑通十遍以上每次都检查库存数量、收费金额、处方状态这几个关键字段是否正确。接口层面我建议写几个简单的单元测试覆盖核心业务的service层不需要多复杂关键是保证数据库事务的回滚逻辑。JWT鉴权接口也值得做接口测试把有效的和过期的token各试一遍。上线当天我还会检查一遍配置数据库账号有没有用最小权限账号而不是root生产环境的日志级别调到INFO而不是DEBUG配置文件中把swagger接口文档关闭或加访问限制避免接口信息对外暴露。医疗数据是敏感数据访问日志我要求保留半年以上。5.5 验收交付与后续维护经验最后说一点交付阶段的经验。源码交付之后我会给客户写一份简短的部署文档里面包含环境要求、数据库初始化脚本、jar包启动命令、Nginx配置示例这四部分。这一步看似简单但能帮你省掉大量售后沟通。如果对方是纯小白我会远程协助把上线流程完整走一遍再录制一个操作视频效果比写十页文档都好。数据库初始化脚本我用了Flyway管理版本化的SQL脚本可以让升级时自动执行数据库变更避免手工执行漏掉某一条。虽然这个项目用不用Flyway都行但当你维护的客户多了之后这套东西的价值就出来了。从我做完这个项目的感受来说最大的收获不是掌握了SpringBoot或者Vue的具体用法而是把一条业务链路从数据库设计到前端交互完整走通了。你敲代码时学到的那些技术点最终都是为业务服务的。如果你正在做类似的系统我建议你也按照“核心业务闭环优先权限完整报表够用”的节奏推进不要一开始就想做得大而全。社区医院管理系统的复杂度不高但麻雀虽小五脏俱全把它吃透了再去接触电商、物流、OA这些系统你会发现很多设计思路都是相通的。最后再分享一个我个人的习惯项目做到后期我每天下班前会打开慢查询日志看一眼再翻一遍前端控制台有没有红色报错。这两个动作养成了很多线上事故都能在用户发现之前被处理掉。做系统开发稳定可靠比花哨重要得多这一点在医疗场景里尤其如此。
返回列表