先说明一句:这个项目是我在多个线上医疗类业务里反复落地过的经典组合,SpringBoot + Vue + MyBatis + MySQL 看起来技术栈“平平无奇”,但真正把它做成能扛住挂号高峰、能应对真实医院业务流程的“企业级”系统,很多细节是文档里看不到的。这篇博文我会按实际开发顺序来拆,从架构设计、数据库建模、后端接口落地到前端页面实现,再到部署排查,把我踩过的坑和常用的处理方式一并写出来。
1. 系统定位与整体架构设计
1.1 这到底是一个什么系统,解决了什么问题
线上医院挂号系统,简单说就是把“患者选科室 -> 看医生排班 -> 选号源 -> 预约挂号”这条线下流程搬到线上。但它的难点从来不在“挂号”这两个字本身,而在并发控制、号源管理、业务流程的状态流转,以及和医院内部科室、医生、门诊数据的联动。拿“科室-医生-排班”这三级结构来说,看起来只是一个组合查询,实际上牵扯到排班规则、号源扣减、停诊改约、退号释放等一系列状态变更,任何一个环节没考虑周全,上线后都会被真实业务打脸。
这个项目适合两类人参考:一类是正在做毕业设计或者找实习项目的同学,另一类是想把 CRUD 项目往“企业级标准”方向靠的初级后端工程师。后端用到的 SpringBoot、MyBatis、MySQL 都是当前中小型医疗信息系统的常见基础件,前端用 Vue 搭建管理后台和挂号页面,也是目前企业内部后台的主流选择。整条链路从需求到部署都能在这个项目里看到完整闭环。
1.2 为什么选 SpringBoot + Vue + MyBatis 这套组合
先说后端。SpringBoot 的本质是“约定大于配置”,它能让你用最少的 XML 配置启动一个可运行的 Web 服务,内嵌 Tomcat 也免去了单独部署容器的麻烦。在医院这类对部署环境要求比较高、往往只有一台 Windows 或 Linux 服务器的场景下,一个可执行 Jar 包直接跑起来,比传统 SSH 架构要省太多事。更关键的是 SpringBoot 的生态足够成熟,Spring Security、Redis、MyBatis Starter 这些都是开箱即用,团队招人也好招。
再说到 MyBatis。有的同学会问,现在 JPA 也挺流行,为什么医院项目里 MyBatis 更常见?我的实际体会是:医疗业务的 SQL 复杂度和定制化程度太高,统计报表、多表关联、动态条件查询、排班数据的复杂聚合,用 MyBatis 写 XML 里的 SQL 会非常直观,也方便 DBA 直接拿 SQL 去执行计划里分析优化。JPA 虽然开发快,但一旦遇到复杂报表,生成的 SQL 往往不够可控,排查问题时还得看 Hibernate 的日志转换,效率反而低。SQL 自己掌控,这是一个务实选择。
前端选择 Vue 而不是 React,并不是说 React 不好,而是 Vue 在国内企业后台项目的学习成本更低,模板语法对从后端转前端的开发者很友好。Vue Router 做页面路由、Vuex/Pinia 做全局状态、Axios 做 HTTP 请求,这三件套配合 Element UI 或 Ant Design Vue,能快速搭出后台管理界面。挂号大厅这类面向普通用户的页面,用 Vue 的组件化能力也能做到较好的交互响应,页面间传参用 query 或者 Pinia 都方便。
1.3 后端分层与前端工程的整体结构
后端我用的是经典分层:Controller 层只做参数接收和结果封装,Service 层放业务逻辑,Mapper 层通过 MyBatis 接口与 XML 和数据库打交道。domain/entity 放实体类,dto 放接口传参对象,vo 放返回给前端的视图对象。这个分层看起来“老土”,但维护成本最低。我见过不少项目图省事直接在 Controller 里写 SQL 查询,前期爽,后期改一个需求就要动好几个文件,非常痛苦。
前端方面,src 目录下我按 views、components、api、router、store、utils 划分。views 放页面组件,components 放复用组件,api 按后端模块封装请求方法,router 统一配置路由和守卫,store 管理登录态和全局数据,utils 里放请求封装和公共工具函数。这里想特别提醒:项目一启动就先把 api 模块抽出来,别等到页面多了再重构。每个页面对应一个 api 文件,比如 user.js、dept.js、schedule.js、registration.js,后端接口路径写死在一个文件里,改起来方便,也不容易漏。
2. 数据库设计与核心业务建模
2.1 核心表结构与字段设计
整个系统我最终拆成了 8 张核心表:用户表、科室表、医生表、排班表、号源表、挂号订单表、支付记录表、操作日志表。再加几张基础配置表,比如公告表、健康资讯表。这里重点说几张容易出问题的表。
用户表(sys_user)除了用户名、密码、手机号、身份证号,一定要加一个 user_type 字段区分患者、医生、管理员三种角色,另外推荐加上 real_name 和 id_card,因为医院业务里有实名制要求,后续开放“在线建档”功能时身份证号是关联病历的关键字段。密码字段我推荐用 BCrypt 加密存储,不要用 MD5,原因后面在后端部分详细说。
医生表(hospital_doctor)要关联科室表,字段除了姓名、职称、简介,还要加一个“擅长领域”文本字段。排班表(outpatient_schedule)是核心中的核心,字段包括医生 ID、排班日期、午别(上午/下午)、出诊时间段、总号源数、剩余号源数、排班状态。最后这个状态字段很容易被忽略,它在停诊、医生临时请假时起关键作用。
挂号订单表(outpatient_registration)是整个系统的“账本”,字段要完整记录:订单号、用户 ID、排班 ID、医生 ID、科室 ID、就诊日期、午别、序号、挂号费、状态、创建时间、支付时间。订单状态我一般用四个值:待支付、已支付、已取消、已完成。有人会把“已取号”“已就诊”也做成状态,我建议这些用另外的就诊记录表去扩展,订单表状态保持精简,否则状态机判断会非常绕。
2.2 号源与排班模型的设计思路
号源设计是整个项目最需要动脑子的地方。很多初学者会把“号源”设计成一张独立的大表,比如一个医生一天上午放 30 个号,就往号源表里插 30 条记录。这种做法不是不行,但数据膨胀很快,而且查询“剩余多少号”“有哪些号还没被挂出去”都要做复杂的聚合,性能堪忧。
我的做法是:排班表本身就是号源池。outpatient_schedule 里保存总号源数 total_count 和剩余号源数 remain_count,用户挂一个号,就在事务里执行一次“剩余号源减一”。这种设计在并发量不大、单天号源几百上千的情境下效率非常高。挂号订单表里保存该排班的 ID,同时冗余一个 order_no。
至于每个号的具体“就诊序号”,不需要单独存,可以根据订单 ID 的自增主键或者当天挂号顺序生成。比如订单表里加一个 visit_no 字段,在确认支付时通过“当前剩余号源数 + 1”就能算出来。这样既避免了并发下序号冲突,也减少了大量无意义的席位记录。
2.3 索引设计与慢查询优化
数据库这一层,索引优化比加缓存还重要。我在实际压测里发现,很多慢 SQL 都是因为没有建立合适的联合索引。以挂号订单表为例,最频繁的查询是“查询某用户某天的挂号记录”,那 (user_id, create_time) 联合索引就是必须的。另一个高频查询是“查看某医生某天的排班情况”,那 (doctor_id, schedule_date) 联合索引也应该建立。
排班表建议加一个 MySQL 8.0 支持的功能索引?不是,这里不用那么复杂,直接在 (schedule_date, dept_id) 上建联合索引即可,因为首页的科室列表需要拉出当天的排班。order_no 字段加唯一索引,这既是业务上的订单号唯一性要求,也能在按订单号查询时直接命中索引。
有条件的话,开启 MySQL 慢查询日志,把超过 200ms 的 SQL 抓出来 explain 一遍。我前面说过,MyBatis 的好处就在于 SQL 是我们自己写的,慢查询排查非常方便。拿 explain 结果看 type、key、rows 这些指标,如果 type 是 ALL 说明全表扫了,就要考虑加索引或者改写 SQL。
3. 后端接口开发:从登录鉴权到挂号落库
3.1 登录鉴权与权限拦截:JWT 方案落地
后端接口开发第一步是登录鉴权。我用的是 JWT(JSON Web Token),整体流程是:用户用手机号和密码登录,后端验证成功后生成一个带过期时间的 Token 返回给前端,前端后续请求在请求头 Authorization 字段带上 Token,后端通过拦截器验证 Token 合法性和过期时间。
为什么选 JWT 而不是 Session?因为这套系统后端是纯 API 形态,前端既要做 PC 后台管理,也可能会接小程序、App 等不同端,JWT 天然无状态,不依赖 Session 存储,多端共用同一套鉴权逻辑非常顺。但是要提醒:JWT 是无状态的,一旦签发就无法主动让其失效,单纯靠 Token 过期时间不够灵活。我在项目里的做法是给用户表加一个 token_version 整数字段,用户修改密码或者被管理员强制下线时,把该值加一,JWT 里也带上这个版本号,校验时比对版本号不一致就拒绝。这个小技巧可以解决 JWT“无法主动下线”的典型痛点。
JWT 生成的签名密钥通过 Jasypt 加密后放在 yml 配置里,或者直接注入环境变量。这里有个容易被忽视的点:很多人会把 token 过期时间设得很长,比如 7 天,这在医院内部系统里问题不大,但如果是面向公众的挂号系统,建议 Access Token 过期时间 2 小时,配合一个 Refresh Token 刷新机制,登录体验和安全性都能兼顾。如果不想引入额外的刷新机制,也可以把过期时间设置为一整天,配合前面说的 token_version 做控制。
3.2 科室-医生-排班的多级联动接口
这类接口是整套系统的“门面”,前端首页打开就要用到,必须保证响应速度和控制好返回的数据量。我设计的接口大致分为三层:
第一层是科室列表,返回所有科室的 id、名称、图标、简介。第二层是根据科室 ID 查医生列表,这里不是简单地把该科室所有医生都查出来,而是只返回“在指定日期有排班”的医生,也就是关联排班表做 distinct 查询。这个细节很关键,不然患者点进科室看到一堆“今天没有号”的医生,体验非常差。第三层是根据医生 ID 和日期查询具体排班信息,返回上午/下午各有哪些排班、剩余号源数、挂号费、出诊位置等。
这三层接口看似关联,但其实没必要合并成一个复杂的嵌套接口,分开几个接口查询更清晰,也方便前端做缓存。如果并发量确实很大,可以在后端加一层本地缓存(Caffeine)缓存科室列表,这类基础数据几乎不变,缓存 5 分钟完全没问题。排班数据的实时性要求高,就不能缓存,必须实时查。
这里推荐用 MyBatis 的动态 SQL 处理查询条件的可空情况。举例来说,查询医生列表时,前端可能传来 deptId、scheduleDate 等多个筛选条件,但每个条件都可能为空。用<if test="deptId != null">这种写法既能避免拼 SQL 字符串的注入问题,又能让查询条件灵活组合。另外一个 MyBatis 的小技巧是把公共的 SQL 片段用<sql>标签抽出来,比如医生列表的查询字段明显,避免了多 SQL 之间复制粘贴导致不一致。
3.3 挂号下单的原子性处理:乐观锁与事务
这部分可以说是整个系统最核心的技术点。挂号动作本质上是“先检查剩余号源,再扣减号源,再生成订单”,这三个步骤必须是一个原子操作。很多人第一时间想到给排班记录加行级锁:
SELECT * FROM outpatient_schedule WHERE id = #{scheduleId} FOR UPDATE然后判断剩余号源大于 0,再执行扣减。这种悲观锁方案在低并发下没问题,但到了高峰期,所有请求都会堵在行锁上,数据库连接被大量占用,系统吞吐量会非常糟糕。我在实际项目中更推荐用乐观锁加条件更新:
UPDATE outpatient_schedule SET remain_count = remain_count - 1 WHERE id = #{scheduleId} AND remain_count > 0重点来了:这个 UPDATE 语句的返回值如果是 1,说明排班记录被成功扣减,此时才继续插入挂号订单;如果返回值是 0,说明剩余号源已经为 0,直接抛出“号源不足”的异常。使用这一句条件更新,配合事务,可以完全避免 SELECT 之后再 UPDATE 的竞态问题,也不用手动加行锁。update 本身在 InnoDB 引擎下会对命中的行加锁,但锁的持有时间极短,比“先 SELECT FOR UPDATE 再 UPDATE”要短得多,并发能力自然高。
为了保证整个流程要么全部成功、要么全部回滚,我在 Service 方法上加了@Transactional(rollbackFor = Exception.class)。这里想强调一点:Spring 默认只对 RuntimeException 回滚,对受检异常(比如 Exception 的直接子类)不回滚。如果业务方法里显式抛出了受检异常,但期望整体回滚,必须加上 rollbackFor 参数,否则会出现“订单插入了,排班却没扣减”这种脏数据。这是非常典型的线上事故点,我至少见过三次有人在事务方法里抛了个自定义异常,结果数据一致性问题被埋了半个多月才发现。
再补充一个防重复挂号的处理。患者大概率会手滑点两次“提交挂号”,或者前端做了重试。解决方案是在挂号订单表上建一个联合唯一索引 (user_id, schedule_id, status),同一用户同一排班只能有一条未取消的订单。这样即使请求重复到达后端,第二次插入订单时也会因为唯一索引冲突而失败。
3.4 订单支付流程与状态回滚
挂号系统的支付流程不需要像电商那样复杂,但订单状态和支付回调必须处理严谨。我的支付流程是这样的:用户提交挂号下单后,订单状态置为“待支付”,同时调用支付接口生成支付二维码;用户扫码支付成功后,支付平台异步回调后端接口,后端验证签名和金额无误后,将订单状态改为“已支付”。
这里有两个坑要讲。第一个是支付回调接口必须是“幂等”的,因为支付平台出于可靠性会重复推送回调。我在回调处理里先查询订单状态,如果已经是“已支付”就直接返回成功,不再做重复更新。第二个是订单超时未支付的处理。方案有定时任务扫描“待支付”且超过 15 分钟的订单,把订单改成“已取消”,并回滚排班表的剩余号源数。有人会问,用 Redis 延迟队列做不是更好?确实可以,但如果项目没有引入 Redis,一个每分钟跑一次的定时任务就够用了,医院挂号的实际并发规模远没到需要上延迟队列的程度。
回滚排班号源时要再次使用条件更新,比如:
UPDATE outpatient_schedule SET remain_count = remain_count + 1 WHERE id = #{scheduleId}为什么这里不用判断 remain_count 小于 total_count?因为能走到这一步的订单之前肯定扣减成功过一次,此刻回滚必能成功。这也是事务的一个好处:同一个事务内,前面的扣减和后面的回滚要么同时生效,要么同时取消。
4. 前端 Vue 实现:页面搭建与接口对接
4.1 路由、权限与状态管理
前端部分第一件事是配置路由。页面主要分为两类:面向普通患者的挂号大厅、面向管理员的运营后台。这两类页面建议用不同的布局组件,通过 Vue Router 的嵌套路由实现。我把患者端的路由和后台路由拆成两个路由模块,统一挂载到根路由下,用 /portal 和 /admin 做前缀区分。
路由守卫是必须的,具体逻辑在 router.beforeEach 里判断:如果访问的是后台管理页面,且当前没有 Token,强制跳转到登录页。如果访问的是挂号大厅页面,有 Token 就读取用户信息,没有 Token 也能浏览——毕竟有些患者只是想先看看科室和医生排班,不登录就强制跳转反而降低转化率。
状态管理我用的 Pinia(Vue 3 项目),区别于 Vue 2 时代的 Vuex,Pinia 的使用更简洁。核心 store 有 userStore 和 scheduleStore:userStore 保存用户信息、Token,以及登录和退出登录的方法;scheduleStore 保存查询排班时选择的科室、医生、日期,避免用户从排班列表进入挂号确认页后刷新,丢失了“就诊上下文中必需的排班数据”。这里其实是状态管理很典型的场景,民营医院挂号流程往往有“确认知情同意书”的页面,这中间跳转拿参就是 store 的用武之地。
4.2 核心页面拆解:从科室列表到挂号确认
前端页面的核心流程我按界面拆开讲:
首页展示医院公告和科室列表。科室列表一般用卡片式布局,每张卡片显示科室图标、名称和一句话介绍,点击进入该科室的医生列表。
医生列表页最核心的信息是“今天能不能挂到号”。我前面设计的接口只返回有排班的医生,前端拿到医生列表后,每个医生卡片上显示“上午余号 8 个 / 下午余号 12 个”,这可以直接从排班数据里取。页面顶部放日期选择器,默认选中今天,切换日期时重新请求接口。余号数量非常少时,比如剩下 1 到 2 个,前端可以用一个醒目的样式提示,刺激患者尽快操作,这在业务上蛮有效的。
医生排班详情页展示某个医生该日的所有排班,上午/ 下午分成两个卡片组,每张卡片上有诊室位置、挂号费用、余号数。余号为 0 的时段要置灰并显示“已约满”,禁止点击,否则患者提交后才被告知没号,体验很差。
挂号确认页就是把排班信息、患者信息、挂号费汇总展示,患者选择就诊人后提交。就诊人信息从用户中心中选择,如果用户还没有填写就诊人,这里就要提醒先添加。提交成功后跳到支付页,展示支付二维码。支付成功页面显示挂号成功的通知,包括就诊序号、科室位置、医生信息,并提醒“提前 30 分钟到院候诊”。
4.3 Axios 请求封装与错误处理
前端和后台交互还有一个关键点是 Axios 的封装。我在 utils/request.js 里创建了一个 axios 实例,设置了 baseURL 和超时时间,然后主要通过请求拦截器和响应拦截器做统一处理。
请求拦截器里从 store 中取出 Token,加到请求头 Authorization 字段。响应拦截器里做全局错误处理:如果 HTTP 状态码是 401,说明 Token 失效或未登录,清除本地登录信息,跳转到登录页;如果是 400 或 500,通过统一对话框提示错误信息,比如“号源不足”“订单不存在”由后端在响应体的 message 字段传给前端。
这里要特别提一下文件下载和支付回调这类请求不能让全局拦截器“一刀切”。我在项目里用responseType: 'blob'获取文件流时就绕过了 JSON 错误提示逻辑。不过这个系统是挂号系统,文件下载场景比较少,更多的还是常规接口的异常提示处理。
还有一个很实际的开发技巧:mock 数据。如果后端接口还没写好,前端可以先用一个 json 文件或第三方 mock 平台模拟后端返回,把页面先搭起来。联调时只需要把 request.js 里的 baseURL 改到后端服务即可,页面逻辑不需要动。
5. 部署上线与环境调优
5.1 多环境配置与配置安全
项目上线的第一步是配置多环境。我会把 application.yml 按 dev、prod 分环境拆几个脚本,比如 application-dev.yml(本地开发库)和 application-prod.yml(正式库)。这里有一个约定:开发环境的数据库密码可以明文写在 yml 里,但生产环境一定要用环境变量占位符,比如${DB_PASSWORD}。SpringBoot 会自动读取服务器上设置的环境变量,这就避免了把生产密码提交进 Git 仓库。
数据库连接池我推荐用 HikariCP,这是 SpringBoot 2.x 默认的连接池,性能和稳定性都有保障。生产环境把 maximum-pool-size 配到 20 到 30 差不多,太小了并发会排队,太大了反而增加数据库负担。还有一个容易踩坑的参数是 connection-timeout,默认 30 秒太长,我一般配 5 秒,避免线程池被慢查询拖垮。
Jasypt 加密敏感配置我简单提一句。如果你非要把密码写进配置文件,可以用 Jasypt 把密码加密成一串密文放进 yml,同时把解密密钥放到环境变量里。这样即使有人拿到配置文件,看不到数据库密码明文,也能提升一点安全感。这个属于锦上添花,不是必须,但面试的时候说出来是加分项。
5.2 部署方案:Nginx + Jar + MySQL
我最终选用的部署方案是:Nginx 部署 Vue 前端静态资源,SpringBoot 以 Jar 包方式跑在后端,MySQL 数据库单独部署在一台机器(或者用云数据库)。前端打包命令:
npm run build打包后 dist 目录就是纯静态文件,把它放到 Nginx 的 html 目录,然后配置反向代理,把 /api 前缀的请求转发到后端的 8080 端口:
location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }后端启动命令我会写一个简单的启动脚本,指定 Spring 的 profile 为 prod,Java 参数里加上 -Xms512m -Xmx1024m,防止 JVM 默认按物理内存比例分配过大堆内存导致服务器内存吃紧。首次启动建议先手动跑一遍 Jar 包看日志,没问题再改成 systemd 服务或直接 nohup 后台运行。用 systemd 的好处是可以开机自启、异常退出后自动拉起,生产环境我优先推荐 systemd 方式。
5.3 新手常问:表不存在能自动建表吗
有人问,SpringBoot + MyBatis 项目里能不能让系统在表不存在时自动建表?MyBatis 本身没有这个能力,它只管 SQL 执行。想要自动建表有三条路可选:第一,在项目启动时用 Spring 的 ApplicationRunner 执行一段初始化 SQL 脚本,判断表是否存在,不存在则创建;第二,集成 Flyway 这类数据库版本管理工具,统一管理建表脚本,这个更规范;第三,直接在 MySQL 初始化脚本里建好了再启动后端。我个人的建议是,小项目用方案一快速实现,将来想做数据库版本管理了就升级成 Flyway,千万别自己写一堆判断逻辑,容易翻车。
6. 项目实战避坑记录与问题排查
6.1 高频问题速查表
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| 前端请求接口提示 404 | 后端 Controller 的 @RequestMapping 路径和前端 Axios 请求路径不一致 | 统一用 Swagger/knife4j 查看接口列表,逐一比对路径;或前后端联调时打开浏览器 Network 面板看实际地址 |
| 挂号成功但订单查询列表为空 | 订单表插入和查询用的不是同一个数据库,比如开发环境一个库、测试环境一个库 | 检查数据源配置,统一环境库;另外检查查询条件是否多传了状态参数 |
| 并发挂号出现超卖,剩余号源变负数 | 扣减号源用的 SELECT 后再 UPDATE,没有做原子操作 | 改为条件更新 UPDATE ... WHERE remain_count > 0,并检查受影响行数 |
| 支付回调重复调用导致订单重复更新 | 回调逻辑没有做幂等处理 | 先查询订单状态,已经是“已支付”就直接返回成功 |
| 页面加载很慢,接口响应 1 秒以上 | 科室列表这种基础数据每次都查数据库,没有缓存 | 用 Caffeine 或 Redis 做本地缓存,设置几分钟过期即可 |
| 表单提交时出现 XSS 攻击痕迹 | 前端没有转义用户输入,后端也没有做参数校验 | 前端模板自动转义,后端在入参 DTO 上加 @NotBlank 等校验注解 |
| MySQL 连接报 Public Key Retrieval is not allowed | 使用了 caching_sha2_password 认证且未开启允许公钥检索 | JDBC URL 加 allowPublicKeyRetrieval=true,或改用 mysql_native_password |
6.2 我踩过的几个印象深刻的坑
第一个坑是事务不生效。我在挂号方法上加了 @Transactional,但同一个类内部另一个方法调用它,导致事务注解被绕过。Spring 的默认代理方式只拦截外部调用,自己调自己是不会被代理的。解决办法是拆分 Service,把核心事务方法放到独立 Bean 中,或者用注入自身的方式调用,这样事务注解才真正生效。
第二个坑是 MyBatis 的如果判断不等于空字符串。有一次我只判断了!= null,结果前端传来一个空字符串,导致 SQL 条件变成WHERE dept_id = '',查询结果莫名其妙为空。后来我统一封装了一个工具方法判断字符串是否为空,并且在关键参数的 XML 里严格要求做空串判断。这个坑不大,但排查起来还挺费时间,因为报错不是异常,而是查询结果不对。
第三个坑是支付宝/微信支付回调验签。我一开始只验了金额和订单号,没有验签名,测试阶段没问题,生产环境回调偶尔会出现异常数据被更新进去。后来严格核对平台公钥验签逻辑,并且把验签失败的情况记入日志,方便排查。支付回调这种入口,“宁可错杀一千、不可放过一个”,注意多校验一步。
6.3 几点个人的项目实操心得
第一,开发阶段就把统一异常处理写好。我一般在项目里定义一个 GlobalExceptionHandler,用 @RestControllerAdvice 捕获业务异常,统一返回 code/message/data 的结构。这样前端判断错误逻辑非常统一,不用每个页面各自写 try-catch。有人嫌前期多写代码,但后面每加一个模块都是节省时间的。
第二,日志埋点要打在关键节点上。挂号下单时打一行日志:用户 ID、排班 ID、订单号、扣减影响行数;支付回调成功打一行日志;订单取消打一行日志。这个习惯在排查生产问题时帮了我大忙,否则只能靠猜。
第三,开发完一个模块立刻做接口测试,不要累积到最后再自测。我通常用 Postman 或 Apifox 建立一个项目级测试集,在后端写完一个接口就顺手跑一遍正常和异常场景。前后端联调时问题才少,不然所有问题堆到联调阶段,节奏会非常难受。
第四,这个项目后续扩展的方向很多。可以考虑加 Redis 缓存科室信息和验证码,加 RabbitMQ 做短信通知(挂号和停诊通知),加 WebSocket 做院内排队叫号功能。这些扩展都是以现有代码为基础的,不会推倒重来,这也是当初选择这套技术栈的底气。
写到这里,其实这套系统的核心脉络已经理得很清楚了:SpringBoot 管接口和业务、MyBatis 管 SQL、MySQL 管数据和索引、Vue 管界面交互,四者各司其职,把医院挂号这条看似简单的链路做扎实,关键就在于并发控制、状态管理和部署运维这几层细节。项目本身并不复杂,复杂的是把这些细节都想透,希望这篇博文能帮你少走一些弯路。