又是一个毕设选题季。每年这个时候,总有人私信问我:小程序方向的课题怎么选才稳妥。我翻了一圈手里的项目列表,发现"基于微信小程序的文化娱乐购票系统"这类题目出现频率极高,尤其以话剧票务、电影票务、演出票务为常见形态。这套基于Spring Boot + 微信小程序的票务管理系统,不夸张地说,是我近几年推荐给学生的"黄金配方"——需求不冷门、技术栈主流、工作量可控,既有前端交互又有后端业务闭环,关键还藏着一堆可以写进答辩PPT的"亮点素材"。
如果你正在筹备毕设,或者打算接一套二手项目做二开,这篇内容直接就是对标清单。我会把架构拆解、核心逻辑、支付对接、答辩包装这几个环节全部过一遍,重点讲那些"文档里不会写、但做起来一定踩坑"的细节。
1. 毕设选题为什么推荐"话剧票务系统":需求定位与功能边界
1.1 票务类系统是毕设的"黄金赛道"
我判断一个毕设课题好不好,不看功能堆得多花哨,而是看三件事:需求是否清晰、技术栈是否完整、答辩是否有故事可讲。票务系统在这三点上几乎满分。
首先是需求清晰。话剧、音乐会、舞台剧这类文化演出的票务管理,业务链路非常典型:用户浏览演出信息→选择场次→选座下单→支付→生成电子票→到场验票。这套流程人人都有生活感知,你不用费劲解释"为什么要做这个系统",开题报告和需求分析都好写。
其次是技术栈完整。前端小程序展示交互,后端Spring Boot处理业务逻辑,MySQL存核心数据,Redis扛并发热点,微信登录体系解决用户身份,微信支付打通交易闭环。从控制层到持久层再到第三方服务对接,一整条链路全都有,几乎覆盖大学四年课程里的重点。
最后是答辩故事多。并发抢票时的余票控制、订单超时自动释放、支付回调幂等处理,每一个都是评委爱听的点。相比"图书馆管理系统""校园二手交易平台",票务系统天然带着"高并发场景"的讨论余地,哪怕你实际没多少人访问,也能把方案讲出层次感。
1.2 核心角色与功能边界明确
这套系统我习惯拆成两个登录端来看,逻辑上非常清晰:
| 角色 | 入口 | 核心功能 |
|---|---|---|
| C端用户 | 微信小程序 | 浏览剧目/场次、选座下单、在线支付、查看订单、电子票夹、退票申请 |
| B端管理员 | 后台管理端(Web管理界面) | 演出剧目管理、场次排期、座位图配置、订单管理、退票审核、数据统计 |
这里的"后台管理端"可以单独做一个Web页面,也可以做成小程序内嵌套的管理身份页。我更推荐做成独立的Web管理端,理由后面在架构篇细说。
用户端的功能不用贪多,把闭环做完整比功能多更重要。我见过很多毕设项目把"会员积分""社区评论""直播抢票"全塞进去,结果核心链路反而做得粗糙。你要记住,评委会追问的永远是"你这个订单状态是怎么流转的""余票并发怎么办",而不是"你做了几个页面"。
1.3 你需要准备的工作量预估
按正常毕设周期3~4个月来算,这套系统的工作量分布大致是:
- 需求分析与数据库设计:2~3周
- 后端接口开发:4~6周
- 小程序前端开发:3~4周
- 后台管理端开发:2~3周
- 联调、测试、部署、文档:2~3周
如果是拿了现成源码来做二次开发,时间可以砍掉一半以上,但你必须把源码里的核心代码逻辑吃透,否则答辩一问就露馅。这也是我一直强调的:源码可以用来节省开发时间,但绝对不能替代理解。
2. 技术栈选型与整体架构:Spring Boot和小程序各司其职
2.1 后端选型:Spring Boot 3 + MyBatis-Plus + MySQL + Redis
后端的技术组合我推荐Spring Boot 3 + MyBatis-Plus + MySQL 8.0 + Redis,这套组合在毕设场景里是"稳如老狗"的存在。
Spring Boot 3相比2.x最大的变化是包名从javax迁到了jakarta,很多老项目的导入代码不能直接用。如果你拿到的源码是基于Spring Boot 2.x的,建议优先看2.x版本,因为网上的资料、教程绝大多数还是2.x的思维,踩坑时更好搜。如果从零开始,直接上Spring Boot 3也没问题,只是要习惯一两个写法差异。
MyBatis-Plus解决的是单表CRUD冗余的问题。票务系统里的剧目、场次、订单、座位,绝大多数操作是单表查询加简单更新,用MyBatis-Plus的BaseMapper能省掉一半的XML编写工作。但这不代表你不写SQL,余票扣减、订单统计这类复杂查询还是要亲手写,否则答辩被问到"你的SQL怎么优化"时容易楞住。
Redis在这套系统里的定位是"热点缓存+分布式锁"。我会把热门演出的场次信息和实时余票数缓存到Redis,避免用户集中刷新时打到MySQL。注意,Redis不是必须的,如果你的毕设不需要讲并发故事,MySQL裸奔也能跑起来。但加上Redis,整个项目的技术含金量会高一个档次。
2.2 小程序端:原生还是uni-app
这个问题我每次都会被问到。我的建议是:没把握就选微信小程序原生,有跨端需求或想省前端工作量的再选uni-app。
原生小程序的好处是资料最多、报错最直观、微信官方文档就是最靠谱的老师。票务系统的小程序端页面不算复杂,核心就几个:首页演出列表、演出详情、场次选择、座位图、订单确认、订单列表、支付结果页。这些页面用原生的WXML + WXSS + JS完全够用。
uni-app的优势在于一套代码编译多端,如果你以后还想发布H5或支付宝小程序,可以省不少事。但代价是调试链路变长,编译到微信开发者工具后再看效果,出了问题要排查的层次更多。对毕设来说,时间比扩展性珍贵。
我通常的页面划分是9个左右:
- 首页(推荐演出、分类入口)
- 剧目列表(按类型/时间筛选)
- 剧目详情(海报、简介、场次列表)
- 选座页面(可视化座位图、选座锁定)
- 订单确认(选择取票方式、优惠计算)
- 订单列表(全部/待支付/已支付/已完成)
- 订单详情(订单状态流转、电子票二维码)
- 个人中心(头像、昵称、票夹、设置)
- 退票/售后页面
每个页面控制在一屏到两屏的交互量,既能撑起工作量,又不至于让自己陷进无尽的前端调优。
2.3 数据库设计:核心表结构与字段规划
数据库设计是评委最爱看的东西之一。票务系统我建议至少拆成七张表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| user | 用户表 | openid, 昵称, 头像url, 手机号 |
| play | 剧目表 | 名称, 类型, 海报url, 剧情简介, 演出时长 |
| session | 场次表 | 关联play_id, 演出时间, 场地, 总座位数, 已售数, 票价档位 |
| seat | 座位表 | 关联session_id, 排号, 列号, 区域类型, 状态(0可售/1锁定/2已售) |
| orders | 订单表 | 订单号, 关联user_id, session_id, 总金额, 状态, 创建时间, 支付时间 |
| order_seat | 订单座位关联表 | 关联order_id, seat_id, 防止一个座位被多订单占用 |
| refund | 退票申请表 | 关联order_id, 申请时间, 审核状态, 退款金额 |
这里最容易出错的是订单状态设计。我建议统一用数字状态码,比如:0待支付、1已支付、2已使用(已验票)、3已完成、4退款中、5已退款、6已关闭。千万不要一个字符串存多种状态名,后面统计和判断逻辑会写得非常痛苦。
座位表这个设计很多人会忽略。有学生问过我"座位不是场次的一部分吗,为什么单独建表",因为一张座位图对应一个场次、且每个座位要独立维护状态。如果你把座位JSON塞进场次表里,做选座锁定和状态更新时就会非常被动。单独建表再配合order_seat做关联,才能保证"一个座位在同一时间只能被一个有效订单占用"。
3. 核心业务逻辑的防坑实现:锁座、余票扣减与超时释放
3.1 并发票务场景下的乐观锁与分布式锁
选座和下单是票务系统最核心的竞争资源操作。两个用户同时抢最后一个座位,怎么保证不超卖?这个问题几乎必被答辩评委问到。
我的建议分两层处理:
第一层,数据库层面。座位表更新时加乐观锁,用一个version字段或直接判断当前状态:
UPDATE seat SET status = 1, locker = #{userId}, lock_time = NOW() WHERE id = #{seatId} AND status = 0这条SQL的妙处在于status = 0条件:只有当座位还是可售状态时才允许锁定,同时修改的受影响行数为0时说明座位已被别人抢走,业务层直接返回"座位已被占用"。这是最朴素的CAS思想,也是我不太推荐先查再改(select-then-update)的原因——中间哪怕有1毫秒间隙,都可能被并发钻空子。
第二层,服务层面。如果有Redis,可以用Redisson或原生SETNX做一个分布式锁,锁的粒度到"场次"级别,保证同一个场次的下单操作串行化:
// 尝试获取锁,key = "lock:session:" + sessionId boolean locked = redisTemplate.opsForValue() .setIfAbsent("lock:session:" + sessionId, userId, 5, TimeUnit.SECONDS);拿到锁之后就校验座位状态、生成订单、锁定座位,最后释放锁。锁过期时间一定要设置,防止进程卡死导致锁永不释放。这里有个小技巧:把过期时间设为5~8秒,比一次正常下单操作时长略长即可,避免影响下一个用户。
顺带提醒一下,很多人会提"用事务就能解决并发",这是误解。事务保证的是操作的原子性和一致性,但不解决两个事务同时读到旧快照的问题。你必须在SQL或锁层面主动做并发控制,事务只是最后一道兜底。
3.2 未支付订单的超时释放:定时任务与延迟消息
用户选好座位下单后,如果一直不支付,座位就会一直被锁定。常规做法是给订单15分钟超时时间,超时自动关闭订单并释放座位。
实现方案有两种。
方案A:Spring自带定时任务+数据库扫描。每30秒扫一次订单表,把创建时间超过15分钟且状态仍是"待支付"的订单批量更新为"已关闭",同时把关联座位释放为可售。
@Component public class OrderTimeoutTask { @Scheduled(fixedDelay = 30000) public void closeTimeoutOrders() { // 1. 查询超时且待支付的订单id列表 // 2. 批量更新订单状态为已关闭 // 3. 批量释放对应座位状态为可售 } }方案B:RabbitMQ延迟队列或者Redis过期事件。Redis可以给每个待支付订单设置15分钟过期key,key过期时触发事件回调关闭订单。这个方案实时性更好,但引入的组件更多,如果你的毕设还想控制复杂度,我建议选方案A。
方案A最大的坑是扫描数据库的批次大小。如果你一次性扫出几千条超时订单,每一单都要回滚座位,事务处理时间长还可能锁表。正确做法是每次只处理100条,处理完再查下一批,避免慢SQL拖垮整个服务。实测下来,毕设Demo场景下订单量不大,这个方案完全够用,而且代码逻辑也容易讲清楚。
3.3 退票回补库存与状态机
退票功能看着简单,真正实现时要注意回补逻辑。用户申请退票,管理员审核通过后,要做这几件事:订单状态改为"已退款",退款金额走原路返回,座位状态恢复为可售。
这里有一个容易被忽略的点:座位释放的时机选择。我的建议是审核通过后再释放座位,而不是用户提交退票申请就立刻释放。因为有些演出规定"开演前24小时不受理退票",如果审核流程还没走完就把座位放出去,后台审核时又必须驳回用户的退票申请,座位已经被别人买走,逻辑就矛盾了。
所以退票的状态机至少是:申请中→审核通过(退款+释放座位)或审核驳回(订单恢复为已支付状态)。这个状态机看似简单,但很能体现你对业务的理解,答辩时可以重点讲一讲。
4. 微信小程序登录与支付对接:最容易卡壳的两个环节
4.1 wx.login换openid,别把openid直接当用户表主键
小程序登录的流程是:前端wx.login拿到临时code→把code传给后端→后端调用微信接口换openid和session_key→后端生成自定义登录态(比如JWT或UUID token)返回给前端。
很多初学者会犯一个错误:把openid当成用户主键来设计表。openid只是微信用户在这个小程序里的唯一标识,但它不一定稳定——如果用户删掉小程序重新进入,openid不会变,但session_key会变,而且用户的业务字段(手机号、头像、昵称)以后可能来自多个渠道。我建议user表里用自增id做物理主键,openid单独加唯一索引。
@PostMapping("/api/auth/login") public Result login(@RequestBody LoginRequest request) { // 1. 用code调用微信接口,获得openid和session_key // 2. 查询user表,没有则自动注册 // 3. 生成JWT token,返回给小程序端 }登录态的令牌建议用JWT,把userId和过期时间放进token里,后续请求通过拦截器解析token即可获得当前用户。比每次都去查数据库更高效,也更好讲解。
要注意的坑是:微信接口调用是HTTP请求,后端服务如果部署在内网测试环境,可能访问不了外网。调试时保证本机可以正常联网访问微信服务接口,否则code换openid这一步就会直接失败。
4.2 微信支付:毕设阶段的落地策略
微信支付是票务系统里技术含量最高的部分,但它也是毕设最容易出问题的环节。原因很现实:微信支付需要商户号,个人主体的小程序没有商户号资质,必须用公司或个体户主体申请。很多学生没有这个条件,所以我会强烈建议毕设阶段用模拟支付代替真实支付,把微信支付的流程做成一个"模拟支付网关"。
模拟支付怎么做才体面?我建议在小程序端弹出一个"模拟支付确认弹窗",点击确认后直接调用后端接口把订单状态从"待支付"变成"已支付",同时记录支付渠道为"模拟支付",订单里留一个log字段。这个方案能让你完整走通"下单→支付→出票→验票"的全链路,而且不影响答辩核心逻辑的展示。
如果你确实想接真实微信支付(比如导师有公司资质配合),流程要按官方文档走一遍:
- 后端调用统一下单接口,拿到prepay_id
- 后端生成签名参数返回给小程序
- 小程序调起wx.requestPayment
- 用户输入密码支付
- 微信服务器异步通知后端回调地址
- 后端验签+核对订单金额+更新订单状态
第5和第6步的坑最多。回调地址必须公网能访问,本地联调可以用局域网或者部署到服务器上测;回调处理必须做幂等,同一笔支付通知可能到达多次,你处理时先查订单状态,已支付就直接返回成功,不要重复更新;回调验签用官方SDK校验,不要自己手写签名算法,十个有八个会验签失败。
4.3 电子票与验票设计
出了票就要有验票环节。毕设里最常见的是把订单的二维码展示在小程序"我的票夹"里,管理员后台用扫码枪或手机摄像头扫一扫实现核销。
二维码的内容不要放订单号明文,建议放一个验票专用ticketCode加密串,后端通过校验ticketCode和订单状态来判断是否有效。核销后订单状态变为"已使用",同一个二维码断不能二次核销。
这个功能在毕设答辩里特别加分。很多同学做到买票就停了,但你多花一两天做了验票闭环,评委就会觉得你的系统"真正从需求出发做完了,而不是交了一个原型"。类似这种"最后一公里"的完整度,往往比多几个页面更值钱。
5. 项目演示与答辩包装:把源码做出自己的东西
5.1 演示环境部署建议:保证现场不翻车
每年答辩我都见有人死在环境问题上:本地跑得好好的,一换到答辩教室的WiFi就崩。我的建议是答辩前一周把你的系统部署到一个稳定的服务器上,前端小程序的request域名配置也提前改好,不要到答辩当天才改配置。
如果不想买服务器,本地演示也有一个非常实用的技巧:准备一个"热启动脚本"。把MySQL、Redis、后端jar包、小程序模拟器的启动命令全部写进一个脚本,一键启动,减少现场敲命令的时间。小程序端记得在微信开发者工具里把"不校验合法域名"选项打开,否则本地调试时所有请求都会被拦截。
部署时还有个小细节:后端接口的地址最好不要硬编码在小程序代码里,放到一个统一的config.js文件里管理。这样切换测试环境和演示环境只改一个文件,省事也避免改错。
5.2 答辩时的高频追问与应答思路
评委问来问去其实就那么几类问题,提前准备比临场发挥靠谱得多:
| 追问方向 | 推荐应对思路 |
|---|---|
| 为什么选这个课题 | 泛文化消费市场大+票务链路完整+小程序入口轻便,三句话收住 |
| 订单超时释放怎么设计 | 讲定时任务扫描+状态机+座位回滚,强调数据一致性由事务保证 |
| 并发抢票如何防超卖 | 讲SQL CAS更新+Redis锁+库存预热,展示两层防线 |
| 数据库为什么这么设计 | 讲清楚每张表的存在理由,尤其座位表独立设计的动机 |
| 这个项目哪里可以优化 | 说2个具体方向:消息队列削峰、分库分表,不要只说空话 |
还有一个通用技巧是准备一个"系统演示脚本":从登录→浏览→选座→下单→支付→票夹→验票→退票,整理成一条顺畅的演示路径,每个环节一句话说明业务背景。这样即使被突发事件打断,你也能快速拉回节奏。
5.3 基于这套系统的三个扩展方向
如果你的目标是拿高分或者做更进阶的毕设,这三个方向可以考虑:
第一个方向是缓存与性能优化。把热门场次的座位余量预热到Redis,用前端定时轮询或WebSocket推送余票变化,展示一套完整的"缓存+推送"方案。
第二个方向是推荐算法。基于用户历史购票记录做简单的协同过滤,推荐相似类型的剧目。不需要做太复杂,一个简单的标签匹配或评分矩阵就能撑起一章内容。
第三个方向是多租户/多剧院支持。把单一剧院扩展成平台化设计,每个剧院管理员管理自己的座位图和场次,由平台统一抽取服务费。这个方向更接近真实行业产品,工作量也更重,适合有余力的同学。
我个人的经验是,一套系统的"好坏"往往不在于技术多炫,而在于异常链路处理得多细。你能想到用户会关掉支付页面、会重复点击下单、会恶意锁座不支付,并且都做了兜底,这本身就是区分度。做毕设这件事,与其追求广度,不如把一个主链路的完整性和健壮性打磨到位,这比十个半成品功能更有说服力。
如果你正准备拿这套系统做毕设,或者已经在改源码阶段,欢迎在评论区说说你的技术选型和卡住的环节,我可以针对具体问题再拆几篇出来聊聊。