基于Vue与SpringBoot的微信小程序电影票务系统实战
2026/8/31 12:15:40 网站建设 项目流程

简介:本资源是一套基于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_nocol_no两个整数存。这里有个很实际的经验:不要用"A1""B3"这种字符串座位号直接存单字段,排序和区间查询都会很痛苦。前台展示时再把行列号拼成"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接口换openidsession_keyopenid是用户在你们小程序里的唯一标识,业务上就用它来关联用户。

这里有一个安全细节:session_key是微信用来解密用户手机号等敏感信息的密钥,它不应该被返回到前端,更不应该存到数据库里。正确做法是后端换取openid后,用自己的JWT生成机制签发一个业务Token,这个Token才返回给小程序端。

我用的方案是:用户第一次登录时,查user表看有没有这个openid,没有就自动注册一条记录。然后生成JWT,payload里放userIdopenid,有效期设成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, CANCELLED
PAID已支付REFUNDING, USED
CANCELLED已取消终态
REFUNDING退款中REFUNDED
REFUNDED已退款终态

为什么要一开始就把状态机完整定义好?因为支付回调是无状态事件,你没法预测它会以什么顺序到来。比如用户支付成功后立刻点了取消订单,如果代码里没做好状态判断,就可能出现"已经取消的订单又被标记为已支付"这种数据错误。我在所有状态流转的地方都加了一个前置校验:当前状态必须符合流转规则,否则直接拒绝。

微信支付回调的幂等处理是另一个重点。微信服务器为了保证消息可靠送达,会重试发送支付结果通知好几次。如果你的回调接口没有做幂等处理,同一笔订单可能被加两次余额、标记两次已支付。

我的处理方式是:回调接口第一步先根据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.navigateTouni.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里。

用户进入小程序后,我们不会强制让他立刻登录。真正的登录动作延后到"加入购物车"或"提交订单"时才触发。这样的设计用户体验更好,避免了用户一进来就被弹窗要求授权的反感。

登录的具体流程是:

  1. 用户点击"立即购买",触发checkLogin
  2. 如果Vuex里没有用户信息,调用uni.login()获取code
  3. code发给后端换取JWT和用户信息
  4. 存储到Vuex和uni.setStorageSync

有一个小细节是:uni.login()得到的code是一次性的,只能用一次,而且有效期很短(通常几分钟)。所以每次需要重新登录时都要重新调用uni.login()获取新的code,不能缓存复用。

待支付订单的倒计时也是前端的一个小难点。我最初的方案是在页面onShow的时候用setInterval每秒刷新倒计时,后来发现如果用户在订单确认页停留太久,倒计时归零但座位实际已经被后端定时任务解锁了,前端还傻傻显示"待支付"。

后来我换了一种思路:前端向后端轮询订单状态,每5秒一次,一旦发现订单变成了CANCELLED,立即提示用户"座位已释放,请重新选座"。这个轮询在用户离开页面时用onUnload清除定时器,避免内存泄漏。

5. 从开发到上线:真机调试、域名备案与版本发布避坑

代码写完了,不代表项目就结束了。小程序开发里最折磨人的往往不是写代码,而是从"我在开发者工具里能跑"到"真机上稳定能用"这段路。这一段我踩过的坑加起来能写满一页A4纸。

5.1 微信小程序合法域名与SpringBoot接口配置

微信小程序的网络请求有一个硬性规定:wx.request(uni-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 上线版本审核注意点

小程序发布前是要过微信平台审核的,审核不通过的常见原因有:

  1. 类目选择不对。电影票务属于"生活服务-票务"类目,需要提供相应的资质证明。如果类目选错了,审核会被驳回。
  2. 页面内容不完整。审核人员会模拟真实用户走一遍流程,如果发现某个按钮点了没反应、某个页面明显是空壳,会被判为"功能不完整"。
  3. 支付流程不规范。微信对涉及支付的类目审核很严,你的商户号需要在微信支付后台配置好关联AppID,否则支付能力在小程序里是调不通的。
  4. 虚拟支付问题。特别注意:小程序里不能卖纯虚拟商品(比如在线视频会员),但电影票属于实体服务类,可以正常用微信支付,前提是你要有对应的类目资质。

上线之前我习惯把主要流程在预览版里完整走一遍,包括:登录、浏览电影、选座、下单、支付、查看订单、取消订单。每一个步骤都截图留档。这样如果审核被驳回,我能根据驳回截图快速定位是在哪一步出了问题,而不是手忙脚乱从头猜。

审核通过后,版本发布还有一个灰度策略可以做。微信后台支持"分阶段发布",可以先给10%的用户放量,观察线上有没有报错和投诉,再逐步放量到100%。我当时是直接全量发布的,后来想想有点冒险,建议你做电影票务这种带有支付环节的小程序,最好还是灰度一下,稳一手。

6. 一次实战联调后的思考:这套架构还能怎么复用

项目上线、稳定运行一段时间之后,我回头审视了一遍整个架构,发现这套"Vue+SpringBoot+微信小程序"的组合,其实能复用的场景比想象中更广。

就拿票务系统来说,改造成其他类型的预约系统非常顺畅。核心的表结构"场次+座位+订单"三件套,只要把movie表换成doctorschedule换成门诊排班seat换成号源,一个预约挂号系统就出来了。演唱会票务、体育赛事、景区门票、甚至自习室座位预约,本质都是同一套逻辑。最多在选座页改成选时间段,或者把座位的行列模型改成时间段列表,业务层几乎不动。

后端的那套Redis分布式锁和订单状态机的设计,在很多涉及库存扣减的场景里都适用。比如电商秒杀、优惠券抢购、库存扣减,只要涉及多个用户同时操作有限资源,这套思路都能直接迁移。我在写这个项目的时候,因为要处理座位锁定和超时释放,把Redis的SET NX EX和定时任务用了好几遍,后来做另一个库存预约项目时,直接把这套代码搬过去改了个名字就上线了。

如果你打算在自己的项目里复刻这套架构,我唯一的进阶建议是:把支付这一层独立出来。微信支付、支付宝支付、聚合支付,每个渠道的接入细节都不一样,但业务层的订单状态、退款状态是一致的。把支付封装成一个独立的模块,接口定义统一为"发起支付、支付回调、查询状态、申请退款、退款回调",会让你的系统干净很多。我当时就是把支付逻辑写在订单Service里,后来要接入支付宝的时候,改起来费了老大的劲。如果重来一次,我一定会先把支付抽象成独立的PaymentService接口。

我个人在实际操作中的体会是:选一座城市的中型影院作为首批合作方,用真实排片和票价跑通全流程,比在后台造一大堆假数据要有用得多。因为真实业务场景里会遇到"影片临时改档""退改签规则不一致""节假日溢价"这些你造数据时根本想不到的意外。这套系统真正成熟,不是在上线那天,而是在你陪运营处理完第一波真实突发状况之后。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询