
医院网站这类系统听起来简单真正落地才发现细节特别多。科室介绍、医生排班、预约挂号、停诊通知……功能不重但角色、状态、页面类型一大堆最忌讳的就是前后端各写各的到联调阶段才发现数据字段对不上。我前阵子帮一家区级医院整理官网和预约系统就把 SpringBoot Vue MyBatis MySQL 这条前后端分离路线完整走了一遍源码、数据库脚本、部署文档都整理成了可直接复用的模板。这篇文章不讲虚的从选型到建表、从接口到页面、从本地联调到打包部署全部按实际项目顺序展开。如果你是刚学完 Java 和 Vue 但缺少完整项目串联的同学或者做毕设需要一份规范可运行的前后端分离示例这份总结值得从头看到尾。1. 项目整体设计与技术选型思路1.1 为什么给中小型医院做网站要选前后端分离很多年前我做过不少服务端渲染的医院官网那时候 JSP 配合中间件一套模板打天下确实省事。但这类网站一旦加入预约挂号、个人中心、在线留言这些交互功能问题就出来了后端改了页面结构前端设计师的 HTML 经常被连带破坏运营人员想改个轮播图又要找开发重新走一遍发布流程。前后端分离的核心价值正是把“数据提供”和“页面呈现”拆成两个独立团队可以并行推进的部分。对中小型医院来说这种分离还有一个实际好处就诊业务系统挂号、排队、电子病历和门户宣传网站往往不是同一时期建设接口先定义好以后将来医院信息科要接入第三方小程序或者自助机前端换一套壳子就行后端基本不用动。反过来如果哪天首页要重新设计也不用触碰业务逻辑代码。前后端通过 HTTP 接口约定沟通职责边界清楚项目参与人多的时候尤其省心。1.2 技术栈分工四个核心组件各管什么这套系统的后端骨架是 SpringBoot。它内嵌容器、自动配置、依赖管理都很顺手本地启动就是一个独立进程调试效率比传统外置容器高不少。版本方面我用了 SpringBoot 2.7.x如果你本机是 JDK 8这个版本最稳妥团队里已经上了 JDK 17 的话直接换 SpringBoot 3.x 也没问题只是个别配置项的写法略有差异。前端选择 Vue。Vue 的学习曲线比很多重型框架友好模板语法直观组件化开发让科室页面、医生卡片、预约表单都能封装成独立组件复用。数据状态管理用简单的方式即可中小项目没必要一上来就搬一大堆状态容器把接口调用和页面展示解耦开反而更清楚。持久层我用 MyBatis。很多同学纠结为什么不直接用 MyBatis-Plus说实话在业务查询规则多、SQL 需要精细控制的中小项目中原生 MyBatis 的灵活性和可控性更强。它把 SQL 写在 Mapper XML 里离数据库近优化和排查都直观。MySQL 在这类系统中是标配8.0 版本自不必多说事务支持够用运维门槛也低。我整理了一张技术栈清单方便你对照自己的环境准备组件选型参考主要负责内容SpringBoot2.7.18 JDK 8接口服务、业务逻辑、身份校验Vue ViteVue 3 Vite 4门户页面、管理后台、前后端联调MyBatismybatis-spring-boot-starter 2.3.x数据库访问、SQL 映射、动态查询MySQL8.0InnoDB 引擎业务数据、账号信息、预约记录1.3 工程目录怎么组织才不乱项目源码通常可以分成三个顶层目录后端服务、前端工程、数据库脚本。后端按经典分层包名组织前端按页面视图划分数据库脚本单独放在 database 目录下避免和业务代码混在一起。具体到本项目我习惯这样放hospital-website/ backend/ src/main/java/com/hospital/ controller/ // 接口层 service/ // 业务逻辑层 mapper/ // MyBatis 数据访问接口 config/ // 拦截器、跨域等配置 common/ // 统一返回体、异常、工具类 src/main/resources/ mapper/ // Mapper XML 文件 application.yml // 后端主配置 pom.xml frontend/ src/ views/ // 按路由页面组织 components/ // 公共组件 router/ // Vue Router 配置 api/ // 请求封装 store/ // 状态管理 package.json database/ init.sql // 建库、建表、初始数据 docs/这种组织的关键点有两个一是后端不要把 SQL 散落在注解和代码里统一集中在 resources/mapper 目录改动时一眼能找到二是前端的 api 目录和后端 controller 的路径一一对应比如前端 api/doctor.js 对应后端 /api/doctor 下的接口联调时快速定位问题。2. 核心功能与数据库设计2.1 面向患者的业务模块梳理医院网站系统最核心的受众是两类人普通患者和医院运营人员。患者侧关心科室信息、医生出诊时间、预约挂号入口、公告动态运营侧要维护科室和医生数据、查看预约记录、处理停诊改期。因此功能模块设计成三个层次门户展示、预约服务、后台管理。门户展示包括首页轮播、医院简介、科室列表、医生介绍、新闻公告这些数据只读接口就可以完成访问压力不大。预约服务是整套系统的业务核心患者选择科室、医生、日期、时段提交姓名和联系方式后台生成一条预约记录。后台管理则面向值班人员包含登录、医生排班维护、预约状态处理、公告发布。2.2 核心表结构设计附 SQL 设计思路数据库设计我坚持一个原则把“描述性信息”和“业务状态信息”适当分开。科室、医生这类基础数据相对固定预约记录则不断增长混在一张表里后期查询会很吃力。下面是几个主要表的字段规划表名用途关键字段department科室信息name, intro, address, sort_nodoctor医生基础信息dept_id, name, title, intro, avatarappointment预约记录patient_name, patient_phone, doctor_id, appoint_date, statussys_user后台登录账号username, password, saltnotice新闻公告title, content, publish_time建表时我用 InnoDB 引擎、utf8mb4 字符集主键统一用 BIGINT 自增凡是涉及金额、状态、排序的字段都声明成合适的数值类型避免将来类型转换。预约状态我用数字表示比如 0 待确认、1 已确认、2 已完成、3 已取消这样扩展状态不需要改表结构。同时给 create_time、update_time 两个通用时间字段所有表都带上排查数据问题时非常重要。下面是 department 和 appointment 两张表的 SQL 示例其他表结构保持一致风格CREATE TABLE department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 科室名称, intro TEXT COMMENT 科室简介, address VARCHAR(200) COMMENT 楼层位置, sort_no INT DEFAULT 0 COMMENT 展示排序, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT科室表; CREATE TABLE appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, patient_name VARCHAR(50) NOT NULL COMMENT 患者姓名, patient_phone VARCHAR(20) NOT NULL COMMENT 联系电话, doctor_id BIGINT NOT NULL COMMENT 医生ID, appoint_date DATE NOT NULL COMMENT 预约日期, time_slot VARCHAR(30) COMMENT 时段上午/下午, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, remark VARCHAR(200) COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约记录表;2.3 接口返回体与分页约定前后端协作的第一件事就是统一接口协议否则前端每个页面都要单独处理状态码代码很难维护。本项目所有接口统一返回一个 JSON 结构{ code: 200, message: 操作成功, data: {} }业务成功 code 是 200参数校验失败返回 400未登录或者登录过期返回 401服务器内部错误返回 500。前端 axios 里只写一次响应拦截器所有异常统一弹提示页面代码只关注 data 部分。列表接口采用固定参数格式pageNum 表示页码、pageSize 表示每页条数返回结构里同时带 total 总数前端分页组件直接对接。3. 后端关键实现与 MyBatis 实战3.1 SpringBoot 配置文件的几个关注点后端启动第一步是配置数据源。application.yml 里数据库连接字符串有四个细节特别容易出问题地址端口、时区、编码、SSL 校验。MySQL 8 默认使用的认证插件和字符集策略和 5.7 不同如果不显式声明启动时经常报时区异常。我习惯这样写spring: datasource: url: jdbc:mysql://127.0.0.1:3306/hospital?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: truemap-underscore-to-camel-case 这个配置强烈建议打开。数据库字段像 patient_nameJava 属性是 patientName打开后 MyBatis 自动映射省去大量 resultMap 编写。有一点要提醒如果 SQL 查出来的是多表关联的字段比如医生表关联科室名称纯自动映射不一定覆盖全那个字段最好在 resultMap 中显式配置。3.2 MyBatis 映射文件与动态 SQL医院网站的列表页非常依赖动态查询医生列表要按照科室筛选预约记录要按照时间和状态过滤关键词还要支持模糊匹配。MyBatis 的where加if配合起来能避免拼 SQL 时到处是“11”这种写法。下面这段是医生分页查询的 Mapper XML我在实际项目里基本是模板化使用select idselectDoctorPage resultTypecom.hospital.dto.DoctorDTO SELECT d.*, dp.name AS deptName FROM doctor d LEFT JOIN department dp ON d.dept_id dp.id where if testkeyword ! null and keyword ! AND (d.name LIKE CONCAT(%, #{keyword}, %) OR dp.name LIKE CONCAT(%, #{keyword}, %)) /if if testdeptId ! null AND d.dept_id #{deptId} /if /where ORDER BY d.sort_no ASC LIMIT #{offset}, #{pageSize} /select这里有几个细节LIKE 查询用 CONCAT 拼接左模糊右模糊避免直接写%#{keyword}%可能出现的参数拼串问题LIMIT 直接在 SQL 中算好 offset 和 pageSize就不用额外引入分页插件减少依赖。分页插件虽然方便但如果报表类统计特别多插件自动 count 的 SQL 有时会脱离手工控制所以我在这类项目里宁可多写两句。3.3 登录认证与访问控制医院网站后台管理部分不能随便让人访问但又不适合引入完整的 Spring Security因为中小项目会显得重。我用一个 HandlerInterceptor 实现登录拦截配合浏览器端存储的会话令牌完成认证。这个选择的理由是调用链短、逻辑直观、依赖少扩展也容易。拦截器配置大致是对所有 /api/** 路径进行拦截放行登录接口、科室列表、医生列表这些无需登录的公开接口后端接口从请求头获取令牌并验证有效期无效则返回 401。前端拿到 401 后自动跳转登录页。实现代码量很少但能覆盖绝大多数场景。3.4 典型接口的代码走读以“查询某科室下的医生列表”为例完整的调用链是Controller 接收参数 → Service 准备查询条件 → Mapper 执行 SQL → 返回给前端。Controller 里我习惯保持轻薄不写业务判断只做参数接收和响应包装RestController RequestMapping(/api/doctor) public class DoctorController { Resource private DoctorService doctorService; GetMapping(/list) public ResultPageResultDoctorDTO list( RequestParam(required false) Long deptId, RequestParam(required false) String keyword, RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize) { return Result.ok(doctorService.pageQuery(deptId, keyword, pageNum, pageSize)); } }Service 层的核心任务是把页面参数转换成 Mapper 需要的查询条件。pageNum 转 offset 这一步我放在 Service 里避免前端传一个 offset 过来导致接口语义混乱。站在后台开发的视角看Controller 轻、Service 重、Mapper 纯 SQL这套流程看多了之后接新需求基本就走流程了。4. 前端 Vue 实现要点4.1 创建工程与基础配置前端用 Vite 创建工程快得多。执行 npm create vitelatest 选择 Vue 模板后把项目放进 frontend 目录。目录结构里我特意把 api 单独拆一层所有后端接口都收敛到 api 目录下页面组件不直接写请求地址。这样做的好处是后端接口路径一旦调整只需要改 api 目录里的常量。开发时第一件事是配置代理转发。Vite 配置文件里加入一段 server 配置让前端开发服务器把 /api 开头的请求送到后端端口本地联调就不存在跨域问题。注意这个过程只是开发环境里的便利方案生产环境走的是另一种部署策略这个后面讲部署时会说清楚。4.2 路由守卫与页面权限医院网站前端有两类页面患者访问的门户页面和后台运营的管理页面。前者不需要登录后者必须登录。Vue Router 通过路由元信息标记页面是否要求认证然后在全局前置守卫中统一判断。我习惯把每一页的标题也用 meta 管理切换路由时自动更新浏览器标签标题。一个简单的路由守卫示例router.beforeEach((to, from, next) { const token localStorage.getItem(hospital_token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })这里提醒一句前端路由守卫只能拦普通用户的操作真正的安全边界在后端接口。后台接口如果没做拦截绕过前端直接调接口一样可以访问数据所以前后端权限校验必须同时做不能只依赖一边。4.3 Axios 封装与跨域处理Axios 是前后端交互的公共通道。我封装出一个 request.js创建独立的 axios 实例统一设置基础路径、超时时间、请求头。请求拦截器把登录令牌带到每个请求上响应拦截器做统一状态码处理比如登录过期跳转登录页、业务错误弹出提示。经过这层封装页面里的接口调用通常就变得非常简洁。跨域问题要分开发和生产两个阶段理解。开发阶段通过 Vite 的转发配置就能解决前端页面访问的相对路径和后端服务路径不同源也不会报错。生产阶段则要看部署方式如果前端打包产物直接放进后端静态目录同源下完全没有跨域问题如果前后端分开部署就需要额外配置允许的跨域来源实际项目中我更推荐前者省心很多。5. 完整部署流程从本地到服务器5.1 首次部署前的环境清单很多同学下载了源码跑不起来八成不是代码问题而是环境不一致。我列一份本次部署需要的基础环境清单JDK 8 或以上、Maven 3.6、Node.js 16、MySQL 8.0。操作系统 Windows 和 Linux 都行但生产环境建议 Linux内存占用小进程管理方便。安装时候有个容易踩的坑JDK 版本和 Maven 版本兼容性。Maven 3.8 配 JDK 17 没问题但如果用的是特别老的 Maven 3.2建议升级否则解析 SpringBoot 依赖时会报一些看不懂的错误。Node 版本低于 16 时Vite 4 可能无法顺利安装依赖打包也会报错所以先确认环境版本。5.2 数据库初始化脚本的执行顺序database/init.sql 文件里把建库、建表、初始数据都写好了导入时注意顺序不能乱。先在命令行或者可视化工具里执行建库语句CREATE DATABASE IF NOT EXISTS hospital DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;再切换到 hospital 库执行建表语句和初始数据包括测试科室、示例医生和管理员账号。导入完成后可以查一下表数量确认不是零张表再继续避免遗漏某一段脚本。可视化工具我建议使用官方或开源的正规版本一些非官方渠道的绿色工具经常伴随各种兼容问题还会把数据误格式化非常不值得。5.3 方案一前端打包进 SpringBoot 的单体部署这是我最常用、也最适合中小型项目的部署方案。前端构建完成后把 dist 目录下的文件复制到后端 src/main/resources/static 目录再打包后端。这样后端 jar 包同时包含接口和页面部署时只需要启动一个 Java 进程。具体执行步骤cd frontend npm install npm run build构建完成后dist 目录里是 index.html 和 assets 静态文件。复制这些文件到后端 resources/static 目录然后执行后端打包cd backend mvn clean package -DskipTests java -jar target/hospital-web.jar访问 http://服务器IP:8080 就能直接打开网站。这个方案最大优势是运维成本极低不用同时维护两个服务缺点是前端静态资源更新必须重新打包后端适合更新频率不高的医院门户场景。5.4 方案二war 包放进 Tomcat如果医院内部已经存在 Tomcat 中间件运维团队更习惯用外置容器管理应用那就打成 war 包部署。需要调整两处地方pom.xml 里把打包方式改成 war并给内置 Tomcat 标记 provided避免和外置容器冲突启动类继承 SpringBootServletInitializer重写 configure 方法。调整完毕后执行 mvn clean package生成的 war 包放到 Tomcat 的 webapps 目录下启动 Tomcat 即可自动解压部署。这个方案可以让多个应用共享一个 Tomcat 实例但也要求外置 Tomcat 的版本和 SpringBoot 兼容匹配不了时会遇到类加载错误排查起来比较费时。5.5 方案三Nginx 托管前端加后端服务如果医院门户将来有多个子系统或者希望前端页面交给 Nginx 托管、后端接口独立扩展那么推荐前后端分开部署的方案。前端 dist 目录部署到 Nginx 的静态站点根目录后端 jar 包单独运行在服务器某个端口Nginx 里增加一条转发规则把 /api 前缀的请求转到后端端口即可。我实际部署时特别关注两个点。第一history 路由模式要配置 try_files否则用户直接访问/doctor/1这种地址会返回 404第二转发规则只针对 /api 前缀其他路径全部走静态资源这样接口和页面互不干扰。方案三的独立性最好前端重部署不影响后端进程后端发版也不用动页面适合团队分工明确的场景。三种方案怎么选我通常这样判断一个人全包且有服务器操作权限选方案一医院内部已有 Tomcat 运维规范选方案二未来要增长多端接入或者持续扩展页面选方案三。6. 常见问题与排查技巧实录6.1 MySQL 连接连续踩坑的三种表现第一个高频问题是启动时提示时区错误。解决办法就是配置文件里加 serverTimezoneAsia/Shanghai同时确认数据库所在服务器和代码服务器在同一时区。第二个是访问被拒绝通常因为 MySQL 8 默认的认证插件是 caching_sha2_password老版本驱动无法解析解决办法是用 8.x 对应的 JDBC 驱动并加上 allowPublicKeyRetrievaltrue。第三个是中文乱码这个多数是连接字符集没设置配置 useUnicodetrue 和 characterEncodingutf8 后重启服务即可。我排查这类问题时有个固定的操作顺序先看应用完整日志再检查配置文件最后用数据库客户端直接执行 SQL。只要 SQL 能查到数据、服务就是连不上几乎都是配置项差异而非数据库本身问题。6.2 跨域、404 与资源路径问题跨域报错最典型的是浏览器控制台出现 CORS 字样。开发环境里先检查 Vite 转发配置是否生效路径有没有写对生产环境一体部署模式一般不会跨域分开部署模式就需要在后端配置允许的来源。404 问题多半是前端路由的 history 模式没有配置 fallback或者后端静态资源目录没有正确指向 dist 内容。资源路径问题出现在前端打包之后最常见的是图片和接口路径写成绝对路径一旦部署到子目录就全部失效。我的做法是前端图片统一走最小化路径接口统一走相对路径加统一前缀这样不管部署到哪里都能适配。6.3 打包环节最容易忽略的小细节Maven 打包时跳过测试是有讲究的。如果数据库脚本还没初始化单元测试里涉及数据库操作一定会失败所以第一次打包执行 -DskipTests 是合理的选择但后续代码迭代过程中千万别一直跳过全部测试至少保留核心的单元测试来兜底。前端 npm install 报错时优先删除 node_modules 和 package-lock.json 重新安装比网上找杂七杂八的配置参数更有效。npm 源在国内如果下载速度不佳可以临时切换到官方镜像加速安装完成之后不必永久保留。最后提醒一个部署时常出现的问题服务器防火墙和云安全组默认拦截端口。很多新手在本地把 8080 访问通了上服务器后一访问就超时第一反应是检查代码实际往往只是安全组没放行端口。这个检查动作建议放在部署第一步省得来回折腾。最后再分享一个个人习惯项目再小我也一定会写一个最简单的健康检查接口比如在 Controller 里放一个/api/health返回当前服务状态和数据库连通情况。上线之后出问题第一件事就是请求这个接口能快速确定是前端没起来、后端挂了还是数据库连不上几分钟就能定位到模块。这个小习惯帮我省过不少排查时间也建议你保留在模板里。