☰
会议室预约系统源码详解:从数据模型到冲突检测与部署
2026/10/29 17:43:21 网站建设 项目流程

简介:面向计算机课程设计与毕业设计场景的会议室预约系统完整源码包,基于SpringBoot与Vue前后端分离架构实现。系统围绕用户信息、预约信息、会议室台账、账户权限与管理员后台进行设计,覆盖角色登录、预约申请、时段冲突检测等常见业务环节,适合正在学习Java Web全栈开发并需要完整项目参考的学生。整个资源包含1049个文件,压缩包大小约为17.73MB。java与class文件承载后端业务逻辑,js/css/html负责前端页面与交互,jsp与xml完成服务端视图及框架整合配置,sql文件用于初始化数据库,其余gz、ttf、woff、png等为项目依赖、字体图标及静态图片资源,结构清晰,可以直接导入开发环境运行调试。目前已有407人学习下载。借助该资源可以快速理清预约类系统的数据库表关系、Controller接口分层、Vue组件调用与权限校验思路,同时可复用现成的页面模板、工具类与部署配置,节省从零搭建系统的时间。

1. 会议室预约系统源码:先看清它解决的是谁的痛点

会议室预约的痛点从来不在“建一个表单”,而在信息和资源的同步:群里喊一声“3楼会议室明天下午有人订吗”,然后四五个人同时回复“我用一下”,最后到底谁用,全靠自觉。这套基于 springboot+vue 的会议室预约系统,是典型的课程设计和项目源码:Spring Boot 提供后端接口,Vue + Element UI/Element Plus 做前端页面,MySQL 存数据,把“用户提交预约 → 管理员审核 → 按时使用 → 取消或过期”这条链路完整串起来。

什么人下它最划算?计算机课程设计、毕业设计、以及第一次想完整做前后端联调的人。它能直接回答你在答辩时最怕问的三个问题:预约冲突怎么判断、角色权限怎么控制、状态怎么流转。先把数据模型拆开,后面每一章都在解决一个具体问题。

2. 数据模型先行:四张核心表与初始化脚本

拿到任何 springboot+vue 源码,我不会先点启动按钮,而是先找 SQL 脚本和实体类。原因很直接:表结构设计不清楚,后面所有页面和接口都会跟着返工。会议室预约系统的核心其实就四张表,把它们的字段和关系理明白了,整个项目的骨架就出来了。

2.1 前后端分离的项目骨架

大部分课程设计源码的目录结构都长这样,我一般会先看几个地方来判断质量:后端有没有按 controller/service/mapper 分层,实体类字段和数据库表能不能对上,前端有没有把 router、api、views 分开,数据库脚本是不是独立放在 sql 目录下。

meeting-system/ ├── backend/ # Spring Boot 后端 │ ├── pom.xml │ └── src/main/java/com/meeting/ │ ├── MeetingApplication.java # 启动类,带 @SpringBootApplication │ ├── controller/ # 接收前端请求 │ ├── service/ # 业务逻辑,预约冲突判断在这层 │ ├── mapper/ # MyBatis 接口,和 XML 文件对应 │ ├── entity/ # 数据库表对应的实体类 │ └── config/ # 跨域、拦截器、定时任务配置 │ └── src/main/resources/ │ ├── application.yml # 端口、数据库连接配置 │ └── mapper/ # MyBatis XML 文件,写 SQL ├── frontend/ # Vue 前端 │ ├── package.json │ └── src/ │ ├── api/ # axios 实例和所有接口封装 │ ├── router/ # 路由表,含登录守卫 │ ├── views/ # 登录、预约、管理页面 │ └── components/ # 复用组件,比如会议室卡片 └── sql/ └── init.sql # 建库建表和测试数据

这个结构本身没什么玄学,但课程设计答辩时老师很吃这一套:分层清晰、职责单一,比把所有代码都塞进 Controller 里高出不止一个档次。前端用 Vue Router 管页面跳转,axios 统一发请求,后端只暴露 RESTful 接口,两边通过 JSON 通信,这就是前后端分离的标准姿势。

2.2 四张核心表的设计与职责

我把这套系统的表设计成下面这样,课程设计里绝大多数功能都围着它们转:

表名职责关键字段
sys_user用户和角色username, password, role, real_name
meeting_room会议室资源name, location, capacity, equipment, status
booking预约记录room_id, user_id, book_date, start_time, end_time, status
booking_audit审核留痕booking_id, operator_id, action, remark

sys_user 里的 role 字段是关键,它区分普通用户和管理员。普通用户只能提交预约、取消自己的预约、查看会议室,管理员才有审核权限和会议室管理权限。真正的工程里不会在前端页面靠字段隐藏来做权限,后端每个接口都要做角色校验,这一点在源码的拦截器里能看到实现。

meeting_room 里我放了 capacity 和 equipment,这两个字段直接服务于预约页面。用户选择会议室时,会根据人数筛选,比如 6 个人的洽谈室预约不到 20 人,设备信息则展示在会议室卡片上,方便用户判断有没有投影仪、视频会议终端。

booking 表是整个系统的核心,room_id 和 book_date 组成了占用判断的基本维度:一个会议室一天内有多个时段,每条记录是一个时间段,同一时间段只能有一条有效预约。status 字段用整型表示状态机,0 是待审核,1 是已通过,2 是已拒绝,3 是已取消,4 是已过期。后面写业务逻辑时,所有判断都依赖这个状态。

2.3 初始化脚本:新库直接跑

课程设计源码最怕一件事:SQL 脚本放在文档里而不是独立文件里。这套系统把 init.sql 单独放在 sql 目录下,我直接拷贝到 Navicat 或命令行里就能执行。

CREATE DATABASE IF NOT EXISTS meeting_booking DEFAULT CHARACTER SET utf8mb4; USE meeting_booking; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT 'BCrypt 加密后的密码', real_name VARCHAR(20) NOT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT '1-普通用户 2-管理员', phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE meeting_room ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, location VARCHAR(100), capacity INT NOT NULL COMMENT '容纳人数', equipment VARCHAR(200) COMMENT '设备描述,逗号分隔', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-启用 0-禁用' ); CREATE TABLE booking ( id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL COMMENT '会议室 ID', user_id INT NOT NULL COMMENT '预约人 ID', book_date DATE NOT NULL COMMENT '预约日期', start_time TIME NOT NULL COMMENT '开始时间,如 09:00', end_time TIME NOT NULL COMMENT '结束时间,如 10:30', purpose VARCHAR(200) COMMENT '预约用途,方便管理员审核', status TINYINT NOT NULL DEFAULT 0 COMMENT '0-待审核 1-已通过 2-已拒绝 3-已取消 4-已过期', audit_user_id INT COMMENT '审核人 ID', audit_remark VARCHAR(200) COMMENT '拒绝理由或补充说明', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_room_date (room_id, book_date) ); INSERT INTO sys_user (username, password, real_name, role, phone) VALUES ('admin', '$2a$10$...改成自己生成的哈希...', '管理员', 2, '13800000000'), ('zhangsan', '$2a$10$...改成自己生成的哈希...', '张三', 1, '13900000000'); INSERT INTO meeting_room (name, location, capacity, equipment, status) VALUES ('大会议室', '3楼301', 20, '投影仪,视频会议', 1), ('洽谈室', '3楼302', 6, '白板', 1);

几点说明:数据库字符集我选 utf8mb4 而不是 utf8,因为一旦预约用途里出现 emoji 表情,utf8 会直接报错。sys_user 表名故意不用 user,user 在某些 MySQL 版本里和系统表概念混淆,用 sys_ 前缀更稳妥。密码字段存的是 BCrypt 哈希,不是明文,课程设计里即便你不想引入 Spring Security,也至少用工具类做一次哈希再入库,答辩时提一句安全设计会加分不少。

索引设计也别忽略。booking 表上的 idx_room_date 是核心索引,预约时先按 room_id 和 book_date 过滤,这个索引能让冲突查询走索引而不是全表扫描。数据量不大的课程设计里这个细节看不出差异,但你有心准备到,面试官问到就说得出理由。

3. 后端预约核心:冲突检测与状态流转

后端是整个项目的技术含量所在,也是答辩时老师最想看的部分。会议室预约系统的业务本质就一个:保证同一会议室在同一个时间段不能被两个人占用。只要把冲突检测和状态流转写对,这套系统的后端基本就立住了。

3.1 冲突检测:两个时间段重叠的数学条件

很多第一次写这个系统的同学会想当然地判断“新的开始时间不能落在已有预约的开始和结束中间”,结果一个 SQL 条件写完,数据一测就漏。两个区间是否重叠,数学上只有四个字:你.start < 我.end 且 你.end > 我.start。

举个例子:已有预约是 09:00 到 10:30,新预约是 10:00 到 11:00,这俩是重叠的,因为 10:00 小于 10:30,同时 11:00 大于 09:00。换一组数据:已有预约 09:00 到 10:00,新预约 10:00 到 11:00,10:00 不小于 10:00,条件不成立,所以不冲突,正好无缝衔接。

在 MyBatis 的 XML 里这样写:

<select id="selectConflict" resultType="java.lang.Integer"> SELECT COUNT(*) FROM booking WHERE room_id = #{roomId} AND book_date = #{bookDate} AND status IN (0, 1) AND start_time &lt; #{endTime} AND end_time &gt; #{startTime} </select>

这里有一个坑:XML 中小于号必须写成&lt;,大于号写成&gt;,否则 XML 解析直接报错。另一个更稳妥的写法是把整段 SQL 包在CDATA里,这样就不用来回转义了。status IN (0, 1)的原因是:待审核和已通过的预约都占着资源,只有拒绝、取消、过期的预约才能被新的预约覆盖。很多源码在这里只判断 status = 1,结果待审核的记录被重复提交,业务上就乱了。

3.2 并发防护:让查询锁住数据行

上面的 SQL 判断的是“当前没有冲突”,但课程设计如果只做到这一步,答辩时可能会被问住:两个用户同时点提交按钮怎么办?A 查了没冲突,B 也查了没冲突,然后两个人都插入成功,会议室就这么被双卖了。

我一般会在事务里用SELECT ... FOR UPDATE把当天的预约记录锁住,这是单库场景最容易理解的解决方案。对应 Service 层代码:

@Transactional public void createBooking(BookingDTO dto) { if (dto.getEndTime().isBefore(dto.getStartTime()) || dto.getStartTime().equals(dto.getEndTime())) { throw new BusinessException("结束时间必须晚于开始时间"); } List<Booking> locked = bookingMapper.selectForUpdate(dto.getRoomId(), dto.getBookDate()); for (Booking b : locked) { if ((b.getStatus() == 0 || b.getStatus() == 1) && b.getStartTime().isBefore(dto.getEndTime()) && b.getEndTime().isAfter(dto.getStartTime())) { throw new BusinessException("该时间段已被预约"); } } Booking booking = new Booking(); BeanUtils.copyProperties(dto, booking); booking.setStatus(0); booking.setCreateTime(LocalDateTime.now()); bookingMapper.insert(booking); }

对应的 Mapper XML:

<select id="selectForUpdate" resultType="com.meeting.entity.Booking"> SELECT * FROM booking WHERE room_id = #{roomId} AND book_date = #{bookDate} FOR UPDATE </select>

这里必须强调@Transactional不能少。事务开启后,FOR UPDATE锁定的行要等事务提交才释放,所以第一个用户插入后提交,第二个用户才能拿到锁,然后重新判断冲突,发现时间重叠就抛异常。没有事务,锁就跟着连接释放,等于白锁。这个点面试常问,答辩也常问,答上来能压住场。

3.3 审核与取消:状态机怎么流转

管理员审核的核心是防止重复审核。预约状态是 0 时才能审核,已经是 1 或 2 的记录再操作要直接报错。接口大致长这样:

@PostMapping("/audit") public Result audit(@RequestBody AuditDTO dto) { Booking booking = bookingMapper.selectById(dto.getBookingId()); if (booking == null) { return Result.error("预约不存在"); } if (booking.getStatus() != 0) { return Result.error("该预约已处理,请勿重复操作"); } if ("approve".equals(dto.getAction())) { booking.setStatus(1); } else if ("reject".equals(dto.getAction())) { booking.setStatus(2); } else { return Result.error("不支持的审核动作"); } booking.setAuditUserId(CurrentUserContext.getUserId()); booking.setAuditRemark(dto.getRemark()); bookingMapper.updateById(booking); return Result.success(); }

状态判断放在 Service 层,Controller 里只做参数接收。审核人 ID 从当前登录上下文拿,而不是相信前端传值,否则任何人都能伪造审核人。虽然课程设计没有真实的安全环境,但这个设计习惯是标答级别的。

用户取消预约的逻辑类似,区别是状态从 0 或 1 变成 3。我建议只允许用户取消“自己的”预约,所以取消接口里要带 userId 条件:

@PostMapping("/cancel/{bookingId}") public Result cancel(@PathVariable Integer bookingId) { Booking booking = bookingMapper.selectById(bookingId); Integer currentUserId = CurrentUserContext.getUserId(); if (booking == null || !booking.getUserId().equals(currentUserId)) { return Result.error("只能取消自己的预约"); } if (booking.getStatus() != 0 && booking.getStatus() != 1) { return Result.error("当前状态不允许取消"); } booking.setStatus(3); bookingMapper.updateById(booking); return Result.success(); }

3.4 定时任务:把过期预约标记掉

预约过了日期还没使用,状态不能一直挂在“已通过”上,否则会议室永远被僵尸预约占着。用 Spring 的定时任务每天凌晨扫一遍:

@Component public class BookingJob { @Resource private BookingMapper bookingMapper; @Scheduled(cron = "0 5 0 * * ?") public void markExpired() { bookingMapper.markExpired(LocalDate.now()); } }

SQL 里这样处理:

<update id="markExpired"> UPDATE booking SET status = 4 WHERE book_date &lt; #{today} AND status = 1 </update>

cron表达式表示每天 00:05 执行,避开整点高峰。这里的边界是“预约日期小于今天但状态还是已通过”,判断粒度到天就够了,不需要精确到时刻。你如果想做得更严谨,把当前时间拼进条件里,也没有问题,看源码怎么设计的。

4. 前端 Vue 接线:路由控制与预约表单落地

后端接口写完,前端的核心不是把页面做多漂亮,而是把登录态、权限和接口数据正确地接起来。Vue 这边的重点有三个:路由守卫控制页面权限、预约表单做好前端校验、axios 统一管理请求。

4.1 路由守卫:没有 token 就踢去登录页

Vue Router 的beforeEach守卫是控制页面访问的第一道门。我惯用的写法是给路由的 meta 里标注哪些页面需要登录、哪个角色才能进:

import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/', component: () => import('@/layout/Index.vue'), children: [ { path: 'rooms', component: () => import('@/views/RoomList.vue') }, { path: 'my-bookings', component: () => import('@/views/MyBookings.vue'), meta: { requiresAuth: true } }, { path: 'admin/audit', component: () => import('@/views/admin/AuditList.vue'), meta: { requiresAuth: true, role: 2 } } ] } ] const router = createRouter({ history: createWebHistory(), routes }) router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') return } const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}') if (to.meta.role && to.meta.role !== userInfo.role) { next('/403') return } next() }) export default router

前端守卫只能拦页面,真正的权限还是要靠后端接口校验。Vue 这边留着这个守卫的意义在于:用户手动输入管理员 URL 时,不会看到一个渲染失败的空页面,而是被友好地引导去 403 页。路由懒加载用() => import()写法,按需加载组件,首屏速度会好一些,项目大的时候这个习惯能救命。

4.2 预约表单:日期、时段和提交校验

预约页面的核心交互是:选择会议室 → 选择日期 → 填开始时间和结束时间 → 提交。前端校验要做两层,第一层是必填项,第二层是时间逻辑,结束时间必须晚于开始时间。后端还会再查一遍冲突,但前端提前提示总比等接口报错体验好。

import request from '@/api/request' import { ElMessage } from 'element-plus' import { reactive } from 'vue' export default { setup() { const form = reactive({ roomId: null, bookDate: '', startTime: '', endTime: '', purpose: '' }) const submit = () => { if (!form.roomId || !form.bookDate || !form.startTime || !form.endTime) { ElMessage.warning('请选择会议室、日期和时间段') return } if (form.endTime <= form.startTime) { ElMessage.error('结束时间必须晚于开始时间') return } request.post('/booking', form).then(res => { if (res.code === 0) { ElMessage.success('预约已提交,等待管理员审核') form.roomId = null form.bookDate = '' form.startTime = '' form.endTime = '' form.purpose = '' } else { ElMessage.error(res.msg || '预约失败') } }) } return { form, submit } } }

reactive是 Vue 3 的组合式 API 写法,Vue 2 的选项式写法也能实现同样功能,看源码用哪套就跟着用哪套。时间字段在 Element Plus 里一般用el-time-select组件,可以限定可选颗粒度,比如每 30 分钟一个选项,用户不会填出 09:03 这种难处理的时段。颗粒度设得合适,数据库存的 TIME 类型也更好处理。

4.3 axios 封装与开发环境跨域

axios 不封装直接用的结果就是:每个页面都重复写 token 头、重复处理错误码。封装一次之后,所有接口统一走一套逻辑:

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }) request.interceptors.response.use(res => { if (res.data.code === 401) { localStorage.removeItem('token') window.location.href = '/login' } return res.data }) export default request

时间超时我设 10 秒,正常情况后端接口毫秒级返回,10 秒足够判断网络问题。401 统一处理成跳转登录页,这样各个页面不用每个 catch 里都写一遍“请重新登录”。

开发环境跨域是新手最容易卡住的点。前端起在 5173 端口,后端跑在 8080,浏览器直接请求localhost:8080会被跨域拦截。两种主流解法:后端加 CORS 配置,或者前端用代理转发。我推荐在 Vite 里配置代理:

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

这样一来,前端请求/api/booking,Vite 开发服务器自动转发到http://localhost:8080/api/booking,浏览器看到的是同源请求,就绕过了跨域问题。生产环境不需要这个代理,Nginx 会把/api/转发给后端,这个在后文部署部分再展开。

5. 避坑排查:本地复现这套源码的常见翻车点

课程设计源码拿到的第一件事往往是“启动”,但我经手的反馈里,十个人里有八个卡在环境问题上,真正写业务代码的时间反而在后面。这里把我在复现这类 springboot+vue 项目时遇到的典型问题整理出来,供你比对。

5.1 MySQL 8 连接报错:驱动、时区、SSL 三连坑

现象:后端启动时抛出Communications link failure,或者最新的 key 在 MySQL 连接时没有被允许检索;项目跑一会儿就断连。

原因:MySQL 8 的驱动类从com.mysql.jdbc.Driver换成了com.mysql.cj.jdbc.Driver,旧代码直接沿用 5.x 的驱动配置会连不上。其次,MySQL 8 默认开启 SSL 和严格时区,JDBC URL 里不显式设置会报错。

解决:application.yml里用下面这份配置做基准:

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

useSSL=false关闭 SSL 握手,serverTimezone=Asia/Shanghai指定时区,allowPublicKeyRetrieval=true解决 MySQL 8 默认公钥检索限制。这三项几乎是 MySQL 8 连接的标准三件套。

5.2 后端端口被占用:8080 的冲突

现象:启动 Spring Boot,控制台直接报Port 8080 was already in use。

原因:本机某个进程已经占用了 8080,最常见的是另一个 Spring Boot 项目、Tomcat 服务,或者 IDEs 缓存的一些进程。

解决:先查出谁占了端口,再决定改配置还是杀进程。命令行执行:

netstat -ano | findstr 8080

Windows 下拿到 PID 后任务管理器结束进程,Linux 下用lsof -i:8080查看。如果不想动别人,直接改application.yml里的server.port为 8081,同时记得把 Vite 代理的 target 端口改一致。

5.3 Vue 依赖安装卡住或报错

现象:执行npm install时长时间停滞,或者报ETIMEDOUT、ERESOLVE错误。

原因:默认 npm 源是国外地址,国内网络环境下访问不稳定,加上不同 Node 版本对依赖解析规则不同。

解决:把 npm 源切到国内镜像:

npm config set registry https://registry.npmmirror.com npm install

如果项目里锁定的依赖版本和本地 Node 版本冲突,报ERESOLVE时可以用npm install --legacy-peer-deps跳过严格校验,但前提是你确认依赖本身没大问题。启动命令按package.json里的 scripts 来:npm run dev起开发服务器,npm run build输出生产包。

5.4 SQL 脚本导入报错:字符集与表名保留字

现象:Navicat 执行 init.sql,中途报语法错误,或者某些表创建成功、某些报错。

原因:常见三类。脚本里的表名用了 MySQL 保留字;字符集声明缺失导致中文乱码;日期时间字符串格式不符合DATE或TIME类型要求。

解决:表名避开保留字,严格执行用sys_user这类带前缀的命名。建库语句DEFAULT CHARACTER SET utf8mb4不能省,它是中文和 emoji 的兜底。插入 DATE 类型统一用YYYY-MM-DD格式,TIME 类型用HH:mm:ss,不要写2025-4-1 9:00这种宽松格式,MySQL 严格模式下会直接拒绝。

5.5 前端请求 404 或 CORS 报错

现象:前端能打开,登录时请求/api/user/login返回 404,或者浏览器控制台直接报 CORS。

原因:404 通常是代理没生效,比如 Vite 配置改了但没重启,或者请求路径里baseURL和代理前缀不一致。CORS 报错则是前端直接请求了后端地址,绕过代理。

解决:先看浏览器 Network 面板的请求 URL。如果请求地址是http://localhost:5173/api/...,代理在正常转发,问题在后端;如果地址是http://localhost:8080/api/...,说明代理没兜住,检查proxy配置并重启 Vite。开发阶段把前后端都跑起来,然后只用前端地址操作页面,能避开大半跨域问题。

6. 验证与进阶:从 Postman 到部署上线

系统跑通以后,我习惯按这个顺序做一轮完整验证,而不是直接打开页面乱点:先注册用户,再登录拿 token,添加会议室,提交预约,管理员审核,再提交一个时间重叠的预约看是否被拒。这轮验证能覆盖 80% 的核心逻辑,漏掉的问题基本都在状态流转上。

接口层面可以用 Postman 做断言,也可以用 curl 快速过一遍核心链路。比如提交一个冲突预约,预期返回业务错误码:

curl -X POST http://localhost:8080/api/booking \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <token>" \ -d '{"roomId":1,"bookDate":"2025-05-20","startTime":"09:00","endTime":"10:30"}'

返回结果应该是该时间段已被预约而不是成功消息,这能直接验证第 3 章的冲突判断逻辑是否生效。申请再次强调一遍:前端的页面展示可以通过隐藏按钮规避冲突,服务端必须在每次预约时执行一次校验,这套链路通过验证后,业务才是真正安全的。

部署到服务器是这套源码加分的方向。后端打包命令走 Maven:

mvn clean package -DskipTests nohup java -jar target/meeting-backend.jar --spring.profiles.active=prod &

前端构建产物是dist目录,交给 Nginx 托管,同时把/api/反向代理到后端端口。关键配置记在这里:

server { listen 80; server_name your.domain.com; location / { root /usr/share/nginx/html/dist; index index.html; 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-bookings页面会直接 404,这也是我最初部署时卡得最久的一个坑——页面点击能进去,一刷新就白屏,排查了半天才意识到是 Nginx 没接管前端路由。

还有一点可以进阶改造:把第 3 章的定时任务从整型状态标记升级成 Spring 事件通知,预约通过后发邮件或者站内信提醒用户,源码里预留了扩展接口,新增一个监听器就行,不影响原有逻辑。

从那以后我每次拿到课程设计源码,都不急着双击启动,而是强制自己先跑数据库脚本,再走一遍接口测试链路,最后才打开页面点按钮。这个顺序帮我避开了无数环境问题和逻辑遗漏。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询