ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Vue与SpringBoot的微信小程序电影票务系统实战

基于Vue与SpringBoot的微信小程序电影票务系统实战 简介本资源是一套基于Vue与Spring Boot开发的电影票务微信小程序完整实现方案面向高校计算机专业毕业设计学生及希望提升全栈开发能力的初中级开发者解决影院在线选座、购票、订单管理等核心业务场景的工程化落地问题。压缩包共767个文件含113个Java后端服务类、53个JS与14个Vue前端组件、31个WXML与36个WXSS小程序视图文件、40个PNG/146个JPG资源图以及2个SQL建库脚本和关键配置文件整体大小为42.64MB。已有68人学习下载项目采用标准前后端分离架构代码结构清晰分层涵盖用户认证、影片管理、智能座位选择算法、订单事务处理及微信支付对接等模块所有核心类如Movie、OrderService、Admin_MovieController等均已实现并经测试验证可直接部署运行助力读者系统掌握小程序开发规范、RESTful接口设计、JWT鉴权及数据库事务控制等关键技术。1. 选型迷雾Vue与小程序之间到底怎么跨过那道桥接手这个项目的时候客户的需求说得很简单做一个小程序用户能看正在热映的电影、选座下单、微信支付后台能管理影院、场次和订单。但真正动手之前最绕不开的一个决策就是——前端到底用什么写。标题里写了基于Vue与SpringBoot但懂行的人都知道微信小程序原生开发用的是WXML、WXSS和JavaScript和Vue语法并不是一套东西。你不可能直接把一个Vue项目扔进微信开发者工具里跑起来。那这个Vue字面意思到底落在哪这其实是很多刚开始做小程序的人第一个踩坑的地方。1.1 你以为的Vue写小程序和实际落地方案不是一回事在技术选型那阵子摆在桌面上的方案大致有三条路。第一条路是微信原生小程序。用微信自己的语法写页面和逻辑后端老老实实提供接口。好处是没有中间层性能好、调试直接微信开发者工具里所有能力都是第一手支持。坏处也很明显代码复用性差如果你后面还要做一个H5端的电影购票页面或者管理后台也想用同一套组件逻辑那基本等于重写。第二条路是mpvue美团团队早年出的Vue版小程序框架。当年确实火了一把但维护状态时好时坏对Vue 3的支持始终慢半拍组件生态也跟不上。如果你今天刚从Vue 3的Composition API习惯了再回到mpvue去写Options API会非常别扭。第三条路就是最终选定的uni-app。它基于Vue语法一套代码可以编译到微信小程序、H5、App等多个平台。从实际开发体验来说你在项目里写的就是template、script、style三段式结构数据绑定、计算属性、生命周期这些Vue的核心思维全部保留但编译产物是可以在微信开发者工具里打开的小程序包。这就是标题里Vue的落地点。我最终选了uni-app核心原因有两个。第一是团队产能我们当时需要同时交付小程序端和一个简单的H5宣传页用uni-app一套代码两头发布节省了差不多一半的重复工作。第二是生态成熟度uni-app在微信小程序端的兼容处理做得比较细像uni.login()、uni.request()这类API把微信的登录、请求都封装好了不用每次翻微信官方文档去写平台判断逻辑。提示如果你的项目只在微信生态里跑、也不考虑将来发App原生小程序完全够用甚至更轻。但如果团队熟悉Vue、同时有多端诉求uni-app是性价比更高的选择。选技术栈不是选最流行的是选最贴合交付范围的。1.2 SpringBoot在后端扮演的角色后端选SpringBoot几乎是顺理成章的事。这个项目的后端需要承担几家影院的数据对接、场次管理、用户登录鉴权、订单扣款、支付回调、座位锁定等一系列业务逻辑SpringBoot的生态能把这些活儿安排得明明白白。结构上我用的是经典的分层架构Controller接收前端请求、Service处理业务逻辑、Mapper操作数据库。SpringBoot的自动配置机制让我不用手工去搭Spring XML配置一个spring-boot-starter-web就把Web能力拉起来了。加上spring-boot-starter-data-redis处理座位锁和登录态mybatis-plus做数据库访问整个后端从脚手架到能跑通接口大概用了不到一天时间。这套技术栈组合的另一个好处是社区资料极多。无论是微信支付接入、JWT鉴权、还是MyBatis-Plus的分页查询随便一搜就是大把现成的踩坑记录。对一个工期紧的团队来说这本身就是一种隐性成本节约。2. 票务系统的地基表结构设计与排片/座位模型电影票务系统表面上看着不复杂无非是用户选电影、选场次、选座位、付款、拿票但真设计数据库的时候会发现这里面的关联关系比想象中多得多。电影院有多个影厅每个影厅在不同时间段上映不同电影每个场次又对应一张座位表用户下单还得记录座位信息、价格、支付状态、取票状态……稍微少设计一张表后面接需求的时候就要改得头破血流。2.1 核心表梳理从电影到订单的完整链路我先把我最终落地的核心表结构列出来你对照着看后面业务逻辑会更清楚。表名核心字段作用movieid, title, cover_url, duration, release_date, detail电影基础信息cinemaid, name, address, phone, longitude, latitude影院信息hallid, cinema_id, name, seat_rows, seat_cols影厅信息行数、列数scheduleid, movie_id, hall_id, start_time, end_time, price, hall_json电影场次冗余影厅座位数据seatid, schedule_id, row_no, col_no, status, order_id, lock_expire_time场次座位状态ordersid, order_no, user_id, schedule_id, seat_ids, amount, status, pay_time订单主表userid, openid, nickname, avatar, phone用户信息hall_json这个字段值得单独说一下。每个场次开映前系统要根据影厅的排布生成对应的座位记录。如果每开一个场次就去hall表动态计算逻辑上没问题但并发高的时候容易出现座位记录生成不及时的问题。我在schedule表里加了一个冗余字段直接保存这个影厅的JSON模板包含几排几列、哪些是过道、哪些座位不可售开映前生成场次的时候一次性写入seat表查询和更新都直接走schedule_id索引性能干净利落。2.2 座位表设计的关键排/列/状态/锁定seat表是整个系统里最容易出并发问题的表。我当时给座位定的是三态模型0可选1已锁定用户正在下单还没支付2已售出订单支付完成这个三态设计的核心目的是处理用户选座后没付款的情况。如果座位只有可选和已售出两种状态用户A选座后必须立刻付款否则座位一直占着体验极差。引入了锁定状态后用户在客户端选好座位会先调用后端的锁座接口把座位标记为锁定同时写入lock_expire_time比如15分钟。这15分钟内用户慢慢付款超时后由一个定时任务回收把状态改回可选。座位的行列号我用了row_no、col_no两个整数存。这里有个很实际的经验不要用A1B3这种字符串座位号直接存单字段排序和区间查询都会很痛苦。前台展示时再把行列号拼成5排6座的展示文案后台只认纯数字。2.3 订单表与座位的关系一对多还是冗余快照订单和座位的关系设计上要特别小心。一个订单可以包含多个座位比如用户一次买两张连座票所以订单和座位是一对多的关系。我最初用orders.seat_ids存了一个逗号分隔的字符串后来想想这其实是个坏味道。虽然查询订单时很方便一次查出来所有座位ID但要做到订单里包含哪些座位、座位属于哪个订单的双向追溯就会很别扭。最终的表结构是orders表存seat_ids用于快速展示seat表也存order_id用于反向关联。这样两个方向都能直接查代价是数据冗余——但在这个场景下冗余是值得的因为用户端查订单详情和后台查某个场次卖出多少座是两个最高频的操作各走各的索引互不干扰。价格设计上也做了一个快照。schedule.price是基准价但实际售票可能有会员价、早鸟价订单表里单独存amount实付金额不直接依赖场次价格字段。万一将来运营改了场次价格历史订单不受影响。3. 后端接口JWT鉴权、选座锁位与支付回调的并发处理后端接口这块的核心难点集中在三个地方怎么确认用户身份、怎么多人在线抢同一个座位时不超卖、怎么保证支付回调不漏单不重复。这三个问题任何一个处理不好上线第一天就会出事故。3.1 微信登录与JWT鉴权session_key不落库token走Redis小程序的登录流程和普通Web登录不太一样。用户在客户端调uni.login()拿到一个code后端拿着这个code去微信的jscode2session接口换openid和session_key。openid是用户在你们小程序里的唯一标识业务上就用它来关联用户。这里有一个安全细节session_key是微信用来解密用户手机号等敏感信息的密钥它不应该被返回到前端更不应该存到数据库里。正确做法是后端换取openid后用自己的JWT生成机制签发一个业务Token这个Token才返回给小程序端。我用的方案是用户第一次登录时查user表看有没有这个openid没有就自动注册一条记录。然后生成JWTpayload里放userId和openid有效期设成7天。每次请求在小程序端的uni.request里带上Authorization头后端用拦截器统一校验JWT合法性。有两点要注意。第一JWT的密钥不能硬编码在代码里尤其不能提交到Git仓库我用的是配置文件加环境变量注入的方式。第二JWT是无状态的它一旦签发在有效期内没法主动让它失效。所以我在Redis里额外维护了一份用户Token黑名单用户主动退出登录时把JWT的jti唯一ID加进黑名单拦截器再校验一道。3.2 选座锁位的并发难题数据库行锁Redis分布式锁双管齐下这是整个系统里我最想展开讲的部分。场景很简单某部热门电影黄金场次开售两三百个人同时抢同一个影厅的黄金座位。如果没有并发控制极端情况下同一个座位可能被两个人同时下单这就是典型的超卖。我当时第一个想到的方案是数据库行锁。具体做法是用户请求锁座接口时后端执行一条带有FOR UPDATE的查询把目标座位行锁住然后检查状态。SELECT * FROM seat WHERE id #{seatId} AND schedule_id #{scheduleId} FOR UPDATE这条SQL执行后其他事务对同一行座位的更新操作会阻塞等待直到当前事务提交释放锁。这样能保证同一时刻只有一个事务在修改某个座位的状态超卖问题从根源上被杜绝了。但你以为这就够了不够。FOR UPDATE锁的是数据库行而高并发场景下数据库连接池是有上限的。如果一个用户同时选6个座位一次锁6行6个并发用户就把连接池打满了其他请求全部排队甚至超时。所以我改用了Redis分布式锁。方案调整为锁座请求先到Redis用座位ID作为锁的key尝试SET NX EX不存在则设置同时设置过期时间。拿到锁的请求再去操作数据库拿不到锁的请求直接返回座位已被锁定。因为Redis是单线程模型SET NX是原子操作不会出现两个请求同时拿到同一个座位的锁。锁的过期时间我设置为10秒这个时间足够完成一次数据库的查询和状态更新。// 伪代码展示核心逻辑 String lockKey seat:lock: seatId; Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofSeconds(10)); if (Boolean.TRUE.equals(locked)) { try { // 检查座位状态更新为已锁定写入订单 } finally { redisTemplate.delete(lockKey); } } else { throw new BizException(座位被其他人锁定请重新选座); }这里还有个细节锁座和创建订单要放在同一个事务里。否则锁座成功、创建订单失败Redis锁删了数据库座位状态却没更新下次还是会被抢。3.3 订单状态机与支付回调幂等处理订单状态我定义了一个状态机看起来像这样状态含义可流转到PENDING_PAY待支付PAID, CANCELLEDPAID已支付REFUNDING, USEDCANCELLED已取消终态REFUNDING退款中REFUNDEDREFUNDED已退款终态为什么要一开始就把状态机完整定义好因为支付回调是无状态事件你没法预测它会以什么顺序到来。比如用户支付成功后立刻点了取消订单如果代码里没做好状态判断就可能出现已经取消的订单又被标记为已支付这种数据错误。我在所有状态流转的地方都加了一个前置校验当前状态必须符合流转规则否则直接拒绝。微信支付回调的幂等处理是另一个重点。微信服务器为了保证消息可靠送达会重试发送支付结果通知好几次。如果你的回调接口没有做幂等处理同一笔订单可能被加两次余额、标记两次已支付。我的处理方式是回调接口第一步先根据order_no查订单如果已经是PAID状态直接返回成功给微信不再做任何重复操作。这里给微信返回的成功响应格式也有讲究必须是{code: SUCCESS}这样规范的JSON否则微信会认为回调失败继续重试。另外支付回调里验证签名是必须的。微信支付回调会带签名头需要用商户密钥验签确认这确实是微信发来的请求而不是伪造的。这一步千万不能省省了等于把退款接口裸奔给黑客。4. 小程序端落地Vue风格开发的核心交互实现后端接口设计好了前端这边要开始干活了。用uni-app写小程序最大的感受是如果你熟悉Vue上手几乎没有成本但如果你带着Web开发的惯性思维去写又会踩到一堆小程序特有的坑。4.1 uni-app项目结构与路由配置项目初始化用vue create搭好之后目录结构跟普通Vue项目很相似src/ ├── pages/ # 页面 │ ├── index/ # 首页-电影列表 │ ├── movie/ # 电影详情 │ ├── cinema/ # 影院页 │ ├── seat/ # 选座页 │ ├── order/ # 订单确认 │ ├── order-list/ # 订单列表 │ └── mine/ # 个人中心 ├── components/ # 公共组件 ├── store/ # Vuex状态管理 ├── api/ # 接口封装 ├── utils/ # 工具函数 └── static/ # 静态资源路由直接用uni.navigateTo和uni.switchTab。这里要提醒一下小程序里tabBar页面也就是底部导航那几个页面必须用uni.switchTab跳转用navigateTo是跳不过去的会报错。这个坑我印象特别深调试了大半天才反应过来。4.2 电影列表、选座组件的实现细节电影列表页的逻辑不复杂调用后端接口拉取正在热映电影列表用v-for渲染卡片。核心的交互细节是这个列表要考虑上拉加载更多。一开始我用的是页面滚动到底部的监听事件后来发现直接使用onReachBottom生命周期钩子更省事写在页面里就行不用自己计算滚动位置。选座页是整个小程序端交互最复杂的页面没有之一。座位的渲染结构是这样的影厅用横向滚动还是纵向滚动市面上主流是纵向排列、左右滑动。我的实现方案是把座位分成左右两个区域中间是过道。左区从第1列到第4列右区从第5列到第10列。渲染时用两个v-for分别循环左右区过道部分用固定宽度占位。每个座位是一个可点击的view标签根据seat.status显示不同样式可选灰色半透明可点击已锁定红色斜条纹不可点击已售出灰色实体不可点击当前选中黄色高亮点击取消后恢复可选用户点击座位时先判断座位状态然后把选中状态push进一个selectedSeats数组。页面底部同步显示已选座位数和总价总价的计算逻辑computed: { totalPrice() { return this.selectedSeats.length * this.scheduleInfo.price; } }这块儿用computed而不是methods里的函数是因为价格依赖座位选择状态用计算属性可以让Vue自动追踪依赖变化不用手动在watch里重复计算。4.3 状态管理登录态与待支付订单小程序端的登录态管理我放在了Vuex里。用户进入小程序后我们不会强制让他立刻登录。真正的登录动作延后到加入购物车或提交订单时才触发。这样的设计用户体验更好避免了用户一进来就被弹窗要求授权的反感。登录的具体流程是用户点击立即购买触发checkLogin如果Vuex里没有用户信息调用uni.login()获取code把code发给后端换取JWT和用户信息存储到Vuex和uni.setStorageSync有一个小细节是uni.login()得到的code是一次性的只能用一次而且有效期很短通常几分钟。所以每次需要重新登录时都要重新调用uni.login()获取新的code不能缓存复用。待支付订单的倒计时也是前端的一个小难点。我最初的方案是在页面onShow的时候用setInterval每秒刷新倒计时后来发现如果用户在订单确认页停留太久倒计时归零但座位实际已经被后端定时任务解锁了前端还傻傻显示待支付。后来我换了一种思路前端向后端轮询订单状态每5秒一次一旦发现订单变成了CANCELLED立即提示用户座位已释放请重新选座。这个轮询在用户离开页面时用onUnload清除定时器避免内存泄漏。5. 从开发到上线真机调试、域名备案与版本发布避坑代码写完了不代表项目就结束了。小程序开发里最折磨人的往往不是写代码而是从我在开发者工具里能跑到真机上稳定能用这段路。这一段我踩过的坑加起来能写满一页A4纸。5.1 微信小程序合法域名与SpringBoot接口配置微信小程序的网络请求有一个硬性规定wx.requestuni-app里是uni.request的URL必须在小程序后台配置合法域名而且必须是HTTPS协议。这就意味着你本地开发时SpringBoot跑在http://localhost:8080是没法直接在小程序里请求的。本地开发阶段的解法通常有两个。第一个是在微信开发者工具里勾选不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书这样可以在开发工具里直接请求http接口。但注意这个选项只影响开发者工具真机上是不生效的。第二个是用内网穿透工具把本地端口映射成一个公网HTTPS地址方便真机联调。到了生产环境你需要一台配置了Nginx的服务器把SpringBoot应用跑起来然后把接口统一挂在你备案过的域名下面配好HTTPS证书。注意这个域名必须要备案。如果你的服务器是海外的域名没备案小程序里是过不了合法域名校验的——这一点在项目排期的时候就要确认清楚因为备案流程通常要一到三周别等到上线前才发现。5.2 真机调试中的常见白屏与样式兼容我在真机调试阶段遇到的最诡异的问题是开发者工具里一切正常一到安卓真机上就白屏。排查了很久最后定位到是某个组件库版本在安卓机的JavaScript引擎上有兼容问题抛了一个运行时错误导致页面直接挂掉。排查方式供你参考在微信开发者工具的真机调试模式下打开vConsole小程序里内置的调试面板查看控制台报错。当时看到的是TypeError: Cannot read property xxx of undefined再对照报错的组件定位到出问题的库升级到修复版本就解决了。样式兼容方面也有好几个坑。最典型的是rpx单位。微信小程序里推荐用rpx做响应式单位它跟屏宽挂钩750rpx等于屏幕宽度。用习惯了px的人在写样式时容易混用导致不同机型上布局错乱。我后来定了一个团队规范所有尺寸一律用rpx除非是1px的边框线用rpx会被缩放裁掉。还有flex布局在老安卓机上的兼容性。gap属性在部分低版本WebView上不支持导致子元素间距失效。我的解法是不用gap改用margin或者padding控制间距稳妥一点。5.3 上线版本审核注意点小程序发布前是要过微信平台审核的审核不通过的常见原因有类目选择不对。电影票务属于生活服务-票务类目需要提供相应的资质证明。如果类目选错了审核会被驳回。页面内容不完整。审核人员会模拟真实用户走一遍流程如果发现某个按钮点了没反应、某个页面明显是空壳会被判为功能不完整。支付流程不规范。微信对涉及支付的类目审核很严你的商户号需要在微信支付后台配置好关联AppID否则支付能力在小程序里是调不通的。虚拟支付问题。特别注意小程序里不能卖纯虚拟商品比如在线视频会员但电影票属于实体服务类可以正常用微信支付前提是你要有对应的类目资质。上线之前我习惯把主要流程在预览版里完整走一遍包括登录、浏览电影、选座、下单、支付、查看订单、取消订单。每一个步骤都截图留档。这样如果审核被驳回我能根据驳回截图快速定位是在哪一步出了问题而不是手忙脚乱从头猜。审核通过后版本发布还有一个灰度策略可以做。微信后台支持分阶段发布可以先给10%的用户放量观察线上有没有报错和投诉再逐步放量到100%。我当时是直接全量发布的后来想想有点冒险建议你做电影票务这种带有支付环节的小程序最好还是灰度一下稳一手。6. 一次实战联调后的思考这套架构还能怎么复用项目上线、稳定运行一段时间之后我回头审视了一遍整个架构发现这套VueSpringBoot微信小程序的组合其实能复用的场景比想象中更广。就拿票务系统来说改造成其他类型的预约系统非常顺畅。核心的表结构场次座位订单三件套只要把movie表换成doctor、schedule换成门诊排班、seat换成号源一个预约挂号系统就出来了。演唱会票务、体育赛事、景区门票、甚至自习室座位预约本质都是同一套逻辑。最多在选座页改成选时间段或者把座位的行列模型改成时间段列表业务层几乎不动。后端的那套Redis分布式锁和订单状态机的设计在很多涉及库存扣减的场景里都适用。比如电商秒杀、优惠券抢购、库存扣减只要涉及多个用户同时操作有限资源这套思路都能直接迁移。我在写这个项目的时候因为要处理座位锁定和超时释放把Redis的SET NX EX和定时任务用了好几遍后来做另一个库存预约项目时直接把这套代码搬过去改了个名字就上线了。如果你打算在自己的项目里复刻这套架构我唯一的进阶建议是把支付这一层独立出来。微信支付、支付宝支付、聚合支付每个渠道的接入细节都不一样但业务层的订单状态、退款状态是一致的。把支付封装成一个独立的模块接口定义统一为发起支付、支付回调、查询状态、申请退款、退款回调会让你的系统干净很多。我当时就是把支付逻辑写在订单Service里后来要接入支付宝的时候改起来费了老大的劲。如果重来一次我一定会先把支付抽象成独立的PaymentService接口。我个人在实际操作中的体会是选一座城市的中型影院作为首批合作方用真实排片和票价跑通全流程比在后台造一大堆假数据要有用得多。因为真实业务场景里会遇到影片临时改档退改签规则不一致节假日溢价这些你造数据时根本想不到的意外。这套系统真正成熟不是在上线那天而是在你陪运营处理完第一波真实突发状况之后。本文还有配套的精品资源点击获取
返回列表