首页 / 资讯中心 / 文章详情

SpringBoot+Vue+MyBatis+MySQL:中小型医院网站系统全栈开发实战

SpringBoot+Vue+MyBatis+MySQL:中小型医院网站系统全栈开发实战 ★ FEATURED ARTICLE
先说明一下这种“中小型医院网站系统”的项目我在实际开发里接过不止一次。别误会不是真的给医院建核心HIS系统而是很多高校实训、毕业设计、以及一些小型诊所或私立机构的对外门户站需求都是这一挂能展示科室和医生、能挂号和预约、有简单的后台管理。技术栈固定得很死SpringBoot Vue MyBatis MySQL前后端分离要完整源码要能跑起来。这篇文章就把我从零搭这套系统的完整思路、表结构设计、核心接口实现、前端联调细节、以及部署时容易踩的坑全部捋一遍。项目不大但五脏俱全搞懂这套同类管理系统你基本都能拿下来。1. 需求定位与功能模块梳理1.1 中小型医院网站到底要做什么很多人一上来就把项目想复杂了一个门诊部级别的医院网站硬要往三甲医院综合信息系统上靠结果前后端代码堆了一堆没用的功能最后演示时连核心流程都跑不通。我做了几个这类项目之后总结下来的经验是中小型医院网站系统的核心价值就三个词——信息展示、预约挂号、后台管理。信息展示是门面包括医院简介、科室介绍、医生排班、健康科普文章。用户进来先看这个觉得靠谱才会用下一步的预约功能。预约挂号是业务核心用户选科室、选医生、选时间段提交预约单后台能够审核或者自动通过。后台管理是运营支撑管理员维护科室、医生、排班、公告、预约记录得能统计每日预约量。我建议项目迭代上先做基础版前台用户端访客可浏览注册登录后预约 管理后台管理员维护基础数据、查看预约。如果你的项目要求带简单问诊或者在线咨询那是二期扩展别塞进基础版里不然后端接口和前端页面都会失控。说白了这套系统本质就是一个“内容管理 预约业务”的复合型Web应用。核心难点不在业务复杂度而在前后端数据交互的一致性、预约排班的边界条件、以及权限控制是否干净。1.2 角色划分与权限设计权限这个东西很多学生项目做得一团糟本质原因是角色和接口没有对齐。我习惯先定义清楚三类角色未登录访客ROLE_GUEST可以浏览科室、医生、公告但不能提交预约。注册用户ROLE_PATIENT登录后可以提交预约、查看自己的预约记录、取消未接诊的预约。管理员ROLE_ADMIN维护科室、医生、排班、公告审核或查询所有预约。权限控制在后端做前端按钮隐藏在安全上没有任何意义。我的做法是Spring Security JWT接口上直接标PreAuthorize(hasRole(ADMIN))之类的注解角色定义就放在 JWT 的 authorities 字段里。访客和患者的公共接口用permitAll涉及预约写操作的接口必须要求登录管理端接口单独收敛到/api/admin/**路径下统一拦截校验。前端路由上也要做对应处理Vue Router 里配置全局守卫根据本地存的 token 和角色信息决定能否进入后台页面。导航菜单按照角色动态渲染比如未登录只显示“登录/注册”入口患者登录后显示“我的预约”管理员登录后显示“后台管理”。前后端双重控制这才是能上线交付的样子。2. 技术选型与项目搭建2.1 为什么是这套技术栈SpringBoot 的社区生态和自动配置能力能让你在 10 分钟内拉起一个可运行的 Web 服务内嵌 Tomcat部署只需要一个 jar 包省去独立配置服务器的繁琐步骤。Vue 对前端组件化和状态管理的支撑很成熟配合 Element Plus 组件库后台管理页面几乎是拼积木一样搭出来的。MyBatis 虽然不是最轻量的但胜在 SQL 可控性强复杂多表联查时直接手写 SQL排除问题时一眼能看到底。MySQL 则中规中矩免费稳定中小型项目没有任何性能瓶颈。这套组合最舒服的地方在于团队里每个人都能找到自己熟悉的部分Java 后端、JavaScript 前端、SQL 数据三层各司其职。如果换成一套全栈框架反而会模糊前后端边界不利于项目演示和后续维护。2.2 工程结构规划后端工程我习惯按模块分包而不是按技术类型分包。对比一下很多项目是 controller、service、mapper 三个包平铺结构是com.hospital.demo ├── controller ├── service ├── mapper这在小项目里还能看一旦功能多了controller 栈会比较混乱比如预约相关接口和用户相关接口都挤在一堆文件里。我更推荐按业务域分包com.hospital.system ├── common # 返回结果封装、异常处理、工具类 ├── config # 安全配置、CORS配置、MyBatis配置 ├── modules │ ├── auth # 登录注册、JWT处理 │ ├── user # 患者用户信息 │ ├── department # 科室 │ ├── doctor # 医生与排班 │ ├── appointment # 预约挂号的业务逻辑 │ └── announcement每个 module 内部再按 controller / service / mapper / entity 分层。这样定位问题时路径清晰比如预约时间冲突问题直接钻到modules/appointment里找 service不用在一堆平铺文件里翻。前端我更建议用 Vue CLI 或者 Vite 创建单页应用目录结构按 views、components、router、store、api 划分。核心原则是组件只负责渲染和交互业务数据请求全部收敛到api/目录里封装成独立函数避免在组件里散落一堆axios.get。2.3 环境版本建议我踩过一个典型坑某台机器上 Java 版本过新SpringBoot 用 2.x结果 JWT 库和某些注解不兼容编译报错报了半天。版本这块先问清楚项目要求没有强制要求的话我这套组合实测最稳JDK 1.8 或者 11SpringBoot 2.7.xVue 2.6.14 或 Vue 3.2我倾向 Vue 3 Vite组件写法更舒服Element Plus配 Vue 3或 Element UI配 Vue 2MyBatis 2.2.x配合 SpringBoot 官方 starterMySQL 8.0注意 MyBatis 不是 MyBatis-Plus虽然 Plus 在开发效率上更高但题意指定的是 MyBatis。如果你确实想省事可以引入 MyBatis-Plus 的通用 CRUD 能力不过项目报告里要说明你用的其实是 MyBatis 体系避免指导教师或评审追问时对不上。3. 数据库设计与核心表结构3.1 设计思路中小型医院网站的数据量不会很大表结构不需要过度设计但有几张表的边界条件必须想明白否则后面写业务逻辑会痛苦。我规划了 7 张核心表每一张都能回答一个具体的业务问题用户表谁能登录系统科室表医院有哪些诊疗方向医生表每个科室有哪些医生排班表医生在哪个时间段出诊预约表患者约了哪个医生哪个时间段公告表首页展示什么通知评论/留言表可选患者对就诊体验的反馈。实际项目中我把用户表设计成统一账号表。管理员和普通患者都用同一张 user 表用 role 字段区分。表结构清晰登录时只查一张表不用去区分患者表和员工表两张表。3.2 核心表结构说明先看用户表CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, gender tinyint DEFAULT NULL COMMENT 0未知 1男 2女, phone varchar(20) DEFAULT NULL, id_card varchar(18) DEFAULT NULL COMMENT 身份证号可选, role varchar(20) NOT NULL DEFAULT PATIENT COMMENT ADMIN/PATIENT, status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表;密码字段必须存加密后的数据用 BCrypt。明文密码是最低级的错误实训项目里如果被问到安全问题这也能证明你考虑过防护。科室表CREATE TABLE department ( id bigint NOT NULL AUTO_INCREMENT, dept_name varchar(100) NOT NULL, intro text COMMENT 科室介绍, location varchar(100) COMMENT 科室位置, sort_order int DEFAULT 0, status tinyint DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT科室表;医生表与排班表是预约功能的基础我把排班设计成一句话排班记录的是一个医生在某天的某个午别上午/下午是否有号、剩余多少号。医生表关联科室排班表关联医生和时间。预约表是整个系统最关键的表CREATE TABLE appointment ( id bigint NOT NULL AUTO_INCREMENT, user_id bigint NOT NULL COMMENT 预约人ID, doctor_id bigint NOT NULL, schedule_id bigint NOT NULL COMMENT 排班ID, appointment_date date NOT NULL, time_slot varchar(10) NOT NULL COMMENT MORNING/AFTERNOON, status tinyint NOT NULL DEFAULT 0 COMMENT 0待接诊 1已完成 2已取消, remark varchar(255) DEFAULT NULL COMMENT 病情描述, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约挂号表;注意这里我把appointment_date和time_slot冗余到了预约表里。虽然通过 schedule_id 可以关联查出来但冗余存储有两个好处一是业务上展示很方便查预约记录时不需要每一条都去连接排班表二是排班表后续调整时预约记录不受影响。3.3 唯一索引与并发控制预约表的(schedule_id, appointment_date, time_slot, user_id)要加唯一索引避免同一个人重复提交同一个时段的预约。这个唯一索引比在 Service 里手工判断要可靠得多因为高并发场景下两个请求同时查到没预约过然后同时插入Service 判断防不住但数据库唯一索引能挡住。类似这种场景的例子是抢课、抢号。医院预约就是典型的有限并发场景数据库层面的兜底必不可少。4. 后端核心功能实现要点4.1 统一返回结构与全局异常处理前后端分离项目必须定义一个统一的返回格式。我习惯下面这种{ code: 200, message: success, data: {...} }对应的 Java 类Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }同时配合RestControllerAdvice做全局异常拦截。业务抛异常、参数校验失败、认证失败统一转换成 Result 结构返回前端不管遇到什么问题解析的都是同一个 JSON 结构联调时不用到处找字段名。4.2 JWT认证流程登录接口流程是这样的接收用户名密码查库用 BCrypt 校验密码通过后生成 JWT 令牌返回给前端。前端把 token 存在 localStorage 或 pinia/vuex 状态里之后每个请求在 axios 拦截器中自动带上Authorization: Bearer token。后端通过 OncePerRequestFilter 过滤器对每个请求做 token 解析和校验把用户信息塞进 SecurityContext后续接口通过AuthenticationPrincipal或者 SecurityContextHolder 获取当前用户信息。关键代码String authHeader request.getHeader(Authorization); if (authHeader ! null authHeader.startsWith(Bearer )) { String token authHeader.substring(7); try { Claims claims Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); String username claims.getSubject(); // 封装并存储到 SecurityContext } catch (Exception e) { // token 无效不设置认证信息后续权限拦截自然会拒绝 } }我建议 token 有效期设置成 2 到 4 小时别太长。别人演示时不会在意这种细节但你项目文档里如果能写清楚令牌过期时间、刷新策略这是加分项。4.3 预约接口的核心逻辑预约提交是整个系统最核心的接口逻辑有几层要堵查出排班记录确认状态是启用中的。校验排班日期是否大于等于今天过去的日期不能预约。检查剩余号数大于 0 才能预约。插入预约记录同时扣减排班表的剩余号数。如果已经约满返回友好提示“该时段已约满”。这里有个事务问题扣减号数和插入预约记录必须放在同一个事务里。我是用Transactional注解包住整个方法然后做乐观锁控制。Transactional(rollbackFor Exception.class) public Appointment createAppointment(Long scheduleId, String remark) { DoctorSchedule schedule scheduleMapper.selectByIdForUpdate(scheduleId); if (schedule.getRemaining() 0) { throw new BusinessException(该时段已约满); } schedule.setRemaining(schedule.getRemaining() - 1); scheduleMapper.updateById(schedule); // 构建预约记录并插入 }selectByIdForUpdate是SELECT ... FOR UPDATE的行级锁把排班记录锁住。这样两个并发请求同时进来时第二个只能等第一个事务提交后才能拿到排班记录此时剩余号数已经被扣减再判断就会返回约满。这是解决并发预约的关键手段。4.4 MyBatis多表查询的写法MyBatis 的设计哲学是写 SQL。预约列表接口需要展示科室、医生、日期、时间段这时候连接查询比多张表分开查再组装要高效代码也直观。典型 Mapper XMLselect idselectAppointmentDetail resultTypecom.hospital.system.modules.appointment.vo.AppointmentVO SELECT a.id, a.appointment_date, a.time_slot, a.status, a.remark, d.dept_name, doc.name AS doctor_name, doc.title AS doctor_title FROM appointment a LEFT JOIN doctor doc ON a.doctor_id doc.id LEFT JOIN department d ON doc.dept_id d.id WHERE a.id #{id} /selectVO视图对象专门用来承接这种联表查询结果尽量别把数据库实体直接吐给前端。理由是前端不关心你的表字段结构只关心页面需要展示什么。比如 user 实体里有密码字段如果你直接返回实体前端虽然拿不到要是写了敏感字段的 getter但一旦序列化疏忽密码字段就漏出去了。安全习惯要从这个层面就养起来。5. 前端页面开发与接口联调5.1 路由与页面结构规划前端路由分成两块访客/患者端和管理端。我按业务设计的页面首页/轮播图、医院简介、公告列表、快捷挂号入口。科室列表/departments卡片式展示科室点进去看到科室和该科室医生。医生详情/doctor/:id医生简介、排班表、预约按钮。登录/login、注册/register注册即患者角色。个人中心/my-appointments当前登录患者的预约记录可取消操作。管理后台/admin仪表盘、科室管理、医生管理、排班管理、预约管理、公告管理。路由守卫实现这样router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path.startsWith(/admin)) { const role localStorage.getItem(role); if (token role ADMIN) next(); else next(/login); } else if (to.path /my-appointments !token) { next(/login); } else { next(); } });5.2 axios封装与请求拦截前端请求层我封装了一个统一的 request 模块导出各业务模块对应的 API 函数。axios 拦截器里做两件事请求时带 token响应时统一处理 code。request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer token; } return config; }); request.interceptors.response.use( res { const { code, message, data } res.data; if (code 200) { return data; } else if (code 401) { localStorage.clear(); router.push(/login); return Promise.reject(new Error(message)); } else { ElMessage.error(message); return Promise.reject(new Error(message)); } }, err Promise.reject(err) );这里要注意401 统一跳登录页面是个技术关键处理。如果不做token 过期之后用户操作没有任何反应页面看起来像卡住了一样。5.3 预约表单的前后交互逻辑前端预约表单有排班选择。点击某个医生排班后前端要拿到该排班的剩余号数实时显示“剩余 12 人”。如果剩余为 0禁止提交。这个判断背后依赖的是后端接口/api/schedule/listByDoctorAndDate。我处理的方式是医生详情页初始加载时调用后端子啊接口拉取该医生 7 天内的排班前端以卡片横排展示。选中某天的排班后下方显示预约按钮若当天已约满按钮置灰并显示“约满”。患者确认提交后写 remark 病情描述提交到/api/appointment接口。前端拿到的数据是展示层模型前端不做业务判断严格要求后端校验排班时间、剩余号数、重复预约等多个边界条件。前后端分离的设计原则是前端负责“展示对不对”后端负责“业务能不能做”。5.4 ECharts图表在管理后台的运用管理后台的仪表盘我一般展示三块数据今日预约量、各科室预约占比饼图、近一周每日预约趋势柱状图。用 ECharts 实现不复杂但关键在于给后端提供对应的统计接口。后端统计接口是一个典型的练习点近七日的每日预约量SQL 里GROUP BY appointment_date即可。各科室预约占比需要连接科室、医生、预约三张表。这个接口返回的格式要和 ECharts 的数据结构对齐比如饼图需要{ name: 内科, value: 35 }后端可以直接构造 List返回前端无需二次加工。这类统计功能虽然业务逻辑简单但把 SQL 聚合、Map 构造、图表数据绑定这几个环节都打通了整个项目的完整度会明显提升。6. 部署流程与常见问题排查6.1 本地快速启动指南拿到源码后的部署流程我提供一个实操过的顺序。假设你用的是 IDEA 命令行前端工具。后端启动创建本地数据库hospital_web执行sql/init.sql脚本初始化表和默认数据。修改application.yml的数据库连接改成你自己的账号密码。运行mvn spring-boot:run默认端口 8080。默认数据里我会预置一个管理员账号admin / admin123一个患者测试账号patient / 123456。演示时直接能登录不用手动注册。前端启动进入frontend/目录执行npm install。npm run serve默认端口 5173Vite或 8081Vue CLI。浏览器访问前端地址用测试账号登录。如果前端接口连不上后端优先检查前端.env文件里的VITE_API_BASE_URL确保代理配置指向http://localhost:8080。Vite 开发服务器配置代理server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/login时会自动转发到后端的http://localhost:8080/api/login不需要跨域配置也能工作。生产环境下Nginx 反向代理也是同样原理。6.2 前后端跨域配置很多初学前后端分离的人在本地联调时遇到跨域乱加一通 header 解决不了我来给一个最可靠的两个位置解决方案后端明确开启全局 CORS 配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }但注意如果你已经用了 Spring SecurityCORS 配置必须生效于 Security 过滤器链之前。所以安全配置里要加http.cors()只是WebMvcConfigurer配置有可能被 Security 层的拦截器挡住OPTIONS 预检请求直接被 401 拦截掉。解决方法是安全配置中放行 OPTIONS 请求。这个坑非常经典。如果你使用了 Vite 或 Vue CLI 开发服务器代理那么前端请求和后端同源跨域问题根本不存在。生产环境由 Nginx 反向代理同样不存在跨域。所以我个人浅见后端 CORS 配置是兜底方案首选是代理。6.3 常见错误清单我把做这类项目过程中遇到频率最高的错误列成一张表你可以直接对照排查错误现象可能原因解决方案前端请求报 401页面反复跳登录JWT 过期、token 没带上、Security 放行了错误路径检查后端放行名单是否包含 /api/login、检查前端拦截器是否带 token登录时报“密码错误”但密码明明是 admin123默认数据脚本里的密码没有经过 BCrypt 加密直接去数据库跑一个 BCrypt 加密工具生成密文或者用注册接口注册一个新管理员预约接口返回 500控制台报“数据被截断”枚举值或长度超限检查 status 字段类型确保用 tinyint 映射 0/1/2页面能出数据但图片全部裂开上传的图片路径是相对路径跨域或代理后失效统一用绝对 URL 组装图片路径生产环境配合 Nginx 设置静态资源路由部署到服务器后接口能通但访问静态资源 404打包后的 dist 目录路径问题Nginx 里配置root /your_project/distlocation 指向准确路径try_files $uri /index.html顺带说一个特别常见的坑SpringBoot 多模块项目里如果 resources 目录下没有mapper文件夹或者 Mapper XML 文件没有被 Maven 打包进 jar运行时 MyBatis 会报Invalid bound statement (not found)。解决方案是在 pom.xml 的 build 插件里配置 resources 包含 XML 文件路径或者把 Mapper XML 放在 resources/mapper 目录下保证打包后 classes 里有对应文件。6.4 Nginx部署实战生产环境部署我通常不用 jar 直接裸奔而是前端打包后交由 Nginx 托管后端 jar 通过 systemd 守护进程方式运行。后端 jar 启动nohup java -jar hospital-system-1.0.0.jar --server.port8080 app.log 21 Nginx 配置同时处理前端静态资源和 API 反向代理server { listen 80; server_name your-domain.com; root /usr/share/nginx/hospital-frontend; index index.html; # 前端路由 history 模式 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里try_files $uri $uri/ /index.html是 Vue Router history 模式的关键不加这行直接访问/my-appointments就会 404。还有一个小细节是数据库连接池配置。默认的 HikariCP 在长时间空闲后MySQL 可能会断开空闲连接报Communications link failure。在application.yml里加spring: datasource: hikari: connection-test-query: SELECT 1 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000这就是保活设置。很多项目跑到第二天突然接口全部超时大概率就是这个原因。7. 项目扩展与进阶心得7.1 可扩展的方向基础功能做完后这套系统往上扩展的空间很大。一个方向是引入 Redis 缓存。目前科室、医生列表这类热点数据每次请求都打 MySQL如果访问量上来压力会逐步增加。引入 Redis 做缓存把科室列表、医生列表先缓存预约时再走数据库强一致操作。另一个方向是增加消息推送。患者预约成功、预约前一天提醒这类需求可以通过接入简单消息服务实现。不过如果是高校项目不建议在这个方向耗费太多时间——评估要求里没有这项价值不大。把基础功能做扎实、写清楚比堆功能更能拿高分。至于在线支付功能我劝你别在基础版里加。支付涉及商户号申请和真实交易流程项目演示时引入复杂度远大于收益。如果导师非要求有支付环节你做一个“线下到付”状态标记即可预约成功显示“请到院支付挂号费”。7.2 代码规范与项目文档项目文档的重要性很多时候被团队低估了。代码写得再好如果 README 里没有启动步骤数据库脚本没给全部署说明一团糊演示的时候手忙脚乱印象分还是会打折扣。我惯用的 README 骨架是项目简介与角色说明。技术栈列表。数据库初始化步骤。后端启动步骤。前端启动步骤。默认账号。接口文档简表路径、方法、鉴权要求。常见问题启动失败、连不上数据库等。接口文档不一定要用 Swagger但可以引入SpringBoot 集成 springdoc 很轻松。访问/swagger-ui.html就能看到在线接口列表。这个对评审、对自测、对前后端联调都非常有用强烈建议加。7.3 一点个人建议最后分享一条我在这类项目上反复体会到的经验功能宁可少而稳不要多而崩。评审老师或者客户在演示时更看重的是核心流程能不能从头到尾走通以及你对自己代码逻辑的理解程度。与其堆一堆花哨功能但到处都是坑不如把挂号预约这条主链路做深做透把事务、并发、权限这些细节处理干净。这套系统的表结构、接口设计、部署方式做完这一次你后面再做 CRM、教务、物资管理本质上就是换皮加改几个业务字段——底层套路都是相通的。
阅读完成 · 觉得有帮助?
咨询建站