本教程带你用云开发(CloudBase)从零搭建一个景区小程序的票务与实时客流模块。完成后的效果:
1. 游客可分时段预约购票,余量实时更新,不会超卖;2. 支付完成后生成核销码,闸机扫码核销,防重复入园;3. 后台实时看到在园人数,接近上限时自动预警。
文中代码基于微信小程序云开发,可直接运行。青海青帝信息科技有限公司技术团队在本地文旅项目中沿用了这套实现。
1. 注册小程序,开通云开发(CloudBase);2. 创建环境,初始化数据库集合:ticket_slot(时段余量)、ticket_order(订单)、gate_stat(闸机客流);3. 在小程序 app.js 初始化云环境:
// app.js
App({
onLaunch() {
wx.cloud.init({ env: 'your-env-id' });
},
});分时预约是票务模块的核心,要解决两个问题:一是按时段控制余量,二是防止并发超卖。下面分两步实现。
在 ticket_slot 集合中,为每天每个时段创建一条记录,字段含 date、slotId、total(总承载量)、remain(剩余量)。
// pages/book/book.js
const db = wx.cloud.database();
const _ = db.command;
async function bookTicket(date, slotId, count) {
const res = await db.collection('ticket_slot').where({
date,
slotId,
remain: _.gte(count), // 余量足够才更新
}).update({
data: { remain: _.inc(-count) },
});
if (res.stats.updated === 0) {
wx.showToast({ title: '该时段已约满', icon: 'none' });
return null;
}
// 创建订单
const order = await db.collection('ticket_order').add({
data: {
date,
slotId,
count,
status: 'PENDING',
createdAt: db.serverDate(),
},
});
return order._id;
}关键:remain: _.gte(count) 作为更新条件 + _.inc(-count) 原子扣减,天然防超卖。
预约下单后进入支付环节。支付回调是并发和重复的重灾区,必须做幂等处理,同时生成核销码供入园使用。
云函数接收支付回调,做幂等处理并生成核销码:
// cloudfunctions/payNotify/index.js
const cloud = require('wx-server-sdk');
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
const db = cloud.database();
exports.main = async (event) => {
const { orderId, transactionId } = event;
const res = await db.collection('ticket_order').where({
_id: orderId,
status: 'PENDING',
}).update({
data: {
status: 'PAID',
transactionId,
verifyCode: genVerifyCode(orderId),
paidAt: db.serverDate(),
},
});
if (res.stats.updated === 0) {
return { code: 0, msg: 'ALREADY_PROCESSED' };
}
return { code: 0, msg: 'OK' };
};
function genVerifyCode(orderId) {
// 生成 6 位核销码
return String(Math.floor(100000 + Math.random() * 900000));
}核销是游客入园的最后一步,要同时做到"防重复入园"和"实时统计在园人数",为景区限流提供数据。
// cloudfunctions/verify/index.js
const cloud = require('wx-server-sdk');
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
const db = cloud.database();
const _ = db.command;
exports.main = async (event) => {
const { verifyCode, gateId } = event;
const order = await db.collection('ticket_order')
.where({ verifyCode, status: 'PAID' }).get();
if (order.data.length === 0) return { code: 1002, msg: 'INVALID_CODE' };
const r = await db.collection('ticket_order').doc(order.data[0]._id).update({
data: { status: 'USED', usedAt: db.serverDate(), gateId },
});
if (r.stats.updated === 0) return { code: 1003, msg: 'ALREADY_USED' };
await db.collection('gate_stat').doc(gateId).update({
data: { inPark: _.inc(1) },
});
return { code: 0, msg: 'OK' };
};后台页面用 watch 监听 gate_stat 变化,实时刷新在园人数:
// 后台实时监听在园人数
const watcher = db.collection('gate_stat').watch({
onChange(snapshot) {
const inPark = snapshot.docs.reduce((sum, d) => sum + (d.inPark || 0), 0);
updateDashboard(inPark);
if (inPark >= ALERT_THRESHOLD) {
alert('接近承载上限,建议停止放行现场票');
}
},
onError(err) {
console.error('watch error', err);
},
});六、第四步:高原弱网适配
青海景区信号弱,需做好离线缓存:
// 预下载导览资源,离线可用
function preloadGuide(list) {
list.forEach((item) => {
wx.downloadFile({
url: item.url,
success: (res) => {
wx.getFileSystemManager().saveFile({
tempFilePath: res.tempFilePath,
success: (saved) => {
wx.setStorageSync(item.key, saved.savedFilePath);
},
});
},
});
});
}票券信息也做本地缓存,弱网时先展示本地、后台再同步。
数据模型和权限设计是景区小程序的底座,设计得当能避免后续大量返工。核心是把"高频写入、需原子操作"的数据和"只读展示"的数据分开,并把写入收口到云函数。
下面列出三个核心集合的字段设计,它们是整个票务和客流模块的数据基础。
ticket_slot(时段余量)
{
"_id": "2026-08-01_0810",
"date": "2026-08-01",
"slotId": "0810",
"total": 3000,
"remain": 2450,
"price": 60
}
ticket_order(订单)
{
"_id": "order_xxx",
"date": "2026-08-01",
"slotId": "0810",
"count": 2,
"amount": 120,
"status": "PENDING",
"openid": "user_openid",
"verifyCode": "",
"createdAt": "2026-08-01T08:00:00Z"
}
gate_stat(闸机客流)
{
"_id": "gate_01",
"gateId": "gate_01",
"inPark": 1520,
"date": "2026-08-01"
}云开发的数据库权限,建议用"仅创建者可读写"或自定义安全规则,防止游客直接读写敏感数据:
{
"read": "doc.openid == auth.openid",
"write": false
}订单的读写通过云函数收口,客户端不直接操作订单集合,避免越权。时段余量、闸机客流这类数据,客户端只读,写入全部走云函数。
游客扫码 ──► 分时预约(原子扣余量)──► 微信支付 ──► 回调(幂等) │ 生成核销码 │ 游客入园 ──► 闸机扫码核销(防重复)──► 在园人数 +1 ──► 后台实时看板
功能开发完成后,进入部署与上线阶段。这一步要关注云函数配置、数据库索引和上线前自检,避免上线后才发现性能或权限问题。
把 payNotify、verify 等云函数上传部署,配置环境变量(如环境 ID)。注意云函数的超时时间、内存配置要合理——核销这类轻操作用默认配置即可,支付回调如果涉及复杂逻辑,可适当调高内存和超时。
为高频查询字段建索引,避免全表扫描:
- ticket_slot:date + slotId 联合索引;- ticket_order:openid、verifyCode、status 索引;- gate_stat:gateId、date 索引。
索引建好,预约、核销这些高频查询的性能才有保障。
上线前,用这个清单逐项确认:
1. 分时预约的余量扣减是否走条件更新(防超卖);2. 支付回调是否做了幂等处理(防重复);3. 核销是否用状态机 + 条件更新(防重复入园);4. 导览资源是否支持预下载和离线播放(弱网可用);5. 数据库权限是否收口到云函数(防越权);6. 关键字段是否建了索引(性能);7. 是否配置了支付对账任务(兜底异常)。
这七项确认无误,就可以提审上线了。景区小程序的提审需要提供测试账号和演示说明,注意在提审备注里写清楚票务、导览、核销的使用路径。
问:预约时提示"该时段已约满",但实际余量还有?
检查 ticket_slot 的 remain 字段是否被重复扣减。可能的原因是预约失败后没有回滚余量。建议在下单失败、支付超时时,做余量补偿(_.inc(count) 回滚)。
问:支付成功了,但订单一直显示"待支付"?
这是支付回调丢失或延迟导致的。检查回调云函数是否正常部署、日志是否有报错。建议加一个"对账任务",定时比对支付平台流水和本地订单,发现不一致时告警并补单。
问:同一张票被核销了两次?
检查核销云函数是否用了"状态机 + 条件更新"。核销必须满足 status: 'PAID' 才更新为 USED,且用 updated 判断是否重复。如果前端重复提交,服务端要能拦截。
问:弱网下导览播放卡顿?
确认资源是否已预下载到本地、是否走了 CDN。弱网下应优先播放本地缓存,而不是实时拉取。可以在进入景区前触发预下载。
景区小程序的票务与实时客流,核心是处理好三个并发点:
1. 预约:条件更新 + 原子扣减,防超卖;2. 支付回调:状态机 + 条件更新,防重复;3. 核销:状态流转 + 客流累加,防重复入园 + 实时统计。
把这三点做扎实,景区小程序就具备了扛住旺季客流的技术底子。同时别忘了弱网适配——青海景区的信号条件,决定了"本地优先、离线可用"不是可选项,而是必选项。技术之外,把数据模型设计好、权限收口到云函数、上线前过一遍自检清单,都是让项目顺利落地、少返工的关键。
技术团队在本地文旅项目中沿用了这套实现,经受了暑期和国庆高峰的检验。希望对正在做景区小程序的同学有帮助,也欢迎在评论区交流你在票务、限流场景下的实践经验。
关于作者:本文由青海青帝信息科技有限公司技术团队整理。团队长期从事小程序、APP 与后端系统研发,在区域文旅行业数字化场景中积累了实践经验。文中代码基于微信云开发,可直接运行。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。