ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot + Vue全栈实战:构建剧本杀门店预约与管理系统

Spring Boot + Vue全栈实战:构建剧本杀门店预约与管理系统 1. 项目概述与选题思路做这个项目的起因是我常去的那家剧本杀店老板手上有三个本子靠Excel排期周末场次全靠群聊拼车等座的玩家在门口干着急。我问他为什么不上一套管理系统他说市面上的门店收银系统偏零售剧本杀这种“按场次卖座位”的玩法套不进去。这个需求缺口正好适合拿来做一套前后端分离的完整项目技术栈也成熟稳定——后端Spring Boot前端Vue数据库MySQL认证走JWT。这个项目适合三类人看一是想学Spring Boot Vue全栈开发、需要一个完整业务链路练手的初学者二是准备把这个题目作为毕业设计或课程设计需要从需求分析做到部署上线的在校生三是确实在经营剧本杀店、想低成本搞一套门店管理工具的小老板。不管你是哪类这个系统覆盖的知识点都挺全权限管理、主子表CRUD、文件上传、订单防并发、前台展示与后台管理分离。系统整体上解决三个核心问题第一剧本、场次、订单这三类核心数据的统一管理第二玩家在线预约座位门店人员店长、DM审批和排班第三经营数据的简单统计——每天卖了多少场、哪些本子最受欢迎、哪个时间段上座率最高。这个逻辑听着不复杂但真正从零写起来前后端加起来大概要二十几张表、三四十个接口能把整个流程跑顺对Spring Boot和Vue的理解会上一个台阶。我在设计阶段就明确了一个原则不追求功能炫技但每个模块都要解决一个真实痛点。比如玩家端不要做复杂的会员积分体系但一定要有“正在拼车的场次展示”和“一键预约”的功能因为这两个是门店老板实际使用频率最高的入口。围绕这个思路技术选型就顺理成章了。2. 技术选型与项目架构拆解2.1 为什么选Spring Boot Vue这套组合现在做管理系统前后端分离基本是默认方案。Spring Boot胜在生态成熟、社区资料多、坑少适合绝大多数Java开发者上手Vue则因为轻量、上手曲线缓、组件化开发效率高在前端三巨头里最适合单人或小团队快速交付。组合在一起后端专注提供接口前端专注页面交互两边可以并行开发联调成本也不高。有一个容易被忽略的点是Spring Boot的自动配置机制和Vue的脚手架工具能很大程度降低项目初始化成本。后端一个spring-boot-starter-web就能把内嵌Tomcat、JSON序列化、参数校验全部搞定前端vite或vue-cli初始化项目后vue-router配路由、pinia或vuex管状态、axios发请求都是在同一个开发生态里顺手的事。对做毕设或练手的同学来说你不需要花大量时间在环境搭建上可以把精力集中在业务逻辑和数据处理上。我在实际开发中选择的技术组合是Spring Boot 2.7 MyBatis-Plus 3.5 MySQL 8.0 Redis做缓存和防重 Sa-Token或Spring Security权限控制二选一。如果你是新手我更推荐Sa-Token配置简单、中文文档友好几行代码就能实现登录认证和角色权限控制如果项目要求里明确写了Spring Security那也别慌套路是一样的就是过滤器链和认证管理器的配置稍微绕一些。前端选了Vue 3 Element Plus Axios Pinia这套组合在社区里的最佳实践很多遇到问题基本都能搜到现成方案。2.2 业务模块与角色权限设计权限设计是管理系统的地基剧本杀店的业务场景天然适合做多角色权限。我把系统分为四个角色每个角色看到的功能菜单和能调用的接口是隔离的。第一个是系统管理员管的是账号和全局配置——哪个店员能登录、剧本分类怎么维护、平台参数怎么设。第二个是店长管的是核心经营数据——剧本上架下架、场次发布、订单查看和统计。第三个是DM主持人管的是执行层面的东西——查看自己分配的场次、确认开本状态、提交场次总结。第四个是玩家/前台玩家在小程序或H5里浏览场次、预约座位、查看订单前台可以代客下单。角色之间是树形上下级关系所以接口权限不能只靠前端隐藏菜单来完成。后端的做法是在JWT令牌里写入角色标识Spring Security的拦截器在进行接口访问校验时通过注解比如PreAuthorize(hasAuthority(ROLE_STORE_MANAGER))来判断当前用户是否有权限访问这个接口。前端再根据登录用户的角色动态生成路由表只渲染当前角色有权限的菜单。这样前后端双重校验才算是比较完整的权限闭环。这里有个经验要分享不要在一开始就把权限设计做得特别复杂比如搞部门、岗位、数据权限那一套。剧本杀店场景下角色权限是固定且清晰的直接写死资源-角色映射关系即可等你以后业务复杂度上来了再重构也不迟。过度设计是新手最容易踩的坑既浪费时间又增加了出bug的概率。2.3 项目目录结构与后端分层规范前后端项目结构一开始就要定好规范不然代码越写越乱。我习惯把项目分成backend和frontend两个根目录后端用Maven管理前端用npm管理。后端代码采用经典的三层架构——controller层接收请求和参数校验service层处理业务逻辑mapper层操作数据库。这里重点说一下后端包结构的划分直接抄作业即可。com.example.dms下分controller、serviceimpl、mapper、entity、dto、vo、config、common、utils这几个子包。entity对应数据库表dto是接收前端参数的传输对象vo是返回给前端的数据封装。前期多花十分钟建好这个目录层级后面写几十个接口的时候就会庆幸当初没偷懒。前端目录也一样src下面按api、assets、components、router、store、utils、views组织。pages下再按角色模块建子目录比如admin、store、dm、player每个模块内部的页面组件就近管理。这样前后端结构清晰一个人开发也好两人协作也好找文件都很快。项目的可维护性往往不是靠后期重构来的而是靠一开始的目录规范打底。3. 数据库设计与核心业务流程3.1 核心数据表设计与关联关系数据库设计是整个系统的地基表结构不合理后面写接口全是泪。剧本杀门店管理系统的核心表我按业务域分成了四组。第一组是用户域用户表、角色表、用户角色关联表。第二组是商品域剧本表、剧本分类表、剧本场次表、房间表。第三组是交易域订单表、订单明细表。第四组是运营域场次评论表、操作日志表。这里挑几张关键表聊一下设计思路。剧本表script包含的基本字段有主键id、剧本名称、题材类型、人数范围minimum_players和maximum_players、时长分钟、难度等级、封面图URL、剧本简介、价格人均单价。场次表session是连接剧本和玩家的枢纽包含的字段有剧本ID、房间ID、DM用户ID、开始时间、结束时间、当前已拼人数、状态招募中/已满员/已开始/已结束。订单表order我用了order_info单独建表字段包括场次ID、购买用户ID、购买数量、总金额、订单状态、创建时间。之所以要把订单和场次拆开是因为一个场次可能对应多个订单——比如8人本A玩家订了2个座位B玩家订了3个座位这两个订单都要记录同时场次表的已拼人数要累加。在设计索引时我给session表加了(start_time, status)组合索引因为业务上最常见的查询是“某天哪些场次还能预约”给order_info加了(user_id, create_time)组合索引支撑个人订单列表查询。很多新手容易忽略索引设计实际数据量一上来全表扫描会直接把接口拖垮。对毕设场景来说数据量不大但这套思路还是要养成的。3.2 场次预约与防并发设计预约功能是这个系统里含金量最高的模块它涉及一个并发问题8人本只剩2个座位两个玩家同时下单怎么保证不会超卖这是面试官最喜欢问的点也是实际开发中真正需要处理的问题。最基础的方案是在座位扣减时使用乐观锁。给session表加一个version字段每次下单时先查询当前拼团人数UPDATE语句写成这样UPDATE session SET joined_players joined_players #{count}, version version 1 WHERE id #{sessionId} AND version #{oldVersion}affected rows大于0说明更新成功等于0说明版本号冲突需要回滚提示“手慢了座位被抢了”。这是比较稳妥的数据库层面防并发方案。但我在实际项目中叠加了一层Redis缓存来减轻数据库压力。场次详情是高频读接口很多玩家进来会反复刷新场次列表每次都查MySQL显然不划算。做法是把正在招募中的场次列表和可预订数量缓存在Redis里以场次ID为key下单时用Redis的decr操作预扣数量预扣成功后再异步写订单。这种二级缓存的方案即使遇到秒杀场景也能顶住压力。还有一个细节下单时要校验这个场次的状态是不是“招募中”以及当前用户是否重复下单防止玩家手滑点了两次。这些业务校验最好都放在事务里配合数据库唯一索引user_id session_id形成兜底确保数据一致性。3.3 完整下单链路梳理我把整个下单流程走一遍方便你理解各模块是如何协作的。玩家在前端选择场次点击“开始预约”前端弹出确认弹窗显示场次信息剧本名、时间、房间、剩余座位数和需要预约的人数。点击确认后前端调用后端POST /api/order/create接口提交的JSON里包含sessionId和buyCount。后端收到请求后先解析JWT获取当前登录用户ID然后校验场次是否存在、是否处于可预约状态、剩余座位是否充足、当前用户是否已经预约过。校验通过后进入座位扣减环节——Redis先预扣再更新MySQL场次表的已拼人数同时插入订单记录。订单状态初始为待支付我这里简化了支付流程线下支付后前台确认即可整个操作加Transactional事务注解包裹。如果全部成功返回订单ID给前端玩家端“我的预约”列表就能看到这个订单了。如果任何一个环节失败统一抛出业务异常全局异常处理器捕获后返回友好错误提示。整个链路走下来你会发现业务的闭环在于各模块之间的数据流转而不仅仅是某个接口写得好不好。4. 前端核心实现与Vue组件化开发4.1 Vue Router动态路由与权限控制前端部分最重要的一个工程细节是动态路由。不同角色登录后看到的菜单不一样玩家看到的是“浏览剧本”“我的预约”店长看到的是“剧本管理”“场次管理”“订单管理”。如果用静态路由表写死所有页面用户其实可以通过URL地址直接访问没有权限的页面这在体验和安全性上都不合适。我的做法是在router/index.js中把不需要登录就能访问的公共路由登录页、首页单独配置业务页面组件全部采用动态import写法实现懒加载{ path: /order/list, name: OrderList, component: () import(/views/order/OrderList.vue), meta: { title: 订单管理, roles: [ROLE_STORE_MANAGER] } }登录成功后前端拿着后端返回的角色信息遍历完整的菜单路由表过滤出当前角色有权限的路由通过router.addRoute方法动态挂载。这样即使有人手动输入URL因为对应路由还没有被添加也只会进入404页面。在具体实现中有个容易踩的坑刷新页面时路由会重置因为Vuex/Pinia里的状态会丢失需要对登录态做持久化。我的方案是把用户信息存一份在localStorage里页面刷新后在路由守卫beforeEach中重新从localStorage读取用户信息调用router.addRoute重新注册动态路由。这一步忘了的话所有人刷新后都会跳到登录页或者404属于高频问题要特别留意。4.2 Axios封装与统一错误处理前端请求后端的第一个门面是Axios。所有请求都通过axios实例发出去所以非常适合在这里做统一处理。我在utils/request.js里创建axios实例设置baseURL为/api设置了请求超时时间10秒请求拦截器里从localStorage获取token并加到请求头中。响应拦截器的核心逻辑是后端返回的结构统一为{ code, message, data }code为200时认为是成功取出data返回给页面code为401时说明token过期或未登录跳转到登录页并清除本地数据code为其他值时用Element Plus的Message提示错误信息。这里分享一个经验后端返回的状态码和HTTP状态码不要混着用。建议HTTP状态码保持200业务成功与否用body里的code字段区分。因为Tomcat默认会对4xx、5xx状态码做日志记录测试人员看到一堆404、500日志容易误以为出故障了。用业务状态码的方式日志更干净排查问题时也更容易定位。4.3 后台管理界面的核心页面拆解后台管理的核心页面可以拆成四屏剧本管理、场次管理、订单列表、经营看板。剧本管理页面是标准的CRUD表格使用Element Plus的el-table加el-pagination顶部是搜索表单按剧本名称、题材类型过滤右侧是新增按钮。编辑和新增共用一个Dialog弹窗里面用el-form做表单校验封面图用el-upload上传组件选择图片后走后端文件上传接口返回URL赋值给表单字段。场次管理页面相对复杂一些因为新增场次时要选择剧本、选择房间、选择DM、设置开始时间和预计结束时间。这里有个联动逻辑选择了剧本后前端根据剧本的人数范围和时长自动带出“人数上限”和“预计时长”两个字段。新增场次后场次列表按时间轴展示当天的所有场次不同状态用不同颜色标签标识。订单管理页面是店长最常用的页面实现上就是一个订单列表加上筛选条件按日期、订单状态、剧本名称点击订单行可以展开查看详情。经营看板页面用ECharts画柱状图和饼图展示近7天销售额、各题材剧本售卖占比、各时间段上座率。前端通过调用后端统计接口拿到聚合数据交给ECharts渲染。如果后端是新手统计SQL可能会写得比较痛苦我建议直接上MyBatis-Plus的queryWrapper配合groupBy或者写原生SQL时用if标签做动态拼接。5. 系统亮点功能与实现细节5.1 文件上传与图片管理方案剧本封面、房间照片这些都是图片资源怎么管理是个实际需求。方案有很多种——上传到本地服务器磁盘、上传到阿里云OSS、上传到七牛云。对非专业的毕设项目来说我推荐先把文件存到本机指定目录数据库里只存访问路径。这样不仅在开发环境运行简单部署到Linux服务器也容易理解。实现方式是后端写一个FileUploadController接收MultipartFile校验文件大小不超过5MB和类型jpg、png生成UUID文件名调用transferTo方法保存到配置文件指定的目录。访问静态资源时在application.yml里配置资源映射spring: mvc: static-path-pattern: /upload/** resources: static-locations: file:D:/upload/Windows上的路径要注意反斜杠的转义Linux上直接配置绝对路径即可。在实际部署时我用Nginx把所有/upload路径的请求直接映射到物理目录这样静态资源的访问就不经过Tomcat效率更高。5.2 基于Spring Boot Actuator的轻量监控项目做完之后我发现排查线上问题还是有点被动的。系统运行是否健康、最近有没有报错这些信息需要主动去查。于是我在项目里集成了Spring Boot Actuator这个Spring Boot自带的功能模块可以暴露一系列运维接口比如health服务健康状态、metricsJVM内存、线程信息、loggers动态调整日志级别。开启方式很简单pom.xml加spring-boot-starter-actuator依赖application.yml里配置暴露端点。我实际开放了health和info两个端点通过Micrometer对接了简单指标上报。对毕设答辩来说能讲清楚“集成了Actuator做服务健康监控”绝对是个加分项。而且面试时Spring Boot actuator也是高频考点项目中真实用过和只看过博客聊起来的感觉完全不一样。有一点必须提醒Actuator端点在生产环境暴露是有安全风险的必须配合Spring Security做访问认证至少不应该把shutdown这类危险端点暴露出来。我在配置里显式设置了只暴露health、info、metrics三个安全端点并加了访问路径前缀这个细节如果你能在答辩或面试中主动说出来会显得考虑问题非常全面。5.3 数据库自动备份与恢复策略数据是门店经营的核心资产万一数据库出问题没有备份老板真的要哭。我在项目中添加了一个简单的自动备份机制利用Spring的定时任务Schedule定时执行mysqldump命令。配置文件里设置备份目录和备份频率每天凌晨三点自动备份一次保留最近七天的备份文件。核心实现是在config包下开启EnableScheduling写一个DatabaseBackupTask类方法上标注Scheduled(cron 0 0 3 * * ?)方法内部用ProcessBuilder调用本机的mysqldump命令把备份文件输出到指定目录。这个功能虽然实现起来不复杂但实用性很强也是期末答辩时一个很好的落地亮点。这里要专门提醒一下mysqldump一般是MySQL安装目录下的可执行文件在Java里调用时记得用绝对路径或者在application.yml里配置命令路径否则很容易出现A fatal error: mysqldump command not found这个经典错误。我在本地开发环境中就踩过这个坑。6. 开发环境配置与项目部署上线6.1 Java、Maven、Node.js环境搭建要点先把环境这件事说透。安装Java JDK时最容易出问题的是环境变量配置JAVA_HOME指向JDK安装目录PATH里加上%JAVA_HOME%\bin很多人漏了这一步或者配置完没重新打开终端导致java -version报错。JDK版本我推荐8或11Spring Boot 2.7两者都支持生产环境也更稳定。Maven的settings.xml建议配一个国内镜像仓库阿里云镜像就行否则首次拉取依赖会等到怀疑人生。Node.js环境更简单装上之后npm自带了注意npm的registry也建议改为国内镜像npmmirror能解决大部分前端依赖安装慢或失败的问题。用IDEA打开Spring Boot项目后右键pom.xml选择Maven的reload即可拉取依赖。前端项目用命令行cd到frontend目录依次执行npm install和npm run dev。有一点容易踩坑如果IDE的终端用的还是旧版本的node或npm需要检查node -v版本是否和项目要求一致Vue 3要求Node 14.18及以上。6.2 Nginx反向代理与前后端联调上网本地开发时前端Vite dev server跑在端口5173后端Spring Boot跑在8080跨域问题不可避免。最简单的解决方案是利用Vite的代理功能在vite.config.js中配置server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端所有/api开头的请求都会被代理到后端不涉及跨域后端也无需额外配置CORS。这个方法干净利落我强烈推荐。生产部署时前后端分开部署Nginx做反向代理。前端Vue项目执行npm run build打出的dist目录上传到服务器某个目录nginx配置大致是这样的80端口监听root指向dist目录location /api/下面的请求反向代理到本机8080端口的Java进程。这样浏览器访问Nginx拿静态资源动态接口由Nginx转发给后端整个链路就很清晰了。这里有一个常见的坑是history模式路由刷新404问题。Vue Router如果用history模式刷新页面时Nginx会去磁盘找对应的路径找不到就404了。解决办法是在Nginx配置里加fallbacklocation / { try_files $uri $uri/ /index.html; }这行配置的含义是找不到对应文件时统一返回index.html让前端路由接管页面渲染。如果你用的是hash模式就没有这个问题但URL里会多一个#号观感稍差。6.3 Maven打包与Linux服务器部署实操后端部署的流程是在IDEA的Maven面板里执行package命令会生成一个target/xxx.jar文件。Linux服务器上如果装了Java环境直接执行java -jar xxx.jar就能启动但为了让进程在后台稳定运行我推荐用nohup方式nohup java -jar /opt/script-server/script-server.jar --server.port8080 /opt/script-server/logs/app.log 21 日志输出到文件配合tail -f logs/app.log实时查看启动情况。如果Spring Boot启动失败先看日志绝大部分问题都能在启动日志里找到明确原因。MySQL的连接地址在部署时要改一下不能用localhost了改成服务器的内网IP或真实IP。数据库初始化方面我用了Spring Boot的SQL初始化功能把schema.sql放在classpath下配置spring.sql.init.modealways首次启动时会执行建表SQL。如果是已有数据用mysqldump导出一份登录服务器后执行source命令导入即可。整体的上线流程梳理一下服务器装JDK、MySQL、Nginx前端打包上传dist目录到/usr/share/nginx/html后端打包上传jar包到/app目录Nginx配置好反向代理启动Java进程浏览器访问服务器IP就能看到系统首页了。从开发到部署的完整闭环就这么跑通了。7. 常见问题与排查技巧实录7.1 跨域问题与前后端联调典型场景前后端分开跑跨域是最容易出现的问题。配置了Vite代理还报CORS错误先看看报错信息是浏览器层面的拦截还是后端抛出的异常。有时候前端直接在浏览器地址栏访问后端地址以为能通结果因为端口不一致出现了跨域。排查思路是先看浏览器Network面板确认请求有没有发出去如果发出去了再看响应头里有没有CORS相关字段如果没有就是后端没配置或者代理没生效。7.2 数据库连接与SQL执行问题速查我在开发中整理了一个排查小表格分享给你。问题表现可能原因解决方案Failed to configure a DataSource未配置数据源或yml配置错误检查application.yml的url、username、password确认MySQL已启动Access denied for user数据库用户名或密码错误用命令行工具测试连接确认授权Unknown database库名填错先在MySQL客户端创建对应名称的数据库Table not found实体类和表名不一致检查TableName注解或建表SQLDuplicate entry for key唯一索引冲突检查业务是否重复插入或清理测试数据中文乱码连接字符集不一致jdbcUrl后加useUnicodetruecharacterEncodingutf87.3 权限验证与路由拦截的坑权限这块最容易出现的问题是明明登录成功了但访问某个接口一直返回401。常见原因是JWT生成和解析时用的签名密钥不一致导致解析出来的token无效。排查方法是把token打印出来在JWT工具类的解析方法里看具体报错是过期还是签名不一致一目了然。还有一类问题是前端动态路由加载了但页面还是空白控制台报No match for path这种基本是addRoute的顺序问题。Vue Router注册完动态路由后需要调用next({ ...to, replace: true })重新触发一次导航否则当前这次路由匹配还是按照旧的静态路由表来解析就会找不到路径。7.4 数据统计页面的SQL性能优化心得经营看板页面的统计SQL一开始我是让前端传日期范围然后三个图表分别请求三个接口每个接口都查一遍数据库。数据量小还好数据量一大性能明显下降。后来我优化成一次性查出聚合数据在Java内存里拆分成三个消费方需要的数据结构。基础的统计SQL别整太复杂尽量用简单的group by加聚合函数复杂的指标在代码里二次计算这样维护性和性能都更好。还有一个值得注意的点日期范围跨度大的统计如果没有索引全表扫描会很慢。应对办法是把订单表中的下单时间字段加了索引如果数据量特别大还可以考虑按月做分区存储。对当前场景来说索引已经足够但知道有这样一个演进方向在面试时能聊出深度。8. 经验总结与扩展建议最后分享一些个人体会。这个项目做完我最深的感受是一个管理系统能不能真正用起来关键不在于功能堆得多满而在于每个核心业务流程是否闭环、交互是否符合实际使用习惯。比如玩家预约剧本这个事门店老板真实需求是减少沟通成本而不是看华丽的动画效果那前端做得简洁清晰就好但订单和场次的数据必须实时准确因为门店要靠它做调度。后续扩展空间也比较大。第一是引入工作流审批门店的场次上新、请假调休可以走多个环节审批第二是接支付剧本杀门店线下支付居多但线上订金是趋势微信/支付宝下单支付可以与订单流程打通第三是接一个小程序端用uni-app或Vue 3重新包一层客人在微信里就能完成预约会比H5体验更好第四是把经营分析做成周报/月报自动发送减少门店人员每天盯后台统计数据的精力。如果你正在做类似的项目我的建议是别急着上来就写代码先花一天画清楚用户角色和核心业务表格写后端接口时多留意参数校验和异常处理这会让联调顺畅很多前端页面每个流程自己先走一遍以玩家身份真实预约一次以店长身份审核一次以系统管理员身份配置一次跑通了再考虑加新功能。这些习惯比任何单项技术都值得养成。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进