☰
SpringBoot2+Vue3+MyBatis-Plus医院网站全栈项目实战解析
2026/10/3 4:44:24 网站建设 项目流程

最近我把一套 Java Web 的中小型医院网站系统完整梳理了一遍,后端用的 SpringBoot2,前端是 Vue3,持久层选了 MyBatis-Plus,数据库落在 MySQL8.0 上,源码和配套文档都齐了。这套东西对正在做毕业设计、课程设计,或者想找一个“麻雀虽小五脏俱全”的全栈练手项目的人来说,价值很直接:它覆盖了从数据库设计、后端接口、前端页面到联调部署的一条完整链路。我在这篇文章里把整个项目的设计思路、核心模块、实操细节和踩坑记录全部拆开讲清楚,照着做,你也能在两周内把一个类似的医院网站从零搭起来。

1. 项目整体设计与技术选型思路

1.1 为什么是 SpringBoot2 而不是 SpringBoot3

先聊技术选型,因为很多人在这一步就开始纠结。SpringBoot3 虽然已经发布挺久了,但中小型项目、教学场景和大部分毕业设计里,SpringBoot2 依然是最“稳”的选择。原因很简单:

  • 生态成熟,网上资料多,遇到问题随便一搜就能找到对应的解决办法。
  • 第三方组件兼容性好,很多 starter、插件、老教程在 SpringBoot2 下可以直接用,不需要额外适配。
  • 对新手来说,SpringBoot2 的自动配置机制和源码阅读难度更友好,能让你把精力放在业务逻辑上,而不是被版本兼容问题消耗掉。

当然,SpringBoot3 的响应式编程、GraalVM 原生镜像这些特性确实很吸引人,但对一个中小型医院网站来说,这些能力属于“杀鸡用牛刀”,而且会增加前期的学习成本。项目里用到 SpringBoot2.7.x,配合 MyBatis-Plus 3.5.x,这套组合在实战里被验证过很多次,可靠、省心。

前端选择 Vue3 也是同样的逻辑。Vue2 虽然还在维护,但 Vue3 已经是事实上的主流,Composition API 写起来更灵活,配合 Vite 构建速度也快。再加上 Element Plus 这套组件库,做后台管理和前台展示页面都很顺手。这里多说一句:如果是团队协作或者以后要维护旧项目,Vue2 的知识依然有用,但新项目直接上 Vue3 是更理性的决定。

1.2 中小型医院网站的核心业务边界

很多同学拿到“医院网站”这种题目后,容易把需求想得太庞大,什么在线问诊、电子病历、药品库存、收费统计全往里面塞。结果做了一半发现根本收不了尾。真正合理的做法是先划定边界,明确这个项目是“中小型”的,只做一条完整的核心业务链。

这套系统里,我按三个角色来划分功能:患者、医生、管理员。

  • 患者端:注册登录、浏览科室和医生、查看排班、在线预约挂号、查看个人预约记录。
  • 医生端:查看自己的排班、查看预约自己号源的患者列表、更新就诊状态。
  • 管理端:科室管理、医生管理、排班管理、公告发布、基础数据维护。

这条链路把“注册 → 查医生 → 选排班 → 约号 → 就诊 → 记录”完整串了起来,既覆盖了医院网站最核心的预约挂号场景,又不会让项目失控。至于支付、消息推送、电子病历这类扩展功能,可以作为二期规划,先不纳入第一版。

2. 核心功能模块拆解与数据库设计

2.1 业务闭环:从注册到预约再到就诊

做数据库设计之前,我建议先把业务闭环在纸上画一遍。不需要画得很复杂,但要把数据流向理清楚。

拿预约挂号来说,整个流程是这样的:

  1. 患者注册账号并登录。
  2. 患者按科室找到医生,查看医生的排班日期和剩余号源。
  3. 患者选择一个时间段提交预约,系统创建一条预约记录。
  4. 患者可以在个人中心看到预约状态,比如“待就诊”、“已完成”、“已取消”。
  5. 医生登录后按日期查看自己的排班和预约患者,就诊完成后把状态改成“已完成”。

这个闭环里,最核心的数据实体是:用户(患者/医生)、科室、医生信息、排班、预约记录。所有的表设计都围绕这几个实体展开,其他的公告、管理员这些都属于辅助数据。

2.2 表结构设计的心得

数据库用的 MySQL8.0,字符集我统一用了 utf8mb4,排序规则选 utf8mb4_general_ci。8.0 对 utf8mb4 的支持已经很完善了,emoji 表情这种特殊字符也能正常存储。

核心表大概有这么几张:

表名用途核心字段
sys_user用户表,包含患者和医生账号id、username、password、real_name、phone、role、status、deleted
hos_department科室表id、name、description、sort、status
hos_doctor医生信息表id、user_id、department_id、title、intro、avatar、status
hos_schedule医生排班表id、doctor_id、schedule_date、period、total_count、remain_count、status
hos_appointment预约挂号表id、user_id、schedule_id、appointment_no、status、create_time
hos_notice公告表id、title、content、publish_time、status

几点设计经验:

  • 主键统一用 BIGINT,由 MyBatis-Plus 的雪花算法生成,方便后续分库分表或数据迁移。
  • 金额字段如果以后要加,一定用 DECIMAL,别用 DOUBLE,精度会出问题。
  • 状态字段用 TINYINT,比如预约状态 0 待就诊、1 已完成、2 已取消、3 爽约。用数字存状态,比用字符串省空间,查询也快。
  • 每张表都加上 create_time、update_time 和 deleted 字段,deleted 用于逻辑删除。

有一点特别提醒:数据库层面我不建议加物理外键。虽然外键能保证引用完整性,但中小型项目里物理外键会拖慢插入和更新性能,而且一旦需要改表结构非常痛苦。外键关系在 Service 层用代码去校验就行,这也是目前企业开发里的主流做法。

2.3 为什么要重点做 SQL 初始化脚本

这套源码里附带文档,而文档里最容易被忽视的其实是 SQL 脚本。我在项目里维护了三个 SQL 文件:

  • init_db.sql:创建数据库和所有表结构的脚本。
  • init_data.sql:初始化管理员账号、科室基础数据、示例医生和排班数据的脚本。
  • test_data.sql:一些用于演示的测试数据。

有人可能觉得 SQL 脚本很简单,随便写写就行。实际上这是整个项目复现的第一步,如果脚本里少了索引、字段长度不对、初始化数据有误,后面所有功能都会跟着出问题。尤其是在换一台全新环境部署的时候,一个能一条命令跑通的初始化脚本,比什么部署文档都管用。

3. 后端 SpringBoot2 与 MyBatis-Plus 的落地细节

3.1 分层架构与统一响应封装

后端我严格按照 Controller → Service → Mapper 三层来写,Controller 层只做参数接收和结果返回,业务逻辑全在 Service 层,Mapper 层只用 MyBatis-Plus 提供的能力,不写多余的 XML。这样做的好处是职责清晰,出问题时能快速定位。

为了前后端交互规范,一定需要一个统一的响应结构。我定义了一个 Result 类:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

前端只看 code 字段就知道请求是否成功,无需每次解析 HTTP 状态码。同时还有一个全局异常处理器,把业务异常和系统异常统一拦截,避免把堆栈信息直接抛给前端。这些细节虽然看起来琐碎,但决定了前后端联调时到底是从容还是手忙脚乱。

3.2 MyBatis-Plus 高效操作的几个关键姿势

MyBatis-Plus 最香的地方就是单表 CRUD 几乎不需要写 SQL。我所有 Mapper 都继承 BaseMapper:

public interface DoctorMapper extends BaseMapper<Doctor> { }

然后在 Service 里配合 IService 和 ServiceImpl,基础的增删改查方法直接就齐了。

真正提升开发效率的是 LambdaQueryWrapper,比如按科室查医生列表:

List<Doctor> doctors = doctorService.list( new LambdaQueryWrapper<Doctor>() .eq(Doctor::getDepartmentId, departmentId) .eq(Doctor::getStatus, 1) .orderByAsc(Doctor::getSort) );

这种写法的好处是类型安全,不会因为字段名写错而悄悄出 bug。相比原生 MyBatis 手写动态 SQL,代码量至少减少一半以上。

还有一个细节必须提:批量插入。MyBatis-Plus 提供 saveBatch 方法,但如果你在 JDBC 连接串里没有加rewriteBatchedStatements=true,那这个“批量”其实是逐条执行的。我在初始化排班数据时,一次性要生成几百上千条排班记录,最开始没加这个参数,跑得特别慢,加了之后速度直接上一个台阶。这是很容易踩的坑。

jdbc:mysql://localhost:3306/hospital_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true

3.3 登录认证与密码安全

医院网站涉及患者隐私数据,登录这块不能随便糊弄。这里用了 JWT 做登录态管理,用户登录成功后后端返回一个 token,前端存在本地,之后每次请求在请求头里带上 token,后端拦截器解析 token 后从上下文取出当前用户信息。

密码存储千万别用明文,也不要再用 MD5。MD5 加盐虽然比明文好一点,但如今破解成本太低。我直接用的 BCrypt 加密:

// 注册时加密 String encodedPassword = BCrypt.hashpw(rawPassword, BCrypt.gensalt()); // 登录时校验 boolean matched = BCrypt.checkpw(rawPassword, encodedPassword);

BCrypt 每次加密结果不同,但校验时能正确匹配,安全性比 MD5 高一个级别。

权限方面,我会在 token 里带上角色信息,后端接口通过自定义注解加拦截器做角色校验。比如管理员的接口只有 ADMIN 角色能访问,医生的接口只有 DOCTOR 角色能访问。前期所有接口先统一加上这个校验逻辑,后面再加新接口的时候就不会漏。

3.4 自动填充与逻辑删除

MyBatis-Plus 的自动填充功能非常实用。create_time、update_time 这种字段不需要每个 Service 手动赋值,配置一个 MetaObjectHandler 就能自动处理:

@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } }

实体类字段上加上对应的注解:

@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;

逻辑删除配置更简单,实体上加一个逻辑删除注解:

@TableLogic private Integer deleted;

以后删除操作会自动变成 UPDATE deleted = 1,查询时自动过滤已删除的数据。这步一定要确保每张表都有 deleted 字段,而且类型一致。我见过不少项目只在部分表加了逻辑删除,结果联调时数据莫名其妙“消失”或“出现”,排查半天才发现是逻辑删除不一致导致的。

4. Vue3 前端搭建与前后端联调

4.1 Vite + Vue3 + Element Plus 项目骨架

前端我用的 Vite 作为构建工具,创建项目很简单:

npm create vite@latest hospital-front -- --template vue

Vite 启动速度非常快,开发体验比 Webpack 时代好太多了。项目结构我习惯分成几个目录:

src/ api/ // 接口请求封装 assets/ // 静态资源 components/ // 公共组件 router/ // 路由配置 stores/ // Pinia 状态 views/ // 页面视图

UI 组件库选的 Element Plus,引入方式我建议中小型项目直接全量引入,省心:

import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import App from './App.vue' const app = createApp(App) app.use(ElementPlus) app.mount('#app')

很多新手在引入 Element Plus 时报过“组件不生效”的错,基本都是因为忘记app.use(ElementPlus),或者只引入了 JS 没引入 CSS。这里先把这个坑标记出来,后面还会详细展开。

4.2 axios 请求封装的几个关键点

前端请求肯定绕不开 axios。我在 api 目录下维护一个 request.js 统一封装:

import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL, timeout: 15000 }) // 请求拦截器:带上 token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = `Bearer ${token}` } return config }) // 响应拦截器:统一处理业务状态码 request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res.data } ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message || '请求失败')) }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } else { ElMessage.error('网络异常,请稍后重试') } return Promise.reject(error) } ) export default request

这套封装的核心价值是:业务代码里只管拿到数据,不用每次判断状态码、不用反复处理 token 和错误提示。

开发环境联调时,最头疼的是跨域问题。我在前端项目里通过 Vite 的 server 配置做了地址映射,把以 /api 开头的请求转发到后端服务地址,这样前端代码里请求路径只需要写/api/auth/login这种相对路径,浏览器不会产生跨域拦截。部署上线后,同样的逻辑交给 Nginx 处理即可。

4.3 核心页面的实现思路

再讲几个关键页面的落地思路。

科室列表页:从后端拉科室列表,用卡片或列表形式渲染。每个科室卡片展示科室名称、简介、医生数量,点击后跳转到该科室的医生列表页。这是一个非常典型的“列表 → 详情 → 操作”模式。

医生列表页:按科室 ID 查询医生,展示医生头像、姓名、职称、擅长领域。点击某个医生后,进入医生详情页,可以看到该医生的排班表。

预约页是最核心的页面。排班表按日期展示,每一天对应上午、下午两个时段,显示剩余号数。实现思路是:

const schedules = ref([]) function loadSchedules(doctorId) { request.get(`/doctor/${doctorId}/schedules`).then(data => { schedules.value = data }) } function canAppoint(item) { return item.status === 1 && item.remainCount > 0 }

当前端判断剩余号数为 0 或排班已停用时,对应的预约按钮就置灰不可点。选好日期和时段后,点击预约按钮,后端创建一条预约记录,返回预约号。

个人中心页展示当前用户的预约列表,每条记录显示医生、科室、日期、时段、状态。如果是待就诊状态,用户可以取消预约,后端逻辑里会把对应排班的 remainCount 加回去。这个并发安全的细节要在 Service 层处理好。

4.4 日期校验和动态表单行的小技巧

预约日期校验是 Vue3 里很常见的一个需求。如果用 Element Plus 的日期选择器,规则可以这样写:

const rules = { appointmentDate: [ { required: true, message: '请选择预约日期', trigger: 'change' }, { validator: (rule, value, callback) => { if (value && dayjs(value).isBefore(dayjs(), 'day')) { callback(new Error('预约日期不能早于今天')) } else { callback() } }, trigger: 'change' } ] }

同时给日期选择器加上:disabled-date="disabledDate",把过去日期直接禁用掉,前端体验更好。

动态增删表单行在管理后台很常用,比如维护医生排班时批量添加日期。核心思路是维护一个数组:

const rows = ref([{ date: '', period: '', totalCount: 30 }]) function addRow() { rows.value.push({ date: '', period: '', totalCount: 30 }) } function removeRow(index) { if (rows.value.length > 1) { rows.value.splice(index, 1) } }

模板里用 v-for 遍历 rows,注意 key 不要直接用 index,因为删除中间行时会导致 Vue 的 diff 误判。最好加上一个自增的唯一字段做 key。

5. 常见问题与排查技巧实录

5.1 MySQL8.0 的安装与连接那些事

MySQL8.0 安装这块,很多人在 Windows 上用 zip 包安装时卡在初始化上。关键步骤是:

  1. 解压到指定目录。
  2. 在根目录创建 my.ini 配置文件,指定端口、字符集和数据目录。
  3. 用管理员身份打开命令行,执行mysqld --initialize-insecure,这个命令会生成一个密码为空的 root 用户。
  4. 执行mysqld -install安装为 Windows 服务,然后net start mysql启动。

如果你机器上有 Docker,也可以直接跑一个 MySQL8.0 容器,开发环境很省事:

docker run --name mysql8 \ -e MYSQL_ROOT_PASSWORD=root \ -p 3306:3306 \ -d mysql:8.0

不管哪种安装方式,SpringBoot 配置数据源时有两点是必踩的:第一,驱动类要写com.mysql.cj.jdbc.Driver,不是老的com.mysql.jdbc.Driver;第二,连接串里要带上时区和 SSL 处理参数:

useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

不带这些参数,启动时大概率会报时区错误或者 SSL 连接警告。allowPublicKeyRetrieval 这个参数是 MySQL8.0 默认认证插件缓存 SHA-2 加密带来的,开发环境必须加上,不然连接会失败。

5.2 后端连接 MySQL8 的常见报错一览

这里整理一个速查表,是我在联调时反复踩过的问题:

报错现象原因解决办法
Access denied for user 'root'@'localhost'密码错误或加密插件不匹配检查密码;确认 root 使用 mysql_native_password 或缓存 SHA-2 认证
The server time zone value is unrecognized连接串未指定时区URL 加 serverTimezone=Asia/Shanghai
Public Key Retrieval is not allowedMySQL8 默认认证插件需要允许公钥检索URL 加 allowPublicKeyRetrieval=true
Unknown database 'hospital_db'数据库未创建先执行 init_db.sql 创建数据库
Table doesn't exist表结构未初始化或表前缀不一致检查表名是否拼接了前缀,核对 SQL 脚本

这些报错信息在搜索引擎里一抓一大把,但真正自己遇到时,光看日志可能还是会懵。我的建议是:先把连接串一步步拆开看,排除掉 URL 参数的问题,再查数据库和账号权限。

5.3 Vue3 与 Element Plus 的经典连环坑

Element Plus 组件“不生效”通常有三种情况:

第一种,没有调用app.use(ElementPlus)。这会导致组件注册失败,模板里写了<el-button>但渲染不出来。

第二种,只引入了组件库的 JS,没有引入 CSS 文件。按钮能渲染,但完全没样式,白板一样。

第三种,按需加载的插件配置不对。如果你用unplugin-auto-import和unplugin-vue-components做自动引入,需要在 vite.config 里配置 ElementPlusResolver,并且确保相关插件安装在 devDependencies 里。一旦漏装或版本不匹配,就会出现“组件导出失败”的报错。

还有一个很有迷惑性的问题:日期选择器的弹出层样式丢失,或者弹层出现在错误位置。这通常是 popup 挂载节点的问题,给 el-date-picker 加上:teleported="false",或者检查弹层是否被父级容器的 overflow 属性裁剪了。

另外,如果 Vite 项目里写了<script setup>,要记得 setup 语法下的组件不需要额外注册,但引入的变量可以在模板中直接用,这是 Vue3 最舒服的地方,也是很多从 Vue2 转过来的人需要适应的习惯变化。

5.4 接口联调时参数接收不匹配

前后端联调里最常见的 bug 就是参数传了但后端收不到。核心原因基本只有一个:Content-Type 不一致。

axios 默认发送的是application/json,后端接收时必须用@RequestBody:

@PostMapping("/login") public Result<String> login(@RequestBody LoginRequest request) { // 处理登录 }

如果后端用@RequestParam接收,而前端传的是 JSON 字符串,自然就报参数缺失。反过来,如果前端用 URL 编码格式传参,后端却用 @RequestBody,同样收不到。

排查技巧很简单:打开浏览器开发者工具的 Network 面板,点击请求,查看 Request Headers 里的 Content-Type,再查看 Request Payload 的格式,一眼就能判断到底哪边不对。我见过太多人来回改代码,结果只是请求格式不匹配,这种问题往往浪费了一个小时。

6. 配套文档的组织与后续扩展方向

6.1 一套合格的开发文档应该包含什么

这套源码标了【含文档】,我建议你拿到文档后先看这几个部分:

  • 项目说明文档:写清楚项目背景、技术栈、功能模块列表。
  • 数据库设计文档:包含 ER 图、表结构说明、字段字典,最好把每个状态枚举值的含义都写出来。
  • 接口文档:列出每个接口的请求方式、路径、请求参数、返回结果。
  • 部署文档:从环境准备、数据库初始化、后端启动到前端打包部署的完整步骤。

其中数据库设计文档是最值得花时间维护的。很多项目代码写完了,数据库文档还是空的,答辩或交接时根本说不清楚表之间的关联。我在这个项目里会用工具把 ER 图导出来放到文档里,哪怕只是截图,效果也比一长串 SQL 好得多。

6.2 这个项目还能扩展哪些业务方向

基础预约挂号功能落地后,扩展方向其实很多,而且每个方向都能独立成为一个新模块:

  • 在线支付:预约成功后跳转支付页面,对接主流的支付服务。
  • 消息通知:预约成功、就诊提醒,接入短信或邮件服务。
  • 健康资讯与饮食推荐:医院网站一般都有健康科普栏目,可以做一个依据患者标签推荐饮食内容的模块,这个方向如果做成毕业设计会很有亮点。
  • 数据统计:管理后台增加预约量趋势、科室排行、医生工作量统计,配合 ECharts 做可视化大屏。

我一直觉得,中小型项目的价值不在于“大”,而在于“完整”。如果这个源码能帮你把全栈开发的整套流程走通,后面无论加什么业务模块,都是水到渠成的事。

最后再分享一个我自己的习惯:拿到这样的项目源码,第一件事不是在 IDEA 里点运行,而是先在全新环境里把 SQL 脚本完整执行一遍,再按文档从零启动后端和前端。这个“干净环境验证”的流程做过一次之后,你对整个项目的依赖关系、启动顺序和配置项都会非常有数,后面排错会轻松一半。

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

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

立即咨询