☰
SpringBoot2+Vue3+MyBatis-Plus实现医院资源管理系统实战解析
2026/9/29 15:58:21 网站建设 项目流程

毕业设计、课程设计、新手练手项目……每年都能在各种源码群里看到同一个标题:Java Web 医院资源管理系统源码,SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0,含文档。如果你正打算做或者正在做这么一套前后端分离的管理系统,你会发现网上资料虽然多,但真正能从头到尾串起来讲清楚的东西反而很少。这篇就把我自己做这类项目的完整思路、踩过的坑、实际能直接照抄的配置一起端出来,希望帮你少走几步弯路。

这套技术组合是当前非常典型的一套“毕业设计黄金搭档”:SpringBoot2 负责后端接口,Vue3 负责页面和交互,MyBatis-Plus 处理数据持久层,MySQL8.0 做数据存储。医院资源管理系统听起来复杂,但本质和“员工管理系统”“图书管理系统”是一路货色:资源台账 + 用户权限 + 业务流转 + 统计报表。搞清楚了核心套路,你换任何业务场景都能快速套用。接下来我从整体设计、数据库、后端、前端、本地部署到排错实录,按实际做项目的顺序展开。

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

1.1 “医院资源管理系统”到底管的是什么

很多同学拿到这个题目第一反应是往“医疗业务”方向想,什么挂号、门诊、电子病历、药房发药…… 如果你真往那个方向做,项目规模会瞬间失控。这里要明确一下,项目名称里写的是“资源管理系统”,核心不是“看病”,而是“管医疗资源”。

我通常把这类系统的业务域拆成四层:

  • 系统管理:用户、角色、菜单权限,解决“谁能进系统、能干什么”的问题。
  • 基础资料:科室、楼栋楼层、资源分类、供应商等基础数据,是后续所有业务的数据底座。
  • 资源台账:医生排班、病床信息、医疗设备、耗材库存,管“资源有哪些、在哪、什么状态”。
  • 业务流转:资源预约申请、审批、分配登记、归还/释放,管“资源怎么被使用、谁在用”。

模块划分清楚了,页面和接口数量也就有数了。这个规模下,前后端加起来大概在二十到三十个接口,工作量恰好适合毕业设计或者课程设计,也能支撑起一份像样的项目文档。

1.2 为什么是 SpringBoot2 + Vue3 + MyBatis-Plus

先说不推荐的方案,你心里就有数了。别在这种项目里引入 Spring Cloud、Dubbo、分布式事务这些技术,一套系统部署在一台服务器上就能跑,引入微服务是给自己找麻烦,无论是答辩还是项目演示,复杂度只会拖垮你。

SpringBoot2 选它的理由很简单:生态成熟、配置少、社区资料全。SpringBoot3 虽然已经出来一段时间了,但不少教程和第三方库的兼容性还在磨合期,做毕设和课程设计用 SpringBoot2.7.x 是最稳的。Vue3 是现在前端的主流版本,配合 Vite 脚手架,开发体验比 Vue2 + Webpack 舒服太多了,组合式 API 写业务代码也比选项式 API 清晰。

MyBatis-Plus 和 JPA 之间我建议直接用 MyBatis-Plus。理由有三条:一是国内教学和技术社区普遍采用 MyBatis 系列,你遇到问题搜资料更容易搜到;二是 MyBatis-Plus 的 LambdaQueryWrapper 写条件查询非常方便,像科室模糊查询、日期范围筛选这类业务场景一句话就能写出来;三是 SQL 可控性强,如果后面想要做报表统计,手写 SQL 比 JPA 的派生查询直观得多。

数据库用 MySQL8.0 更是没什么好犹豫的。相比 5.7,8.0 的 JSON 类型、窗口函数都是实打实有用的能力,资源使用率统计、楼层资源排名排行这类需求在后面写 SQL 时会用得上。而且现在新服务器和本机安装的 MySQL 默认基本都是 8.0,没必要为了兼容老环境强行降级。

1.3 功能模块与页面结构的划分

一张表看明白模块设计:

模块核心功能页面 / 接口说明
系统管理用户、角色、菜单权限登录页、用户管理页、角色分配页
基础资料科室、楼栋楼层、资源分类分类树、科室列表、楼栋楼层管理
资源台账医生排班、病床、设备、耗材排班管理页、床位分布页、设备台账页
业务流转预约、审批、分配、归还申请单列表、审批页、资源分配页面
统计报表资源利用率、使用排行ECharts 图表页面、报表数据接口

前端页面就围绕这些模块铺开。实际开发时建议先把系统管理和资源台账做扎实,这两个模块能打通登录、权限、CRUD 三条主线,剩下的业务流转其实就是在这三条主线上加状态字段和操作按钮。

2. 数据库设计与 MySQL8.0 的实践细节

2.1 核心表结构与设计原则

数据库设计是这类系统最值得花时间的部分。我习惯先画一张主干表关系图:sys_user 关联 sys_role,sys_role 关联 sys_menu;department 关联上级科室形成树结构;doctor_schedule 关联 sys_user 和 department;bed_info 关联楼栋楼层和科室;medical_device 关联科室和资源分类;consumable_inout 关联耗材和科室。

建表的时候有几个通用原则可以直接套用:

  • 主键用 BIGINT,自增或者雪花 ID 都行。
  • 每张表留 create_time、update_time、deleted、remark 这四个通用字段,后面写日志和排查数据的时候非常有用。
  • 金额字段用 DECIMAL,一律不用 DOUBLE 存钱。
  • 状态字段用 TINYINT,配合代码里的枚举或者状态字典表,不要直接存中文。

以病床管理这张核心表为例:

CREATE TABLE `bed_info` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', `bed_no` VARCHAR(32) NOT NULL COMMENT '床位编号', `building` VARCHAR(32) DEFAULT NULL COMMENT '楼栋,如内科楼', `floor_no` TINYINT DEFAULT NULL COMMENT '楼层', `room_no` VARCHAR(32) DEFAULT NULL COMMENT '房间号', `department_id` BIGINT DEFAULT NULL COMMENT '所属科室ID', `bed_type` TINYINT DEFAULT 0 COMMENT '床位类型:0普通床位 1重症监护 2隔离床位', `status` TINYINT DEFAULT 0 COMMENT '状态:0空闲 1占用 2维修 3停用', `patient_name` VARCHAR(64) DEFAULT NULL COMMENT '当前占用患者姓名', `start_time` DATETIME DEFAULT NULL COMMENT '入住时间', `remark` VARCHAR(500) DEFAULT NULL, `deleted` TINYINT DEFAULT 0, `create_time` DATETIME DEFAULT NULL, `update_time` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_department_status` (`department_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='病床信息表';

这里设计了一个联合索引idx_department_status,因为页面上的高频操作就是“查某个科室下所有空闲床位”,这个联合索引能直接覆盖查询条件,数据量上来之后性能也不至于太难看。

2.2 MySQL8.0 的坑与连接配置

MySQL8.0 和 5.7 最大的一个连接变化是默认使用了 caching_sha2_password 认证插件。你在 JDBC 连接串里如果不做任何处理,很多老教程里的驱动可能直接报错。SpringBoot2 项目里我使用的连接配置是这样的:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hospital_resource?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 你自己的密码

allowPublicKeyRetrieval=true这条参数是很多人忽略的。MySQL8.0 在首次连接时如果需要获取服务器的 RSA 公钥来加密密码传输,客户端默认不会自动获取公钥,于是就会抛出Public Key Retrieval is not allowed的异常。开发环境加上这个参数没问题,但上线时如果对安全性敏感,可以改用useSSL=true的方法,这里不展开。

其次就是时区。serverTimezone=Asia/Shanghai必须设置,否则 JDBC 连接会报时区错误,或者数据库时间和 Java 返回的时间相差八小时。另外建库时务必将字符集指定为 utf8mb4:

CREATE DATABASE hospital_resource DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

utf8mb4 是 MySQL8.0 的推荐字符集,能完整支持中文、生僻字和 Emoji,避免出现“存入正常,查询变乱码”的问题。

2.3 初始化数据与权限菜单

数据库建完后,至少要准备好三类初始化数据:超级管理员账号、基础菜单、基础科室数据。生产环境里 admin 密码应该用 BCrypt 加密存储,开发环境为了省事我会直接放一个已经生成好的 BCrypt 密文,用户名 admin、密码 admin123,这样跑起来的时候直接登录,不用再额外写注册接口。

菜单数据的设计比较简单,sys_menu 表结构大概包含这样几个字段:menu_name、parent_id、path、component、menu_type(目录/菜单/按钮)、sort_order。后端登录接口返回当前用户可见的菜单树,前端根据这棵树动态生成路由。这一套逻辑本身不复杂,但它是整个系统最有含金量的地方,答辩时能讲清楚“你前端路由是怎么根据权限生成的”,非常加分。

3. 后端实现:SpringBoot2 + MyBatis-Plus

3.1 项目分层和包结构规划

后端代码结构我习惯这样划分:

com.example.hospital ├── common // 统一返回体、全局异常、常量 │ ├── Result.java │ ├── ResultCode.java │ └── GlobalExceptionHandler.java ├── config // MybatisPlusConfig、CorsConfig、WebMvcConfig ├── controller // 接口层 ├── service // 业务层及其实现 ├── mapper // MyBatis-Plus Mapper接口 ├── entity // 数据库实体 ├── dto // 请求参数对象(接收前端数据) └── vo // 返回给前端的数据对象(响应体)

entity、dto、vo 的分离是很多新手觉得麻烦的地方。这里说清楚:entity 和数据库表字段一一对应,不该被前端请求直接绑定;dto 用来接收查询条件和表单提交数据;vo 用来组装需要返回给页面的数据。比如查询病床列表时,前端提交的是一个包含 departmentId、status、keyword 的查询 dto,后端返回的是包含科室名称、楼栋楼层信息的 vo,而不是直接返回 entity。这样设计的价值在项目后期调整字段时就能体现出来——不会因为前端某个展示需求改动而影响数据库层的实体。

3.2 统一返回体与全局异常

无论什么接口,返回给前端的结构都应该是统一的。我用一套最简单的 Result 封装:

public class Result<T> implements Serializable { 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(ResultCode rc) { Result<T> result = new Result<>(); result.setCode(rc.getCode()); result.setMessage(rc.getMessage()); return result; } }

配合全局异常处理器,业务代码里抛一个自定义异常,前端就能拿到结构固定的错误提示。比如资源分配时床位已被占用,你只需要在 service 层抛出BusinessException("该床位已被占用,请重新选择"),全局异常处理器捕获并返回 code 为 500 的错误 JSON。前端 axios 拦截器统一弹消息提示,不用每个接口都写一遍错误处理。

3.3 MyBatis-Plus 的高频使用姿势

MyBatis-Plus 在绝大多数 CRUD 场景里几乎没有 SQL 要写,但这几个能力你务必要配好。

第一个是分页插件。只引入依赖还不够,MyBatis-Plus 的分页功能依赖拦截器,不注册的话调用page方法返回的数据永远不带分页信息:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

第二个是公共字段自动填充。创建时间和更新时间如果靠每个 service 手动 set,很容易漏掉。我的做法是给实体字段加@TableField(fill = FieldFill.INSERT)注解,再实现一个 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()); } }

第三个是逻辑删除。实体字段上加上@TableLogic,全局配置里指定逻辑未删除值为 0、已删除值为 1,之后调用 deleteById 实际执行的是 UPDATE。这个机制能让所有查询自动带上deleted=0条件,但要注意:如果某些统计 SQL 是手写的,就必须自己拼接这个条件,别指望 MyBatis-Plus 帮你处理原生 SQL。

第四个是批量操作。做耗材入库时一次可能要给几十条耗材记录写流水,MyBatis-Plus 的 IService 内置了 saveBatch,底层会分批执行批量插入,比在 for 循环里逐条 insert 效率高很多。如果你手写 Mapper 做批量插入,注意 MySQL 单条 INSERT 语句有 max_allowed_packet 限制,几个 GB 级的大文件导入场景要分批执行。

3.4 登录鉴权的轻量实现

很多教程一到权限就开始引入 Spring Security + JWT,配置类一堆,新人刚上手很容易被搞晕。我的建议是:课程设计和毕业设计这种规模下,用一个拦截器加 JWT 就够用了,逻辑清楚,答辩也讲得明白。

具体做法三步:登录接口校验用户名密码后签发 JWT;前端把 token 存到 localStorage,每次请求在 axios 拦截器里带上Authorization: Bearer token;后端写一个 HandlerInterceptor,拦截除登录接口之外的所有请求,解析 token、检查过期时间。需要做细粒度权限时,再配一个自定义注解@RequirePermission("bed:delete"),配合拦截器查询该用户角色对应的菜单权限集合,进行比对即可。

这个方案的优点是代码完全在自己手里,出了任何问题都能讲清楚前因后果。Spring Security 重度封装后的过滤器链,在答辩时被追问“你的鉴权流程到底怎么走的”,你大概率会很难把每一层都说透。轻量方案反而让人放心。

3.5 核心业务接口示例:床位管理

以床位管理模块为例,展示一下实际的 service 代码长什么样。

Service 实现类里写查询列表:

@Override public Page<BedVo> pageBed(BedQueryDto dto) { Page<Bed> page = new Page<>(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapper<Bed> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(dto.getDepartmentId() != null, Bed::getDepartmentId, dto.getDepartmentId()) .eq(dto.getStatus() != null, Bed::getStatus, dto.getStatus()) .like(StringUtils.hasText(dto.getKeyword()), Bed::getBedNo, dto.getKeyword()) .orderByAsc(Bed::getBuilding).orderByAsc(Bed::getFloorNo); Page<Bed> bedPage = this.page(page, wrapper); // 调用转换器组装Vo,填充科室名称等字段 Page<BedVo> bedVoPage = bedPage.convert(bed -> convertToVo(bed)); return bedVoPage; }

这套写法里最值得学习的地方是wrapper.eq(condition, column, value)的用法。condition 为 false 时这一条件不会拼进 SQL,这样查询接口面对多条件组合时非常优雅,不会出现 if 套 if 拼 SQL 的灾难现场。

写资源分配(比如病床分配)接口时,务必要加上事务控制:

@Transactional(rollbackFor = Exception.class) public void allocateBed(AllocateBedDto dto) { Bed bed = this.getById(dto.getBedId()); if (bed == null || bed.getStatus() != 0) { throw new BusinessException("该床位不可用"); } // 更新床位状态、写入分配记录、更新患者入住时间 UpdateWrapper<Bed> updateWrapper = new UpdateWrapper<>(); updateWrapper.eq("id", dto.getBedId()).eq("status", 0); boolean updated = this.update(updateWrapper); if (!updated) { throw new BusinessException("床位状态已变化,请刷新后重试"); } }

如果你对并发的理解足够深,会发现这里不仅用事务保证一致性,还在更新时用条件status=0做了一次乐观锁判断。这样即使用户同时操作同一张病床,也只有一条更新能成功,不会被覆盖。乐观锁的思想贯穿整个 resource 分配类业务场景,比单纯加锁更简单高效。

4. 前端:Vue3 + Element Plus 的后台管理页面

4.1 脚手架选型和项目结构

前端部分我用的是 Vite + Vue3 + Element Plus + Pinia 的组合。Vite 的开发服务器启动速度快,改动生效基本是毫秒级,这是 Webpack 时代的开发体验没法比的。Element Plus 是 Vue3 的组件库,后台管理页面里的表格、表单、弹窗、分页它都给好了,省下大量造轮子的时间。

项目目录按“模块分包”组织:

src ├── api // 接口定义,按模块拆文件 ├── assets // 静态资源 ├── components // 通用组件 ├── layout // 后台布局框架 ├── router // 路由配置 ├── store // Pinia 状态管理 ├── views // 页面 └── utils // axios 封装、工具函数

Vue3 项目有疑问的时候,多去翻一下 Vue3 中文官网的文档,特别是组合式 API 那块。网上很多教程还是 Vue2 时代的老写法,别把旧习惯带到 Vue3 项目里,容易踩响应式丢失的坑。

4.2 axios 封装与请求拦截

前端请求层是所有后台系统的基础设施。我封装 axios 的时候只有两个核心逻辑:请求拦截自动带 token,响应拦截统一处理业务码和后端传来的错误消息。

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动携带token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理返回 service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.message)) } return res.data }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } )

这段代码没什么高深的技巧,但它能给后续所有页面的开发带来巨大便利。后面写床位列表、审批页面时,只需要在 api 文件里定义接口方法,页面中调用后拿到就是后端返回的数据,不用每个接口都单独写 loading 和报错处理。

4.3 动态路由与权限

动态路由的具体实现是这样的:用户登录成功后,调用后端“获取用户信息和权限菜单”的接口,拿到菜单树后,递归把每个菜单项转换成 Vue Router 的 RouteRecordRaw 对象,再调用router.addRoute注册进去。为了避免刷新页面时路由还没来得及注册导致 404,我通常在 Pinia 里存一份“已加载路由标记”,刷新时在全局路由守卫里先加载路由,再放行进入目标页面。

这套逻辑需要你理解 Vue Router 的动态路由 API,不是什么魔法。核心代码大概长这样:

router.beforeEach(async (to) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { return '/login' } const store = useUserStore() if (token && !store.menusLoaded) { await store.fetchUserInfo() const routes = buildRoutes(store.menus) routes.forEach(route => router.addRoute(route)) store.menusLoaded = true return { ...to, replace: true } } })

有个细节提醒一下:addRoute是异步生效的,所以第一次加载路由后需要return { ...to, replace: true }重新导航一次,否则刷新页面的时候第一次点击某个菜单还是会判定为“没有匹配的路由”。

4.4 核心页面示例:床位管理列表

床位管理页是典型的“搜索 + 表格 + 分页 + 状态操作”页面。在 Vue3 组合式 API 下,我会这样组织:

<script setup> import { ref, reactive, onMounted } from 'vue' import { getBedPage, allocateBed, updateBedStatus } from '@/api/bed' const loading = ref(false) const tableData = ref([]) const total = ref(0) const queryForm = reactive({ departmentId: undefined, status: undefined, keyword: '', pageNum: 1, pageSize: 10 }) const loadData = async () => { loading.value = true try { const data = await getBedPage(queryForm) tableData.value = data.records total.value = data.total } finally { loading.value = false } } const handleSearch = () => { queryForm.pageNum = 1 loadData() } onMounted(() => { loadData() }) </script>

页面模板部分就是 el-table 配合 el-pagination。很多新人容易把分页的@current-change事件写成handleCurrentChange但还是不生效,其实就是事件名没对上或者方法没在setup里暴露。用<script setup>的写法后所有顶层声明自动暴露到模板,不需要 return,这也是 Vue3 开发体验提升最明显的地方。列表里按钮的权限控制,我习惯用自定义指令v-permission,没有权限就不渲染这个按钮。

4.5 Vue3 开发中容易踩的坑

第一个坑是 reactive 对象直接赋值会丢失响应式。比如reactive({ list: [] })里用state.list = res.data是没问题的,但如果你写let state = reactive([]),然后state = res.data,那页面就再也不会更新了。处理这类情况的原则是:reactive只针对对象内部属性变化,整体替换请用ref。

第二个坑是组件通信的 v-model。给子组件用v-model="form.status"时,子组件内触发的是update:status事件,而不是update:modelValue。这个在小场景下容易困惑,平时没事看看 Element Plus 表单组件的源码实现,会有很大帮助。

第三个坑是异步组件卸载后的警告。比如分页查询没有加 loading 状态,用户快速翻页时前一次请求还没返回,组件可能已经卸载了,控制台会报 “Component is no longer mounted” 警告。虽然不影响功能,但答辩和检查代码时被问起会很尴尬。在 loadData 里做好 loading 控制,或者取消上一次请求,是个好习惯。

5. 环境准备与本地部署:从安装到跑起来

5.1 版本清单

做之前先统一环境,免得在版本兼容上浪费时间:

组件推荐版本说明
JDK8 或 11SpringBoot2.7 对这两个版本支持最好
Maven3.6.3 及以上包管理工具
Node.js16.x 或 18.xVite 4 要求 Node 14.18+
MySQL8.0.x必须使用 8.0
前端包管理器npm/yarn/pnpm建议用 npm 配国内镜像

5.2 MySQL8.0 安装的两种方式

Windows 下最简单的官方安装方式是下载 MySQL8.0 的 ZIP 包免安装版。下载后解压,在 bin 目录下执行:

mysqld --initialize-insecure

这条命令会初始化数据目录,--initialize-insecure表示 root 账号初始密码为空。初始化完成后,执行:

mysqld --install mysql8 net start mysql8

然后在命令行进入 MySQL,设置 root 密码:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';

如果你不打算在本机装 MySQL,直接用 Docker 更省心,一条命令搞定:

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

注意 Compose 方式或者正式使用时要加--restart=always、挂载数据目录,避免容器删除后数据全没。开发环境用-v /my/own/datadir:/var/lib/mysql挂一下本地目录很推荐。连接时用 Navicat 或者 DBeaver 都行,字符集在连接配置里选 utf8mb4,基本不会遇到乱码。

5.3 后端配置文件实战

后端真正改动的配置文件主要是 application.yml。除了前面说的数据源配置,还有几个能让开发体验更舒服的配置:

server: port: 8080 spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

map-underscore-to-camel-case必须开启,数据库字段bed_no才能自动映射到 Java 属性bedNo。log-impl开启 SQL 日志在开发时很有用,能让你直接看到每一条请求背后执行的 SQL 长什么样,排查问题效率翻倍。

启动后端就是mvn spring-boot:run,或者打包成 jar 后java -jar target/hospital-resource.jar。第一次启动时如果提示找不到数据库,去检查数据库是否建好、密码是否正确,不要慌着改代码。

5.4 前端安装依赖与启动

前端项目拿到后,第一件事是安装依赖:

npm install --registry=https://registry.npmmirror.com

指定国内镜像能避免很多网络问题。依赖装完直接启动开发服务器:

npm run dev

默认端口一般为 5173。如果你后端的接口地址不是 8080,或者想用 Nginx 做代理,就在 vite.config.js 里配置 proxy,把/api前缀的请求转发给后端服务,这样前端代码里写的 baseURL 就一直保持/api,前端部署时不用再改动。

server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

很多项目在部署时就挂在代理配置上。后端 CORS 配置一般开着就好,但如果前端是走同源代理,后端不需要额外开启跨域,两边配置别重复,否则可能出现奇怪的 OPTIONS 请求问题。

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

这套技术栈的项目,大家遇到的基本都是下面几类问题,我把它们的根因和解决方案贴出来。给队友排查时从这往下查,命中率很高。

6.1 MySQL8 的连接报错:Public Key Retrieval is not allowed

java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed是最常见的问题。根因是 MySQL8.0 默认 caching_sha2_password 插件,客户端首次建立安全连接时要向服务端请求 RSA 公钥。解决办法是在 JDBC URL 末尾加上allowPublicKeyRetrieval=true,或者把 root 用户的认证插件改成 mysql_native_password。开发环境我一般两个都处理,避免图形化客户端也遇到同类问题。

6.2 MyBatis-Plus 分页不生效

分页接口返回的 total 一直是 0 或者所有数据一次性返回,基本就是没配置MybatisPlusInterceptor。新版本 MyBatis-Plus 的配置方式和我上面写的是一致的,注意DbType.MYSQL不要写错,别拿 Oracle 的 DbType 来配置 MySQL 数据库。配置完成后重启项目,再观察控制台 SQL,确认是否出现了LIMIT关键字。

6.3 SpringBoot 启动时 MySQL 时间差 8 小时

这个坑的根源往往不在数据库,而在 JDBC 连接串和应用时区。先检查 application.yml 里是否指定了serverTimezone=Asia/Shanghai,然后检查 Jackson 的time-zone: GMT+8。两点都配置后,前端显示的时间基本就正确了。如果后端返回的时间是类似2024-05-01T10:00:00的 ISO 格式,多半是 Jackson 序列化配置没生效,检查一下有没有引入 jackson-datatype-jsr310 依赖。

6.4 Vue3 项目刷新后页面 404

问题描述一般是:项目启动后在首页能正常跳转,一刷新某个二级页面就出现 404 白屏。这是前后端路由方式冲突。开发环境是 Vite 的 history 路由没有 fallback 到 index.html,打包部署到 Nginx 时更明显。解决方式是使用 hash 路由(createWebHashHistory),或者开发环境在 vite.config 里配置appType: 'spa',生产环境在 Nginx 的 location 里加一句:

location / { try_files $uri $uri/ /index.html; }

我个人的建议是,毕设和内部系统直接用 hash 模式最省心。HTML5 History 模式地址好看,但刷新 404 这个问题会一直在,除非你 Nginx 配置特别扎实。

6.5 npm install 卡住或报 ERESOLVE 错误

install 卡住基本是网络问题,换成 npmmirror 镜像后一般能解决。如果出现ERESOLVE unable to resolve dependency tree,常见于三个依赖包的版本要求互相冲突。不要硬刚版本,最稳妥的办法是删掉 node_modules 和 package-lock.json,然后重新用npm install --legacy-peer-deps安装。npm 7 以后对 peerDependencies 的解析更严格,老项目的 package.json 如果没跟上,就容易这样。

6.6 手写 SQL 查询结果字段全部为 null

用 MyBatis-Plus 的注解 SQL 或者 XML 写自定义查询时,返回的实体字段全是 null,控制台 SQL 也正常。这种情况大概率是 resultType 映射问题。如果实体属性名和数据库字段名遵循驼峰到下划线的映射,必须在 MyBatis 配置里开启map-underscore-to-camel-case;如果开了还是不生效,检查 mapper.xml 里是不是写了自定义 ResultMap,一旦写了就以它为准,不会走全局配置。

6.7 前端表格拖拽排序不生效

Vue3 项目里如果用了 sortablejs 做拖拽排序,初始化时机要在 DOM 更新之后,否则绑定的 table 元素还没渲染出来。常见的解法是在nextTick里初始化Sortable.create(el, options)。如果是 el-table 的场景,组件可能会在数据更新后重建表格 DOM,拖拽实例会失效,需要在数据渲染后重新创建或者利用v-if控制表格渲染。

7. 文档和答辩准备:这个项目怎么讲才加分

既然标题里强调“含文档”,那文档质量就直接影响这套源码的使用价值。我建议至少包含三份文档:需求说明、数据库设计说明、接口说明。

需求说明把模块划分和用户角色讲清楚,别堆套话,最好配使用场景描述。比如“护士长每天查看本科室病床占用情况,并登记患者入院分配床位”,这样写比“系统支持床位管理功能”强得多。数据库设计说明就是把表关系图画清楚、字段注释写全,前提是建表语句中的 COMMENT 都完善了,文档里可以直接引用。接口说明最简单的方式是用 Apifox 或 Postman 导出一份接口集,再配上统一返回格式说明和几个核心流程的调用时序,比如“登录 -> 获取权限 -> 床位查询 -> 床位分配”。

答辩的时候,讲解重点放在三个地方:数据库表设计的关联关系、MyBatis-Plus 如何简化开发、前端如何根据权限动态生成路由。这三个地方是这套系统技术含量最高的位置。另一个比较容易出彩的点是并发下的资源分配,哪怕只是用乐观锁解决床位重复分配的问题,也能证明你认真考虑过真实业务场景,而不是简单做了一层 CRUD。

我个人做完这类项目的体会是:系统本身不复杂,但“资源管理”这类题目的业务状态流转一定要设计完整。床位有空闲、占用、维修、停用,设备有在库、使用、维护、报废,每一个状态的转变都要有对应的页面操作和记录,这样整个项目答辩时就有完整的故事线、有可演示的流程。最后再送你一个小技巧:开发阶段先跑端到端流程,也就是从登录到新增资源再到分配审批,全程走一遍,不要按模块一个一个开发逐个验收。因为最耗时间的 bug 往往不在单个页面里,而在前后端联调过程和状态衔接处。

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

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

立即咨询