ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot2+Vue3+MyBatis-Plus医院网站全栈项目实战解析

SpringBoot2+Vue3+MyBatis-Plus医院网站全栈项目实战解析 最近我把一套 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 业务闭环从注册到预约再到就诊做数据库设计之前我建议先把业务闭环在纸上画一遍。不需要画得很复杂但要把数据流向理清楚。拿预约挂号来说整个流程是这样的患者注册账号并登录。患者按科室找到医生查看医生的排班日期和剩余号源。患者选择一个时间段提交预约系统创建一条预约记录。患者可以在个人中心看到预约状态比如“待就诊”、“已完成”、“已取消”。医生登录后按日期查看自己的排班和预约患者就诊完成后把状态改成“已完成”。这个闭环里最核心的数据实体是用户患者/医生、科室、医生信息、排班、预约记录。所有的表设计都围绕这几个实体展开其他的公告、管理员这些都属于辅助数据。2.2 表结构设计的心得数据库用的 MySQL8.0字符集我统一用了 utf8mb4排序规则选 utf8mb4_general_ci。8.0 对 utf8mb4 的支持已经很完善了emoji 表情这种特殊字符也能正常存储。核心表大概有这么几张表名用途核心字段sys_user用户表包含患者和医生账号id、username、password、real_name、phone、role、status、deletedhos_department科室表id、name、description、sort、statushos_doctor医生信息表id、user_id、department_id、title、intro、avatar、statushos_schedule医生排班表id、doctor_id、schedule_date、period、total_count、remain_count、statushos_appointment预约挂号表id、user_id、schedule_id、appointment_no、status、create_timehos_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 ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }前端只看 code 字段就知道请求是否成功无需每次解析 HTTP 状态码。同时还有一个全局异常处理器把业务异常和系统异常统一拦截避免把堆栈信息直接抛给前端。这些细节虽然看起来琐碎但决定了前后端联调时到底是从容还是手忙脚乱。3.2 MyBatis-Plus 高效操作的几个关键姿势MyBatis-Plus 最香的地方就是单表 CRUD 几乎不需要写 SQL。我所有 Mapper 都继承 BaseMapperpublic interface DoctorMapper extends BaseMapperDoctor { }然后在 Service 里配合 IService 和 ServiceImpl基础的增删改查方法直接就齐了。真正提升开发效率的是 LambdaQueryWrapper比如按科室查医生列表ListDoctor doctors doctorService.list( new LambdaQueryWrapperDoctor() .eq(Doctor::getDepartmentId, departmentId) .eq(Doctor::getStatus, 1) .orderByAsc(Doctor::getSort) );这种写法的好处是类型安全不会因为字段名写错而悄悄出 bug。相比原生 MyBatis 手写动态 SQL代码量至少减少一半以上。还有一个细节必须提批量插入。MyBatis-Plus 提供 saveBatch 方法但如果你在 JDBC 连接串里没有加rewriteBatchedStatementstrue那这个“批量”其实是逐条执行的。我在初始化排班数据时一次性要生成几百上千条排班记录最开始没加这个参数跑得特别慢加了之后速度直接上一个台阶。这是很容易踩的坑。jdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltruerewriteBatchedStatementstrue3.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 vitelatest hospital-front -- --template vueVite 启动速度非常快开发体验比 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-datedisabledDate把过去日期直接禁用掉前端体验更好。动态增删表单行在管理后台很常用比如维护医生排班时批量添加日期。核心思路是维护一个数组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 包安装时卡在初始化上。关键步骤是解压到指定目录。在根目录创建 my.ini 配置文件指定端口、字符集和数据目录。用管理员身份打开命令行执行mysqld --initialize-insecure这个命令会生成一个密码为空的 root 用户。执行mysqld -install安装为 Windows 服务然后net start mysql启动。如果你机器上有 Docker也可以直接跑一个 MySQL8.0 容器开发环境很省事docker run --name mysql8 \ -e MYSQL_ROOT_PASSWORDroot \ -p 3306:3306 \ -d mysql:8.0不管哪种安装方式SpringBoot 配置数据源时有两点是必踩的第一驱动类要写com.mysql.cj.jdbc.Driver不是老的com.mysql.jdbc.Driver第二连接串里要带上时区和 SSL 处理参数useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue不带这些参数启动时大概率会报时区错误或者 SSL 连接警告。allowPublicKeyRetrieval 这个参数是 MySQL8.0 默认认证插件缓存 SHA-2 加密带来的开发环境必须加上不然连接会失败。5.2 后端连接 MySQL8 的常见报错一览这里整理一个速查表是我在联调时反复踩过的问题报错现象原因解决办法Access denied for user rootlocalhost密码错误或加密插件不匹配检查密码确认 root 使用 mysql_native_password 或缓存 SHA-2 认证The server time zone value is unrecognized连接串未指定时区URL 加 serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowedMySQL8 默认认证插件需要允许公钥检索URL 加 allowPublicKeyRetrievaltrueUnknown database hospital_db数据库未创建先执行 init_db.sql 创建数据库Table doesnt 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 加上:teleportedfalse或者检查弹层是否被父级容器的 overflow 属性裁剪了。另外如果 Vite 项目里写了script setup要记得 setup 语法下的组件不需要额外注册但引入的变量可以在模板中直接用这是 Vue3 最舒服的地方也是很多从 Vue2 转过来的人需要适应的习惯变化。5.4 接口联调时参数接收不匹配前后端联调里最常见的 bug 就是参数传了但后端收不到。核心原因基本只有一个Content-Type 不一致。axios 默认发送的是application/json后端接收时必须用RequestBodyPostMapping(/login) public ResultString 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 脚本完整执行一遍再按文档从零启动后端和前端。这个“干净环境验证”的流程做过一次之后你对整个项目的依赖关系、启动顺序和配置项都会非常有数后面排错会轻松一半。
返回列表