ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SSM电影院订票系统:选座并发控制与微信支付闭环实现

2026/9/11 22:03:47 拓冰建站 浏览量
SSM电影院订票系统:选座并发控制与微信支付闭环实现 简介本资源是一套完整的计算机专业毕业设计项目聚焦微信小程序端电影院订票与选座功能实现采用SSMSpringSpringMVCMyBatis后端架构与微信原生小程序前端技术栈适用于本科毕设、课程设计及工程实训场景尤其适合具备Java Web与小程序开发基础的学习者进行二次开发与功能拓展。压缩包共1227个文件含111个Java后端核心类、132个Vue组件用于管理后台、175个JS逻辑脚本、86个WXML页面结构、88个WXSS样式文件、232张界面截图及2个可直接导入的SQL数据库脚本整体体积38.14MB结构清晰、模块分离明确。已有109人学习下载资源经实测可直接运行涵盖登录、影片浏览、座位可视化选择、订单生成与支付模拟等全流程功能配套论文文档便于答辩准备同时提供bat一键安装/启动脚本及备份文件.bak显著降低环境配置门槛与调试成本。1. 这不是“微信小程序商城模板”而是一个基于 SSM 框架、完整闭环的电影院订票选座业务系统很多计算机专业学生拿到“微信小程序SSM”这类毕设标题时第一反应是去 GitHub 搜个带后台管理的电商小程序套壳——但真正能过答辩、经得起老师现场提问的恰恰是像“068电影院订票选座”这样业务边界清晰、数据流向可追溯、技术栈耦合合理的垂直场景系统。它不追求大而全的小程序商城功能而是聚焦「座位状态实时校验」「场次-影厅-座位三级关联」「微信支付回调与订单状态同步」三个硬核环节。前端用原生微信小程序非 uni-app 封装后端用 Spring SpringMVC MyBatis 组合即 SSM数据库用 MySQL非 SQL Server标题中 sql 指建表语句而非数据库类型所有模块都围绕“用户选座不可重复”这一核心约束展开。适合需要展示真实业务建模能力、前后端联调经验、以及对事务边界理解深度的本科毕设场景——尤其当答辩老师问“你怎么保证两个用户同时点同一个座位不会超卖”时你能立刻说出SELECT ... FOR UPDATE的加锁时机和Transactional的传播行为而不是只答“用了 Redis 缓存”。2. 用 SSM 搭建订票后台为什么选 MyBatis 而不是 JPA以及三层结构如何对应影院业务实体2.1 选型依据MyBatis 对复杂查询与手写 SQL 的不可替代性在电影院订票场景中“查某场次剩余可选座位”不是简单WHERE status 0能解决的。它需联合movie_schedule场次表、cinema_hall影厅表、seat_layout座位布局表三张表按影厅座位矩阵如 10 行 × 15 列动态生成已售/可售/锁定状态并排除已被其他用户暂存但未支付的座位即“选座锁定期”。JPA 的Query或 Criteria API 在处理这种带行列计算、状态聚合、分页渲染的查询时SQL 可读性差、调试成本高而 MyBatis 允许直接编写带foreach动态拼接、choose条件分支的 XML 映射且能精准控制执行计划。例如以下查询片段!-- mapper/SeatMapper.xml -- select idselectAvailableSeatsByScheduleId resultTypecom.example.seat.SeatVO SELECT s.id AS seat_id, s.row_num, s.col_num, CASE WHEN o.id IS NOT NULL AND o.status LOCKED THEN locked WHEN o.id IS NOT NULL AND o.status PAID THEN sold ELSE available END AS status FROM seat_layout s LEFT JOIN order_seat os ON s.id os.seat_id LEFT JOIN order o ON os.order_id o.id AND o.schedule_id #{scheduleId} WHERE s.hall_id ( SELECT hall_id FROM movie_schedule WHERE id #{scheduleId} ) ORDER BY s.row_num, s.col_num /select提示此处LEFT JOIN关联订单是为了区分“已售”和“被锁定”两种状态避免仅靠seat_layout.status字段导致状态歧义。MyBatis 的 XML 方式让这种多状态判断逻辑一目了然比注解式更易维护。2.2 SSM 分层设计Controller → Service → Mapper 如何映射影院业务动作SSM 的分层不是为分而分而是为隔离变化。以“用户提交选座请求”为例Controller 层OrderController.java只做三件事校验微信 openid 是否合法、解析前端传来的座位 ID 数组、调用 Service 方法。不处理任何业务规则。Service 层OrderService.java承担核心业务逻辑① 根据scheduleId查询场次是否可售检查开始时间、余票数② 对传入的seatIds执行批量SELECT ... FOR UPDATE锁定座位防止并发冲突③ 创建订单主记录并插入order_seat关联表④ 调用微信统一下单接口获取prepay_id⑤ 若支付创建失败回滚整个事务Transactional(rollbackFor Exception.class)。Mapper 层OrderMapper.xml只负责 SQL 执行不包含任何 Java 逻辑。例如锁定座位的语句必须显式声明FOR UPDATEupdate idlockSeats UPDATE seat_layout SET status locked WHERE id IN foreach collectionseatIds itemid open( separator, close) #{id} /foreach AND status available /update注意status available是乐观锁条件确保只锁定当前空闲座位。若并发请求中某座位已被他人锁定该UPDATE影响行数为 0Service 层需捕获并抛出“座位已被占用”异常而非静默失败。2.3 数据库表设计为什么seat_layout必须冗余存储row_num和col_num很多初学者将座位抽象为纯 ID靠前端计算行列位置。但在订票系统中座位的物理坐标是业务强约束后台需按行列生成座位矩阵图供小程序渲染用户投诉“选了第 5 排第 3 座却坐到第 3 排”时必须能通过row_num5 AND col_num3精确定位影厅改造后调整座位布局只需更新seat_layout表无需改代码。因此seat_layout表结构必须包含字段名类型说明idBIGINT PK座位唯一标识hall_idBIGINT FK所属影厅row_numTINYINT行号112col_numTINYINT列号120statusVARCHAR(10)available/locked/sold而movie_schedule表需关联hall_idorder_seat表则记录order_id与seat_id的多对多关系。这种设计使“查某场次所有座位状态”只需一次 JOIN避免前端反复请求单个座位信息。3. 微信小程序前端如何用原生 API 实现选座交互与支付闭环3.1 座位选择 UICanvas 渲染 vs WXML 布局的取舍小程序中渲染影厅座位图有两种主流方式WXML CSS Grid适合座位数 ≤ 200 的中小型影厅。用view wx:for{{seats}} classseat {{item.status}}动态生成通过class控制颜色.available{background:#4CAF50}/.locked{background:#FFC107}。优点是开发快、易调试缺点是 DOM 节点过多时滚动卡顿实测 300 个view会触发性能警告。Canvas 绘制适合大型影厅或需自定义高亮动效的场景。先用wx.createCanvasContext(seatCanvas)获取上下文再循环调用fillRect()绘制每个座位方块用drawImage()加载“已售”图标。关键代码如下// pages/seat/seat.js onLoad() { this.drawSeatMap() }, drawSeatMap() { const query wx.createSelectorQuery() query.select(#seatCanvas).fields({ node: true, size: true }).exec((res) { const canvas res[0].node const ctx canvas.getContext(2d) const dpr wx.getSystemInfoSync().pixelRatio canvas.width res[0].width * dpr canvas.height res[0].height * dpr ctx.scale(dpr, dpr) // 遍历 seats 数组绘制 this.data.seats.forEach(seat { const x (seat.col_num - 1) * 30 20 const y (seat.row_num - 1) * 30 20 ctx.fillStyle seat.status available ? #4CAF50 : seat.status locked ? #FFC107 : #F44336 ctx.fillRect(x, y, 24, 24) ctx.fillStyle #fff ctx.font 12px sans-serif ctx.fillText(${seat.row_num}-${seat.col_num}, x 4, y 17) }) }) }提示Canvas 方案必须配合wx.canvasToTempFilePath()实现座位图分享功能而 WXML 方案无法截图——若毕设要求“分享选座结果”Canvas 是刚需。3.2 支付流程从wx.requestPayment到订单状态轮询的完整链路微信小程序支付不是调一个 API 就结束。标准流程需覆盖小程序端调用wx.login()获取code传给后台换取openid后台调用微信统一下单接口https://api.mch.weixin.qq.com/pay/unifiedorder传入body电影名称、out_trade_no订单号、total_fee单位分、notify_url支付结果回调地址后台返回prepay_id小程序用wx.requestPayment()发起支付支付成功后微信服务器异步通知notify_url后台更新订单状态为PAID小程序端不能依赖回调即时刷新需启动setInterval每 2 秒调用GET /order/status?orderIdxxx查询状态直到返回PAID或TIMEOUT。关键点在于notify_url的幂等处理微信可能多次推送同一笔支付结果后台必须用SELECT ... FOR UPDATE锁定订单记录后再更新状态避免重复扣款。// OrderController.java PostMapping(/notify) public String handlePayNotify(HttpServletRequest request, HttpServletResponse response) { try { // 解析微信回调 XML String xmlData IOUtils.toString(request.getInputStream(), UTF-8); MapString, String notifyMap WXPayUtil.xmlToMap(xmlData); // 幂等校验查订单是否存在且状态非 PAID Order order orderService.findById(notifyMap.get(out_trade_no)); if (SUCCESS.equals(notifyMap.get(result_code)) !PAID.equals(order.getStatus())) { // 加锁更新 orderService.updateStatusLocked(order.getId(), PAID); } return xmlreturn_code![CDATA[SUCCESS]]/return_codereturn_msg![CDATA[OK]]/return_msg/xml; } catch (Exception e) { return xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[ERROR]]/return_msg/xml; } }注意notify_url必须是公网可访问的 HTTPS 地址本地开发用 ngrok 或 frp 内网穿透且返回 XML 格式字符串不能返回 JSON。4. SSM 与小程序联调排错常见 5 类错误的定位方法与修复命令4.1 CORS 跨域问题SpringMVC 的CrossOrigin不生效怎么办小程序前端域名https://servicewechat.com/...与本地 SSM 后台http://localhost:8080必然跨域。若配置CrossOrigin(origins *)仍报错原因通常是Spring 版本 4.2CrossOrigin注解无效需改用WebMvcConfigurer全局配置Nginx 反向代理未透传 Origin 头若部署到服务器Nginx 配置中必须包含add_header Access-Control-Allow-Origin *微信开发者工具缓存旧响应头清除工具缓存菜单栏 → 工具 → 清除缓存 → 全部清除。正确做法Spring 5.0是在 Controller 类上添加RestController RequestMapping(/api) CrossOrigin(origins https://servicewechat.com, allowCredentials true) public class OrderController { ... }提示allowCredentials true时origins不能为*必须指定具体域名。开发阶段可临时设为https://servicewechat.com上线前替换为小程序正式 AppID 对应的域名。4.2 MyBatis 参数绑定失败org.apache.ibatis.binding.BindingException: Parameter xxx not found典型场景Mapper XML 中写了#{scheduleId}但 Service 方法传参是Long scheduleId而 XML 里没声明parameterType。解决方案有二方式一推荐在select标签中显式声明parameterTypejava.lang.Long方式二改用Param(scheduleId) Long scheduleId注解强制 MyBatis 将参数名注入。// OrderService.java ListSeatVO selectAvailableSeatsByScheduleId(Param(scheduleId) Long scheduleId);!-- SeatMapper.xml -- select idselectAvailableSeatsByScheduleId parameterTypejava.lang.Long resultTypeSeatVO SELECT * FROM seat_layout WHERE hall_id ( SELECT hall_id FROM movie_schedule WHERE id #{scheduleId} ) /select注意若方法有多个参数如selectSeats(Long scheduleId, String status)必须全部用Param注解否则 MyBatis 无法区分#{arg0}和#{arg1}。4.3 微信登录code换openid失败{errcode:40029,errmsg:invalid code}此错误表明小程序前端传给后台的code已失效5 分钟过期或被重复使用。排查步骤检查前端是否在wx.login()回调中立即上传code而非用户操作延迟后才传后台接收code后是否在 1 秒内发起微信接口请求https://api.weixin.qq.com/sns/jscode2session确认appid和secret是否与小程序后台一致尤其注意测试号与正式号差异。验证命令Linux 终端curl -X GET https://api.weixin.qq.com/sns/jscode2session?appidwx1234567890secretyour_secretjs_codeYOUR_CODEgrant_typeauthorization_code若返回{errcode:0,openid:...}则配置正确若返回40029说明code无效需前端重新调用wx.login()。4.4 MySQL 事务不回滚Transactional注解失效的 3 个隐藏原因即使 Service 方法加了Transactional支付失败时订单仍可能残留。常见原因异常被吞掉Service 方法内try-catch了异常但未throw new RuntimeException(e)调用本类方法methodA()调用本类的methodB()后者有Transactional但 Spring AOP 无法拦截需用ApplicationContext.getBean(ThisService.class).methodB()数据库引擎非 InnoDB执行SHOW CREATE TABLE order;查看引擎若为MyISAM则不支持事务需ALTER TABLE order ENGINEInnoDB;。验证事务是否生效在OrderService.createOrder()方法开头加日志log.info(事务开始);结尾加log.info(事务提交);若只打印开头日志说明事务未提交。4.5 小程序wx.request返回404后端接口路径匹配失败微信开发者工具 Network 面板显示GET http://localhost:8080/api/order 404但curl http://localhost:8080/api/order正常。原因在于小程序wx.request默认header[Content-Type] application/json而 SpringMVC 的RequestBody方法要求Content-Type: application/json若 Controller 方法用RequestParam接收参数但前端未加header: {Content-Type: application/x-www-form-urlencoded}则 Spring 无法解析。修复方案统一使用RequestBody接收 JSON前端设置wx.request({ url: http://localhost:8080/api/order, method: POST, data: { scheduleId: 123, seatIds: [1,2,3] }, header: { Content-Type: application/json }, success: (res) { ... } })后端 ControllerPostMapping(/order) public Result createOrder(RequestBody OrderRequest request) { ... }5. 毕设答辩高频问题应对从 SQL 优化到微信支付安全的 4 个硬核回答点5.1 “你的座位并发控制怎么防超卖只靠数据库锁够吗”这不是考你背概念而是看你是否理解锁粒度与业务场景的匹配。标准回答应包含三层行级锁保障SELECT ... FOR UPDATE锁定具体座位 ID避免全表扫描应用层校验在lockSeats()方法中UPDATE seat_layout SET statuslocked WHERE id IN (...) AND statusavailable的AND statusavailable是二次校验确保锁住的确实是空闲座位降级策略若数据库锁等待超时如innodb_lock_wait_timeout50Service 层捕获LockAcquisitionException返回“座位紧张请稍后重试”而非让用户无限等待。加分细节可补充说明“我们测试过 100 并发用户抢同一场次最后 1 个座位成功率 100%无超卖”并展示 JMeter 压测报告截图。5.2 “微信支付回调地址怎么保证不被伪造”重点讲签名验证和IP 白名单微信回调时POST Body 是 XML其中含sign字段。后台必须用约定密钥key对除sign外的所有字段按字典序拼接再 MD5 加密与sign比对在微信商户平台配置notify_url的 IP 白名单微信服务器固定 IP 段182.140.166.0/24,182.140.167.0/24Nginx 层用allow/deny限制访问来源。验证签名的 Java 代码片段public boolean verifySign(MapString, String params, String key) { String sign params.get(sign); params.remove(sign); String signStr WXPayUtil.mapToUrlString(params) key key; return MD5(signStr).toUpperCase().equals(sign); }5.3 “MySQL 的explain你看哪些字段判断 SQL 性能”针对selectAvailableSeatsByScheduleId这类查询必须关注type应为ref或eq_ref若出现ALL全表扫描说明缺少hall_id或schedule_id索引key显示实际使用的索引名确认是否命中idx_hall_id或idx_schedule_idrows预估扫描行数若远大于实际座位数如影厅 200 座却扫 10000 行说明索引失效Extra出现Using filesort或Using temporary是性能红灯需优化排序字段或增加覆盖索引。建索引命令示例-- seat_layout 表加速按 hall_id 查座位 ALTER TABLE seat_layout ADD INDEX idx_hall_id (hall_id); -- movie_schedule 表加速按 id 查场次 ALTER TABLE movie_schedule ADD INDEX idx_id (id);5.4 “论文里写的‘系统采用 SSM 框架’但 MyBatis 和 SpringMVC 是怎么集成的”拒绝泛泛而谈“配置了 xml 文件”。要指出具体文件与关键配置项spring-dao.xml中bean classorg.mybatis.spring.SqlSessionFactoryBean定义dataSource和mapperLocationsspring-service.xml中context:component-scan base-packagecom.example.service/扫描Servicespring-mvc.xml中mvc:annotation-driven/启用RestControllerbean classorg.springframework.web.servlet.view.InternalResourceViewResolver配置视图解析器虽 RESTful 接口不用但需存在web.xml中ContextLoaderListener加载 Spring 容器DispatcherServlet加载 MVC 容器——二者父子关系决定 Bean 可见性。终极技巧答辩时随身带 U 盘里面存着spring-dao.xml截图和explain执行计划截图。当老师问“你真懂这个”时直接打开演示比背诵文档有力十倍。本文还有配套的精品资源点击获取