背景
腾讯数字文创节(TGC),于11月19日-24日在海口万绿园举行,本次主题为“冲个文化热浪”,汇集腾讯旗下游戏、电竞、动漫等多领域30+款精品IP,带来数字文创融入海岛风情的奇妙体验。
任务
QQ飞车此次的场馆主题是“赛车手训练营”,玩家在场馆内完成了一系列的体验,就可以领取到赛车手证书。我是负责贯穿了场馆4大线下体验的小程序前后台开发。
1. 选择车厂
2. DIY车模
3. 沙盘赛车
4. 制作赛车手证
5. 打印属于自己的环保袋
场馆的设计
正面:入口在右边,出口在左边
1. 选择车场
玩法介绍: 玩家行走到上图箭头标识的地点,视频提示让玩家手掌触摸屏幕; 玩家将看到系统匹配的QQ飞车IP设定的4大车厂之一的介绍视频; 视频结束后提示玩家掏出自己的手机,打开微信扫小程序码,加入车厂,成为其中一名准赛车手。
2. 玩家DIY车模,并在大屏幕展示
玩法介绍: 玩家走到上图箭头标识的ipad面前,ipad上呈现的是小程序码和游客登录; 玩家可以用手机打开微信扫码小程序码; ipad将会获取玩家之前的车厂数据,调出该车厂所拥有的QQ飞车模型,供玩家在ipad上编辑自己的赛车。
编辑完毕后,身边的大屏幕会以动画的新式展示最近提交的玩家的赛车模型。
3. 沙盘赛车
玩法介绍: 玩家走到沙盘(水平放置的大屏幕)前,这里摆放着4台手机,手机上也是有小程序码和游客登录两种模式供选择。玩家用自己的手机扫小程序码后,就相当于入座,等待比赛开始。 当4名玩家都扫码完毕后,运营人员开启比赛。 沙盘将调取玩家之前DIY的车模数据,玩家使用场地提供的手机,控制沙盘上自己DIY的车进行赛车游戏; 游戏的玩法采用了QQ飞车的道具赛模式进行。
沙盘正上方还有2个屏幕,在等待选手入席阶段,会显示赛车手成绩总排行榜; 当玩家开始游戏的时候,屏幕会直播当场比赛; 当比赛结束时,会呈现当场比赛结果以及排名。
4. 制作赛车手证
玩法介绍 玩家走到触摸屏前,依然如之前的交互一样,扫码或者选择游客进行体验。 玩家将依赖屏幕上的摄像头进行自由自拍; 照片将使用了AI-lab的人脸融合技术,把玩家的相貌跟玩家之前选择的QQ飞车车厂代表人物的模型进行融合,并结合之前选择的车厂名称和图标、DIY的车模照片、沙盘赛车的成绩,生成一张新的照片; 玩家满意后,可以选择打印照片,然后在边上等待片刻即可拿走自己的照片。 也可以在小程序实时看到该照片并一键保存到手机上或是分享给朋友。
5. 打印属于自己的环保袋
玩法介绍 玩家可以到TGC兑换总台,凭二维码打印属于自己的环保袋,二维码也在小程序的“我的涂鸦”查看里,环保袋上有自己DIY的车模图案。
整体实现逻辑
整个设计比较复杂,大脑的缓存始终比不上烂笔头,如果时间允许的话,画画图舒畅脉络,有益身心~ 如果要详细的逻辑,可以看看我在前期processOn规划的流程图+ER图。 超级细节的流程设计和ER图设计
如果没耐心看完上面链接的内容,这里简单介绍一下核心逻辑。 玩家掏出自己的手机,打开微信扫线下设备的小程序码,每个设备的小程序码都不相同,都带有各自不同的参数:
?scene=diy&room=1 //代表DIY车模的ipad1号机
?scene=race&room=2 //代表沙盘赛车的2号操作机
...
小程序打开后,根据参数的场景值(scene)和座位号(room)的值,执行云函数,进行表(此处的表名是roomdata)的update操作,把用户的信息存到该表对应的doc(如图DIY车模就是diyroom)中的对应数组上。
而承载该小程序码的线下设备页面,其实也是个浏览器打开的web,通过监听小程序云数据库(watch)roomdata表中的doc(如图diyroom)来进行入座判断,并把头像昵称等信息呈现在线下设备的页面中,这样玩家就不需要在线下设备进行登录的操作,线下设备也能获取到玩家的个人信息了; 当玩家完成了体验,将会携带玩家的unionId(其实也可以用openid代替)来进行上传数据,在云端,云函数将会把上传过来的数据进行2次操作: add到diylogs表中,用以记录每一位玩家(游客)的体验历史; 根据unionId查询,update该玩家自己的userdata中diy_data项,这样该玩家就可以在其他页面快速拉取到体验的成果了。
如上图,每一个doc代表一个玩家。 每一位玩家在授权的时候,都会生成以上数据(除了红框圈出的); 而红框圈出的数据,则是玩家在进行对应的线下体验后,才会生成的、属于玩家自己的成果数据。
技术侧思考
本想着目前的篇幅已经很长了,新开一篇聊这个项目中的一些思考,但考虑到整体连贯性,还是放在一篇里说吧。 我将从以下角度去阐释我在此项目中的思考,以及爬坑记录。
1. 腾讯云的http请求
2. 排行榜的思考
3. 云函数触发器
4. 一个简单的token设计
5. 订阅功能的小细节
6. 小程序scene场景值
7. 用户误操作拒绝了“相册权限”
1. 腾讯云的http请求
小程序云的数据如果要被web端或者外部的服务器端访问,可以通过腾讯云(小程序云依托在腾讯云)的http方式进行。 而http请求的使用方式,也可以简单分成2种(腾讯云文档):
1. 直接请求:https://${环境id}.service.tcloudbase.com/云函数名 如:
https://production-ds2d422s.service.tcloudbase.com/funtionName
2. 引入sdk再使用
<script src="https://imgcache.qq.com/qcloud/tcbjs/1.6.1/tcb.js"></script>
const app = tcb.init({env:环境id}) app.callFunction({ name, data })
在大部分场景下,两种方式都可以满足我们的需求。 第一种就跟常规的前端请求后端接口的方式相似;第二种则跟在小程序的page的环境里的能力一样。 而对我们当前的项目来说,最直接的影响是,第一种方式是不能使用watch API监听databse的变化的,也不能自由查询databse,所有的访问都必须经过事先定义好的云函数,才能对云存储和databse进行访问,而这也会增加云函数的复杂程度。
2. 排行榜的思考
项目预研阶段,我曾经对沙盘塞车的总排行榜的获取性能有疑问,有点担心比赛数据一旦多了,会不会造成数据排序慢的问题。直到我搜索到了这么一条资讯:
而我们的赛车数据体量明显不是这种级别的,低头掐着手指脚趾稍微算了一下,2500条也才1M的数据,再对比一下腾讯云4年前就拥有的强大排序能力,此时请把“呵呵”打在公屏上。
3. 云函数触发器
定时触发器 之所以使用到这个能力,是因为我们的沙盘赛车排行榜会在每2小时结算这2小时内的第一名,然后通过微信订阅功能通知该玩家回来领取实体奖励。而我们的开馆时间并不是每天都一样的:
TGC运营时间: 19-20日:下午2点-晚9点半 21-22日:下午1点-晚9点半 23-24日:下午3点-晚9点半
所以这里设置的触发器,则是在以上时间段里的每2个小时触发一次排行+推送的函数,触发器的配置:
"triggers": [
{
"name": "tgc19-20",
"type": "timer",
"config": "0 0 14,16,18,20 19,20 11 * *"
},
{
"name": "tgc21-22",
"type": "timer",
"config": "0 0 13,15,17,19 21,22 11 * *"
},
{
"name": "tgc23-24",
"type": "timer",
"config": "0 0 15,17,19 23,24 11 * *"
},
{
"name": "tgc-per-day-end",
"type": "timer",
"config": "0 30 21 19,20,21,22,23,24 11 * *"
}
]
我们先说一下排行榜获取的逻辑。请求云函数rank的当前时间(Date.now())处在以上哪个区间段,比如如果处在19日的14-16点间,那么提取前6名玩家信息的条件将会是:
colleciton(rank).aggregate().match({create_at:_.gt(14点)._lt(16点)}).limit(6)
而每两小时触发的“提取第一名”逻辑也复用了以上逻辑,只是limit是1. 测试的时候,我们发现第一名一直没有收到推送。仔细打印信息,得到第一个忽略的问题:
云函数中的时区为 UTC+0,不是 UTC+8,在云函数中使用时间时需特别注意。如果需要默认 UTC+8,可以配置函数的环境变量,设置 TZ 为 Asia/Shanghai。
问题不大,设置一下时区后,继续测试。仍然发现没成功。 当时在tgc海南现场的休息室,已经是开幕前一天晚上21点了。有点着急,但并不感到慌,因为身边一直有着四姑娘和雨哥高垒等人在帮我出谋划策,激烈讨论着删库跑路,去哪买泳裤,策划路线,如何游泳回深圳等事项,可谓是贴心暖男,暖你一整天。 最终不负众望,我发现了我没考虑到的一个问题:触发器的设置是正点后1,2秒内(比如设置的是16:00,从日志里看到执行的时间大都是在16:00:01或者02秒以内),这意味着。。。我之前的设计是存在疏忽的:我的本意是取时间区间(14:00-16:00)的第一名,但是根据逻辑,执行云函数的当前时间处在的是(16:00-18:00),将会进行这个时间段的排序。而这个时间段才刚刚开始,是不会有任何比赛记录的,也就自然没有第一名,没有推送了。 找到问题就简单了。。。稍稍把Date.now()往前偏差5秒,跳回之前的时间段里去取排行,就行了。可以不用游泳回深圳了。
4. 一个简单的token设计
我们的线下设备默认展示的是小程序码,用户可以通过扫码的方式“进入座位”。那如果有“淘气的用户”把设备的小程序码拍照了,发到他的群里,让不在场的其他人都扫着玩,恶意占席,又该如何处理呢。 首先,我们有设计管理端,可以直接踢人。 而如果有更恶意的无限刷的人怎么办?虽然这种考虑非常极端,但是我们也不能不考虑,毕竟树大招风。于是我们设计了一个简单的token机制,可以在控制端实时打开和关闭。
而现场设备上也会在扫码的时候,出示一个4位数的弹窗。这样就能杜绝不在场的人,能“入座”了。 而这个设计使用到了前端crypto模块。
var crypto = require('crypto');
function encrypt(data, key, iv) {
let cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
return cipher.update(data, 'utf8', 'hex') + cipher.final('hex'); }
function hash(str) {
var hash = 5381, i = str.length; while (i) { hash = (hash * 33) ^ str.charCodeAt(--i); }
return hash >>> 0;
}
var key = 'from::tgc.qq.com'; var iv = 'to:speedm.qq.com';
module.exports = function (uid = 0, num = 3) {
var sid = ((Math.floor(new Date().getUTCMinutes() / num) num) 60 60 1000);
var s = new Date().getUTCDate() + new Date().getUTCHours() + Math.sqrt(sid) + '' s = uid + s;
let enc = encrypt(s, key, iv) return Number((hash(enc) + '').slice(0, 4))
}
简单的说就是线下设备端通过crypto+自定义密钥,得到一个1分钟(可配置)内不变的4位数(可配置),并显示在线下设备中。 而云函数端也会通过同样的原理计算出一个4位数,当玩家在自己手机输入一个4位数并提交的时候,云函数会核对。成功则“入座”。
5. 订阅功能的小细节
wx.requestSubscribeMessage({
tmplIds: [''],
success (res) { }
})
这里的tmplIds其实是一串常量字符串集合,我们可以将其存储在云函数中,随时请求随时更改,避免写死在小程序的代码中,每次修改都得走审核流程。 但是这里要注意的是官方没有提及的,模版的id需要提前获取,而不能跟requestSubscribeMessage同时封装在一起:
//伪代码
subscribe:async function(){
let tmplId = '';
await db.collection(xx).get().then(res=>{
tmplId = res.data.tmplIds
console.log(“订阅模版:”,tmplId)
})
wx.requestSubscribeMessage({
tmplIds: [tmplId],
success (res) { }
})
console.log(“订阅执行完毕”)
}
如上,将只会执行console,而跳过requestSubscribeMessage,就很神奇。 所以这里我是在page-onload的时候,就预先从database获取了tmplId,而不是在等交互点击订阅按钮的时候才去获取。
6. 小程序scene场景值
我们扫码入席的交互,不但设置了管理端踢人,token机制,还有这个最简单的门槛,就是需要扫码进入,才会进行“入座”逻辑。看起来没啥可聊的,看起来是那么的合乎常理,而代码逻辑也只需要加一个简单的判断就能实现,但思维陷进往往都会出现在显而易见之处。 我们先来看看小程序的场景值 我们再看看二维码对应的场景值,嗯,对应的是1011。
if(scene===1011){执行入座}
然而当发布一次小程序,进行真实环境的线下联动实测的时候,我们一直对着设备里展示的小程序码,怎么扫都入席不了。 你品,是不是有什么地方不对? 我不断的检查代码逻辑,因为涉及到权限部分,分支很多,消耗了我大量时间,最终发现: 嗯。。。测试的时候,我们都用开发工具生成的二维码进行(小程序码只有发布过一次正式版才能生成)。而线下实测的时候,咱们扫的是正式版的小程序码.scene===1047 我的日志里是这么记录的:
7. 用户误操作拒绝了“相册权限”
我们有海报保存到相册的需求。这里常规代码大概如此:
<!-- wml -->
<button bindtap="savePoster">保存</button>
js
//js
wx.saveImageToPhotosAlbum({...})
这里将会触发相册授权的系统级弹窗,如果玩家误操作,点击了拒绝。那小白用户就懵逼了,因为你再怎么点击保存,也不会有任何反应。 所以建议这里要做一层判断,如果用户拒绝了授权,我们把原“保存”按钮替换成授权按钮。
<button wx:if="{{saveFile===1}}" bindtap="savePoster">保存</button>
<button wx:else bindopensetting="savePoster" open-type="openSetting">保存</button>
我们在拒绝授权的时候,更改saveFile的值,驱动界面显示“授权按钮”代替常规按钮。 下面2张图则是这2种交互的前后对比。
<==>
可以在拒绝权限后,继续让用户授权,进行“保存到相册”的操作。
技术小结
复杂的项目需要提前预研,依赖大脑的缓存做全盘的规划并不可靠,值得信赖的是笔记与流程图等思维沉淀。 看起来是一个并不怎么复杂的小程序,但是“看起来”所看到的,仅仅是冰山的一角,仅仅是体验分支中的少数几条分支。 而我们80%的工作,都在对体验中各种分支进行梳理,兼顾方案设计,防范设计,容错设计,补救方案的技术铺垫,方便运营修改等设计。 而且我们也设计了非线性的兼容方案,玩家可以不拘泥于以上的顺序体验,也一样能顺畅进行线下的玩法。 我们甚至设计了游客登录,让一些不想留下足迹的玩家也可以快速体验线下环节。 虽然上线前,我们觉得已经完成了100%甚至更多的工作,但后面我们发现,依然做的不够,比如场馆太热,电视直接被晒“爆”;ipad设备的部署没考虑充电以及散热,导致短暂的脱机;比如由于我对腾讯云的权限体系不熟悉导致四姑娘的后期工作量增加等等。 没有完美的项目,只有复盘不够深刻的项目。 我们通过每次的不断的深刻总结,复盘,虽然不能改变已经发生的,但是我们可以避免后续的项目不再踩坑。
总结
通过小程序串连,玩家通过统一的交互形式,用自己的手机扫线下的设备进行入座,用线下的设备继续体验流程,玩家不但能体验线下的玩法,玩出自己的花样,还能将自己的线下成果记录在小程序中,最后生成海报进行分享。 通过以上方式,玩家不但得到了体验本身的乐趣,也从中了解到QQ飞车本身的内容。 对于QQ飞车老玩家而言,加入QQ飞车世界的车厂,成为一名准赛车手,经历一系列QQ飞车的特训,最终领取赛车手资格证,成为一名真正的赛车手,体验方式的设计提高了玩家的代入感。 而对于没有玩过QQ飞车的用户,也可以通过体验了解更多QQ飞车本身的故事,通过有趣的沙盘赛车,玩家了解到QQ飞车的道具赛模式,也会对QQ飞车本身产生兴趣。 最后的最后,贴一下开馆后的场景,场馆内人头攒动,场馆外熙熙攘攘。