☰
SpringBoot+Vue门诊挂号系统实战:从数据库设计到部署避坑
2026/9/30 4:13:31 网站建设 项目流程

1. 项目概述:这套门诊挂号系统到底解决了什么问题

先说实话,我当初做这个项目的时候,市面上已经有不少现成的挂号系统了,但要么太重、要么太贵、要么就是“阉割版”功能少得可怜。医院门诊的挂号场景其实比想象中复杂得多——不是简单在网页上挂个号就完事,背后涉及号源管理、排班规则、退号逻辑、医生看诊状态同步、患者排队叫号等等,每一个环节都得考虑到,否则上线第一天就会被门诊护士骂死。

这个项目用的是 SpringBoot + Vue + MyBatis + MySQL 这个组合,算得上是目前 Java Web 开发里最经典、也最适合练手和二次开发的一套技术栈了。SpringBoot 负责后端接口和业务逻辑,Vue 负责前端页面交互,MyBatis 负责数据库访问层,MySQL 存数据,各司其职又足够轻量。我选这套组合的原因很简单:团队上手快、社区资料多、部署起来不折腾,而且对于中小型医院或诊所的信息化改造来说,性价比非常合适。

这篇文章会把这套系统从整体设计、数据库建模、核心模块实现,到部署上线中踩过的一堆坑,原原本本拆开讲一遍。如果你正准备做一个类似的管理系统,或者想拿一个完整的 SpringBoot+Vue 项目来学习全栈开发,这篇文章值得认真看完。我尽量不堆废话,讲的都是实际开发中碰到的真实问题和解决方案。

2. 核心功能拆解:挂号系统远不止“挂个号”

2.1 功能盘点:从患者端到管理端的一整条链路

整个系统我按用户角色分成了三大块:患者端、医生端、管理端,外加一个公共的基础数据模块。

患者端的核心功能有这些:

  • 在线挂号:选择科室 → 选择医生 → 选择号源时段 → 确认挂号信息
  • 我的挂号记录:查看历史挂号、当前待就诊号、退号记录
  • 个人中心:维护个人信息、就诊人管理(尤其是有老人小孩的家庭,一个账号下可以绑定多个人)

医生端的核心功能:

  • 查看当日排班和已挂号患者列表
  • 更新看诊状态(待诊 / 已诊 / 过号)
  • 查看患者历史就诊记录摘要

管理端的核心功能:

  • 科室管理:新增、停用、编辑科室
  • 医生管理:绑定科室、排班配置
  • 号源管理:设置每个时间段的号源总量、剩余量
  • 退号审核:处理患者退号请求并释放号源
  • 数据统计:按日/周/月查看挂号量、医生工作量等

听上去功能不算特别多,但真正做起来就会发现,每个功能模块之间都有数据联动。比如“退号”这个动作,看似只是改一条记录的 status 字段,实际上要同步处理号源表里的余量、更新医生端的患者列表、还要在挂号记录里留下完整的日志,任何一个环节漏了都会出问题。

2.2 角色权限设计:为什么说这里不能省

我见过太多项目在权限设计上敷衍了事,结果上线后不是医生误改了别人的排班,就是患者看到了不该看的内部数据。这套系统的权限模型用的是经典的 RBAC(Role-Based Access Control)设计:

  • 用户表(sys_user)只存账号信息和密码密文
  • 角色表(sys_role)定义系统里的三种角色:ADMIN、DOCTOR、PATIENT
  • 菜单/权限表(sys_menu)维护前端路由和后端接口的访问权限
  • 用户-角色、角色-权限分别用中间表关联

前端拿到登录用户的角色后,动态生成路由菜单;后端则通过 Spring Security + JWT 做接口鉴权,每个接口上标注需要的权限标识。比如POST /api/doctor/updateStatus这个接口只允许 DOCTOR 角色访问,管理员就算登录了也调不动。

注意:这里一定要记住,前端的路由控制只是“用户体验层面的隐藏”,真正的安全校验必须留在后端接口上。因为前端传过来的任何角色信息都是可以被篡改的,只有后端认的 JWT 里的角色才是可信的。

2.3 号源排班逻辑:搞清楚这层的设计,整个系统就懂了

号源管理是整个系统的核心环节。我的做法是把号源分成两层:医生排班表和号源明细表。

  • 排班表(doctor_schedule)记录某医生在某个日期是上午、下午还是全天出诊,以及每个时段的总号源数
  • 号源明细表(schedule_slot)进一步把“上午”拆成多个具体时间段,比如 08:00-08:30、08:30-09:00,每个时间段一个 slot,记录该时间段内已经挂了几个号、剩余几个号

患者挂号时,系统会先查这个 slot 的剩余量,如果大于 0,就生成一条挂号记录,同时把剩余量减 1(记得用乐观锁或 SQL 里的update ... where remain > 0防止并发超挂)。退号时再把这个 slot 的剩余量加回来。

这样设计的好处很明显:以后不管医院是想按“半天”排班还是按“半小时”排班,都只需要控制排班表的数据粒度就行,不需要改业务代码。而且每个 slot 都有独立的余量数据,天然支持并发控制和精细化管理。

3. 系统架构与数据库建模:先把地基打牢

3.1 技术选型与整体分层

这套系统的后端分层是典型的单体分层架构,从上到下依次是:

Controller(接收请求、参数校验) Service(业务逻辑、事务管理) Mapper/DAO(MyBatis 数据访问) MySQL(数据存储)

前端 Vue 这边用的是 Vue 3 + Vite + Element Plus + Pinia + Axios。组件按模块划分,路由通过 Vue Router 配置,支持基于用户角色的动态路由。后端没有拆微服务,因为一个门诊挂号系统的体量用单体内聚架构就够了,硬拆成微服务只会增加运维复杂度,这也是我踩过对比坑后回归的选择。

3.2 数据库表结构设计复盘

数据库我总共设计了 11 张表,核心的几张列一下,方便你对照自己的需求做调整:

用户相关:

  • sys_user:用户主表,字段包括 id、username、password(BCrypt 加密)、real_name、phone、role_id、status、create_time
  • sys_role:角色表
  • sys_user_role:用户角色关联表

业务核心表:

  • department:科室表,字段有 dept_name、description、status、sort_order
  • doctor_info:医生信息表,doctor_id、user_id(关联登录账号)、dept_id、title(职称)、introduction、status
  • doctor_schedule:医生排班表,doctor_id、schedule_date、period(上午/下午/全天)、total_slots
  • schedule_slot:号源时段表,schedule_id、start_time、end_time、total_count、used_count、remain_count、version(乐观锁用)
  • registration:挂号记录表,registration_no(挂号编号)、patient_id、doctor_id、slot_id、visit_date、status(待诊/已诊/退号/爽约)、create_time
  • patient_info:就诊人表,user_id、patient_name、id_card、phone、gender、birth_date

辅助表:

  • operation_log:操作日志表,记录用户的关键操作行为
  • sys_menu/sys_role_menu:菜单和权限表

3.3 几个关键字段设置的实战心得

建表看起来简单,但字段设计上坑很多,我列几个最有代表性的经验:

第一,status 字段一定要用状态位而不是直接删记录。患者挂完号之后可能退号,医生看诊后会有“已诊”和“过号”状态,管理员删除科室也不应该物理删除,直接用status=0表示停用。这样保留完整的数据链,以后做统计报表才知道真实数据是多少。

第二,version 字段必须加。号源余量更新的时候,如果两个患者几乎同时在同一个时段挂号,常规的UPDATE schedule_slot SET remain_count = remain_count - 1 WHERE id = ?在并发下存在超卖风险。我的做法是加上乐观锁:先查出版本号,更新时WHERE id = ? AND version = #{oldVersion},更新成功后 version + 1,失败则重试或提示用户“该时段已被抢完”。

第三,金额相关的字段如果以后想扩展缴费模块,最好用 decimal 类型。挂号费、诊疗费等字段用 decimal(10,2),别用 float,否则后期算账的时候精度问题会让你崩溃。

数据库这块的 ER 图我建议你在设计阶段就画好,哪怕用 Navicat 的模型工具倒腾一版也值得,后面写 Mapper 的时候会省很多事。因为如果你的表结构不清晰,MyBatis 里的 ResultMap 映射会让你写到怀疑人生。

4. 登录鉴权与用户体系的实现细节

4.1 JWT + Spring Security 的集成过程

登录这块我用的是 Spring Security + JWT 的方案,流程是这样:

  1. 用户输入账号密码,后端调用AuthenticationManager进行认证
  2. 认证成功后,生成 JWT 令牌,包含用户 id、角色、过期时间等信息
  3. JWT 返回给前端,前端存到 localStorage 里,每次请求在 Axios 拦截器里加上Authorization: Bearer <token>
  4. 后端通过 OncePerRequestFilter 拦截所有请求,解析 token 并设置 SecurityContext

实现的时候有几个坑必须提一下:

密码加密一定要用 BCrypt。不要用 MD5,更不要明文存储。Spring Security 自带BCryptPasswordEncoder,直接用就行,成本极低。

JWT 的过期时间要合理。我一般设置 8 小时过期,并且前端会拦截 401 状态码跳转到登录页。有的项目懒省事设置好几天不过期,一旦 token 泄露,别人能一直用,这等于把安全大门敞开。

无状态会话的代价就是无法主动踢人。如果管理员拉黑了一个医生账号,只要他的 JWT 还没过期,理论上他还能访问接口。解决思路是维护一份 token 黑名单存到 Redis,但综合考虑后,这类系统没必要为此引入 Redis,简单做法是把 JWT 的 jti(唯一ID)存到数据库里,请求时查一遍,虽然多了次查询,但安全性和实现成本是划得来的。

4.2 动态路由菜单的实现

前端根据登录用户的角色动态生成路由菜单,这块我用的是 Vue Router 的addRoute接口。简单来说:

  • 后端提供一个/api/user/routes接口,根据用户的角色返回对应的菜单树
  • 前端拿到菜单树后,通过router.addRoute()动态注册页面组件
  • 刷新页面时,因为 localStorage 还在,再从后端重新拉取菜单注册一遍

组件映射时有一个比较 tricky 的地方:后端返回的是菜单字符串(比如department/index),前端需要把它映射到真正的组件对象。这就是为什么通常需要提前 import 所有页面组件,然后建一个映射表。用 Vite 的import.meta.glob可以省很多事:

const modules = import.meta.glob('../views/**/*.vue')

把路由地址拼成路径(比如../views/department/index.vue),就能动态加载对应的组件。你搜索热词里看到“vue动态路由”“vue路由参数”,大概率就是卡在了这里,当初我也是折腾了一个晚上才搞明白。

5. SpringBoot 后端核心业务模块的代码级解读

5.1 挂号接口的事务与并发处理

下面这段是挂号接口的核心逻辑,虽然我做了简化,但整体思路是一样的:

@Service @RequiredArgsConstructor public class RegistrationService { private final ScheduleSlotMapper slotMapper; private final RegistrationMapper registrationMapper; @Transactional(rollbackFor = Exception.class) public Result register(RegisterRequest request) { // 1. 查询号源时段 ScheduleSlot slot = slotMapper.selectById(request.getSlotId()); if (slot == null) { return Result.error("号源时段不存在"); } // 2. 用乐观锁更新余量 int updated = slotMapper.decrementRemainCount( slot.getId(), slot.getVersion()); if (updated == 0) { return Result.error("该时段号源已被抢完,请选择其他时间"); } // 3. 生成挂号记录 Registration reg = new Registration(); // ... 填充患者、医生、时段、状态等信息 reg.setStatus("WAITING"); String regNo = generateRegNo(); // 例如:YYYYMMDD + 6位序号 reg.setRegistrationNo(regNo); registrationMapper.insert(reg); // 4. 写操作日志 logService.record(reg.getId(), "CREATE_REGISTRATION", regNo); return Result.success(reg); } }

注意@Transactional一定要加,因为“更新余量”和“插入挂号记录”必须保证原子性,否则某个步骤失败会出现“号被扣了但没生成记录”的问题。

对应的 Mapper 乐观锁更新 SQL 长这样:

<update id="decrementRemainCount"> UPDATE schedule_slot SET remain_count = remain_count - 1, used_count = used_count + 1, version = version + 1 WHERE id = #{id} AND version = #{version} AND remain_count > 0 </update>

这里务必要注意:MySQL 的默认隔离级别是可重复读(REPEATABLE READ),在高并发下如果只是先 SELECT 再 UPDATE,仍然会读到“脏”状态,所以直接把判断条件写进 UPDATE 的 WHERE 里是最稳妥的。

另外挂号编号的生成也值得说一下。不能用自增 ID 直接给患者看,因为太容易预测了,别人可以枚举你的系统数据。我用的方案是“日期 + 随机序列”,比如20250612000123,虽然撞号概率极低,但为了保险还是加了一个唯一索引兜底。

5.2 退号回退号的完整状态机设计

挂号记录的状态流转像个小型状态机:待诊 → 已诊 / 过号 / 退号,其中退号又分“患者自助退号”和“管理员强制退号”两种场景。

职责分离之后,退号逻辑也变成一步都不能少:

  1. 校验当前状态是否为待诊,已诊或过号的订单不能退
  2. 更新挂号记录状态为退号(CANCELLED)
  3. 把号源时段的 remain_count 和 used_count 回退
  4. 记录退号日志

如果退号只更新了挂号记录、忘了回退号源余量,那么医生放出的号源会凭空消失,久而久之号源数据就会严重失真。这类问题在测试阶段极难发现,一定要靠代码 review 和定时核对任务兜底。

我当时还做了一个每分钟跑一次的定时任务(用 Spring 的@Scheduled),检查当天所有待诊状态挂号记录对应的 slot 是否存在“余量未回退”的情况,如果有异常会直接报警。定时任务虽然土,但在实际生产环境里非常有用。

5.3 MyBatis 动态 SQL 的实战用法

如果搜索过“mybatis 面试题”或“mybatis 中 typehandler 的工作流程图”,应该知道 MyBatis 最大的爽点之一就是动态 SQL。拿我这个系统的“号源列表查询”为例,前端会传日期、科室、医生姓名等多个筛选条件,用 XML 里的<where>+<if>轻松搞定:

<select id="selectSlotList" resultType="com.example.vo.SlotVO"> SELECT s.id AS slotId, s.start_time AS startTime, s.end_time AS endTime, s.remain_count AS remainCount, d.dept_name AS deptName, u.real_name AS doctorName FROM schedule_slot s INNER JOIN doctor_schedule ds ON s.schedule_id = ds.id INNER JOIN doctor_info di ON ds.doctor_id = di.id INNER JOIN sys_user u ON di.user_id = u.id INNER JOIN department d ON di.dept_id = d.id <where> <if test="deptId != null"> AND di.dept_id = #{deptId} </if> <if test="doctorKeyword != null and doctorKeyword != ''"> AND u.real_name LIKE CONCAT('%', #{doctorKeyword}, '%') </if> <if test="visitDate != null"> AND ds.schedule_date = #{visitDate} </if> AND s.remain_count > 0 AND ds.schedule_date >= CURDATE() </where> ORDER BY ds.schedule_date ASC, s.start_time ASC </select>

多表 JOIN 的查询一定要小心性能问题。初期数据量小看不出什么,但运行半年后挂号记录上了几十万条,如果连表的索引没建好,打开列表页就会明显变卡。我的经验是:外键关联字段全部建索引,高频过滤字段也建索引。

这里记住一个原则:索引不是越多越好,但挂号记录表的 patient_id、doctor_id、visit_date 这几个字段必须要有索引,否则查询一定会随着数据量增长而垮掉。

5.4 为什么没用 MyBatis-Plus?聊一下选择就懂原理

我知道现在很多新项目都在用 MyBatis-Plus,搜索热词里也出现了“mybatis-plus 批量”这种需求。但我这个项目选择的还是原生 MyBatis + 手写 XML,原因如下:

  • 对查询性能的控制更精细。原生 MyBatis 里每一句 SQL 都是你自己写的,索引优化和查询逻辑完全可控。MyBatis-Plus 虽然提供了lambdaQuery()等便捷方法,但多表复杂的查询最后还是得回到自定义 SQL。
  • 团队协作更规范。手写 Mapper XML 会让项目里的 SQL 都有迹可循,code review 时更容易发现问题。MyBatis-Plus 的链式调用写多了之后,很多人根本不知道底层到底执行了什么 SQL,这在大一点的项目里挺要命的。

如果你个人开发或公司项目偏向 CRUD 快速开发,用 MyBatis-Plus 没问题;但如果你想把数据库层面的能力掌握得更深,或者以后要面对复杂查询和调优,原生 MyBatis 的经验永远值得有。

6. Vue 前端实现细节:从登录页到挂号页面的落地过程

6.1 前端工程初始化和目录结构

前端用的 Vue 3 组合式 API,Vite 构建。初始化命令是:

npm create vite@latest frontend -- --template vue

然后安装核心依赖:

npm install vue-router@4 pinia axios element-plus

目录结构大致是这样的:

src/ ├── api/ // 所有接口请求封装 ├── assets/ ├── components/ // 通用组件(上传、表格、弹窗等) ├── router/ // 路由配置 + 动态路由逻辑 ├── stores/ // Pinia 状态管理(用户信息、token等) ├── utils/ // 工具函数、axios实例、拦截器 ├── views/ // 页面组件 │ ├── patient/ │ ├── doctor/ │ └── admin/ ├── App.vue └── main.js

6.2 门诊挂号页面:时间选择与号源余量的交互联动

患者端挂号页核心交互是:选科室 → 选医生 → 选时段 → 确认挂号。其中选时段这一步最需要关注的就是号源余量的实时性。

余量数字不能是静态的。我的做法是当前端选中一个医生后,调用接口拉取这个医生未来 7 天的排班和每个时段的剩余量,渲染成类似“场次列表”的卡片。每个卡片上有“剩余 N 个号”的字样,如果剩余量为 0 就置灰禁用。

前端因为要拿到剩余量字段,后端接口返回的 VO 一定要把remain_count带上,不要只返回一个“是否有号”的布尔值,这样页面上才能清晰展示余量梯度,帮用户做选择。

// api/modules/patient.js export function getDoctorSlots(params) { return request({ url: '/api/patient/slots', method: 'get', params }) }

选好时段点击“确认挂号”后,前端弹确认框,把挂号信息、费用信息列全,用户点了确认才真正调POST /api/patient/register接口。因为挂号成功后会锁号,前端回调里一定做“成功提示 + 跳转到我的挂号单详情页”的处理,别让用户卡原地不知道怎么操作。

6.3 Axios 拦截器与 401 统一处理

所有请求通过一个统一的 Axios 实例发出,拦截器里做的事有三件:

  1. 请求拦截:从 Pinia 里拿 token,加到请求头
  2. 响应拦截:如果后端返回业务状态码非 200,弹 ElMessage 提示
  3. 遇到 HTTP 401,清空本地 token,跳回登录页
// utils/request.js const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 15000 }) service.interceptors.request.use(config => { const userStore = useUserStore() if (userStore.token) { config.headers['Authorization'] = `Bearer ${userStore.token}` } return config }) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response?.status === 401) { const userStore = useUserStore() userStore.reset() router.push('/login') } ElMessage.error(error.response?.data?.message || '网络异常') return Promise.reject(error) } )

Axios 拦截器这套是 VUE 项目里最常用的模式,公共逻辑收口之后,业务代码就清爽很多。清理用户信息的时候记得同时清掉 localStorage 里的 token 和 Pinia 里的用户 state,否则会出现“页面跳走了但状态还在”的诡异问题。

6.4 高复用组件:挂号信息卡片和通用表格

前端页面多的时候,组件复用非常重要。我这里抽了两个通用组件,几乎每个页面都在用:

第一个是挂号信息卡片组件RegistrationCard.vue。患者端展示挂号记录、医生端展示待诊列表都用它,包含患者名、科室、时间段、号码、状态等字段,不同角色通过 prop 传配置决定展示哪些按钮。

第二个是统计表格组件DataTable.vue。管理员端的各个列表页都基于它封装,统一了分页、排序、工具栏按钮的样式出口。切页、刷新、导出 Excel 这些功能都复用同一套逻辑,省了很多重复代码。

组件化的价值在项目初期不明显,但在功能迭代到后期时,优势是实打实的。如果你是在做一个会长期维护的项目,从第一版开始就规划好通用组件,后面省下的时间绝对是按天计算的。

7. 门诊看诊流程与医生工作台的设计

7.1 医生端工作台的产品思路

医生端的工作台,本质上就是“今天我要看哪些病人”的任务列表。医生登录后默认展示的是当天待诊列表,按号码顺序排列,状态分为待诊、已诊、过号。

设计上我参考了医院里真实叫号系统的逻辑:过号的患者不会直接消失,而是会标记为“过号”,排在当前所有待诊患者的最后重新叫号。所以数据库里除了状态字段,还要加一个queue_order字段,用于记录同一时段内的叫号顺序,保证过号重排时能准确插到队尾。

7.2 医生端接口与前端交互细节

医生端点击“开始看诊”按钮后,前端调用的接口是:

@PostMapping("/api/doctor/updateStatus") public Result updateStatus(@RequestBody UpdateStatusRequest request) { // 校验当前医生是否是该挂号记录对应的医生 // 更新挂号记录状态:WAITING -> IN_CONSULT }

注意一个细节:这个接口必须做权限校验,确定当前登录医生的 user_id 与挂号记录里的 doctor_id 匹配。否则医生可以从前端改请求参数,直接操作别人的患者记录。

前端页面上医生工作台长这样:左侧是当前患者列表,右侧是选中患者的挂号详情。点“开始看诊”之后,按钮变成“完成看诊”,点击后状态改为FINISHED,同时左侧列表自动刷新。

7.3 实时刷新与轮询策略

医生端页面需要感知最新的挂号状态,但我不想为这个项目引入 WebSocket 增加部署复杂度,所以采用了轻量级的轮询方案:前端每 30 秒调一次列表接口,拉取最新待诊患者列表。数据量不大时这种方式完全够用,也稳定可靠。

有人可能会问:为什么不直接用 WebSocket?因为引入 WebSocket 会带来更多维护成本——连接管理、心跳、断线重连等。对于门诊挂号这种对实时性要求中等偏上的场景,30 秒轮询的体验足够好,而且代码简单到不会出乱子。做技术选型永远要记得:够用、稳定、易于维护,往往比“技术新潮”更重要。

8. 报表统计模块的实现思路

8.1 按日/周/月统计挂号量

管理员端需要看三组数据:每日挂号量、各科室挂号量排行、各医生接诊量排行。

这几类统计的核心都用 MySQL 的 GROUP BY + COUNT:

-- 按日期统计挂号量 SELECT DATE_FORMAT(visit_date, '%Y-%m-%d') AS visitDay, COUNT(*) AS regCount FROM registration WHERE visit_date BETWEEN #{startDate} AND #{endDate} GROUP BY visitDay ORDER BY visitDay -- 按科室统计挂号量 SELECT d.dept_name AS deptName, COUNT(r.id) AS regCount FROM registration r INNER JOIN doctor_info di ON r.doctor_id = di.doctor_id INNER JOIN department d ON di.dept_id = d.id WHERE r.visit_date = #{date} GROUP BY d.id ORDER BY regCount DESC

统计 SQL 看起来简单,但要保证速度的话,visit_date字段必须建索引。我在实际项目中还碰到了“时间索引没生效”的坑,后面定位发现是查询条件里对 visit_date 做了一层函数处理(比如DATE_FORMAT(visit_date, '%Y-%m-%d')),导致索引失效。解决办法是拿到一天的数据后,用BETWEEN '2025-06-12 00:00:00' AND '2025-06-12 23:59:59'直接在日期字段上过滤,不要再包一层函数。

8.2 用 ECharts 画图的前端展示

数据接口返回后,前端用 ECharts 渲染,图表包括折线图、柱状图和饼图。ECharts 是独立库,不依赖 Vue,配合 Vue 3 用一个v-echarts包或者手动初始化都行。

import * as echarts from 'echarts' const chart = echarts.init(document.getElementById('chart')) chart.setOption({ xAxis: { type: 'category', data: dates }, yAxis: { type: 'value' }, series: [ { name: '挂号量', type: 'line', data: counts, smooth: true, areaStyle: { opacity: 0.2 } } ] })

ECharts 画图本身不难,但要注意组件销毁时一定要调用chart.dispose(),否则页面频繁切换会内存泄漏。在 Vue 的onBeforeUnmount生命周期钩子里做清理就行。

8.3 导出 Excel 报表的小工具

表格导出功能我也加了,用到的不是复杂依赖,一个xlsx库就够了:

npm install xlsx

前端把后端返回的列表数据拼成二维数组,然后XLSX.utils.json_to_sheet转成工作表,再调用XLSX.writeFile下载。前后端分离项目里,这种纯前端的导出方案最简单,不用后端额外做 POI 处理,适合中小型系统。

9. 常见问题排查与避坑实录

9.1 SpringBoot 版本太高导致的兼容性问题

搜索热词里出现“springboot版本太高”,我猜很多人是被版本坑过。我是过来人,确实踩过。最开始用了 SpringBoot 3.2,JDK 21,结果发现 Spring Security 6 的密码加密写法跟网上 90% 的教程都不一样,网上大部分旧资料全是 Spring Security 5 的写法,照抄肯定跑不起来。

我的建议很简单:拿现成教程入门时,尽量跟着教程作者用的版本走,别自己“升级”到最新版。等把原理吃透,再考虑版本升级。技术圈总有人喜欢用最新版本彰显极客精神,但在实际交付项目中,稳定压倒一切。

9.2 MySQL 连接 SSL 错误和时区问题

很多学生和刚工作的人第一次连 MySQL 都会碰到这个报错:

SSL connection error: protocol version mismatch

这是因为新版 MySQL 的连接驱动默认开启 SSL,而本地 MySQL 的 SSL 配置不符合条件。解决方案有几种,最直接的在连接 URL 上强制关闭 SSL 并指定时区:

jdbc:mysql://localhost:3306/hospital_reg?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8

注意:useSSL=false在生产环境且数据链路没有额外加密通道时并不推荐,但在本地开发环境非常常见。生产环境建议由运维在数据库层面统一配置 SSL 或走内网加密链路。

9.3 Navicat 连接不上 MySQL 的排查顺序

排错顺序很有讲究。Navicat 连不上,我的排查步骤一般是这样:

  1. 从本机 ping MySQL 服务器 IP,通不通
  2. 用命令行工具mysql -h host -P 3306 -u root -p测试能不能连
  3. 检查 MySQL 服务是否启动:systemctl status mysqld或 Windows 服务管理器里看
  4. 确认 MySQL 的端口是否被防火墙拦截
  5. 检查 MySQL 用户表的 host 权限是不是%(如果用户只允许 localhost 登录,Navicat 远程肯定连不上)

我和团队踩过一次比较隐蔽的坑:MySQL 8 的认证插件默认是caching_sha2_password,某些版本的 Navicat 不支持,需要在新建用户时指定mysql_native_password,或者升级 Navicat 版本。

9.4 JWT 过期后前端页面的集体跳转

系统上线初期碰到过这样一个问题:患者正在挂号流程中,token 过期了,前端拿到 401 响应后直接跳登录页,结果他填的表单全丢了,用户直接打电话投诉。

后来我做了两个优化:一是所有表单页面在组件内做草稿自动保存(localStorage 存一份,每输入一个字段就保存);二是 401 跳登录之前记录当前路由路径,登录成功后自动跳回原来的页面。这些体验优化虽然技术上不难,但对系统的口碑影响很大。

9.5 备份与恢复策略

医院数据不能丢,这是底线。我采用的备份策略是每天凌晨全部库备份一次 + 每 6 小时增量备份一次。系统的运维脚本非常简单,就是一个 mysqldump 加 crontab:

# 每天凌晨两点备份 0 2 * * * mysqldump -u root -pYourPassword hospital_reg > /backup/hospital_reg_$(date +\%Y\%m\%d).sql

另外强烈建议做异地复制或云备份,至少别把所有备份放在同一台机器上。系统数据是医院日常运转的关键资产,备份出问题的代价远大于那点备份存储成本。

10. 扩展方向与实际应用价值

这个系统做完之后,实际用在小型门诊和二级医院的信息科场景是完全没问题的。后续如果要做功能扩展,我列几个方向供参考:

线上支付接入:挂号和缴费打通微信/支付宝支付,只需在挂号成功回调里增加支付页面跳转,后端增加支付订单表和回调接口。支付这种业务,建议直接使用平台官方的 SDK 对接,不要自己从头写支付流程。

排队叫号屏对接:医生完成一个患者看诊后,系统主动推送“下一位患者”信息到叫号屏。局域网内用 WebSocket 或者 UDP 广播都可以实现,这块我之前做了一版后续再展开讲。

小程序端:如果想让患者用微信小程序挂号,后端接口基本可以复用,只要前端把 Vue 包换成 Taro 或 uni-app 类的多端框架就行。

消息通知:挂号成功、就诊提醒、医生临时停诊通知,都可以通过公众号或短信模板推送。接入倒是简单,主要注意模板审核合规的问题。

这个系统的价值在于:你做完之后,从数据库设计、后端事务处理、前端交互到部署上线的完整链路都亲手走过一遍。这套经验,远比你背一百道面经来得实在。

最后分享一个我个人的体会:这类系统最难的从来不是技术,而是对业务场景的理解。只写代码而不理解为什么要有“号源”“排班”“退号”这些概念,写出来的东西大概率只是“看起来功能都有”,真到医院场景一跑,全是逻辑漏洞。希望大家在做系统之前,先花时间把业务梳理清楚——这一步做好了,后面全是水到渠成的事。

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

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

立即咨询