☰
社区医院管理系统实战:SpringBoot+Vue+MyBatis+MySQL全栈开发与优化
2026/10/3 14:45:05 网站建设 项目流程

1. 项目整体设计与技术选型拆解

1.1 为什么是SpringBoot+Vue+MyBatis+MySQL这个组合

一套社区医院管理系统,从立项到上线,前后花了大概半年。说它是“企业级”,并不是说用了多复杂的中间件。恰恰相反,SpringBoot+Vue+MyBatis+MySQL这套组合能扛住社区医院每天上千次的挂号、缴费、取药操作,已经足够实用。项目覆盖患者建档、分诊挂号、门诊医生站、收费、药房管理、住院登记等完整流程,也是不少同学毕业设计或者入行医疗信息化的首选样例。

先说后端选型。SpringBoot是目前Java生态里启动成本最低、生态最成熟的框架。社区医院这类场景的特点是业务流程复杂、技术并发量不大,但对数据准确性要求极高。用SpringBoot能快速把接口拆出来,配合强大的starter体系,集成MyBatis、拦截器、分页插件、定时任务都很顺手。如果换成微服务架构,反而会因为服务拆分、注册中心、链路追踪这些额外组件增加部署成本,对一家社区医院的信息科来说并不友好。

前端选Vue,核心原因是组件化开发和渐进式引入。项目管理端、医生工作台、药房窗口、收费窗口都有各自的交互逻辑,如果用传统JSP加jQuery强撸,页面一多维护成本就上来了。Vue的响应式数据绑定让表单、弹窗、动态表格开发效率明显提升。配合Vue Router做页面路由,Vuex或Pinia做状态管理,整个前端工程可以按模块拆分成独立的目录。

MyBatis在这个项目里的地位比很多框架选择更关键。医疗系统里有大量动态查询条件,比如按医生、按日期区间、按科室、按病情关键字复合筛选挂号记录。MyBatis的<where>、<if>动态SQL能让这类查询写得很优雅,而且SQL是手写的,运行效率完全可控。相比之下,JPA在复杂统计和报表场景下要么得写JPQL,要么容易生成低效SQL,反而不如直接手写。

MySQL则是没有什么悬念的选择。社区医院的数据量级,单库单表就能扛住,MySQL 8.0的窗口函数、公共表表达式对统计报表支持很完善。加上MySQL安装、维护、备份的生态资料极多,招人也容易,是性价比最高的数据存储方案。这套组合选下来,技术上没有炫技,但每一层的选型都踩在“够用、稳定、好维护”这个点上。

1.2 核心业务模块与角色权限设计

社区医院管理系统和我之前做过的通用后台管理系统有一个明显差别:它的业务模块是强耦合的。患者挂了号才能看诊,医生开了处方才能去药房拿药,收费完成才能做检查检验,住院登记之后才有床位分配。模块之间是一条完整的业务链,所以设计时一定是先梳理流程、再拆分模块。

这个系统里我拆出了八个核心模块:

模块核心职责关联业务
患者管理建档、信息修改、历史就诊记录查询所有业务的基础数据
分诊挂号号源生成、挂号、退号、排队叫号关联门诊、收费
门诊医生站接诊、病历书写、开具处方关联药房、收费
收费管理挂号费、药费、检查费结算关联挂号、处方、检查
药房管理库存维护、发药、退药、盘点关联处方、收费
住院管理入院登记、床位分配、医嘱管理关联病区、收费
统计报表门诊量、收入、科室绩效统计汇总各业务数据
系统管理用户、角色、菜单、字典管理全局支撑

角色权限设计上,我直接采用RBAC模型,也就是“用户-角色-权限”三层结构。社区医院麻雀虽小,但在权限上必须严谨。比如收费员只能查看收费界面,不能打开药品库存编辑;医生可以写病历、开处方,但不能改药品单价。用药房的登录账号不小心进了门诊医生站,这在真实的医疗环境里是绝对不能发生的。

后端我用SpringBoot拦截器加自定义注解做接口权限校验,每个接口在Controller层标注需要的角色码,拦截器统一校验当前登录用户的角色权限。前端则根据登录返回的角色列表,动态生成可访问的菜单路由,没权限的页面根本不渲染。这两层权限校验各管一段,后端是保障数据安全的底线,前端更多是提升操作体验。做完之后我才意识到,权限设计这种事,宁可一开始多花两天建模,也不要上线后补丁式打补丁。

2. 数据库设计:从表结构到索引的实战经验

2.1 核心表结构设计

数据库是整个系统的地基,表结构没设计好,后面所有功能都会跟着别扭。我按业务链拆成了几个主题域:基础数据(患者、医生、科室、药品)、业务数据(挂号、处方、收费、住院、发药)、系统数据(用户、角色、菜单、字典)。

患者表是最基础的一张表,字段上除了姓名、性别、身份证号、手机号这些常规信息,还加了medical_record_no作为院内病历号,这个号在挂号、处方、收费里都要频繁使用。身份证号设了唯一索引,防止同一个人反复建档。体检里,身份证号是天然的业务主键,用它在数据库层面做防重最可靠。

挂号表是整个系统的核心表之一,字段包括patient_id、doctor_id、dept_id、visit_date、time_period(上午、下午、晚间)、registration_fee、status(已挂号、已就诊、已退号)。这里有个容易踩坑的点:挂号费、药品金额、检查费这些涉及钱的字段,一律用DECIMAL(10,2),不要用FLOAT和DOUBLE。浮点数在累计求和时会出现精度误差,比如0.1加0.2变成0.30000000000000004,在收费这种场景里属于重大事故。

处方表和处方明细表是典型的一对多关系。处方主表存医生ID、患者ID、开单时间、总金额,处方明细表存每一条药品的药品ID、药品名、单价、数量、用法用量。设计时我特意没有在明细表里只存drug_id,而是冗余了drug_name和unit_price字段。为什么?因为药品价格会调整,药品名称可能被修改,如果全部关联实时查药品表,历史处方单子在打印时就可能显示现在的价格,这在医疗纠纷中是巨大的隐患。冗余字段保存的是开单时刻的快照,这个设计思路在报表和历史追溯场景里很管用。

药品表、药房库存表、药品出入库流水表又是另一组关键设计。药品表维护基础信息,比如通用名、商品名、规格、生产企业、零售价;库存表记录了当前所在药房的实时库存;出入库流水表则记录每一次入库、出库、盘点、报损。任何一次库存变动都写流水,保证每一盒药品都能追溯到来源。数据表还加了del_flag逻辑删除字段,所有删除操作都做成软删除,避免误删后无法恢复。

2.2 索引设计:不用物理外键,但必须有对应索引

很多刚入行的同学在画ER图的时候都习惯加物理外键,比如订单表里加一个FOREIGN KEY指向患者表。在这个项目里我明确建议:正式环境不用物理外键,改用逻辑关联加索引。

物理外键的痛点在于插入和删除时要额外检查关联表的完整性,在业务高峰期会产生很多不必要的锁竞争。比如挂号表和号源表如果存在物理外键,患者集中挂号时,数据库每次都要去检查号源ID是否存在,一旦碰到大事务,非常容易死锁。社区医院虽然没有电商那么大的并发,但上午开诊时段集中挂号,也经常有几十个请求同时进来,没必要让数据库在这种地方浪费性能。

不用物理外键不代表不建索引。相反,我在所有关联字段上都手动建了索引。挂号表的patient_id、doctor_id、visit_date建了联合索引(doctor_id, visit_date),因为系统里最常见的查询是“某个医生某一天有多少号源、看了多少患者”。处方明细表里的prescription_id建了索引,查询某一单处方时不会全表扫描。药品库存表对drug_id建了唯一索引,防止同一种药品在同一个库存表里出现两条记录。

这里说一个实际优化的例子。上线跑了一周后,发现统计报表模块里“按科室查询最近一个月的门诊量”这个接口总是慢,慢查询日志显示要扫二十多万行。EXPLAIN一看,表里为了排序使用了文件排序,没有合适的联合索引。后来我在挂号表上加了(dept_id, visit_date, status)联合索引,查询效率直接从1.8秒降到了80毫秒。所以说,索引不是建得越多越好,而是要根据实际查询场景去匹配。最忌讳的是每个字段都建索引,写入时索引维护成本反而拖垮性能。

2.3 金额、时间和状态字段的规范

数据库设计里最琐碎但也最影响开发效率的,就是各种字段的规范统一。这个项目里我定了三条规矩,写死在开发文档里:金额全部用DECIMAL(10,2)、时间统一用DATETIME、状态字段全部用TINYINT并配上状态字典表。

时间字段为什么要特别强调?因为我在联调阶段就被MySQL的时区问题坑过一次。后端服务器和数据库服务器的系统时区不一致,导致插入的create_time和实际时间差了8个小时。后来在连接串上强制指定了serverTimezone=Asia/Shanghai,再把所有时间字段的默认值统一设为CURRENT_TIMESTAMP,这个问题才算彻底根治。后来写任何表的建表语句,我都会顺手把create_time和update_time加上,并给update_time配上ON UPDATE CURRENT_TIMESTAMP,这样更新时间根本不用Java代码里面手动维护。

状态字段用TINYINT是为了稳定。比如挂号状态,我约定0表示已挂号、1表示已就诊、2表示已退号、3表示爽约,这些映射关系统一维护在系统的字典表里,前端通过接口读取字典项渲染成中文标签。有些人喜欢直接存pending、finished这样的字符串,看着直观,但存字符串的查询效率和存储空间都不如数字类型,而且字符串一旦拼写不统一,数据就乱了。数字状态码加字典表,既能保证性能,又能保证显示可配置。

3. 后端实现:SpringBoot分层与MyBatis实战记录

3.1 项目分层和统一响应体

后端我采用经典的四层结构:Controller层负责接收和校验参数,Service层处理业务逻辑,Mapper层操作数据库,Entity/DTO/VO三个数据模型各司其职。很多同学做项目时喜欢把Entity直接返回给前端,这是一个容易被忽视的问题。数据库实体可能包含密码、内部备注、逻辑删除标记这些敏感或多余字段,直接暴露给前端既不安全也不专业。

DTO是接口的入参模型,VO是接口的出参模型。比如挂号接口,前端传过来的是一个包含patientId、doctorId、visitDate、timePeriod的DTO,后台处理完返回的是一个包含挂号单号、医生姓名、科室名称、排队序号的VO。这样Service层在做对象转换时,顺便把关联表查询到的医生姓名、科室名称填充进去,前端拿到的是“可以直接展示的数据”,而不是需要前端自己再去拼接的裸数据。

统一响应体也是后端项目中容易被忽略的设计。所有接口的返回格式都统一为{code: 200, message: "success", data: {...}}这种结构,前端axios拦截器统一判断code,非200时直接弹出错误提示。这样做的最大好处是,前端不用在每个接口调用处都写一遍错误处理逻辑。最开始时我偷懒,部分接口直接返回了一个Map,前端联调时发现格式不统一,被迫写了大量兼容代码,后来花了半天时间全部重构到统一响应体,立刻清爽了。前端所有接口方法写起来都是同一个套路,代码量减少约三分之一。

3.2 MyBatis动态SQL与事务管理

MyBatis在这个项目里最出彩的地方是动态SQL。以挂号记录查询为例,现实中用户很少只按一个条件筛选,更多时候是“时间区间+医生+科室+状态”组合查询。用MyBatis的<where>加<if>标签,可以只写一个SQL就支持七八种筛选组合,代码可读性还很高。

<select id="listRegistration" resultType="com.hms.vo.RegistrationVO"> SELECT r.id, r.registration_no, p.name AS patient_name, r.visit_date, r.time_period, r.status, d.dept_name FROM registration r LEFT JOIN patient p ON r.patient_id = p.id LEFT JOIN sys_dept d ON r.dept_id = d.id <where> <if test="deptId != null"> AND r.dept_id = #{deptId} </if> <if test="doctorId != null"> AND r.doctor_id = #{doctorId} </if> <if test="startDate != null and endDate != null"> AND r.visit_date BETWEEN #{startDate} AND #{endDate} </if> <if test="status != null"> AND r.status = #{status} </if> </where> ORDER BY r.visit_date DESC, r.id DESC </select>

事务管理是这个项目里另一个必须重点看的设计。挂号、开处方、扣库存这类操作,必须保证原子性。比如患者挂号的同时,号源余量要减一,同时还要生成一条收费流水,如果其中任何一步失败,整个操作都不能生效。我就在这里踩过坑:最初挂号成功了,但生成流水的时候因为参数传递错误抛了异常,导致号源减了但费用没记上,患者拿着挂号单去缴费时收费员一脸懵。

解决办法是在Service层的方法上加@Transactional注解,把挂号、更新号源、生成流水三个操作放进同一个事务。除此之外,我在ServiceImpl里还特别注意了代理方法失效的问题:@Transactional只对通过Spring代理调用外部方法生效,如果同一个类里面的A方法调用了内部B方法,B上的@Transactional不会生效。所以我把事务性的操作都拆到独立的Service类里,避免内部自调用导致事务失效。

3.3 MyBatis缓存与TypeHandler实际应用

MyBatis的缓存机制是很多人在面试里背得滚瓜烂熟、但在实际项目里又经常搞混的点。一级缓存默认开启,作用范围是同一个SqlSession,也就是一次请求中多次执行完全相同查询会命中缓存。二级缓存是跨SqlSession的,但如果你用的是SpringBoot集成的方式,默认情况下二级缓存往往没有真正开启,或者需要手动设置。

我在这个系统里做了一个决定:管理端和医生工作台这类要求数据实时性高的模块,保持默认关闭二级缓存。为什么?因为社区的挂号、库存、收费都是强实时业务,如果缓存了某条挂号数据,另一个收费窗口立刻取消了它,旧缓存里的数据还在,就非常尴尬。MyBatis的二级缓存本身也不是分布式缓存,对集群部署并不友好。与其小心翼翼地配置刷新时机,不如把缓存用在刀刃上,比如字典数据、科室列表这样几乎不变的数据,直接在Service层用本地缓存框架来管,更可控。

TypeHandler在MyBatis里算是一个进阶话题,但在医疗项目里,它处理状态字段特别好用。比如挂号状态是TINYINT,我不想在Java代码里到处写if (status == 0)这种魔法数字,就可以定义一个StatusTypeHandler,在Java对象中使用枚举类型,存数据库时自动转换成数字,读出来后自动转换成枚举。配置方式也简单,在application.yml里指定type-handlers-package,然后在枚举字段上加上@EnumTypeHandler注解即可。前几次写的时候有点绕,但用顺了之后,业务代码里再没有一个裸数字状态,读代码的体验大幅提升。

4. 前端实现:Vue工程搭建与前后端联调

4.1 环境配置与工程目录

前端我基于Vue 2 + Vue CLI搭建,为什么不选Vue 3?这里不是保守,主要考虑到社区医院已经有现成的Vue 2生态组件,比如老牌的Element UI,对表格、表单、弹窗这种后台管理场景非常成熟。如果换成Vue 3加Element Plus,思路差不多,但团队上手有个过渡期。项目从零开始的时候,我建议先跑通主流程,再根据团队情况决定要不要升级。

环境配置上,Node.js版本建议用14或16,Vue CLI 5.x对Node版本有一定要求。第一次创建项目时,我直接用了vue create命令行选择Manually select features,勾选了Router和Vuex。这里有个实际提醒:国内网络环境下,npm install非常慢,甚至经常报错ERR_SOCKET_TIMEOUT,我自己是在项目根目录创建.npmrc文件并配置了淘宝镜像源才顺利装完依赖。镜像配置后,安装依赖速度能从五六分钟缩短到几十秒。

工程目录我是这样组织的:src/api按模块存放请求方法,src/router存放路由表,src/store放Vuex状态,src/views放页面组件。src/api这个目录很多人会忽略,觉得一个页面里直接axios.get也方便,但系统功能一多之后就明白了,页面里散落着大量请求代码,后端接口一旦变动,改起来极其痛苦。统一封装以后,每个接口都在api目录里有一个方法,函数名就是接口含义,改动只需要维护一个文件。

4.2 登录态管理与角色动态路由

前端路由这块,我踩过最多的坑是权限路由。直接写死的静态路由很简单,但问题是所有角色都能看到全部菜单,收费员也能点进药房管理页面。后来我用动态路由方案:登录成功后,后端返回当前用户的角色列表和可访问的菜单编码,前端根据菜单编码动态生成路由表,用router.addRoutes挂载到Vue Router上。

router.beforeEach(async (to, from, next) => { const token = getToken() if (!token) { if (to.path === '/login') { next() } else { next('/login') } return } const hasRoutes = store.getters.hasRoutes if (!hasRoutes) { const userInfo = await store.dispatch('user/getInfo') const accessRoutes = await store.dispatch('permission/generateRoutes', userInfo.roles) router.addRoutes(accessRoutes) next({ ...to, replace: true }) } else { next() } })

这里有一个反复出现的坑:刷新页面时,Vuex里的数据全部清空,动态路由也会丢失。如果不做处理,刷新后就会出现白屏或者404。我当时的处理方案是上面这段代码的路由守卫逻辑:刷新后进入beforeEach,发现store里没有已经生成路由的标记,就重新请求用户信息、重新生成路由,再通过next({ ...to, replace: true })重新进入目标路由,这样就能保证刷新后页面仍然正常。

路由参数这个点也提一下。从挂号列表页点击某一条记录跳转到详情页时,我会用this.$router.push({ name: 'RegistrationDetail', query: { id: row.id } })传递患者ID。也有同学习惯用动态路径参数/registration/:id,两者都行。用query的优势是参数在URL里可见,刷新也不会丢;用params的路径方式则更美观,但要注意如果配置了params参数,跳转时漏传会导致页面异常。我在这个项目里统一用query传ID,简单直接,问题最少。

4.3 打包部署与SpringBoot集成

前端开发时通过proxy代理解决跨域问题。在vue.config.js里配置devServer.proxy,把/api开头的请求转发到http://localhost:8080SpringBoot服务上。但上线部署时,不能再依赖node服务,我采用了两种方案,各有适用场景。

第一种是最省事的:前端npm run build生成静态文件,复制到SpringBoot项目的src/main/resources/static目录下,再通过SpringBoot直接访问。这种方式把前端页面当成静态资源托管,只占用一个端口,适合一台服务器搞定所有服务的场景。但要注意,如果使用Vue Router的history模式,直接访问http://ip/registration这样的路径时,后端没有对应的Controller,会返回404。我的解决方法是使用hash模式,URL上带着#,无论如何刷新后端始终加载index.html,虽然URL不太好看,但稳定性优先。

第二种是经典的前后端分离部署:前端静态文件放Nginx,后端SpringBoot跑在某个Tomcat端口,通过Nginx反向代理/api请求到后端。这种方案的好处是前端页面和后端服务可以独立升级,负载能力更强。但对社区医院这种内部系统来说,Nginx本身也是一个额外的运维节点,需要有人会维护配置。所以我最终的推荐是:如果图省心,用第一种;如果后续有公网访问或者多前端入口,再上Nginx。

5. 核心业务场景的落地细节

5.1 挂号号源控制与并发处理

社区医院上午八点到十点是挂号高峰,虽然没有电商那种万人抢购的场面,但几十个患者窗口同时挂号还是存在的。号源控制是整个系统里对数据一致性要求最高的场景之一:一个号源既不能被两个患者同时挂上,又不能因为并发高就拒绝服务。

最直观的思路是在挂号逻辑里先查询号源剩余数,判断大于零就执行减一操作。但这里有一个经典并发问题:两个请求同时读到剩余数为1,都判断可以挂号,然后都执行减一,最终号源变成负数,两个患者都拿到了同一个号。这个问题叫超卖。

我在这个系统里用了两种手段兜底。第一种是数据库乐观锁,号源表设计时加一个version字段,更新语句带上WHERE version = #{version},如果更新影响行数为0,说明有其他人改过了,重新读取再重试。第二种是数据库层面的唯一约束,把doctor_id + visit_date + time_period + visit_no设计成唯一索引,哪怕是极端并发下,数据库也会强制只允许一条记录插入成功。这两层保护组合起来,号源数据基本上是铁板一块。

5.2 药品处方扣减库存的实现逻辑

药房发药是另一个关键场景。医生开了处方,患者缴费后到药房窗口取药,药房管理员点击发药,后台要做两件事:扣除库存、生成出库流水。这里的难点在于药品库存和处方明细必须保持强一致,不可能出现处方已开但药房库存不足的情况。

我的实现逻辑是:发药时先查询处方明细,逐条判断对应药品库存是否充足,如果某条不满足,整个发药操作要全部回滚,并且提示药房人员哪一味药库存不足。代码上使用SELECT ... FOR UPDATE对库存行加锁,保证库存判断和扣减操作之间不会有其他请求插入。当时考虑过不加锁、直接UPDATE drug_stock SET stock = stock - #{num} WHERE drug_id = #{id} AND stock >= #{num}这种原子操作,判断影响行数是否为1,如果不为1再整体回滚,也是一种方案。但我为了打印清晰的库存不足提示,采用了先锁定再判断的方式,逻辑更直白。

还有一个容易被忽略的细节:处方状态流转。发药完成后,处方状态要更新为“已发药”,收费记录里对应的处方状态也要同步更新。这些操作都在同一个事务里完成,任何一个环节失败,药品库存都不会扣减,避免出现患者交了钱却拿不到药,或者在系统里药已发出但库存没减的尴尬状态。

5.3 统计报表中的SQL优化思路

社区医院也要定期汇报门诊量、收入、科室绩效。系统里我做了三个核心报表接口:按日门诊量统计、按科室收入统计、按医生接诊量排名。这三个接口的本质都是对业务表做聚合查询。

按科室收入统计的SQL大概长这样:

SELECT d.dept_name, COUNT(DISTINCT r.id) AS visit_count, SUM(s.total_amount) AS total_income FROM registration r LEFT JOIN sys_dept d ON r.dept_id = d.id LEFT JOIN settlement s ON r.id = s.registration_id WHERE r.visit_date BETWEEN #{startDate} AND #{endDate} GROUP BY d.id, d.dept_name ORDER BY total_income DESC

刚开始这个接口在导出月度报表时非常慢,排查后发现问题出在settlement表按registration_id关联时没有索引。后来补建设了registration_id索引,情况好了很多。另一个优化点是避免在WHERE条件中对索引字段做函数运算。比如把WHERE MONTH(r.visit_date) = 6改成WHERE r.visit_date >= '2025-06-01' AND r.visit_date < '2025-07-01',后者才能用上联合索引。

这里我也分享一个报表优化的通用原则:报表查询往往需要跨表关联和聚合计算,但这类查询如果每次都实时扫大表,性能一定越来越差。比较好的做法是每天晚上定时任务把前一天的数据汇总到统计表中,报表查询只读取汇总结果,速度会有质的提升。我在系统二期就加入了这种汇总表机制,后台的月度统计接口从两秒多降到一百毫秒以内,患者和领导都觉得系统“变快了”。

6. 常见问题与排查记录

6.1 MySQL 8.0连接报SSL错误与安装配置

新装MySQL 8.0后,SpringBoot启动时大概率会遇到SSL connection error或Public Key Retrieval is not allowed这样的报错。原因是MySQL 8.0默认开启了SSL和caching_sha2_password认证插件,而较老的JDBC驱动或默认连接参数没有正确适配。

我在application.yml里的处理方式是:

spring: datasource: url: jdbc:mysql://localhost:3306/community_hospital?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver

useSSL=false表示放弃SSL连接,内网环境没有必要为了合规而增加加密开销;allowPublicKeyRetrieval=true允许客户端从服务端获取公钥,是配合caching_sha2_password认证方式的。如果用的是8.0之后的版本而且安全要求高,也可以在服务器端创建使用mysql_native_password插件的用户,但要注意新版本对旧插件支持在收紧,不能盲目照抄老教程。另外serverTimezone一定要加,否则时间字段容易差8小时。

6.2 SpringBoot版本太高导致的依赖冲突

这个项目的初期,我直接用当时最新的SpringBoot版本搭建骨架,结果MyBatis启动时的包路径报错,找了一圈才发现是新版本用了jakarta.*命名空间,而老版本的MyBatis依赖还在找javax.*的类。这个问题的本质是SpringBoot 3.x完成了从Java EE到Jakarta EE的包迁移,很多老第三方库没有及时适配。

对于社区医院管理系统这种偏业务、偏稳定的项目,我明确建议用SpringBoot 2.7.x系列,配合匹配的MyBatis Spring Boot Starter版本。2.7.x是SpringBoot 2.x的最后一个稳定分支,既保留了javax命名空间,又能兼容JDK 8到17,稳定性经过了长时间验证。没有必要盲目追新,框架版本的选择要服务于业务稳定性。如果你已经用了高版本,排查方向就是检查所有第三方依赖的包名是javax还是jakarta,逐一替换相关依赖的版本。

6.3 Vue打包后刷新404与静态资源路径问题

Vue项目打包后放进SpringBoot里,经常会遇到两种情况:一是刷新页面后404,二是CSS和JS加载路径不对导致白屏。404的问题在上面已经提到,使用hash模式即可解决。资源路径问题则需要在vue.config.js里设置publicPath。

module.exports = { publicPath: process.env.NODE_ENV === 'production' ? './' : '/', outputDir: 'dist', assetsDir: 'static' }

设置成相对路径以后,打包生成的index.html里引用的JS、CSS地址就不会以/开头,而是./static/js/xxx.js,这样不管部署在什么子路径下,资源都能正常加载。这个配置排查起来很迷惑,页面有时候是白屏,打开控制台才看到一堆Failed to load resource。如果遇到,优先检查这两个配置。

6.4 高频问题速查表

问题现象常见原因解决办法
MySQL连接被拒绝端口未开或密码错误检查3306端口,用Navicat客户端验证账号
MyBatis执行SQL不打印日志级别未配置配置mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
前端npm install超时网络下载慢配镜像源,重试安装
请求跨域前后端端口不一致开发环境配置Vue proxy,生产环境Nginx或后端静态托管
时分秒丢失JSON序列化时间格式问题配置Jackson时间格式或加@JsonFormat
数据库连接数耗尽连接池配置太小调整HikariCP最大连接数,排查大事务阻塞

排查这些问题时,我的经验是先看日志,再看数据库,最后怀疑前端。很多看起来是前后端联调的问题,实际是后端事务没提交或异常被吞了。如果你发现接口返回正常但数据没变,先看Service代码里有没有真正把异常抛出来,再看有没有加@Transactional和异常回滚配置。用日志把SQL打出来,问题基本能定位个八九不离十。

整套做下来,我个人最深的体会是:社区医院管理系统这种项目,真正决定成败的不是用了多落伍或多先进的技术,而是数据链路是否完整、权限边界是否清晰、状态流转是否闭环。“企业级”三个字在我的理解里,不是中间件全家桶的代名词,而是每一笔挂号、每一张处方、每一次发药都能正确落库,任何一个环节出了故障都能快速定位。最后分享一个从这次开发里沉淀下来的操作习惯:建表时就把金额类型、时间默认值、逻辑删除标志、版本号字段全部统一好,后面少说能省掉八成以上的返工。这套系统后续如果要扩展,我会优先做微信预约挂号和云胶片影像浏览,把院内系统延伸到患者端,让社区医院的数字化服务更完整一些。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询