项目概述与整体设计思路
1.1 需求场景与核心痛点
这个项目是典型的医院预约挂号系统,技术栈是 SpringBoot + Vue + MySQL,整套源码可以直接运行。我把整个项目完整跑通了一遍,前端、后端、数据库都能正常联调,登录、查科室、找医生、选号源、提交预约、查看记录这一套流程走得很顺。如果你是做 Web 全栈练习、课程设计或者毕业设计参考,这个项目是一个很好的切入点。
为什么这么说?因为医院挂号系统虽然看起来只是“约个号”,但它的业务流程其实覆盖了很多经典的后端场景:用户注册登录、权限区分(普通用户/管理员)、一对多关联查询(科室下有医生,医生有排班)、事务处理(号源扣减不能超卖)、接口鉴权(JWT token)、前后端分离联调。这些场景几乎是企业级业务系统的缩影,把这个项目吃透,很多通用的开发思路就通了。
系统的核心角色有两类:普通用户和管理员。用户在系统里注册账号,登录后按科室浏览医生,查看医生的排班和剩余号源,选择合适的时间段预约挂号,还能在“我的预约”里查看记录或取消;管理员负责维护科室信息、医生信息、排班表,号源的上下架和预约记录管理也都在后台完成。整个数据流路径清晰:用户发起预约 -> 校验 token -> 查询排班号源 -> 事务扣减余号 -> 生成预约记录 -> 前端刷新状态。
我特别想强调的是“余号扣减”这个环节。这就是典型的库存问题,和秒杀系统的核心逻辑同源。很多新手项目在写预约时都是先查出剩余号数,判断大于0再减1,但这样在并发场景下会有严重的超卖风险。这套源码里的处理方式是直接使用数据库的条件更新语句,把判断和扣减放在一条 SQL 里完成,从机制上避免超卖。这个设计细节是懂行的人和普通练习者的分水岭,后面我会专门展开。
1.2 技术选型:为什么是 SpringBoot + Vue + MySQL
这套组合是当前中小型管理系统最经典的技术栈,没有之一。后端用 SpringBoot 2.7.x,搭配 MyBatis-Plus 做数据访问,鉴权用 JWT;前端用 Vue 2 + Element-UI;数据库用 MySQL 5.7 或 8.0 都可以。选这套方案主要有几个考虑。
SpringBoot 的优势不用多说,内嵌 Tomcat、自动配置、约定大于配置,开发效率高。对练习项目来说,SpringBoot 能让你把精力集中在业务逻辑而不是繁琐的 XML 配置上。MyBatis-Plus 则是 MyBatis 的增强工具,单表 CRUD 不用手写 SQL,分页插件也已经内置,非常适合这种管理类系统,能节省大量重复的 Mapper 代码。JWT 做登录鉴权是目前前后端分离项目的事实标准,服务端不需要维持 Session,前端拿到 token 后存起来,每次请求带上就行,集群部署时也天然友好。
前端选 Vue 2 而不是 Vue 3,不是因为它更好,而是因为它更成熟、资料更多、Element-UI 组件库与之配合最顺手。如果你之前用的是 Vue 3 + Element-Plus,这套系统的切换成本也不高,API 层面的差异主要集中在入口文件和路由配置,业务组件的写法基本一致。MySQL 则是最稳妥的关系型数据库,这类业务数据结构清晰,用关系型数据库管理最合适。
这套技术栈对初学者友好、对实际开发够用、对后续扩展也不设限。后续想接支付、短信通知、微信小程序端,都可以在现有结构上平稳扩展。
1.3 系统整体架构与目录结构
系统采用前后端完全分离的开发模式。后端只提供 RESTful API,不渲染页面;前端通过 axios 调用接口,拿到 JSON 数据后由 Vue 动态渲染。这样做的最大好处是前后端可以并行开发、独立部署,也是目前企业级 Web 项目的主流形态。
后端代码按经典分层架构组织:Controller 层负责接收请求和参数校验,Service 层负责业务逻辑,Mapper 层负责数据库操作。实体类放在 entity 包,与数据库表结构对应;配置类放在 config 包,比如 CORS 跨域配置、JWT 拦截器配置;工具类放在 util 包,比如 JWT 工具类、统一返回结果封装类。前端则按页面维度组织,views 目录下每个功能对应一个 Vue 组件,router 目录配置路由,api 目录统一管理接口请求,components 目录存放公共组件。
我第一次看这个项目结构时最满意的一点是:它没有为了“看起来高级”而过度设计,每个文件的职责都清楚,顺着调用链从页面到 Controller 到 Service 再到 Mapper,没有任何突然跳转的地方。这意味着你拿到的不是一团乱麻,而是真正可以跑通、可以读懂的工程。
这种清晰的结构对学习非常有价值。我身边很多朋友拿到源码第一反应是先跑起来,跑起来之后再从自己最关心的功能点入手,比如预约流程、权限控制,跟着调试器走一遍,很快就能建立起对整套系统的整体认识。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表关系
2.1 表结构拆解
数据库是整个系统的地基。这套系统一共设计了六张核心表:用户表、科室表、医生表、排班表、预约记录表,以及管理员表。表数量不多,但每一张的设计都有讲究,下面逐一拆开看。
用户表(sys_user)存储普通用户的基础信息,字段包括主键 ID、用户名、密码、真实姓名、手机号、身份证号、创建时间。密码在入库前使用 MD5 加盐处理,不是明文存储,这是最基本的安全意识。
科室表(department)字段比较简单:科室 ID、科室名称、科室简介。科室是业务链的最顶层,用户挂号的第一步通常是选科室,所以科室表要有稳定且唯一的主键,方便医生表通过外键关联。医生表(doctor)相对丰富一些,包含医生 ID、姓名、所属科室 ID、职称、擅长领域、头像、个人简介。医生和科室是多对一关系,设计时在医生表中保存科室 ID 作为外键关联。
排班表(schedule)是预约系统的核心表。字段包括排班 ID、医生 ID、排班日期、时段(上午/下午)、总号源数、剩余号源数、创建时间。这张表的设计有非常关键的点:同一医生在同一天的同一时段只能有一条排班记录,否则会导致号源数据混乱,所以建表时一定要给“医生 ID + 排班日期 + 时段”这三个字段增加唯一约束。总号源数是一天放出的号量,剩余号源数则随着用户预约逐个递减。
预约记录表(appointment)记录每一次挂号的明细,字段包括预约 ID、用户 ID、医生 ID、排班 ID、就诊日期、时段、预约状态、创建时间。状态字段用整数表示:0 待就诊、1 已完成、2 已取消。这张表承担的是流水记录职能,数据量会随时间增长,后续如果做性能优化,可以考虑按时间分表,但在练习阶段保持单表即可。
管理员表(admin)与用户表分开设计,这和业务逻辑有关。普通用户和管理员的角色完全不同,管理员的账号是预置的,不像普通用户可以自由注册,分开存储逻辑更清晰,权限控制也更简单。
2.2 表关联关系与设计要点
六张表之间的关系用一句话可以概括:用户通过预约记录关联到排班,排班关联到医生,医生关联到科室。最终形成科室 -> 医生 -> 排班 -> 预约记录 -> 用户这样一条完整的业务链路。
每个“关联”在数据库层面都体现为一个外键字段:医生表通过 department_id 关联科室表;排班表通过 doctor_id 关联医生表;预约记录表同时保存 user_id、doctor_id、schedule_id 三个外键,分别关联用户表、医生表、排班表。这样设置的好处是业务查询时非常灵活,比如要查某位医生一个月内的排班情况,直接按 doctor_id 过滤排班表即可;要查某用户的历史预约记录,按 user_id 过滤预约表后连表查医生和科室名称就行。
这里我要特别强调一个设计细节:预约记录表中冗余存放了 doctor_id 和排班日期、时段。这是有意为之,不是重复。因为如果只依赖 schedule_id 关联查排班,一旦排班记录后续被删除或修改,预约记录的历史信息就会丢失或变动;而冗余保存这些字段后,用户的预约记录就拥有了独立的快照,即便排班表的数据调整,历史预约仍然可追溯。实际项目中的数据表设计,很多时候不是在追求极致的范式,而是在范式和查询效率之间做取舍。
除此之外,还需要注意字符集统一。建库时建议直接指定 utf8mb4,因为医生姓名、科室名称、擅长领域这些字段都可能包含中文,如果默认字符集是 latin1,导入数据时很容易出现乱码。
2.3 初始化数据说明
这套系统自带的 SQL 脚本里已经预置好了演示数据,导入后可以直接体验完整功能。管理员账号是 admin,预置在管理员表里;普通用户测试账号是 test,密码经过加密处理后保存在用户表中。科室方面预置了内科、外科、儿科、妇产科、骨科、眼科等常见医院科室,每个科室下都挂了一到两位医生,医生有姓名、职称、简介。
排班表也生成了最近一周的数据,每天、每个医生、每个时段的总号源数一般在 10 到 20 之间。这些数据足够你跑通完整的预约流程。需要提醒的是,如果导入数据后发现某些医生在今天的排班是空的,这是很正常的,因为排班是滚动生成的,你可以通过管理后台新增一条排班来测试。
拿到 SQL 脚本后,我建议你先不要急着运行,而是用数据库管理工具打开脚本文件通读一遍,看看每张表的字段定义和注释,同时把六张表之间的外键关系理清楚。这一步做扎实了,后面看代码时你会发现自己对很多业务判断的理解速度快了很多。
3. 后端 SpringBoot 核心功能实现
3.1 项目结构、依赖配置与核心配置类
后端项目的包结构非常清晰。com.hospital 作为根包,下面按照职责划分为 controller、service、mapper、entity、config、util、common 等子包。common 包里主要放统一返回结果的封装类和状态码常量,这样每个接口的返回值结构都是一致的,前端解析起来非常方便。
打开 pom.xml,核心依赖就那么几个:spring-boot-starter-web 提供 Web 能力;mybatis-plus-boot-starter 负责数据库操作;mysql-connector-java 提供 MySQL 驱动;jjwt 处理 JWT 的生成和解析;lombok 简化实体类代码。这里我特别想说一下为什么不引入 spring-boot-starter-security。因为这类系统的鉴权需求其实很轻,只需要验证登录后签发 token、拦截非白名单请求,引入 Security 不但配置复杂,还会给新手增加理解负担。用拦截器配合 JWT 简单实现,代码直观、易读、易调试,足够了。
application.yml 里的配置是运行时的关键。数据源配置需要指定数据库地址、用户名、密码,还有一个容易踩坑的点:数据库连接 URL 中必须加上 serverTimezone=Asia/Shanghai 参数。MySQL 8.0 默认对时区敏感,不加这个参数启动时会直接报错提示时区异常。端口我建议后端改成 9090,因为前端开发服务器默认跑在 8080,两个服务如果都在 8080 会发生冲突。
启动类只需在 SpringBoot 的启动主类上增加 @MapperScan 注解,指定 mapper 包路径。这一步配合 MyBatis-Plus 使用,之后所有的 Mapper 接口都不需要挨个加 @Mapper 注解了。
3.2 登录鉴权:JWT 令牌设计与拦截器配置
登录鉴权是前后端分离系统的基础设施。整套机制的核心流程是这样的:用户登录时提交用户名和密码,后端校验密码通过后,使用 JWT 生成一个令牌返回给前端。令牌中包含用户 ID、用户名、角色和过期时间。前端将令牌存储在 localStorage,每次发请求时在 HTTP 头中携带 Authorization 字段。后端通过拦截器统一校验这个字段,判断请求是否合法。
JWT 工具类中通常会封装三个核心方法:生成 token、解析 token、判断 token 是否过期。生成 token 时使用 HS256 算法,密钥保存在配置文件中,签名有效期的设置同样在配置文件中完成,不要写死在代码里。解析 token 失败或过期时,工具类直接抛出异常,拦截器捕获异常后向客户端返回 401 状态的 JSON 数据。
拦截器的配置是一个容易被忽视的重点。我们需要在 config 包里定义拦截器注册类,继承 WebMvcConfigurer 接口,重写 addInterceptors 方法。对于注册接口、登录接口、首页轮播图等无需身份验证的接口,必须加入白名单,否则用户还没登录就把请求拦截了,会陷入死循环。白名单匹配时要注意路径匹配规则,比如 /api/login 和 /api/register 要单独列出,而 /api/admin/** 则要交给专门的校验逻辑。
如果你看到后端代码中有一个类上标着 @Configuration,同时实现了拦截器注册逻辑,那大概率就是这一环。它让所有受保护接口都纳入统一管理,省掉了每个 Controller 里重复的“判断当前用户是否登录”的代码。
3.3 预约挂号核心业务逻辑:事务、防超额与唯一性校验
预约挂号是整个系统里最有技术含量的点。它表面上只是一个用户点击确认后的写库操作,实际上背后牵涉三个环节:业务校验、号源扣减、记录生成。三个环节必须在一个事务内完成,任何一个步骤失败都要全部回滚,否则会出现“号源扣了但预约记录没生成”或者“预约记录有了但号源没扣”这种数据不一致的情况。
具体代码逻辑是这样的。Service 层的预约方法上标注 @Transactional 注解。方法内部先做三件事:查询排班是否存在、查询该用户当天是否已经预约过这位医生、检查剩余号源是否大于 0。前两步是业务校验,第三步是数据兜底。
预约状态中的唯一性校验特别重要。实际场景中用户重复点击提交按钮、前端重试或者并发请求,都会导致同一个用户对同一个医生在同一时间段提交多次预约。所以在保存预约记录之前,一定要在 appointment 表中查询是否存在 user_id、doctor_id、schedule_id 都相同的待就诊记录,存在就抛异常提示“请勿重复预约”。
号源扣减用的是并行安全写法,SQL 语句长这样:
sql复制UPDATE schedule SET remain_num = remain_num - 1
WHERE id = #{scheduleId} AND remain_num > 0
这条 SQL 是防超卖的关键。它在数据库层面同时完成“检查余号大于0”和“余号减1”两个操作,并且是原子性的。多个请求同时执行这条语句时,数据库行锁会保证每次只有一个请求能成功把余号减下去。第一个请求成功扣减后,第二个请求执行时发现 remain_num 已经变成 0,条件不满足,影响行数为 0,就视为号源已满无法预约。这种做法比传统的“先 select 查询余号,再 update 扣减”安全得多,后者在两个人同时查到余号 1 的情况下会导致超卖。
扣减成功后再插入预约记录,插入失败则整个事务回滚,排班表的余号也会自动恢复。这一整套设计思路,和电商秒杀系统的核心逻辑如出一辙。很多朋友在面试中提到自己做过类似系统,但问到“十万个人同时抢 100 个号源会不会超卖”时答不出来,就是这个细节没吃透。
3.4 管理端功能:排班管理与号源初始化
管理端的功能围绕“维护”二字展开。管理员登录后可以做科室的增删改查、医生信息的增删改查、排班管理以及预约记录查看。
排班管理最需要关注的业务逻辑是:新增排班时必须做重复校验。如果数据库里已经存在同一医生、同一日期、同时段的排班记录,再插入一条就会数据冗余,用户在预约时看到两个相同的号段,会彻底乱套。为了避免这个问题,建议在生成排班的 SQL 或 Service 代码里先查询唯一组合是否存在,存在就直接提示。当然,最稳妥的方式是在数据库表中建立联合唯一索引,把 doctor_id、work_date、shift 三个字段绑定成一个唯一约束,让数据库做最后一道防线。
初始化排班号源时还有一个细节:不要把 total_num 和 remain_num 做成两个独立传参的字段。在管理界面上应该只录入“总号源数”,剩余号源默认与总数一致,由代码自动填充。如果管理员能随意修改剩余号源数,就会出现“总号 20、剩余 30”这种明显的数据错误。
预约记录管理相对简单,主要是分页列表加状态筛选。管理员可以查看全部预约记录,也可以按日期、科室、医生名称搜索。系统预设了一个“标记已完成”的操作,用户实际就诊后,管理员把当天的预约状态置为已完成,这背后就是一次 status 字段的更新操作。
4. 前端 Vue 页面与接口联调
4.1 前端工程结构与路由权限控制
前端项目基于 Vue CLI 创建,使用 Vue Router 管理路由。src 目录下的主要结构包括:views 目录存放页面级组件;router 目录配置路由表;api 目录集中管理接口请求;utils 目录封装 axios 实例和 token 读写工具;components 目录存放公共组件,比如头部导航栏、医生卡片等。
页面路由有两种类型:普通用户页面和管理员后台页面。用户端访问的路径包括首页、登录、注册、科室列表、医生列表、预约页、我的预约;管理端路径以 /admin 开头,包括后台首页、科室管理、医生管理、排班管理、预约管理。
路由权限控制使用 Vue Router 的前置守卫实现。在 router/index.js 中定义 beforeEach 钩子函数,每次路由跳转前判断目标路由是否需要登录权限。判断依据是 localStorage 中是否存在 token,不存在则跳转到登录页并携带当前路径的参数,登录成功后再跳回原页面。管理端路由还多一层角色判断,如果当前登录用户不是管理员,访问 /admin 下的页面会被强制跳转到首页。
这里有一个实用的设计细节:路由表元信息 meta 字段里设置 requireAuth 和 requireAdmin 两个布尔值。守卫里通过 to.matched.some 遍历所有匹配路由的 meta 信息,只要任何一个路由要求登录就必须校验 token。这个设计比在每个组件里单独判断权限要优雅得多,维护路由表时只需在 meta 上标注权限等级,逻辑集中、易扩展。
4.2 页面功能拆解与组件设计
前端的每个页面都对应一个明确的用户操作场景。
登录页和注册页是最基础的。登录表单提交后调用后端接口,成功后把 token 和用户信息存入本地存储,然后根据用户角色跳转到对应首页。注册页需要校验两次密码一致,同时验证手机号格式,这些校验在提交前用 Element-UI 的表单校验规则完成,体验比只靠后端校验好得多。
首页承载的是流量分发。页面顶部是导航栏,展示系统名称、科室入口、我的预约、登录状态等;页面主体是医院简介和科室分类入口。科室列表的展示方式是卡片式布局,点击任一科室卡片跳转到该科室下的医生列表页。医生列表页顶部显示科室名称,下面是医生卡片,每张卡片展示医生的头像、姓名、职称、擅长领域,以及“查看排班”按钮。点击按钮跳转到排班页,排班页展示该医生未来一周的排班表格,每个日期下分“上午”“下午”两列,单元格里显示剩余号数和可预约按钮。号源为 0 或时间已过则按钮置灰,用户无法操作。
我的预约页展示当前登录用户的历史预约列表,按照时间倒序排列。每条记录显示就诊日期、时段、医生姓名、科室、状态,状态通过标签颜色区分:绿色表示待就诊、灰色表示已取消、蓝色表示已完成。用户对待就诊状态的记录可以进行取消操作,取消后要调用接口修改预约状态,同时后端将该排班的剩余号数加回,保证号源数正确释放。
管理端页面以表格为主体。科室管理、医生管理、排班管理都是典型的 CRUD 页面,表格展示数据列表,顶部提供新增按钮,每行提供编辑和删除操作。排班管理页面因为是业务核心,还增加了按医生筛选和按日期范围筛选的功能,排班表格直观展示每个医生一周内的排班情况。
4.3 axios 请求封装与跨域问题处理
前端的接口请求通过 axios 实例统一管理。在 utils 目录下创建一个 request.js 文件,使用 axios.create 创建一个独立的请求实例,然后设置 baseURL、超时时间,并在请求拦截器中统一注入 token。具体做法是在请求拦截器里从 localStorage 读取 token,如果存在,就在请求头中加入 Authorization 字段。响应拦截器则统一处理两种异常:HTTP 状态码为 401 时说明 token 缺失或过期,直接清空本地登录信息并跳转登录页;返回的业务状态码非 200 时,弹出对应的错误提示信息,比如“该时段号源已约满”。
接口路径在 api 目录按业务模块拆成多个文件,比如 user.js 负责登录注册相关的接口调用,appointment.js 负责预约相关的接口调用。这样做的直接好处是页面组件里不再出现零散的请求代码,所有接口定义集中管理,后端接口路径调整时只需修改一处。
前后端跨域问题是联调时绕不开的坎。由于前端开发服务器跑在 8080 端口,后端跑在 9090 端口,两个服务的域名和端口不同,浏览器会自动拦截跨域请求。这个问题的解决方案在后端,而不是前端。通常在后端创建一个配置类实现 WebMvcConfigurer 接口,重写 addCorsMappings 方法:
java复制registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
注意 allowedOriginPatterns 不能写 allowedOrigins("*") 配上 credentials(true) 的组合,这在较新的浏览器版本里会被拦截。allowedMethods 里千万别漏了 OPTIONS,预检请求会用到它,漏掉之后前端跨域请求会神秘地失败。
4.4 前后端接口约定与联调心得
前后端联调最痛苦的问题,不是逻辑 bug,而是接口约定不一致。这套系统在实践过程中沉淀了一套简单的约定,我建议你接手后也延续这套约定。
统一返回结构是所有后端接口必须遵守的规则。返回格式固定为:状态码 code、提示信息 msg、业务数据 data。成功时 code 为 200,失败时 code 比如为 500 或自定义业务码,例如 40001 表示号源不足、40002 表示重复预约。前端在响应拦截器里直接判断 code 是否为 200,做到统一处理。后端在使用这个结构时,尽量让成功时 data 的数据结构稳定,比如列表接口固定返回 { total: 总数, records: 当前页数据 },这样前端分页组件接数据时不需要再兼容多种格式。
时间格式也是容易出问题的地方。后端返回的日期字段默认可能是 ISO 格式的字符串,比如 2025-07-01T10:00:00,而前端 Element-UI 的时间选择器和表格展示默认期望的是 2025-07-01。为了避免格式问题,后端在日期字段上直接使用 @JsonFormat 注解统一格式化为 yyyy-MM-dd,前端拿到字符串直接展示,省去转换。这是我在调试中吃过亏后总结出的做法:时间格式问题看着小,但排查起来很费时间,不如在一开始就定死规范。
还有一个我在实践中最常用的联调技巧:在浏览器开发者工具的 Network 面板里观察每个请求的地址、请求头、请求体、响应体。前端报错时,先判断请求是否发出,再确认请求头有没有带 token,最后看请求体和后端参数名是否对齐。500 以内的报错大概率是后端业务抛出的异常,响应体里会有具体的错误信息;网络层报错则要检查路由是否写对、后端是否启动、跨域配置是否正确。这种从外到里的排查顺序,能帮你快速定位绝大多数联调问题。
5. 本地直接运行指南
5.1 环境准备清单
既然标题里写了“可直接运行”,那我们就要做到真正意义上的开箱即用。动手之前先把环境准备好。
后端运行需要 JDK 1.8 或以上版本,推荐 JDK 8,大多数 SpringBoot 2.x 项目在 JDK 8 下运行最稳定。Maven 需要 3.6 版本以上,用于依赖下载和项目打包。如果 Maven 官方仓库下载依赖速度慢,建议在 Maven 的 settings.xml 文件中配置阿里云公共镜像,把速度提起来,否则等依赖可能要等到怀疑人生。
前端运行需要 Node.js 环境。Vue 2 项目建议使用 Node 14 或 16 版本,不要一上来就装最新的 Node 20,Vue 2 和最新 Node 的兼容性存在不确定性,容易出现依赖编译报错。npm 是随 Node 一起安装的,不需要额外配置,但可以用 cnpm 镜像加速依赖安装。
数据库使用 MySQL 5.7 或 8.0 均可。如果你之前没装过 MySQL,建议直接上 8.0,安装完注意设置密码和字符集。数据库管理工具用 Navicat 或者 DataGrip 都可以,这个按照个人习惯来。
前端开发工具建议直接使用 VS Code,安装 Vetur 或 Vue Language Features 插件,能提供基础的代码提示。后端开发工具可以用 IntelliJ IDEA,社区版就够用。
5.2 数据库初始化步骤
数据库初始化是整个运行过程中最容易出错的一步,按顺序操作就不会有问题。
先启动 MySQL 服务,用管理工具连接上本地数据库,新建一个数据库,名称建议和配置一致,比如 hospital,字符集选择 utf8mb4,排序规则选择 utf8mb4_general_ci。数据库建好后,把项目源码中 SQL 文件导入进去。导入方式有两种:一种是在管理工具中直接打开 SQL 文件执行整个脚本,另一种是在命令行执行 mysql -u root -p hospital < hospital.sql。执行后刷新数据库表列表,你应该能看到六张表。
导入完成后,建议先挨个表检查一下数据是否完整。重点看 sys_user 表里有没有预置用户,schedule 表里有没有排班记录。如果表是空的,说明 SQL 文件没导入完整或导入到了错误的目标库中。数据库这步完成,后端才能正常启动,否则启动时数据源初始化就会报 404 或 socket 超时错误。
5.3 后端启动步骤
用 IDEA 打开后端源码目录后,等待 Maven 完成依赖导入,这个过程中注意观察右下角的进度条和 Console 窗口是否有异常提示。
打开 src/main/resources/application.yml 文件,确认数据库连接配置。你需要把 URL 中的数据库名、用户名和密码修改成你自己环境的值:
yaml复制server:
port: 9090
spring:
datasource:
url: jdbc:mysql://localhost:3306/hospital?serverTimezone=Asia/Shanghai
username: root
password: 你的数据库密码
确认配置无误后,找到启动类,点击运行。看到 Console 窗口打印出 SpringBoot 启动成功的横幅,以及 Tomcat started on port(s): 9090 的字样,说明后端已经启动成功。此时可以在浏览器访问一个后端接口测试,比如访问 http://localhost:9090/api/health 之类的健康检查接口,返回 JSON 数据就证明后端可用。如果启动过程中报缺失类或版本冲突,优先检查 JDK 版本和 Maven 依赖是否完整。
5.4 前端启动步骤
用 VS Code 打开前端项目目录,在终端中执行依赖安装命令:
bash复制npm install
等待依赖安装完成。如果中途报错,排查一下 Node 版本和网络镜像源。安装完成后,执行启动命令:
bash复制npm run serve
编译成功后,终端会显示本地访问地址,默认是 http://localhost:8080。用浏览器打开后,能看到系统首页和登录注册入口。启动成功后先做一次完整自测,比如注册一个新账号,用新账号登录并尝试预约,再切换到管理员账号登录后台查看数据,这样可以确认前后端联调正常。
如果前端请求后端接口时出现跨域问题,上面提到的后端 CORS 配置类必须工作正常;如果仍然不行,先在浏览器控制台看具体报错信息,然后针对性地检查 CORS 配置的允许源和方法。绝大多数跨域问题都能通过这一步解决。
5.5 演示账号与测试流程
系统默认的管理员账号是 admin,密码是 admin123;普通测试用户是 test,密码是 123456。登录后按下面流程走一遍,能快速验证系统功能是否齐全。
普通用户流程:登录 test 账号,进入首页,点击某个科室进入医生列表,再点查看排班,选择一个有剩余号源的时间段提交预约,提示预约成功之后进入“我的预约”,能看到新增的记录。测试取消功能,点取消按钮,回排班页确认剩余号源数量确实加了一。
管理员流程:退出当前账号,登录 admin 账号,进入后台。在医生管理中添加一个新医生,然后在排班管理中为这位医生新增一条明天的排班,再到前台预约页确认能看到这位医生和对应的排班,验证数据联动没有问题。最后到预约管理里查看刚才测试用户创建的记录,状态显示待就诊或已取消。
这一步走完,整个系统的核心闭环就全部验证通过了。
6. 常见运行问题与踩坑记录
6.1 数据库连接失败与时区异常
数据库连接失败是最常见的问题,表现形式也很多样。启动后端时提示 Communications link failure 或者 Access denied for user,基本可以确定是数据库连接信息错误或数据库服务没有启动。检查顺序是先确认 MySQL 服务在运行,再确认 URL 中填写的数据库名称、用户名、密码完全正确。如果你改了 root 密码却用旧密码连接,IDEA 控制台会提示权限拒绝,这个很容易发现。
时区问题是 MySQL 8.0 特有的。报错信息中如果出现 The server time zone value 或 You must configure either the server or JDBC driver via the serverTimezone property,说明连接 URL 里没有加时区参数。解决办法是在 JDBC URL 末尾添加 ?serverTimezone=Asia/Shanghai,注意字符编码参数也可以一起加上,最终格式类似 jdbc:mysql://localhost:3306/hospital?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8。
这类问题看着低级,但实际遇到的频率非常高。我排查过很多因为时区配置导致项目无法启动的案例,几乎都是切了 MySQL 版本之后没同步更新数据源配置。
6.2 端口冲突与跨域请求失败
后端和前端开发服务器默认端口都是 8080。如果先启动前端再启动后端,后启动的项目会直接报端口占用错误。解决办法是把后端端口修改为 9090,这个在配置文件里天然可以通过修改 server.port 实现,推荐一开始就这么配置。
跨域请求失败的典型表现在浏览器控制台出现类似 Access to XMLHttpRequest has been blocked by CORS policy 的信息。排查方向很明确:检查后端 CORS 配置类是否生效,即是否加上了 @Configuration 注解;检查 allowedOriginPatterns 是否包含了前端地址;检查 allowedMethods 是否包含了实际请求所用的方法,比如 PUT、DELETE。有一种比较容易忽略的情况:前端携带了自定义请求头 Authorization,而后端 allowedHeaders 没有开放这个头,也会被拦截。配置时允许所有头最省事。
6.3 前端依赖安装失败与启动白屏
npm install 失败的原因通常是 Node 版本过高或镜像源不稳定。Vue 2 项目在 Node 16 以下的兼容性最好,如果你用 Node 18 以上安装时出现安装中断或编译报错,建议先切换到 Node 14 或 16 再试。切换工具可以使用 nvm,一条命令就能完成版本切换。如果 node_modules 目录已经装了一半,删掉重新装,别留着残留文件继续折腾。
前端启动后白屏是最让人头疼的。白屏通常意味着 JavaScript 运行时错误,检查浏览器控制台。最常见的原因有两个:路由模式使用了 history 模式,导致刷新页面时无法正确匹配路由;或者接口请求失败后未做处理,页面渲染时读取了空数据。路由问题可以把 router 的 mode 配置成 hash 模式解决,上线时再用后端做 history 模式重定向;数据问题则逐个接口排查响应结构。
6.4 预约超卖与数据一致性风险
如果你在实际运行中用多个浏览器同时操作同一个号源进行预约,可能会发现一个潜在问题:通过接口并发提交时,如果后端 Service 方法没有加 @Transactional 注解或者没有使用条件更新扣减,很容易出现超卖。表现是同一个排班的剩余号数变成了负数,或者剩余号数大于实际预约记录数。
排查顺序是先看 Service 方法上有没有事务注解,再看扣减号源的 SQL 是不是 UPDATE schedule SET remain_num = remain_num - 1 WHERE id = ? AND remain_num > 0。如果是用“先查余号再手动减1”的写法,并发场景下必然出问题。还有一种情况:前端在预约成功后没有刷新排班数据,导致用户看到的是过期数据,点进去才发现没有号了。这种情况属于体验问题,前端在请求返回“号源不足”后应该自动重新刷新排班列表。
6.5 典型问题速查表
我整理了一份运行过程中最常遇到的问题速查表,覆盖了环境配置、依赖、端口、拦截器等容易踩坑的方向。
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 后端启动报时区异常 | MySQL 8.0 时区敏感 | JDBC URL 加 serverTimezone=Asia/Shanghai |
| 后端启动报端口占用 | 8080 被前端或其它程序占用 | 修改 server.port 为 9090 后重启 |
| 前端请求 404 | API 路径或 baseURL 不正确 | 核对 request.js 中的 baseURL 与后端路径 |
| 前端请求 401 | token 缺失或过期 | 重新登录,检查请求头是否携带 Authorization |
| 跨域请求被拦截 | CORS 配置不完整 | 检查后端 addCorsMappings 方法 |
| npm install 失败 | Node 版本不兼容 | 使用 Node 14/16 重装 |
| 登录成功后刷新失效 | token 未存入 localStorage | 检查登录页存储 token 的代码 |
| 排班重复 | 未做唯一约束 | 表加联合唯一索引,Service 加校验 |
| 预约后余号不变 | 扣减 SQL 未生效 | 检查 update 语句是否传入了正确的 scheduleId |
这套速查表是实际运行中真金白银踩出来的经验,建议你遇到问题时先对照表里排查一轮,能省省下很多盲目试错的时间。
整个项目跑通之后,我对这套系统的评价是:麻雀虽小,五脏俱全。从技术角度看,它覆盖了前后端分离项目最核心的链路,登录鉴权、权限控制、事务操作、并发扣减、接口联调,每一个都是真实工作里高频使用的技能点。从业务角度看,预约挂号的流程完整度很高,不是那种只搭了壳子的半成品。如果后续想继续扩展,可以从这几个方向入手:给系统增加短信验证码登录,让预约流程对接在线支付,用定时任务在每天凌晨自动释放过期的未支付号源,或者给管理后台加一套数据统计面板。这些扩展方向都建立在已有的表结构和代码框架之上,难度不大,但能让这个系统更接近真实的商用形态。我个人在实际操作中的体会是,这类项目最大的学习价值不在高深的技术,而在于把一条完整的业务链路从头到尾打通。真正动手跑一遍、改一遍、坏一遍、修一遍,比对着文档看十遍都管用。
