
1. 项目概述这套门诊挂号系统到底解决了什么问题先说实话我当初做这个项目的时候市面上已经有不少现成的挂号系统了但要么太重、要么太贵、要么就是“阉割版”功能少得可怜。医院门诊的挂号场景其实比想象中复杂得多——不是简单在网页上挂个号就完事背后涉及号源管理、排班规则、退号逻辑、医生看诊状态同步、患者排队叫号等等每一个环节都得考虑到否则上线第一天就会被门诊护士骂死。这个项目用的是 SpringBoot Vue MyBatis MySQL 这个组合算得上是目前 Java Web 开发里最经典、也最适合练手和二次开发的一套技术栈了。SpringBoot 负责后端接口和业务逻辑Vue 负责前端页面交互MyBatis 负责数据库访问层MySQL 存数据各司其职又足够轻量。我选这套组合的原因很简单团队上手快、社区资料多、部署起来不折腾而且对于中小型医院或诊所的信息化改造来说性价比非常合适。这篇文章会把这套系统从整体设计、数据库建模、核心模块实现到部署上线中踩过的一堆坑原原本本拆开讲一遍。如果你正准备做一个类似的管理系统或者想拿一个完整的 SpringBootVue 项目来学习全栈开发这篇文章值得认真看完。我尽量不堆废话讲的都是实际开发中碰到的真实问题和解决方案。2. 核心功能拆解挂号系统远不止“挂个号”2.1 功能盘点从患者端到管理端的一整条链路整个系统我按用户角色分成了三大块患者端、医生端、管理端外加一个公共的基础数据模块。患者端的核心功能有这些在线挂号选择科室 → 选择医生 → 选择号源时段 → 确认挂号信息我的挂号记录查看历史挂号、当前待就诊号、退号记录个人中心维护个人信息、就诊人管理尤其是有老人小孩的家庭一个账号下可以绑定多个人医生端的核心功能查看当日排班和已挂号患者列表更新看诊状态待诊 / 已诊 / 过号查看患者历史就诊记录摘要管理端的核心功能科室管理新增、停用、编辑科室医生管理绑定科室、排班配置号源管理设置每个时间段的号源总量、剩余量退号审核处理患者退号请求并释放号源数据统计按日/周/月查看挂号量、医生工作量等听上去功能不算特别多但真正做起来就会发现每个功能模块之间都有数据联动。比如“退号”这个动作看似只是改一条记录的 status 字段实际上要同步处理号源表里的余量、更新医生端的患者列表、还要在挂号记录里留下完整的日志任何一个环节漏了都会出问题。2.2 角色权限设计为什么说这里不能省我见过太多项目在权限设计上敷衍了事结果上线后不是医生误改了别人的排班就是患者看到了不该看的内部数据。这套系统的权限模型用的是经典的 RBACRole-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/DAOMyBatis 数据访问 MySQL数据存储前端 Vue 这边用的是 Vue 3 Vite Element Plus Pinia Axios。组件按模块划分路由通过 Vue Router 配置支持基于用户角色的动态路由。后端没有拆微服务因为一个门诊挂号系统的体量用单体内聚架构就够了硬拆成微服务只会增加运维复杂度这也是我踩过对比坑后回归的选择。3.2 数据库表结构设计复盘数据库我总共设计了 11 张表核心的几张列一下方便你对照自己的需求做调整用户相关sys_user用户主表字段包括 id、username、passwordBCrypt 加密、real_name、phone、role_id、status、create_timesys_role角色表sys_user_role用户角色关联表业务核心表department科室表字段有 dept_name、description、status、sort_orderdoctor_info医生信息表doctor_id、user_id关联登录账号、dept_id、title职称、introduction、statusdoctor_schedule医生排班表doctor_id、schedule_date、period上午/下午/全天、total_slotsschedule_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_timepatient_info就诊人表user_id、patient_name、id_card、phone、gender、birth_date辅助表operation_log操作日志表记录用户的关键操作行为sys_menu/sys_role_menu菜单和权限表3.3 几个关键字段设置的实战心得建表看起来简单但字段设计上坑很多我列几个最有代表性的经验第一status 字段一定要用状态位而不是直接删记录。患者挂完号之后可能退号医生看诊后会有“已诊”和“过号”状态管理员删除科室也不应该物理删除直接用status0表示停用。这样保留完整的数据链以后做统计报表才知道真实数据是多少。第二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 的方案流程是这样用户输入账号密码后端调用AuthenticationManager进行认证认证成功后生成 JWT 令牌包含用户 id、角色、过期时间等信息JWT 返回给前端前端存到 localStorage 里每次请求在 Axios 拦截器里加上Authorization: Bearer token后端通过 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 iddecrementRemainCount 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 退号回退号的完整状态机设计挂号记录的状态流转像个小型状态机待诊 → 已诊 / 过号 / 退号其中退号又分“患者自助退号”和“管理员强制退号”两种场景。职责分离之后退号逻辑也变成一步都不能少校验当前状态是否为待诊已诊或过号的订单不能退更新挂号记录状态为退号CANCELLED把号源时段的 remain_count 和 used_count 回退记录退号日志如果退号只更新了挂号记录、忘了回退号源余量那么医生放出的号源会凭空消失久而久之号源数据就会严重失真。这类问题在测试阶段极难发现一定要靠代码 review 和定时核对任务兜底。我当时还做了一个每分钟跑一次的定时任务用 Spring 的Scheduled检查当天所有待诊状态挂号记录对应的 slot 是否存在“余量未回退”的情况如果有异常会直接报警。定时任务虽然土但在实际生产环境里非常有用。5.3 MyBatis 动态 SQL 的实战用法如果搜索过“mybatis 面试题”或“mybatis 中 typehandler 的工作流程图”应该知道 MyBatis 最大的爽点之一就是动态 SQL。拿我这个系统的“号源列表查询”为例前端会传日期、科室、医生姓名等多个筛选条件用 XML 里的whereif轻松搞定select idselectSlotList resultTypecom.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 testdeptId ! null AND di.dept_id #{deptId} /if if testdoctorKeyword ! null and doctorKeyword ! AND u.real_name LIKE CONCAT(%, #{doctorKeyword}, %) /if if testvisitDate ! 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 组合式 APIVite 构建。初始化命令是npm create vitelatest frontend -- --template vue然后安装核心依赖npm install vue-router4 pinia axios element-plus目录结构大致是这样的src/ ├── api/ // 所有接口请求封装 ├── assets/ ├── components/ // 通用组件上传、表格、弹窗等 ├── router/ // 路由配置 动态路由逻辑 ├── stores/ // Pinia 状态管理用户信息、token等 ├── utils/ // 工具函数、axios实例、拦截器 ├── views/ // 页面组件 │ ├── patient/ │ ├── doctor/ │ └── admin/ ├── App.vue └── main.js6.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 实例发出拦截器里做的事有三件请求拦截从 Pinia 里拿 token加到请求头响应拦截如果后端返回业务状态码非 200弹 ElMessage 提示遇到 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.2JDK 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?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8注意useSSLfalse在生产环境且数据链路没有额外加密通道时并不推荐但在本地开发环境非常常见。生产环境建议由运维在数据库层面统一配置 SSL 或走内网加密链路。9.3 Navicat 连接不上 MySQL 的排查顺序排错顺序很有讲究。Navicat 连不上我的排查步骤一般是这样从本机 ping MySQL 服务器 IP通不通用命令行工具mysql -h host -P 3306 -u root -p测试能不能连检查 MySQL 服务是否启动systemctl status mysqld或 Windows 服务管理器里看确认 MySQL 的端口是否被防火墙拦截检查 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 类的多端框架就行。消息通知挂号成功、就诊提醒、医生临时停诊通知都可以通过公众号或短信模板推送。接入倒是简单主要注意模板审核合规的问题。这个系统的价值在于你做完之后从数据库设计、后端事务处理、前端交互到部署上线的完整链路都亲手走过一遍。这套经验远比你背一百道面经来得实在。最后分享一个我个人的体会这类系统最难的从来不是技术而是对业务场景的理解。只写代码而不理解为什么要有“号源”“排班”“退号”这些概念写出来的东西大概率只是“看起来功能都有”真到医院场景一跑全是逻辑漏洞。希望大家在做系统之前先花时间把业务梳理清楚——这一步做好了后面全是水到渠成的事。