从零搭建话剧票务系统:Spring Boot + 微信小程序完整实战

发布时间:2026/10/23 3:15:38
从零搭建话剧票务系统:Spring Boot + 微信小程序完整实战
这个题目我太熟悉了前前后后帮学生和同行看过不少类似的项目自己也动手搭过几套票务类的管理系统。你现在看到的这套“基于微信小程序的话剧票务管理系统”走的正是课程设计里最常见也最稳妥的一条路Spring Boot 做后端接口微信小程序做用户端MySQL 存数据再配一套给管理员用的后台管理页面。整体业务不算复杂但恰好把前端交互、后端接口、数据库设计、权限控制这些毕业设计评委最爱问的点全都覆盖了所以每年这个题目都会反复出现。这篇文章我尽量按照实际做项目的顺序来讲从需求拆解、表结构设计到接口实现、小程序端联调再到常见的报错排查和答辩准备把整个过程摊开给你看。不管你是准备拿它做课程设计还是想扩展成更完整的毕业设计都能从这里找到可以直接抄作业的部分。1. 项目定位与功能拆解1.1 为什么选话剧票务这个方向先聊两句选题。票务系统这个方向在课程设计里属于“看起来小众做起来顺手”的类型。比起电商、外卖这类大而全的业务票务系统的核心流程非常清晰用户浏览演出、选场次座位、下单付款、退票改签管理员维护演出信息、处理订单、查看数据报表。业务边界明确数据关系单纯非常适合用一两个星期完成一版能跑通全流程的 Demo。而且话剧、演出这类市场比普通电影票更有话题性项目展示的时候评委更容易理解“为什么需要选座”“为什么票有时效性”“为什么要做退票规则”这些设计点。答辩时你不会被问“你这个项目和别人的有什么区别”而是会被引导着讲业务流程和技术实现这对学生来说是好事。如果用电影票做题目往往要处理复杂的影院排片、座位分区定价逻辑会重很多。话剧票务就简单多了一个剧目对应多个场次每个场次有固定座位表价格分几档整体就是一个标准的“商品-库存-订单”模型非常适合入门。1.2 核心角色与功能清单这个系统我最终拆成了三类角色普通用户小程序端、管理员后台管理端、系统本身承担的一些自动任务如订单超时关闭。普通用户端微信小程序的功能清单微信授权登录拿到用户唯一标识浏览剧目列表、查看剧目详情页包含简介、演员、时间地点选择某一场次进入选座页面按区域票价选座提交订单生成待支付订单支持模拟支付和关闭订单查看我的订单列表对已支付订单发起退票个人中心展示用户信息、票据凭证管理后台的功能清单管理员登录账号密码或 JWT 鉴权剧目管理新增、编辑、上下架剧目场次管理为每个剧目添加演出时间、场地、座位总数订单管理查看全部订单、退款审核、手动改状态数据统计按剧目统计销量、按时间段统计营收系统自动任务下单后超过 15 分钟未支付自动取消订单并释放座位1.3 技术选型的思路标题里已经限定了 Spring Boot 和微信小程序这是国内课设最常见的组合。我补全的部分是持久层和数据存储后端框架Spring Boot 2.7.x稳定版本网上资料最多遇到问题容易搜到答案持久层MyBatis-Plus不要问为什么不用 JPA课设答辩老师基本都见过 MyBatisMP 代码生成器还能帮你省很多重复工作数据库MySQL 8.0数据库名建议叫 theatre_ticket管理员端为了省事直接用 Vue Element Admin 生成一套管理界面或者只做几个简单的 HTML 页面都行小程序端原生微信小程序开发不要用 uni-app因为课设要展示的是你对小程序生命周期和原生 API 的掌握原生框架最直接为什么不用 Redis按课设的标准如果你的题目没有明确要求可以不上 Redis用数据库的行锁或乐观锁就能解决选座并发问题。但如果你想在答辩时加分可以引入 Redis 缓存热门的演出列表和座位状态这个我后面在扩展部分会展开说。2. 数据库设计先把表结构理清楚2.1 实体关系梳理票务系统的实体不多但表与表之间的关系如果没理清后面写 SQL 会非常痛苦。我按照“用户-剧目-场次-座位-订单-票”这条链路来建模。完整的关系是这样的一个用户对应多张订单一个剧目下挂多个场次一个场次包含多个座位一个订单包含多个座位票。这里最关键的是一张中间表把“订单”和“场次座位”关联起来因为一个订单可以买多张票每张票对应一个具体的座位。数据库一共设计 7 张表表名作用核心字段user用户表id, openid, nickname, avatar, phone, create_timeplay剧目表id, name, description, cover_url, duration, statussession场次表id, play_id, hall_name, show_time, sale_start_time, sale_end_timeseat座位表id, session_id, row_num, col_num, area_type, price, seat_statusorders订单表id, order_no, user_id, session_id, total_amount, status, create_time, pay_time, cancel_timeorder_ticket订单票据表id, order_id, ticket_no, seat_idadmin管理员表id, username, password, role2.2 关键字段设计说明先说说 orders 表。订单号 order_no 一定要自己生成不要用数据库自增 id因为订单号要给用户看要支持查询需要一定的可读性和唯一性。我用的生成规则是时间戳加随机数比如20241207153012001格式是年月日时分秒加三位随机数。真机环境如果有高并发要求可以再加一个用户 id 的后四位基本不会重复。status 字段是订单状态机的中枢我定义了一组整数来表示0待支付1已支付2已出票3已退票4已关闭超时或主动取消有些同学喜欢用字符串表示状态比如WAIT_PAY、PAID这样读起来更直观。但课设项目里用整数加注释代码里定义常量类也完全够用。我个人的习惯是整数枚举因为 MySQL 比较快而且状态流转时用 switch 很舒服。seat 表的 seat_status 我设计了两种方案最终选了第二种方案一只存座位是否被占用通过订单表反查方案二座位状态直接标记 0 可选、1 锁定、2 已售方案二的问题是如果用户下单后没支付座位就一直是锁定状态必须靠订单超时逻辑去释放。方案一的问题是查询已售座位要 join 订单表逻辑复杂。我做的是折中seat 表存座位基础信息和价格座位状态在选座时临时从 Redis 或内存缓存里读取落到数据库时依靠 order_ticket 表来验证冲突。如果你不想引 Redis最简单可靠的办法是选座接口里对 session 加悲观锁或者用数据库唯一索引约束(session_id, seat_id)在插入 order_ticket 时做防重。这一块的并发细节我放到第 5 章讲。2.3 建表脚本示例直接贴几张核心表的建表 SQL你复制过去改一改就能用。CREATE TABLE play ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 剧目名称, description text COMMENT 剧目简介, cover_url varchar(255) DEFAULT NULL COMMENT 封面图地址, duration int(11) DEFAULT 120 COMMENT 演出时长单位分钟, status tinyint(4) DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT剧目表; CREATE TABLE session ( id bigint(20) NOT NULL AUTO_INCREMENT, play_id bigint(20) NOT NULL, hall_name varchar(50) DEFAULT NULL COMMENT 演出厅名称, show_time datetime NOT NULL COMMENT 演出时间, sale_start_time datetime DEFAULT NULL COMMENT 开售时间, sale_end_time datetime DEFAULT NULL COMMENT 停售时间, PRIMARY KEY (id), KEY idx_play_id (play_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT场次表; CREATE TABLE seat ( id bigint(20) NOT NULL AUTO_INCREMENT, session_id bigint(20) NOT NULL, row_num int(11) NOT NULL COMMENT 排号, col_num int(11) NOT NULL COMMENT 列号, area_type varchar(20) DEFAULT normal COMMENT VIP/normal, price decimal(10,2) NOT NULL COMMENT 价格, PRIMARY KEY (id), UNIQUE KEY uk_session_seat (session_id, row_num, col_num) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT座位表;注意座位表里那个唯一索引uk_session_seat这是防超卖的最后一道保险。哪怕你的选座接口并发控制写得再烂只要插入订单票据时两个用户同时抢同一座位数据库层面也会直接报唯一约束冲突兜住最坏情况。3. 后端接口设计与业务代码实现3.1 接口列表一次看全我把后端接口分成三类小程序端接口、管理端接口、内部定时任务接口。下面是完整清单模块方法路径说明用户认证POST/api/user/login微信 code 换 openid返回 token剧目浏览GET/api/play/list分页获取上架剧目剧目详情GET/api/play/detail/{id}剧目详情场次列表座位查看GET/api/session/seats/{sessionId}获取场次座位状态创建订单POST/api/order/create传入座位 id 列表生成订单模拟支付POST/api/order/pay支付接口实际只改订单状态取消订单POST/api/order/cancel用户取消未支付订单退票申请POST/api/order/refund已支付订单退票我的订单GET/api/order/list/{userId}当前用户订单列表管理员登录POST/api/admin/login账号密码登录发放 JWT剧目管理POST/GET/api/admin/play/*新增、列表、上下架场次管理POST/GET/api/admin/session/*新增场次、生成座位订单管理GET/api/admin/order/list查询所有订单数据统计GET/api/admin/statistics销量与销售额统计最关键的是用户登录。微信小程序端通过wx.login()拿到临时 code然后传给后端后端用 code 去微信接口换 openid。这里有个坑如果小程序没有上线用的是测试号AppID 和 AppSecret 需要替换成你自己的并且要在微信公众平台配置合法域名。很多同学在真机调试时发现wx.request直接报url not in domain list就是这个原因后面会细说。3.2 登录鉴权流程用户登录我用的方案是后端接收 code调用微信的jscode2session接口需要用到小程序 appid 和 secret换 openid库里查不到就自动注册一个新用户查到就更新登录时间最后用 JWT 生成 token 返回给小程序小程序每次请求在 header 里带Authorization: Bearer token。这里有个需要说明的点这个接口涉及外部网络调用课程设计环境可能无法访问微信的域名所以我在代码里做了一层兼容如果调用微信接口失败就根据 code 生成一个模拟 openid。这样在没有外网的环境里也能演示登录流程。public String login(String code) { // 尝试调用微信接口失败则 fallback String openid wechatService.code2Session(code); if (openid null) { // 兼容本地开发固定前缀 code 生成模拟 openid openid mock_ code; } User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setNickname(观众 (int)(Math.random() * 1000)); userMapper.insert(user); } return JwtUtil.createToken(user.getId(), user.getNickname()); }受保护接口用拦截器解析 token把用户 id 放到 ThreadLocal 里后面下单、查订单都直接从这个上下文拿用户 id。这个思路跟 Spring Security 里的SecurityContextHolder一样但你课设手写一个小拦截器答辩反而更好讲清楚。3.3 选座与下单核心事务逻辑下单是整个项目最容易翻车的环节。一个订单包含多个座位需要做的事务操作至少有四步校验用户登录、校验场次是否还在售卖期、校验座位是否全部可选、计算总价并创建订单记录、插入订单票据记录。我把核心流程写在OrderServiceImpl.createOrder方法里用Transactional注解包起来避免出现订单 insert 成功但票据 insert 失败的数据不一致问题。Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateRequest request) { Long userId UserContext.getUserId(); Session session sessionMapper.selectById(request.getSessionId()); if (session null || session.getSaleEndTime().before(new Date())) { throw new BusinessException(该场次已停止售票); } // 批量查询座位 ListSeat seats seatMapper.selectBatchIds(request.getSeatIds()); if (seats.size() ! request.getSeatIds().size()) { throw new BusinessException(存在无效座位); } // 锁定座位使用乐观锁更新 seat_status 为 1 int rows seatMapper.lockSeats(request.getSeatIds(), session.getId()); if (rows ! request.getSeatIds().size()) { throw new BusinessException(座位已被锁定请重新选座); } // 计算订单金额 BigDecimal totalAmount seats.stream() .map(Seat::getPrice) .reduce(BigDecimal.ZERO, BigDecimal::add); Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setSessionId(session.getId()); order.setTotalAmount(totalAmount); order.setStatus(0); orderMapper.insert(order); // 插入订单票据并回写座位状态 2已售 for (Seat seat : seats) { OrderTicket ticket new OrderTicket(); ticket.setOrderId(order.getId()); ticket.setSeatId(seat.getId()); ticket.setTicketNo(generateTicketNo()); orderTicketMapper.insert(ticket); } return order.getId(); }你可能会问为什么不先查座位状态再插入因为查和插之间有时间差并发场景下两个请求同时查到了同一个空座位就超卖了。所以必须靠第二次的“锁定”动作来防并发。lockSeats的 SQL 是核心UPDATE seat SET seat_status 1 WHERE id IN (#{seatIds}) AND session_id #{sessionId} AND seat_status 0这个 UPDATE 自带行锁同一时刻只有一个事务能成功把座位从 0 改成 1另一个事务的影响行数是 0直接报“座位已被锁定”。这就是乐观锁的典型写法写起来简单效果也够用。3.4 订单状态机与超时关闭订单状态这一块我强烈建议你画一张状态机图哪怕只是自己理解用。我的状态流转是这样的0 待支付可以主动取消也可以等系统超时关闭0 待支付 → 用户点取消 → 4 已关闭0 待支付 → 模拟支付成功 → 1 已支付1 已支付 → 用户申请退票 → 3 已退票同时释放座位1 已支付 → 演出结束后自动变 2 已出票只是状态标记可加可不加超时关闭用 Spring 的Scheduled定时任务每 30 秒扫一次待支付订单Scheduled(cron 0/30 * * * * ?) public void closeExpiredOrders() { LocalDateTime deadline LocalDateTime.now().minusMinutes(15); ListOrders expiredOrders orderMapper.selectExpiredOrders(deadline); for (Orders order : expiredOrders) { order.setStatus(4); order.setCancelTime(new Date()); orderMapper.updateById(order); // 释放对应的座位 orderTicketMapper.releaseSeats(order.getId()); } }注意定时任务必须在启动类上加EnableScheduling这个细节经常有人忘记。另外释放座位时要把 seat 表对应座位的状态从 1 改回 0让其他用户能看到并购买。4. 微信小程序端开发实录4.1 小程序页面结构小程序端我用的是原生框架页面结构是典型的 tabBar 加业务页面pages/index/index首页剧目轮播加列表pages/detail/detail剧目详情pages/select/select选场次、选座位pages/order/order订单确认页pages/my/my个人中心我的订单入口pages/orderList/orderList订单列表页pages/orderDetail/orderDetail订单详情页app.json 里注册页面和 tabBartabBar 只放首页和个人中心两个 tab。选座页不进 tabBar因为它需要从详情页进入要保持页面栈的连续性。4.2 登录态管理小程序端不能像网页一样用 cookie我用的是最基础的 token 方案启动时先调wx.login()拿 code再请求后端登录接口拿到 token把 token 存到wx.setStorageSync(token, token)。每个请求在wx.request的 header 里带上 token后端拦截器校验。一个容易踩的坑tabBar 页面不能调用wx.navigateTo只能wx.switchTab非 tabBar 页面之间用wx.navigateTo。我的首页轮播图点击跳详情页就遇到过这个报错当时以为是路径写错了排查半天才发现是跳错了 API。// 封装统一的请求方法 const request (url, method, data) { const token wx.getStorageSync(token) || return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}${url}, method: method || GET, data: data || {}, header: { Authorization: Bearer token }, success: (res) { if (res.data.code 200) { resolve(res.data.data) } else { wx.showToast({ title: res.data.msg, icon: none }) reject(res.data) } }, fail: (err) reject(err) }) }) }4.3 选座页面的前端交互选座页面是小程序端最复杂的部分。我用scroll-view横向滚动画布区域用view组件画座位格子每排从 A 到 J每列 1 到 12。座位的颜色分成三种灰色表示已售或已锁定不可选亮色表示可选选中后变成橙色。数据加载逻辑很简单进入页面后请求/api/session/seats/{sessionId}后端返回一个座位列表前端根据seatStatus字段渲染颜色。选座的交互逻辑要自己维护一个选中数组每次用户点击座位格时判断当前座位是否已被别人占用如果是可选状态则加入选中列表否则给一个轻提示。价格汇总直接在点击时累加。一个很重要的细节后端返回的座位数据里有seatId你在渲染时不能直接用数组下标当唯一 key否则删掉一个选中的座位其他座位的点击状态会错乱。必须用seatId作为组件的>Transactional(rollbackFor Exception.class) public void refund(Long orderId) { Orders order orderMapper.selectById(orderId); // 只有已支付状态才能退票 if (order.getStatus() ! 1) { throw new BusinessException(当前订单状态不可退票); } order.setStatus(3); orderMapper.updateById(order); // 释放座位 orderTicketMapper.releaseSeats(orderId); }5.3 数据统计的 SQL 思路数据统计不要盲目在内存里循环算直接用 SQL 聚合最方便。我的统计页面展示了几个关键数字总订单数、总销售额、今日订单数、今日销售额、剧目销量 Top5。-- 剧目销量 Top5 SELECT p.name, COUNT(o.id) AS order_count, SUM(o.total_amount) AS sales_amount FROM orders o LEFT JOIN session s ON o.session_id s.id LEFT JOIN play p ON s.play_id p.id WHERE o.status IN (1, 2) GROUP BY p.id, p.name ORDER BY sales_amount DESC LIMIT 5;这里注意订单状态筛选退票和关闭的订单不应该计入销售额。很多同学第一次写统计报表时忘了加状态条件结果退票订单还把金额算进营收里答辩时被老师问得哑口无言。这种细节我都帮你踩过坑了。6. 项目部署与环境配置要点6.1 后端配置与启动后端的 application.yml 里重点配置三块数据源、Redis如果用了、JWT 密钥。如果不用 Redis就把 spring.redis 注释掉。数据库连接串注意加上useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai否则中文乱码和时区问题会让你调试到怀疑人生。本地演示时后端跑在 8080 端口小程序开发者工具里的域名校验可以直接关闭在“详情-本地设置”里勾选“不校验合法域名”。但如果你做的是毕业设计需要演示给老师看建议把后端部署到云服务器上小程序配上 https 域名效果会专业很多。有人说课设不需要部署到服务器本地演示就够。我的建议是看学校要求。如果答辩现场需要线上跑通提前把服务器环境准备好是值得的。如果是课程设计本地演示完全没问题因为评委更关注你的代码结构和设计思路。6.2 小程序端的环境切换小程序端要维护一个环境变量文件我用config.js统一管理 BASE_URLconst BASE_URL http://localhost:8080 // 本地 // const BASE_URL https://api.example.com // 线上真机调试的时候localhost是手机本机访问不到你电脑上的服务。要把 BASE_URL 改成你电脑的局域网 IP比如http://192.168.1.100:8080并且关闭域名校验。我第一次真机调试时没注意这个手机一直请求失败排查了两小时才发现是地址写错了。还有一个细节云服务器如果用了 HTTPS小程序合法域名要求必须是备案过的域名。很多学生图省事买的是 IP 直连小程序真机上线是不允许的。但只要演示环境不扫码发布只在开发者工具里跑这个问题就不存在。6.3 数据库初始化脚本项目附带的 SQL 文件除了建表语句还要自动生成一批初始数据至少包含3 个演示剧目带上封面图和简介每个剧目 2-3 个场次时间分布在最近一周每个场次 8 排 12 列座位分 VIP 区和普通区1 个管理员账号 admin / 加密后的密码没有初始数据小程序首页就是空白演示效果大打折扣。我建议 SQL 脚本里直接用INSERT语句插入演示数据以后端启动时用 CommandLineRunner 自动检查数据量再初始化也可以。但最稳妥的还是单独一个init_data.sql方便评委用 Navicat 直接导入查看。7. 高频踩坑问题与排查手记7.1 问题速查表我把做这个项目过程中亲眼见过、亲手排查过的高频问题整理成一张速查表按优先级排现象原因解决方案小程序 request 报 url not in domain list开发者工具域名校验开启工具右上角“详情-本地设置-不校验合法域名”真机调试连不上后端BASE_URL 用了 localhost改成电脑局域网 IP手机和电脑连同一 WiFi登录报 code invalid使用测试号且 AppSecret 不对检查 appid 和 secret或走 mock 兼容逻辑下单报座位已被锁定座位状态没释放查超时任务是否开启 EnableScheduling订单表中文乱码数据库连接没指定 utf8mb4连接串加 characterEncodingutf8定时任务不执行忘了加 EnableScheduling启动类加注解订单已支付统计却翻倍统计 SQL 没过滤退票状态加 WHERE status IN (1,2)页面跳转失败navigateTo 传了 tabBar 页面换 wx.switchTab7.2 并发选座的实际模拟我当时在本地用 Jmeter 并发创建订单接口把同一场次的同一个座位提交 50 次并发请求结果只有 1 个成功其余全部返回“座位已被锁定”。这就是第 3.3 节乐观锁起的效果。如果你没有压测工具也可以用一个小程序端的两个账号手动抢座账号 A 先进入选座页但不要下单账号 B 进入选座页看到座位是可选状态先下单成功。此时账号 A 刷新选座页面会发现刚才可选的座位变成灰色因为后端已经把它锁了。这个演示过程不需要代码只要座位状态逻辑正确就能跑通。7.3 日志排查技巧多打印日志尤其是异常场景。我在 service 层的每个核心方法入口都打了一条日志包含方法名、用户 id 和关键参数。比如log.info(createOrder start, userId{}, sessionId{}, seatIds{}, userId, request.getSessionId(), request.getSeatIds());这样出了问题后看日志就能快速定位。有些同学不喜欢写日志靠断点调试但在分布式或远程服务器上断点根本不好使日志是基本功。8. 常见问题与扩展建议8.1 课设答辩可能被追问的问题你把这个项目做出来不提前准备几个追问答辩现场容易慌。我整理了几个高频提问和可以参考的回答思路问题座位表为什么不用一张表直接存已售状态而要额外设计 order_ticket 回答因为一张订单可以买多个座位order_ticket 是订单和座位的关联表是典型的多对多关联。直接把座位状态存在 seat 表也能做但订单取消时还需要反向维护数据和业务会耦合在一起。问题超时取消订单是怎么实现的 回答Spring 定时任务每分钟扫描一次待支付订单超时自动改状态并释放座位。如果项目要求更高会改成消息队列延迟任务来替代定时扫描降低对数据库的无效查询压力。问题如何防止座位被超卖 回答核心靠数据库唯一索引和事务下单时 UPDATE seat 锁定座位的乐观锁机制保证并发下只有一个请求能成功。问题为什么选 MyBatis-Plus 而不是纯 MyBatis 回答MP 提供了内置的单表 CRUD减少了重复代码也保留了自定义 SQL 的能力适合快速开发课程设计。如果业务复杂到需要多表复杂关联查询仍然可以手写 XML SQL。8.2 可以继续扩展的方向如果你拿这个项目当毕业设计想在“工作量”和“技术含量”上再往上提一档我建议在现有基础上加这三个扩展点第一把 Redis 引进来存场次座位状态减少数据库压力。选座接口先查 Redis没有就回源数据库并写缓存下单时用 Redis 的原子操作标记占用配合数据库事务做最终一致性。这个方案能很好地回答“为什么选 Redis 提升系统并发能力”。第二增加二维码电子票。下单支付后为每个订单生成一个二维码作为入场凭证。小程序端用canvas绘制二维码后端用 ZXing 生成图片保存验票端扫码即可核销。这个功能视觉效果好答辩展示很有冲击力。第三做一个小程序端的数据看板用ec-canvas组件画一些饼状图和柱状图展示剧目销量占比和每日营收趋势。不需要后端额外开发调统计接口返回 JSON 数据前端渲染即可。8.3 文档和演示怎么准备标题里提到的“万字文档”通常是完整的设计说明书建议包含这几部分绪论背景和意义、需求分析用例图、流程图、系统设计架构图、数据库 E-R 图、表结构说明、核心功能实现带核心代码截图、系统测试测试用例和结果截图、总结与展望。答辩演示流程也要想清楚。我建议按这个顺序打开小程序首页展示剧目列表和搜索功能点进详情页选一个场次选 3 个座位下单展示支付成功页面在“我的订单”里展示这笔订单然后退票回到选座页确认座位被释放最后切换管理后台展示剧目管理、订单列表、统计图表。整个流程控制在 8 分钟内节奏很舒服。9. 写在最后的一点建议项目本身工程量不算大真正拉开分差的往往是你对技术选型理由的理解深度以及对每个业务判断的解释能力。做课设不是“代码能跑就行”评委一眼就能看出你是真的理解了还是照抄代码。我见过不少把代码结构理得很清楚、每个模块都能讲出设计原因的学生即使功能简单也能拿高分也见过代码千行但问一个问题就卡壳的情况。做这个项目时我建议你带着“如果我是产品经理我会怎么设计这个流程”的思路去看需求。为什么退票要限时限条件为什么座位在用户长时间占用后必须释放为什么订单金额一定要从服务端计算而不是前端传回答得了这些问题才是真正把这个项目吃透了。最后再分享一个小经验代码里所有你觉得“不太对劲”“自己也不懂为什么这么写”的地方不要直接跳过去动手查一下写成注释放在旁边。你的文档和代码里那些注释最后的收益一定比你想象的大得多。