我做这类系统的经验是:剧本杀预约系统看起来很常规,网上随便一搜能出来一堆“XX管理系统”代码,但如果真按那种思路去做,很容易做成一个只能交差的CRUD堆砌。剧本杀的预约逻辑跟普通商品预约完全不一样,核心区别在于“拼车”和“场次”的概念。一个剧本一个场次,必须凑够指定人数才能开,有人临时跳车还要补位,老板还要控制同一房间在同一个时间段不冲突……这些细节如果前期业务建模做不好,后期写代码就是灾难。
这篇我就以“springboot+vue基于web的剧本杀预约管理系统的设计与实现”为题目,把从需求拆解到表结构设计、后端接口、前端页面、联调排错的一条龙思路完整讲一遍。文章基于我一贯使用的技术栈——SpringBoot 2.7 + MyBatis-Plus + MySQL 8.0,前端Vue 3 + Vite + Element Plus + Pinia。如果你用的是Spring Boot 3.x或者Vue 2,整体架构思路是一样的,具体版本差异我会在踩坑部分单独说。
1. 场景还原:剧本杀预约系统到底要管什么——业务建模先行
很多人拿到这个题目第一反应是:用户表、剧本表、订单表,三个表一建,后端CRUD,前端列表页加个预约按钮,完事。这种思路做出来的东西,去答辩的时候老师一问业务细节就容易卡壳,因为真实场景根本不是这样的。
1.1 从老板的日常看系统需求
去跟一家剧本杀店的老板聊过之后会发现,他每天最烦的不是“没有客人”,而是“协调”。一个周末下午,店里可能有四个房间同时在跑本,每个房间对应一个DM(主持人),每个场次又对应一个剧本。客人订的不是某个时间点的“票”,而是“今天下午3点这个房间开《某某本》,还差两个人,你们能来吗”。
这就引出了几个关键业务概念:
- 剧本:需要管理剧本名称、类型、难度、时长、人数范围、简介、封面图。
- 场次:某一天某个时间,某房间开某个剧本,需要几人成团,当前已报名几人,由哪个DM主持。这个“场次”是预约的核心对象。
- 拼车:一个场次的人数可能不满,用户可以约一个“加入拼车”的意向,满人之后系统才真正锁定资源。
- 房间与DM:这是资源约束。房间一天只有几个时段可用,DM也有休息时间,不建模进去,预约冲突迟早爆发。
如果你设计表结构时忽略“场次”这个中间概念,直接在订单表里存“用户选了剧本A,时间填了某个时间段”,那系统就是残缺的。同一个房间、同一个小时,两个玩家各自约了不同剧本,老板接单时才发现撞了,这就是典型的业务建模缺失。
1.2 表结构设计:核心五张表
基于上面的业务分析,这套系统里最少需要下面这些核心表。我直接用建表语句说明,字段类型和注释都写上,方便你直接改改拿去用。
用户表(sys_user)主要用于登录认证和基础信息管理,角色字段我用的是普通字符串,一个值为USER,一个值为ADMIN,简单直接,不需要引入Spring Security那套复杂的角色体系。如果你后面想扩展权限,比如店长、DM、玩家三种角色,把这个字段改成整数类型,再做一层角色菜单映射表就行。
sql复制CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '登录名',
`password` varchar(100) NOT NULL COMMENT 'MD5加密后的密码',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`role` varchar(20) NOT NULL DEFAULT 'USER' COMMENT '角色:USER/ADMIN',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
剧本表(script)相对简单,但注意我特意加了status字段。为什么?因为剧本可能下架、可能暂时不能预约,不能一删了之。删除是物理动作,下架是业务动作,两者混在一起会出问题。
sql复制CREATE TABLE `script` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '剧本名称',
`type` varchar(50) DEFAULT NULL COMMENT '类型:欢乐/恐怖/硬核/情感',
`difficulty` tinyint(4) DEFAULT 1 COMMENT '难度:1-5',
`duration` int(11) DEFAULT NULL COMMENT '建议时长,单位分钟',
`min_players` int(11) DEFAULT NULL COMMENT '最少人数',
`max_players` int(11) DEFAULT NULL COMMENT '最多人数',
`cover` varchar(255) DEFAULT NULL COMMENT '封面图地址',
`introduction` text COMMENT '剧本简介',
`price` decimal(10,2) DEFAULT NULL COMMENT '单人价格',
`status` tinyint(4) DEFAULT 1 COMMENT '1上架 0下架',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
场次表(script_session)是这套系统的灵魂。它把“剧本”“房间”“时间”“当前人数”这几个关键信息绑到一起。注意这里的current_players字段,它是拼车进度的实时体现。为了避免并发更新问题,这个字段不能用简单的“每次预约就+1”的方式硬算,后面我会专门讲并发控制。
sql复制CREATE TABLE `script_session` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`script_id` bigint(20) NOT NULL COMMENT '剧本ID',
`room_id` bigint(20) NOT NULL COMMENT '房间ID',
`dm_id` bigint(20) DEFAULT NULL COMMENT '主持人用户ID',
`start_time` datetime NOT NULL COMMENT '开场时间',
`end_time` datetime DEFAULT NULL COMMENT '预计结束时间',
`min_players` int(11) DEFAULT NULL,
`max_players` int(11) DEFAULT NULL,
`current_players` int(11) DEFAULT 0 COMMENT '当前已报名人数',
`price` decimal(10,2) DEFAULT NULL COMMENT '实际单人价格',
`status` tinyint(4) DEFAULT 0 COMMENT '0招募中 1已满员 2进行中 3已结束 4已取消',
`version` int(11) DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
预约表(reservation)记录的是“谁约了哪个场次”,它才是订单的核心载体。一个用户可以在多个场次有预约,但同一个场次只能预约一条。这个约束靠业务代码保证,不靠数据库唯一索引,因为数据库无法表达“同一个人同一场次只能一条”之外的复杂逻辑。
sql复制CREATE TABLE `reservation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '预约用户ID',
`session_id` bigint(20) NOT NULL COMMENT '场次ID',
`status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0待拼车 1已确认 2已完成 3已取消',
`remark` varchar(255) DEFAULT NULL COMMENT '备注,比如是否跳车',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uk_user_session` (`user_id`, `session_id`),
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
房间表(room)和用户表结构类似,就是记录房间名称和可容纳人数。很多学生做项目时忽略这张表,直接把“房间”做成剧本表的一个字段,这会带来一个隐患:同一个时间点,两个不同的场次可能被安排在同一个房间,而系统根本没法检测这个冲突。所以房间表必须独立出来,后面做时间冲突检测才能写SQL。
我第一次做这个系统时,把“场次”设计成了一个虚拟概念,没有存数据库,直接在预约接口里写死逻辑——用户传入剧本ID和开始时间,系统自己判断。这样也能跑通Demo,但问题是:老板想手动创建一个“今晚8点《某某本》拼车局”推送给大家,系统做不到。所以场次一定要有独立表,而且要支持后台管理员主动创建。
1.3 MySQL外键:能用但别真用
很多学生习惯在数据库设计工具里把外键约束画出来,然后生成SQL时也带上FOREIGN KEY。我强烈建议在项目里不启用物理外键,只保留逻辑外键(即普通索引字段,在代码里做关联查询)。
原因有两点:一是MyBatis-Plus的BaseMapper对多表联查支持比较弱,加了物理外键反而影响插入删除的灵活性;二是后期做数据归档、批量导入的时候,物理外键就是一颗定时炸弹。你的教练或答辩老师如果问起,你可以理直气壮说:外键约束放在应用层做,是为了提升系统扩展性和高并发下的写入性能。这句话在答辩时非常加分。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 后端硬骨头:预约状态机、并发防超卖与环境配置
这一章节是整个项目最核心的部分。预约系统的“预约”动作不是简单往表里插一条记录就完事,它牵扯到场次人数的原子性变更、状态的合法流转、以及防止重复预约的多重校验。
2.1 状态机设计:让场的生命周期清晰可见
场次状态这里我用了一个整数类型的status字段,从0到4。整个状态流转是这样的:
- 0(招募中)-> 1(已满员):当
current_players >= max_players时触发。 - 0(招募中)-> 4(已取消):管理员手动取消,或者开场前一定时间内未满员自动取消(这个定时任务不是必须,但做了会很加分)。
- 1(已满员)-> 2(进行中):到达
start_time后,系统自动或在管理员确认后改为进行中。 - 2(进行中)-> 3(已结束):同样可以定时或手动。
- 1(已满员)-> 0(招募中):有用户取消预约,空出一个位置,自动回滚。
预约表的status字段对应关系比较简单:待拼车、已确认、已取消、已完成。需要注意,预约表的“已确认”对应场次表的“已满员”,但也可以约定:只要报名成功就是“已确认”,因为是否满员是场次的状态,不是单个用户预约的状态。我倾向后一种设计,更简单。
这个状态机的核心原则是:状态的变更要集中在后端Service层处理,不能散落在Controller里。我封装了一个方法专门处理状态推进,用一个Map<Integer, List<Integer>>配置好当前状态允许跳转到哪些状态,凡是不在映射表里的流转直接抛业务异常。这样既能防止前端乱传状态值,也方便日后扩展。
2.2 预约与取消的并发控制:不用锁就等着被别人抢座
现在来讲最重要的并发问题。一个热门场次只剩最后一个位置,两个用户同时点击“预约”,如果代码是这样写的:
java复制ScriptSession session = sessionMapper.selectById(sessionId);
if (session.getCurrentPlayers() < session.getMaxPlayers()) {
session.setCurrentPlayers(session.getCurrentPlayers() + 1);
sessionMapper.updateById(session);
}
那在高并发下极大概率会出现:两个线程都读到currentPlayers=4,都判断4小于5,然后都执行+1,最终数据库里是5,场上坐了6个人。
解决这种超卖问题,行业标准做法是“乐观锁 + 条件更新”。在script_session表上加了一个version字段,每次更新时带上版本号:
java复制int rows = sessionMapper.update(
new LambdaUpdateWrapper<ScriptSession>()
.eq(ScriptSession::getId, sessionId)
.eq(ScriptSession::getVersion, version)
.set(ScriptSession::getCurrentPlayers, currentPlayers + 1)
.set(ScriptSession::getVersion, version + 1)
);
if (rows == 0) {
throw new BizException("手速太慢,座位被抢了");
}
这里的关键是eq(ScriptSession::getVersion, version),如果另一个线程已经改了版本号,当前线程的update会更新0条记录,直接抛出业务异常。这就是乐观锁的思路,不加数据库悲观锁,不阻塞读操作,性能好,实现也简单。对毕设项目来说,这个设计足够拿得出手。
预约操作还要加一层事务控制。先插入reservation记录,再更新场次人数。如果第二步失败,第一步必须回滚,不能出现“有预约记录但场次没加人”的数据不一致问题。给Service方法加上@Transactional(rollbackFor = Exception.class)注解是基本操作。
2.3 时间冲突检测:用SQL还是用Java?
房间里同一时间段不能同时开两个场次。这个校验不做,整个系统就是纸糊的。
我当时设计了一个冲突查询方法:给定一个房间ID、开始时间、结束时间,查script_session表里有没有重叠时间的记录。如果存在状态不是“已结束”和“已取消”的场次,就说明时间冲突。
这个检测可以用SQL写,也可以用Java逻辑写。我的建议是:如果场次数据量不大,直接在Java中把该房间所有状态非取消、非结束的场次查出来后逐一比较,代码更清晰;如果数据量大,可以写SQL用时间范围交叉条件:
sql复制SELECT COUNT(*) FROM script_session
WHERE room_id = #{roomId}
AND status IN (0, 1, 2)
AND start_time < #{endTime}
AND end_time > #{startTime}
比较的逻辑其实很简单,两个时间段[start1, end1]和[start2, end2]有重叠的条件是:start1 < end2 AND end1 > start2。这个交集判断规律建议写进项目注释,后期维护的人一看就懂。我当时还做了个更细节的容错:提前15分钟清理房间,所以创建场次时默认把开始时间减去15分钟,结束时间加上15分钟,再拿去查冲突,这样两场之间不会出现“上一场还没走,下一场已经来人”的尴尬。
取消预约的逻辑比预约稍微复杂一点。用户取消时,需要先把reservation记录的status改为已取消,然后在事务里判断场次状态:如果场次之前是“已满员”,现在人数要减一,且状态回退为“招募中”。这里同样要用乐观锁版本号,防止两个取消请求同时到达导致状态覆盖。需要注意的是回退不能直接把status改成0,还要检查当前人数是否真的降到maxPlayers以下,不然会是逻辑漏洞。
2.4 环境配置的几个关键点:JDK版本、pom依赖和三方配置
现在说一下SpringBoot项目的环境选型。我用的是SpringBoot 2.7.18,不是3.x。原因很简单:3.x要求JDK 17,而且很多老教程、老项目代码在3.x下跑不通,比如javax.*包要改成jakarta.*。对毕设和练手项目来说,2.7 + JDK 8是最稳的搭配,教程多、遇到问题能搜到答案。如果你坚持用SpringBoot 3.2以上版本,记得把代码里所有javax.servlet替换成jakarta.servlet,这个坑我见过好几个人踩。
pom.xml里核心依赖就五个:spring-boot-starter-web、mybatis-plus-boot-starter(注意MyBatis-Plus版本要和SpringBoot版本匹配,2.7配3.5.x没问题)、mysql-connector-java(8.0.33)、jjwt(0.9.1用于JWT令牌)、lombok。不用引入Spring Security,太重了,单用一个拦截器校验JWT就够了,新手也更好理解。
数据库连接配置里有一点特别容易踩坑——时区。MySQL 8.0连接串必须带serverTimezone=Asia/Shanghai,不然会报“The server time zone value”错误。还有useSSL=false建议加上,本地开发用SSL没啥必要,反而会拖慢连接速度。这些配置我每次都会写到项目笔记里,防止换电脑环境时重新踩坑。
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/script_kill?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
JWT这块我用了两个工具方法:生成令牌和解析令牌。登录成功后把用户ID和角色塞进token,前端每次请求在Authorization头里带上,后端拦截器解析后放到ThreadLocal里,Controller方法直接从ThreadLocal拿当前用户信息,不用每次从token里手动解析。这里有一个实战小技巧:拦截器只负责鉴权,真正的业务校验(比如预约时确认用户是否被封禁、场次是否还存在)放在Service层处理,这样职责更清晰。
3. 前端不是套模板:Vue页面结构、路由守卫与状态管理
前端部分很多人喜欢直接下载一个现成的后台管理模板,改改颜色和菜单就交差,这种思路做出来的页面跟系统业务是脱节的。我的建议是:管理后台可以适当参考模板,但用户端(也就是玩家用户预约的那套界面)必须自己写,因为玩家端要的是清晰、快,不是功能堆叠。
3.1 路由划分:用户端和管理端分开
我用Vue 3 + Vue Router 4,路由结构分两块。用户端路由有:首页(剧本列表)、剧本详情、场次选择、我的预约、登录注册。管理端路由有:仪表盘、剧本管理、场次管理、预约管理、房间管理。两者通过路由元信息meta: { requiresAuth: true, role: 'ADMIN' }来控制权限。
这里有一个容易忽略的小点:路由懒加载。直接把每个页面组件用() => import('/views/xxx.vue')的方式引入,好处是首屏只加载必要的JS,整个项目打包出来不会一坨大文件。这个优化虽然简单,但面试或答辩时提一嘴“我做了路由懒加载”,会加分不少。路由守卫部分,我用全局前置守卫beforeEach,逻辑很简单:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next('/login')
} else if (to.meta.role && to.meta.role !== localStorage.getItem('role')) {
next('/403')
} else {
next()
}
})
这里有个细节:不要在守卫里直接调后端接口校验token有效性。每次都请求后端,路由跳转会变慢,体验很差。正确做法是:只要前端存在token就放行,后端接口返回401时再统一跳转登录页。我用axios响应拦截器处理401,一句话代码实现全站登录失效跳转。
3.2 状态管理:Pinia存用户信息,不要塞进每个组件
Vue 3项目我选的Pinia(Vuex 4也可以用,但Pinia更简洁,且官方推荐)。核心store建一个userStore,存token、用户昵称、头像、角色。登录成功后一次性把用户信息拉取出来存进store,同时持久化到localStorage。这样刷新页面后,通过store里的初始化逻辑从localStorage恢复状态,不会刷新一下就丢失登录态。
有一个很常见的错误是:把用户信息只存在组件data里,切换路由后组件销毁重建,数据全没了。哪怕有token,页面也不知道当前用户是谁。因此必须在App.vue的onMounted里,或者路由守卫里调一次“获取当前用户信息”的接口,把信息灌入store。千万不要每个页面自己去调,那样白白增加请求次数。
3.3 核心页面拆解:列表、详情、预约弹窗
用户端首页的剧本列表,我推荐用卡片布局。每张卡片显示封面、名称、类型标签、难度星级、人数范围、参考价格。点卡片进入详情页,详情页除了剧本介绍外,最重要的区域是“选择场次”。
场次选择区是用户端的关键交互。我按日期维度分组展示:今天、明天、后天,每个日期下列出该剧本下的场次卡片,显示开场时间、剩余名额、当前人数/总人数、房间号、价格。如果剩余名额为0,则卡片置灰并显示“已满员”。点击“预约”按钮弹窗确认,弹窗里显示当前拼车状态:
- 若未满员,提示“当前为拼车状态,满员后系统确认”
- 若刚好差1人,提示“即将满员,手慢无”
- 若已满员,按钮禁用
这里的前端逻辑并不复杂,主要是后端返回的数据结构要设计好。我后端返回的场次列表VO中,除了场次基本信息,还带上了remainPlayers(剩余名额)、isFull(是否满员)、isReserved(当前用户是否已预约)这三个字段。尤其是isReserved,没有它,用户很可能重复点击预约,前端必须提前禁用“已预约”按钮。
管理端的场次管理页面,核心操作是创建新场次。创建表单需要选择剧本、房间、DM(从用户表里筛选出DM角色的人)、开场时间、人数限制。提交之后,后端在做完基础校验和冲突检测后返回成功。前端这边用一个DateTimePicker组件,设置好disabledDate,让用户只能选择今天和以后的日期,从源头减少后端校验压力。
3.4 Axios请求封装:统一错误处理和加载状态
我要求项目里所有HTTP请求必须走统一的request模块。这个模块做的事情包括:设置baseURL、请求头带token、响应拦截器统一处理业务码和HTTP错误、统一弹出错误提示信息。
javascript复制const service = axios.create({
baseURL: '/api',
timeout: 10000
})
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = token
}
return config
})
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.msg || '请求失败')
return Promise.reject(new Error(res.msg))
}
return res.data
},
error => {
if (error.response && error.response.status === 401) {
localStorage.removeItem('token')
router.push('/login')
}
ElMessage.error(error.message || '网络异常')
return Promise.reject(error)
}
)
这个封装有几个细节值得注意:
- 后端统一返回结构是
{ code, msg, data },code为200才是成功。这样业务异常和HTTP异常分开处理。 - 请求头里Authorization的值不要加“Bearer ”前缀,直接放token字符串,后端解析时就不用处理前缀,简化代码。
- 响应拦截器里成功时
return res.data而不是return res,这样调用方拿到的就是业务数据本身,不用每处都写.data.data。
其实很多学生的前端代码里,每个页面单独写axios请求,错误处理散落各处,体验非常不统一。统一封装这个工作,花30分钟做完,但在答辩时你可以说“设计了统一的前端请求层,提升了代码复用性和错误处理的一致性”,这句话本身就有技术含量。
4. 联调、测试与常见翻车点:前端404、日期格式、跨域
前后端分离项目,单独写后端、单独写前端通常都顺风顺水,一旦开始联调,各种问题就全冒出来了。这一章我把这个项目中我实际遇到过的、以及身边人反复踩的几个典型问题,按排查链路完整写出来,方便你避坑。
4.1 前端请求404:先看他请求到了哪里
前端页面访问后端接口报404,这是我见过最多的问题。排查顺序应该是:
- 浏览器F12打开Network,看请求的实际URL是什么。如果请求的完整地址是
http://localhost:5173/api/login,而后端接口路径是http://localhost:8080/api/login,说明前端的baseURL配置或Vite代理没生效。 - 检查
vite.config.js里的代理配置。我这里用Vite开发服务器代理到后端8080端口。
js复制server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
如果你用的是Vue CLI(即Vue 2的脚手架),对应配置是在vue.config.js里的devServer.proxy,写法大同小异。我见过有人前端开发服务器跑在8080,后端也跑在8080,两个端口冲突导致后端完全起不来,这种低级错误也要留意。
- 如果URL没问题、网络请求也发出了,再看后端控制台有没有报错。404通常是后端没这个路径,检查Controller类上有没有
@RequestMapping("/api"),方法上路径是否拼接正确。千万别忘了类上的路径前缀,这是新手最容易漏的。
4.2 跨域报错:CORS的三种解决办法
开发时如果不用Vite代理,直接让前端请求http://localhost:8080,浏览器就会报跨域错误。解决办法有三类,按推荐程度排序:
- 方案一(开发期推荐):前端配置代理,上面已展示。这个方案的好处是浏览器的请求URL和前端页面同源,跨域问题根本不出现。打包部署后,再用Nginx反向代理把
/api转发到后端服务,同样没有跨域问题。 - 方案二(快速但不够优雅):后端加个全局CORS配置类,允许所有来源跨域。
- 方案三(正式项目推荐):Spring Security框架中配置CORS,但你没引Security,跳过。
我在项目中是开发期用Vite代理,生产部署用Nginx转发,后端不写任何CORS配置。这个做法最干净,也符合前后端分离项目的标准部署姿势。如果你非要在后端加CORS,记得使用@CrossOrigin注解时,通配符*和allowCredentials(true)不能同时使用,否则启动报错。
4.3 后端返回的日期格式跟前端预期不一致
预约系统里时间字段特别多。SpringBoot默认的日期序列化会把LocalDateTime转成数组或者时间戳,前端展示时就显示成一串数字或者[2024, 5, 20, 14, 30]这种难看的东西。解决办法是在application.yml里统一指定:项目里全局统一时间格式。
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
这一个配置就能解决大部分LocalDateTime的格式问题。如果你有LocalDate字段,那要用@JsonFormat(pattern = "yyyy-MM-dd")单独标注。另一个坑是:前端用new Date()对象传给后端,如果序列化格式是ISO字符串(带T和Z),后端字段用String类型接收可能直接弹出格式错误,所以前后端时间传递最好都按“yyyy-MM-dd HH:mm:ss”这个标准字符串来,字段类型直接用String接收,后端需要比较时再解析成LocalDateTime。
4.4 登录后刷新页面就退出,或不刷新但点其他页面就跳登录页
出现这种问题通常有两个原因。一是前端没有在刷新后恢复用户信息,只在登录成功时把用户信息存到了内存store里,一刷新内存清空,路由守卫里虽然看到有token,但用户信息为空,某些依赖用户信息的页面就报错。解决办法是store里加一个fetchUserInfo方法,在应用启动时调用一次,从localStorage拿到token后再请求后端获取用户信息。
二是后端拦截器把获取用户信息的接口也拦截了,而这个接口又没有正确解析token。要特别留意拦截器路径配置,/api/user/info这种接口必须放行或者允许通过token解析用户,不能拦截后直接返回401。我自己常用的配置是:登录注册接口放行,其他所有/api/**都走拦截器,但拦截器里如果解析token失败,返回401时前端要做统一跳转。这样就形成了完整的闭环。
4.5 预约成功但人数没加,或加了表格不刷新
这个问题通常不是并发,而是前端缓存问题。预约成功后,我调用了sessionStore里更新场次列表的方法重新请求后端接口,而不是简单修改当前页面的局部数据。这么做最稳,因为后端返回的数据经过了全部校验,是准确的。
如果你发现预约成功后返回列表时,座位数变化了但状态颜色不对,这是前端判断条件写错。前端判断满员时,不要用currentPlayers === maxPlayers这种相等判断,因为人数可能因为取消操作变成maxPlayers - 1,正确判断是currentPlayers >= maxPlayers。这个等号问题看着小,但极其容易在边界情况翻车。
4.6 部署后首页打不开
本地一切正常,打包后部署到服务器就白屏。排查路径:先F12看控制台报错,如果是资源加载404,大概率是打包路径问题。Vue项目打包后默认资源引用路径是绝对路径/js/xxx.js,部署到服务器某个子路径(如/script-kill/)下就会404。解决办法是在vite.config.js里设置base: './',这样打包后资源引用是相对路径。
还有一个常见问题是后端接口的跨域。部署后如果前端静态页面跟后端不在同一域名,同样会有跨域问题,此时最好是把前端打包产物放到后端的src/main/resources/static目录下,或者用Nginx把前端静态资源和/api反向代理到同一路径。我一般用后者,因为前后端分离的部署方式更适合后续扩展。
5. 从及格到优秀:可扩展模块与项目亮点提炼
很多人的毕设做完能跑就停手了,但如果想在答辩或作品集中拿到高分,建议在以下几个方向做适度增强。这里说的不是堆功能,而是做那些能体现你思维深度的模块。
5.1 拼车邀请码:把社交属性做进系统
剧本杀天然是熟人社交或半熟人社交活动。一个玩家预约了某个场次,最希望的是拉上朋友一起来。你可以给每个场次生成一个6位邀请码,用户预约成功后在“我的预约”页面看到“分享拼车码”,朋友输入邀请码也能快速加入这个场次。
实现并不复杂:在script_session表加一个invite_code字段,创建场次时随机生成。预约接口支持一个可选参数inviteCode,如果传了,则自动加入对应场次。这个功能在答辩时非常亮眼,因为大多数人的系统只有“一个人自己约”,没有多人拼车的互动体验。
5.2 微信通知、短信通知:可选但不建议在毕设中硬做
预约成功、拼车满员、即将开场这些节点,如果能通知用户,体验会好很多。但微信模板消息需要公众号或小程序资质,短信通知需要购买服务,这些在实际毕设环境中都有门槛。我可以提供一个降级方案:系统站内信。在数据库里建一张notification表,预约成功后往表里插入一条通知记录,用户登录后首页消息图标显示红点,点击能看到“您的拼车已满员,请准时到场”等通知。
这个功能纯前后端CRUD,没什么技术挑战,但能体现你考虑到了业务闭环。
5.3 数据看板:管理端的统计图表
管理端仪表盘加几个数据看板:今日预约量、本周营收(按订单金额汇总)、热门剧本Top5、场次满员率。前端用ECharts展示柱状图和折线图,后端写几个聚合查询SQL。这些统计功能不复杂,但能证明你不仅会写增删改查,还能做数据分析和可视化展示。有一个SQL可以重点准备:热门剧本Top5其实就是按script_session关联预约记录后group by剧本ID再count,按count倒序排。
5.4 项目文档与答辩亮点的两三句话
答辩时不要一张一张念页面截图,而是要讲设计决策。我这套系统里你可以重点讲三个点:
- 乐观锁解决拼车并发超卖问题。这句话直接点出你考虑到了高并发场景下的数据一致性。
- 场次作为中间层模型,将玩家预约、房间时间、剧本资源三者解耦。这句话说明你有业务抽象能力。
- 前端统一请求层设计,二次封装axios实现全局错误处理和登录态管理。这句话展示你的工程化意识。
这三句话每句都能展开讲几分钟,比“我用了SpringBoot和Vue做了一个管理系统”这种干瘪介绍强太多。记住,老师要听的不是你用了什么框架,而是你面对具体问题时怎么思考、怎么做决策。
写在最后的项目建议
如果让我重新做一遍这个题目,我会先花一个晚上把业务场景梳理成流程图:玩家浏览剧本、选场次、预约拼车、满员确认、到店开本、结束评价。然后决定哪些环节这个版本必须做,哪些可以后续扩展。至于代码,反而是比较机械的部分,因为你已经把最复杂的业务逻辑想清楚了,代码只是在实现一个清晰的规划。
落地的过程中,后端表结构建议建好之后先写一段测试数据,用SQL批量插入几个剧本、几个房间、未来三天的场次,这样前端联调时才有数据可看。前后端联调不要等全部写完再开始,我习惯后端先把登录、剧本列表、场次列表、预约、取消预约这五个核心接口写完,前端同步开始写页面,两边对好接口文档后直接对接,效率最高。最后祝你的项目顺利跑通,如果这篇文章里的某个方案帮你少踩了一个坑,那我也算没白写。
