
毕业设计、课程设计、新手练手项目……每年都能在各种源码群里看到同一个标题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.78.0 的 JSON 类型、窗口函数都是实打实有用的能力资源使用率统计、楼层资源排名排行这类需求在后面写 SQL 时会用得上。而且现在新服务器和本机安装的 MySQL 默认基本都是 8.0没必要为了兼容老环境强行降级。1.3 功能模块与页面结构的划分一张表看明白模块设计模块核心功能页面 / 接口说明系统管理用户、角色、菜单权限登录页、用户管理页、角色分配页基础资料科室、楼栋楼层、资源分类分类树、科室列表、楼栋楼层管理资源台账医生排班、病床、设备、耗材排班管理页、床位分布页、设备台账页业务流转预约、审批、分配、归还申请单列表、审批页、资源分配页面统计报表资源利用率、使用排行ECharts 图表页面、报表数据接口前端页面就围绕这些模块铺开。实际开发时建议先把系统管理和资源台账做扎实这两个模块能打通登录、权限、CRUD 三条主线剩下的业务流转其实就是在这三条主线上加状态字段和操作按钮。2. 数据库设计与 MySQL8.0 的实践细节2.1 核心表结构与设计原则数据库设计是这类系统最值得花时间的部分。我习惯先画一张主干表关系图sys_user 关联 sys_rolesys_role 关联 sys_menudepartment 关联上级科室形成树结构doctor_schedule 关联 sys_user 和 departmentbed_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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你自己的密码allowPublicKeyRetrievaltrue这条参数是很多人忽略的。MySQL8.0 在首次连接时如果需要获取服务器的 RSA 公钥来加密密码传输客户端默认不会自动获取公钥于是就会抛出Public Key Retrieval is not allowed的异常。开发环境加上这个参数没问题但上线时如果对安全性敏感可以改用useSSLtrue的方法这里不展开。其次就是时区。serverTimezoneAsia/Shanghai必须设置否则 JDBC 连接会报时区错误或者数据库时间和 Java 返回的时间相差八小时。另外建库时务必将字符集指定为 utf8mb4CREATE 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-Plus3.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 ResultT implements Serializable { 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(ResultCode rc) { ResultT 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)注解再实现一个 MetaObjectHandlerComponent 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。这个机制能让所有查询自动带上deleted0条件但要注意如果某些统计 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 PageBedVo pageBed(BedQueryDto dto) { PageBed page new Page(dto.getPageNum(), dto.getPageSize()); LambdaQueryWrapperBed 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); PageBed bedPage this.page(page, wrapper); // 调用转换器组装Vo填充科室名称等字段 PageBedVo 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(该床位不可用); } // 更新床位状态、写入分配记录、更新患者入住时间 UpdateWrapperBed updateWrapper new UpdateWrapper(); updateWrapper.eq(id, dto.getBedId()).eq(status, 0); boolean updated this.update(updateWrapper); if (!updated) { throw new BusinessException(床位状态已变化请刷新后重试); } }如果你对并发的理解足够深会发现这里不仅用事务保证一致性还在更新时用条件status0做了一次乐观锁判断。这样即使用户同时操作同一张病床也只有一条更新能成功不会被覆盖。乐观锁的思想贯穿整个 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-modelform.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.18MySQL8.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 rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;如果你不打算在本机装 MySQL直接用 Docker 更省心一条命令搞定docker run --name mysql8 -p 3306:3306 -e MYSQL_ROOT_PASSWORD123456 -d mysql:8.0注意 Compose 方式或者正式使用时要加--restartalways、挂载数据目录避免容器删除后数据全没。开发环境用-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: GMT8 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: 0map-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 --registryhttps://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 allowedjava.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed是最常见的问题。根因是 MySQL8.0 默认 caching_sha2_password 插件客户端首次建立安全连接时要向服务端请求 RSA 公钥。解决办法是在 JDBC URL 末尾加上allowPublicKeyRetrievaltrue或者把 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 里是否指定了serverTimezoneAsia/Shanghai然后检查 Jackson 的time-zone: GMT8。两点都配置后前端显示的时间基本就正确了。如果后端返回的时间是类似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 往往不在单个页面里而在前后端联调过程和状态衔接处。