每年三四月后台就会涌来一批问毕设选题的消息。看了太多“超市管理系统”“图书管理系统”之后我通常建议换个更聪明的赛道做SpringBoot博物馆预约管理系统。这名字看着普通实际它是标准的基于SpringBoot的文博场馆分时预约平台再往深走一步就是SpringBoot驱动的智慧博物馆访客预约与票务系统。一个项目业务链条完整、技术点有得讲、演示效果好非常适合作为计算机毕业设计。这套系统解决的是线下文博场馆最头疼的三个问题人流不可控、排队体验差、运营数据缺失。相应地它要求你实现面向用户的健康分时预约、面向管理员的场馆与场次配置、预约码的发放与核销、以及每日入园数据的统计。技术侧以SpringBoot为底座数据层用 MySQL持久层用 MyBatis-Plus前端配 Vue做完以后既能答辩又能写进简历。本文会把整套系统的设计思路、表结构、核心代码和踩坑记录都摊开讲从零开始带你把项目落地。1. 项目定位与整体设计为什么这套系统能拿毕业设计高分1.1 需求拆解从标题的三层含义看项目边界标题里三个说法其实不是三套系统而是一个系统的三种切入角度。“计算机毕业设计”是身份定位说明它需要完整的业务闭环和技术栈“文博场馆分时预约平台”是业务核心——按时间段控流、凭预约入场“智慧博物馆访客预约与票务系统”则强调了数字化运营侧的能力包括预约实名制、电子凭证、执行数据统计。把这三个点合成一句话访客在线上选择场馆、日期和时段完成实名预约系统按时段控制预约总量并生成电子预约码馆方在后台配置放票策略并扫码核销所有数据沉淀后用于统计与决策。顺着这个定位去拆功能系统可以分成双端四个模块。用户端做注册登录、浏览场馆与展览、选日期时段预约、查看我的预约、取消预约管理端做场馆维护、展览维护、时段容量配置、预约订单审核与核销、数据统计。第四块是公共能力包括JWT登录鉴权、统一异常处理、身份证与手机号校验、定时任务处理过期订单。注意一点分时预约和票务在这个系统里的关系是预约成功即生成一张电子票票的状态跟随预约订单的状态流转。免费场馆放“0元票”收费特展可以预留金额字段和支付状态位这样“票务”的题眼就立住了。毕业设计选题最怕什么怕需求空洞怕没有业务张力。预约系统天生自带“并发”“限流”“状态机”这些可以讲深的话题这在答辩时是实打实的加分项。哪怕你的功能实现得很朴素只要能把“如何防止同一时段超卖”讲明白评委就很难给你低分。1.2 技术选型权衡SpringBoot为底座为什么不用别的整套系统的后端基础是SpringBoot这并不是因为它热门而是因为它在“开发效率”和“可解释性”之间取得了最舒服的平衡。SpringBoot天然内置了Tomcat、Spring MVC和自动装配体系只要引入starter依赖就能快速把Web能力跑起来。如果你把配置类拆开给学生看他们会发现之前SSH时代需要手写一堆XML配置的东西现在几行注解就搞定了这就是自动装配的价值——SpringBoot在启动时会根据classpath中的依赖和配置动态注册Bean你在pom里引入mybatis-plus-boot-starter它就知道要去装配SqlSessionFactory你在application.yml里写了redis配置它就知道要创建RedisTemplate。用不用Spring Cloud、Dubbo这类重框架我的建议是不用。毕设项目的核心是业务完整性和技术亮点不是分布式表演。单体应用加上合理的分层架构已经足够撑起整个系统。模块划分按经典的controller-service-mapper三层走再加一个config包放各类配置、一个common包放统一返回结果和异常处理结构清晰就是很好的架构。持久层选MyBatis-Plus也有一些细节考虑。它和原生MyBatis相比最大的优势是内置了通用的CRUD方法简单的单表查询不需要写XML同时它保留了我最需要的能力——自定义SQL和动态SQL。预约扣减余票那种带条件更新的语句用它的Wrapper或者直接写注解SQL都很顺手。ORM层面我不推荐在毕设里上JPA因为自定义复杂查询的灵活度差一些而且大多数网上的SpringBoot毕设教程都走MyBatis系列遇到问题也更好查资料。前端方面Vue是稳妥选择。如果你跟着B站或CSDN的教程走Vue2加Element UI的资料最全坑最少如果想吃新也可以上Vue3加Element Plus。只要接口约定好选哪套对后端没有影响。前端打包后的dist目录放进SpringBoot的static资源目录这是毕设最常见的部署方式我在第4部分会专门讲这一步怎么操作。2. 数据库建模与分时预约核心表设计2.1 六张表的角色划分与字段细节数据库设计决定了项目能撑到多深。我的方案是六张核心表加一张字典表简单直接答辩也容易讲。用户表保存账号和身份信息场馆表保存开放时间、每日容量、地址等基础资料展览表挂在场馆之下负责展示具体展览信息时段表是整个系统最关键的表它存储“某场馆在某日期的某个时间段”的可预约容量预约订单表保存每一次预约的完整信息新闻表是锦上添花的内容模块用来体现系统信息发布能力。用户表字段我建议至少这些id、username、password、real_name、phone、id_card、rolerole用0和1区分普通用户和管理员。密码存MD5或BCrypt毕设阶段用MD5加盐也够用但用了BCrypt可以在答辩时说是安全考虑。手机号和身份证都需要后端校验手机号用正则身份证按18位GB 11643标准校验——这部分可以做成工具类能体现你的工程素养。场馆表和展览表是管理端维护的数据主体。场馆表里有open_time和close_time字段例如09:00到17:00每日最大容量max_capacity字段则是默认放票总量。展览表通过venue_id关联场馆补充展览名称、简介、封面图、起止日期等字段。如果一次预约事件同时关联场馆和展览订单表里两个外键都会有——不过实际取证中很多博物馆的预约只是进馆不一定绑定特定展览所以我把展览设置为可选关联表结构上保留exhibition_id允许为空。预约订单表的字段要说得细一点reservation_date存预约参观的日期比如2025-05-20slot_id关联时段表status是订单状态用int类型方便扩展ticket_code放预约码id_card放实名信息用于入馆核验。另外加上create_time、update_time、cancel_time等时间戳字段。每个字段为什么存在、在什么场景被用到答辩时都应该说得出来这比背八股文强得多。2.2 时段容量模型分时预约的核心是怎么设计出来的分时预约的落点是时段表。它的设计思路是把场馆一天的开放时间切分成多个固定时段每个时段单独计算容量。拿一个9点到17点开放的场馆举例按2小时一段可以切出4个时段09:00-11:00、11:00-13:00、13:00-15:00、15:00-17:00。每个时段在数据库里对应一条记录包含venue_id、start_time、end_time、max_people和remaining字段。remaining是当前剩余名额这个字段是整个预约扣减逻辑的抓手。至于每天生成哪些时段的记录两种方案各有利弊。第一种是提前一次性生成未来N天的时段数据比如管理员配置好时段模板后系统先生成未来30天的记录。优点是查询时段余票时直接走单表查询速度快缺点是需要定时任务去补数。第二种是不做物理时段记录查询时按模板动态计算优点是省事缺点是并发查询下没有统一的数据入口扣减逻辑不好做。我推荐第一种因为预约系统天然需要“日期时段”作为可售资源把可售资源物化成数据表字段是确保扣减正确性的最好方式。时段表里还有一个容易被忽略的字段版本号version。这不是业务字段而是为乐观锁准备的。当两个用户同时抢最后一个名额时如果没有版本控制最终数据就可能超卖。扣减SQL从“update set remaining remaining - 1 where id ?”变成“update set remaining remaining - 1, version version 1 where id ? and remaining 0”就是一个标准的乐观锁保护。我后面在代码部分详细展开。2.3 状态机设计一张订单从生到死的完整流转预约订单一定不是简单的新增和删除它要经历多个状态。用int字段来定义0表示待参观1表示已核销2表示已取消3表示已过期。待参观是预约成功后、还未到入场时间已核销是到现场扫码验票完成已取消是用户在参观前主动取消已过期是预约日期过去了但用户没有入场。再加一个支付状态位pay_status0和1表示未支付/已支付。常设免费展的票务价格为0pay_status直接置1收费特展则走支付流程这个系统预留了扩展点。状态流转的规则要认真设计。待参观可以转已核销也可以转已取消已取消不能转回待参观已核销不能再取消。用户取消预约时必须检查状态只有待参观状态才能取消。取消之后要立刻把时段表的remaining加回去——这一点非常关键如果只改订单状态而没有恢复余票后续用户会发现明明没人约却显示满员。定时任务负责待参观到已过期的流转每天凌晨扫描所有预约日期早于当天的未核销订单把它们批量打成过期状态。这个逻辑用一句SQL就能完成SpringBoot里的Scheduled注解配合Update语句就能搞定。3. 后端核心模块实现预约链路、并发控制与定时任务3.1 用户登录与JWT鉴权统一身份校验的落地姿势系统所有接口分成两类公开接口和需要登录的接口。公开接口包括首页场馆列表、展览列表、注册登录预约、取消、核销、后台管理都必须带token。JWT是这类系统的主流选择它的用法很简单用户登录成功后后端生成一个签名token返回给前端前端在localStorage里保存每次请求在axios拦截器里加入Authorization请求头后端通过拦截器统一解析并校验。JWT生成我建议用jjwt 0.11.5版本代码看起来很干净。密钥要配置在application.yml里不要写死在代码中。引入依赖后工具类里写两个方法一个generateToken接收用户ID和角色生成token一个parseToken解析token拿到用户信息。拦截器里注意两点一是放行白名单比如/login、/register、静态资源、前端入口二是解析失败要返回统一的401 JSON而不是让请求直接报500。如果想让代码更规整可以定义一个PassToken注解标记免登录接口走注解驱动的拦截逻辑。实际使用中还有一个细节管理端和用户端的权限区分。同一个用户对象里role字段持有角色JWT里也要带上role。写一个拦截器判断当前用户是否管理员只有role等于1的才能访问/admin/**接口。我当时在这个地方踩过一个坑用户注册时role默认值忘记设置所有新用户都成了管理员后台完全裸奔。后来在数据库字段上加默认0又在后端注册逻辑里显式setRole(0)双保险才解决。3.2 预约下单事务包裹下的余票扣减与订单生成预约接口是整个项目的核心代码必须写得漂亮。我贴一段核心逻辑的流程说明实际代码也遵循这个顺序第一步校验用户是否已登录第二步校验预约日期是否在今天之后第三步按venueId和date查询时段记录确认时段存在且remaining大于0第四步执行乐观锁扣减SQL第五步检查扣减影响行数如果影响0行说明名额已被抢光第六步创建预约订单最后统一提交事务。扣减SQL用一个注解写死在Mapper接口里大致是这样的UPDATE t_time_slot SET remaining remaining - 1, version version 1 WHERE id #{slotId} AND remaining 0这条语句是防超卖的关键。数据库行锁会保证同一时段在同一时刻只有一个扣减成功影响行数为0就代表没有剩余名额。加上Transactional注解让扣减和订单创建同生共死如果后面创建订单失败扣减自动回滚不会出现余票少了订单却不存在的情况。订单创建时给ticket_code赋值我用的方案是Hutool的IdUtil.fastSimpleUUID生成32位的短UUID去掉横杠之后截取16位。对普通预约场景来说碰撞概率可以忽略。如果想更正式可以做成“日期场馆编号随机数”的复合码核销时再按规则解析不过毕设用随机码就已经够了。这里再补一句创建订单之前还要按userId、reservationDate、slotId做一次重复预约检查防止同一用户同一个时段预约两次。数据库层面可以加唯一索引业务层面用Mapper的count查询做前置拦截两层双保险。3.3 定时任务的正确打开方式过期订单与容量恢复预约系统的定时任务有两个第一个是每天凌晨2点把历史上未核销且已过期的订单统一置为过期状态第二个是每隔5分钟扫描未支付订单超时未支付自动取消并释放名额。毕设一般没有支付功能但未支付超时取消这个逻辑可以保留它体现了对真实业务的理解。Scheduled是SpringBoot的定时任务注解用起来很简单。在启动类上加EnableScheduling然后在方法上配置cron表达式即可。这里有两个我踩过的坑要特别提醒。第一个坑是默认只有单线程执行器如果项目里有多个定时任务它们默认排队执行一个任务卡住阻塞了其他任务。解决办法是配置一个线程池在application.yml里加spring.task.scheduling.pool.size5或者自定义SchedulingConfigurer。第二个坑是超时扫描要处理好事务边界。扫描更新的语句是一次性批量更新如果不希望每条记录都在独立事务里提交把方法打成Transactional即可。批量更新过期订单时有两条SQL值得注意。过期操作把预约日期小于当前日期且状态为0的订单更新为3。释放未支付的名额需要先查询这批订单关联的时段ID再逐个把对应时段的remaining加回。第二种方案因为涉及回补容量必须是查询、计算、更新三步不能简单一条UPDATE完成举个例子3个未支付用户都约了10点场次释放时要把那个时段的remaining统一加3而不是每条订单各加1。这里建议用一个UPDATE语句按订单条件聚合后回补。3.4 核销验票逻辑与实名校验让票务系统有闭环核销是票务系统的“最后一公里”不能随便做了一个按钮就完了。我的做法是管理端提供核销页面输入预约码或扫码枪扫码后端接收ticketCode后做三件事第一按ticketCode查询订单第二校验订单状态必须是待参观否则返回“无效预约”或“已核销”的提示第三将状态更新为已核销同时记录核销时间。如果展览收费还应该在核销时校验支付状态。免费场馆的订单创建时pay_status直接置1收费展览则必须确认支付成功。这个逻辑用枚举类做状态常量的管理不要散落一堆魔法数。值得一提的是身份证校验会在预约时进行核销时也可以再校验一次身份证是否与订单一致现场核销时用户掏出身份证或预约码都能入场。双通道核销在答辩里讲出来会是一个不错的亮点。我建议把核销接口设计成幂等。也就是说同一张预约码被扫两次第二次不会重复扣减或修改状态返回提示“该预约已核销”即可。这样前端扫码枪重复触发也不会出错。这个只需要在更新语句里带status 0条件影响行数0就说明已经不是待参观状态自然达到了幂等效果。4. 前端页面设计与SpringBoot联调部署全流程4.1 用户端核心页面预约流程的前后端交互逻辑前端页面的核心是预约主流程我拆成四步来讲。第一步首页展示场馆列表和展览列表用户点击场馆进入详情页第二步详情页显示场馆介绍、展览信息和“立即预约”按钮第三步进入预约页面后先选日期日期一变就向后端请求该场馆当天的时段列表和每个时段的剩余名额第四步选定时段填写参观人姓名、手机号、身份证提交预约成功后跳转到“我的预约”页面生成电子凭证。这里有一个前后端交互的关键点时段列表的剩余名额数据与订单提交之间是有时间差的。用户看到余票5张犹豫了一会儿再提交实际可能只剩1张甚至没有。这是正常现象后端通过乐观锁拦截超卖前端只需要在提交失败时弹出友好提示即可。我在前端写axios响应拦截器时把后端统一返回的code等于1视为成功code等于0会弹出message字段的内容这样后端抛出的“该时段已被约满”就能直接显示给用户体验非常顺畅。“我的预约”页面要注意状态展示。后端返回status字段前端需要用文本映射0显示“待参观”并展示预约码二维码1显示“已核销”2显示“已取消”3显示“已过期”。二维码可以用qrcode.js在前端生成预约码字符串当成二维码内容。展示二维码这个功能虽然实现简单但它让系统看起来非常完整答辩演示的时候我强烈建议保留。4.2 统一返回体与全局异常前后端约定比实现更重要前后端协作最容易吵架的就是返回格式不统一。我在项目里定义一个Result类包含code、message、data三个字段code为1表示业务成功0表示业务失败。所有Controller返回Result对象异常由全局异常处理器捕获。SpringBoot里的RestControllerAdvice配合ExceptionHandler可以统一处理异常业务异常自定义一个BizException全局处理器捕获后返回Result.fail(e.getMessage())。这套约定在下单接口里作用明显。当扣减影响行数为0时我直接抛new BizException(500, 该时段预约已满请更换时段)。全局异常处理器把它转成统一的JSON返回前端拦截器读取到data.message直接展示。相比起冗长的try-catch嵌在Controller里这种写法让业务代码非常干净也体现了你对工程化的理解。有一点要提醒全局异常处理器里校验异常MethodArgumentNotValidException也要单独捕获否则前端拿到的就是一堆繁琐的字段错误信息。捕获后把第一条校验错误信息拼装出来返回即可。Hibernate Validator结合Validated注解做参数校验是毕设里性价比很高的方案比手写一长串if判断优雅得多。4.3 Vue打包放进SpringBoot的两种方式与静态资源路由前端写完后要部署最常见也是最让毕设新手困惑的一步就是“Vue打包放进SpringBoot”。我说一下标准流程前端项目执行npm run build会生成一个dist目录把dist目录下的所有文件复制到后端项目的src/main/resources/static目录下重新打包SpringBoot应用为jar文件访问http://localhost:8080就能看到完整系统前端所有接口请求如果是相对路径/api/**后端的Controller映射在/api/**上就不会有跨域问题。开发阶段前后端分离运行会碰到跨域这时有两种处理方案后端配置CorsFilter允许跨域或者前端在vue.config.js里配置devServer的proxy代理把/api转发到后端地址。我建议用第二种因为代理方式对后端代码零侵入生产环境dist里又是相对路径前后端一致。还有一个坑是前端路由的history模式。Vue Router用history模式时访问根路径没问题但直接刷新一个子路径比如/reserve后端没有对应的路由处理会返回404。解决办法有两个一是改用hash模式URL里带#号刷新不会出问题二是后端写一个路由回退Controller把所有非API路径都转到index.html。如果图省事选hash模式就好反正毕设不追求URL美观。5. 从开发到答辩高频问题与避坑指南5.1 SpringBoot版本选择的教训别一上来就装最新版我把这个问题放在避坑指南的第一条因为太常见了。SpringBoot 3.x发布之后很多同学下载项目模板直接把版本拉到3.2、3.3。结果JDK8环境下跑不起来javax.servlet变成jakarta.servlet很多网上搜索到的2.x代码直接编译不过。我在一开始就明确使用SpringBoot 2.7.18这是2.x最后的一个稳定版本兼容JDK8资料多各种依赖踩坑记录齐全对毕设来说是最省心的选择。你们做项目追求的不是版本新而是方向快、问题少。顺带说一句IDEA里配置启动端口时有些人会在启动配置里乱改端口实际上SpringBoot的端口是由application.yml里的server.port控制的。做毕设时默认8080就够了如果端口被占用在application.yml里改成8081再重启应用即可不需要去动IDE的启动配置。每次答辩前把这一项确认好可以避免现场莫名奇妙打不开页面。5.2 MyBatis-Plus使用细节与事务失效的坑用MyBatis-Plus做CRUD虽然方便但有三件事要特别注意。第一件是逻辑删除。如果字段上加了TableLogic注解MyBatis-Plus的所有内置方法都会自动带上deleted0条件。但自定义SQL不会自动加哪怕你写的是Mapper接口里的注解SQL也必须自己拼上deleted0否则删除的数据还会被查出来。第二件是自动填充。create_time和update_time可以用MetaObjectHandler实现自动填充但实体类里这些字段必须加TableField(fill FieldFill.INSERT)注解不然填充逻辑不会触发。第三件是驼峰映射。数据库字段是下划线风格的比如create_time实体类属性createTimeMyBatis-Plus默认开启动map-underscore-to-camel-case这个配置别主动关掉。事务失效是另一个高频坑。SpringBoot默认使用CGLIB代理如果你在一个类内部通过this调用本类的另一个方法比如Controller调用Service的A方法A方法内部调用了同类里带Transactional的B方法B的事务是不生效的——因为this调用不会经过代理对象。解决办法是把事务方法放在不同的Service组件里或者注入自身代理对象。这也是我在代码里坚持把预约下单逻辑直接写在一个Service方法内的原因一个public方法把扣减和订单创建都包进去事务边界一目了然。5.3 压测与查漏两个浏览器抢最后一张票的演示方案答辩时怎么证明你的系统真的能防超卖一个最简单的演示方案是把某个时段的剩余名额故意配成1然后打开两个浏览器窗口同时登录两个账号同时点击同一个时段的预约按钮。如果系统正确会有一个预约成功、一个提示“已约满”。为了确保演示效果后端代码里要对同一时段秒杀做保护乐观锁在并发200以下表现是稳定的正常答辩场景绰绰有余。如果想在这个基础上再深聊性能优化可以谈谈Redis的原子扣减。用Redis的String结构存储时段剩余名额扣减时使用Lua脚本保证原子性再把Redis中的余票同步回MySQL。这个方案工程上更可靠但排查难度也更高。我的建议是毕业设计用MySQL乐观锁完完全全够用如果你还有余力可以把Redis优化当作扩展点在论文中阐述——这样既有实践支撑又有人无我有的亮点。还有一个容易在演示时翻车的点定时任务在演示过程中刚好触发数据变更把用户正在查看的订单状态改了。这是正常业务但在答辩前最好说明定时任务的执行时间和触发逻辑避免评委觉得数据“不稳定”。5.4 数据统计面板与答辩现场的准备心得数据统计是很多管理系统都有的功能但预约系统里统计口径更丰富更值得做。我的后台统计页做了三个图表每日预约量折线图、各时段预约热度柱状图、各场馆预约占比饼图。图表用ECharts实现数据由后端接口按日期范围聚合返回。ECharts的引入方式不复杂npm安装之后直接按组件引入或者用CDN方式加载都是可行的建议提前把所有初始化代码准备好不要在答辩现场连网络。关于答辩本身我个人的体会是评委最关心的不是界面多炫酷而是你对业务逻辑是不是真懂。几个高频问题提前准备好分时预约的时间段是怎么切分的余票扣减流程图为什么用乐观锁而不是悲观锁事务回滚的边界在哪里前端打包部署的路径规则。这些问题都会在平时开发中被问到你能回答出原理再配合实际操作整个答辩过程就会顺畅很多。演示时不要照着PPT念打开系统把预约流程从头到尾走一遍比任何图片都更有说服力。最后再分享一个小技巧开发的时候顺手把项目文档整理成一份Markdown包含项目背景、技术架构、接口列表和数据库设计说明。这份文档不仅是论文的底稿也会在你被导师问“这个接口的参数是什么”时保住你的头发。我的经验是认真做完这套系统你能把SpringBoot、MyBatis-Plus、MySQL、Vue和Redis的知识串成一条线这对后续找实习或者正式工作也是实打实的底气。