微信小程序电影院订票选座系统是一个典型的O2O(Online To Offline)应用场景解决方案。这个系统通过微信小程序作为前端入口,结合SSM(Spring+SpringMVC+MyBatis)后端框架,实现了从影片查询、场次选择到座位预订、在线支付的全流程闭环服务。
在实际开发中,这类系统通常需要解决三个核心问题:首先是实时座位的并发控制,避免超卖;其次是微信生态的深度集成,包括用户授权、支付接口等;最后是影院业务场景的特殊需求,比如退改签规则、座位分区定价等。
前端技术栈:
后端技术栈:
选择SSM框架而非Spring Boot主要考虑到:
code复制├── 用户模块
│ ├── 微信授权登录
│ ├── 个人信息管理
│ └── 订单历史查询
├── 影片模块
│ ├── 热映影片展示
│ ├── 即将上映
│ └── 影片详情
├── 场次模块
│ ├── 排期管理
│ ├── 场次查询
│ └── 票价策略
├── 座位模块
│ ├── 影厅布局
│ ├── 实时选座
│ └── 座位状态同步
└── 订单模块
├── 订单创建
├── 微信支付
└── 退票处理
java复制// 后端登录接口示例
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private WxService wxService;
@PostMapping("/login")
public Result login(@RequestParam String code) {
// 1. 通过code获取openid
String openid = wxService.getOpenId(code);
// 2. 查询或创建用户
User user = userService.findOrCreate(openid);
// 3. 生成JWT token
String token = JwtUtil.createToken(user);
return Result.success(token);
}
}
关键点:
注意:微信接口调用频率有限制,建议在后端做适当缓存
采用Redis的BitMap结构存储座位状态:
code复制键设计:cinema:{影院ID}:schedule:{场次ID}
值类型:BITMAP(每个bit代表一个座位状态)
操作命令:
- SETBIT key offset 1/0(设置座位状态)
- GETBIT key offset(查询座位状态)
- BITCOUNT key(统计已售座位数)
并发控制流程:
支付时序图:
code复制小程序 -> 后端: 创建支付订单
后端 -> 微信支付: 统一下单
微信支付 -> 后端: 返回prepay_id
后端 -> 小程序: 返回支付参数
小程序 -> 微信支付: 发起支付
微信支付 -> 小程序: 支付结果
微信支付 -> 后端: 异步通知
关键参数处理:
java复制// 支付签名生成
public String createSign(SortedMap<String,String> params){
StringBuilder sb = new StringBuilder();
for(Map.Entry<String,String> entry:params.entrySet()){
String k = entry.getKey();
String v = entry.getValue();
if(null!=v && !"".equals(v) && !"sign".equals(k)){
sb.append(k).append("=").append(v).append("&");
}
}
sb.append("key=").append(API_KEY);
return MD5Util.MD5Encode(sb.toString()).toUpperCase();
}
影片表(film)
sql复制CREATE TABLE `film` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '影片名称',
`cover` varchar(255) NOT NULL COMMENT '封面图',
`duration` int(11) NOT NULL COMMENT '时长(分钟)',
`category` varchar(50) NOT NULL COMMENT '类型',
`release_date` date NOT NULL COMMENT '上映日期',
`score` decimal(3,1) DEFAULT NULL COMMENT '评分',
`description` text COMMENT '剧情简介',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
场次表(schedule)
sql复制CREATE TABLE `schedule` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`film_id` int(11) NOT NULL,
`hall_id` int(11) NOT NULL COMMENT '影厅ID',
`show_time` datetime NOT NULL COMMENT '放映时间',
`price` decimal(10,2) NOT NULL COMMENT '基础价格',
`seat_info` text COMMENT '座位模板JSON',
PRIMARY KEY (`id`),
KEY `idx_film` (`film_id`),
KEY `idx_time` (`show_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
核心实现逻辑:
javascript复制Page({
data: {
seats: [], // 座位状态数组
selected: [] // 已选座位
},
// 初始化座位图
initSeats() {
wx.request({
url: '/api/schedule/seats',
success: (res) => {
this.setData({seats: res.data})
this.drawSeatMap()
}
})
},
// 绘制座位图
drawSeatMap() {
const ctx = wx.createCanvasContext('seatMap')
this.data.seats.forEach((row, i) => {
row.forEach((seat, j) => {
ctx.setFillStyle(this.getSeatColor(seat.status))
ctx.fillRect(j*40+10, i*40+10, 30, 30)
})
})
ctx.draw()
},
// 处理选座点击
handleTap(e) {
const {row, col} = e.currentTarget.dataset
// 状态切换逻辑...
}
})
xml复制<image lazy-load src="{{film.cover}}"></image>
javascript复制// app.js中预加载常用数据
App({
onLaunch() {
this.loadCities()
this.loadHotFilms()
}
})
javascript复制// 优先使用缓存数据
wx.getStorage({
key: 'hotFilms',
success(res) {
this.setData({films: res.data})
},
fail() {
this.loadFromServer()
}
})
最低配置:
推荐配置:
业务指标:
系统指标:
报警阈值:
yaml复制# Prometheus告警规则示例
- alert: HighOrderFailureRate
expr: rate(order_failed_total[5m]) / rate(order_attempted_total[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "High order failure rate"
现象:用户看到座位可选但下单时提示已售出
排查步骤:
解决方案:
java复制// 定时任务补偿逻辑
@Scheduled(fixedRate = 60000)
public void checkPendingOrders() {
List<Order> orders = orderService.getPendingOrders();
for(Order order : orders) {
WxPayOrderQueryResult result = wxPayService.queryOrder(order.getNo());
if("SUCCESS".equals(result.getTradeState())) {
orderService.completeOrder(order.getId());
}
}
}
防御措施:
lua复制-- seat.lua
local key = KEYS[1]
local seats = ARGV
for i,offset in ipairs(seats) do
if redis.call('GETBIT', key, offset) == 1 then
return 0
end
end
for i,offset in ipairs(seats) do
redis.call('SETBIT', key, offset, 1)
end
return 1
营销功能:
运营工具:
用户体验增强:
技术升级:
在实际开发中,我们发现影院业务存在明显的时段性高峰(如周末晚间),因此建议在资源分配上采用弹性扩容策略。对于中小型影院,可以考虑使用云数据库的只读实例来应对查询压力,而核心的交易流程仍保持主库操作以确保数据一致性。