如何做一个小程序口令红包功能

作者:张先生 原文:https://segmentfault.com/a/1190000011014127

在做小程序后端支持的过程中遇到不少有意思的功能,有些比较考你的思维散发及解决问题的实际能力,这里摘录一下记录下来,是为抛砖引玉、如能帮到别人,自然是最好不过了。

先放几张设计图看下大概功能:

大概便是如此。

通过图片可以看到,涉及到的稍微复杂一点的功能点有:语音文字识别、红包分配算法,周边红包算法等等。 其余的都是些简单的CRUD操作。我CODING+TESTING用了差不多一周,以下说下各个功能点的大概实现思路及方法。

语音识别

应用场景:A用户设置了一个中文的口令红包,接收到该红包的B用户需要用语音说出该口令,完全匹配的话则获取该红包的某个比例金额。

录音自然是调用小程序提供的原生接口,不过这里比较坑的是微信的录音格式是 .silk。网上搜索的方法是先将.silk格式转成wav或者MP3格式,然后再调用各大云服务平台的接口实现语音识别功能。

这里使用了 https://github.com/kn007/silk-v3-decoder 提供的库用来转成wav格式,然后使用百度的语音识别开放接口 https://ai.baidu.com/tech/speech/asr 来识别语音结果。

业务实现步骤如下:

1.前端实现录音功能 2.upload接口上传.silk语音文件,入库 3.触发语音识别task,返回成功给前端(异步) 4.前端轮询识别结果。

因为从上传到识别到返回结果是一个耗时操作,所以识别过程最好是异步操作。(第三步)

upload语音接口部分代码:

// ... 业务代码略
$voice = $this->getCreatedVoiceByBody(); // 上传并入库
$this->identifyVoice($voice); // 触发语音识别task

// ...

public function identifyVoice($voice)
{
    WorkerUtil::sendTaskByRouteAndParams('task/detectvoice', ['voiceid' => $voice->id, 'type' =>'redpack']);
}

如上可见,将一条包含了语音文件地址的记录id及类型发送到了后端task服务。

后端task服务处理如下:

class DetectVoice extends Action
{
    public function run($voiceid, $type = 'redpack')
    {
        if ($type == 'redpack') {
            $voice = Voices::findOne($voiceid);
            $url = $voice->voice;
            $saveName = '/runtime/redpack-'.$voiceid.'.silk';
            $convertName = '/runtime/redpack-'.$voiceid.'.wav';
        }
        $this->saveToLocalByRemoteVoiceUrlAndLocalFileName($url, $saveName);
        $cfg = [
            'appKey' => 'xxx',
            'appSecret' => 'xxx',
            'appId' => 'xxx',
        ];
        $util = new BaiduVoiceUtil($cfg);
        $code = exec("bash /www/silk-v3-decoder/converter.sh {$saveName} wav");
        if ($code == 0) {
            $result = $util->asr($convertName);
            if ($result['err_no'] == 0) {
                $voicesResult = json_encode($result['result'], JSON_UNESCAPED_UNICODE);
                $voice->result = $voicesResult;
                $voice->save();
                @unlink($saveName);
                @unlink($convertName);
            }
        }
    }
    ...
}

task服务的处理逻辑也很清晰:接收需要识别的voiceid,查找记录,把语音文件下到本地某个tmp目录,调用shell转换格式,将转换后的格式调用baidu的语音接口进行识别,再将结果入库。

voice表结构如下:

如此,便完成了语音识别功能。

红包分配

应用场景:创建红包时

打开红包一般有两种分配方法,一种是使用创建时便分配好每一份的份额。一种是打开时再动态分配,这里采取的是第一种。

具体讨论可在知乎:https://www.zhihu.com/question/22625187 找到。

说实话,看完这个答案还是学到了一些东西的,如微信红包的架构实现,分配写法等等。

因为我们的应用没有微信的量级,自然不需要考虑太多(负载,并发等),产品的要求也只是说金额这方面要实现类微信红包的分配方法即可。因此,考虑到扩展及性能以及时间,分配写法我直接采用了 陈鹏 的答案里的写法,不过是变成了PHP的版本。并且搭配了redis 作为红包份额的存储及可能的并发问题处理方案。

先上代码(redpack/create):

$redpack = $this->getCreatedRedPackByBody();
// ... 业务逻辑代码略
// 设置随机红包份额
$this->setRedPackOpenOdds($redpack);

protected function setRedPackOpenOdds($rp)
{
    $remainNum = $rp->num;
    $remainMoney = $rp->fee;
    $key = 'redpack:'.$rp->id;
    $redis = yii::$app->redis;
    while (!empty($remainNum)) {
        $money = $this->getRandomMoney($remainNum, $remainMoney);
        $redis->executeCommand('RPUSH', [$key, $money]);
    }
    $redis->executeCommand('expire', [$key, 259200]);
}

protected function getRandomMoney(&$remainNum, &$remainMoney)
{
    if ($remainNum == 1) {
        $remainNum--;
        return $remainMoney;
    }
    $randomNum = StringUtil::getRandom(6, 1);
    $seed = $randomNum / 1000000;
    $min = 1;
    $max = $remainMoney / $remainNum * 2;
    $money = $seed * $max;
    $money = $money <= $min ? $min : ceil($money);
    $remainNum--;
    $remainMoney -= $money;
    return $money;
}

这部分代码逻辑也相对简单,主要就是:

将当前金额和份数传入函数( getRandomMoney),在计算出当次的随机金额后,将该金额写入redis的一个list (key=redpack:id),然后将总金额和总份数减去,一直减完为止。

有几点值得注意的地方:

1.原答案里的随机数生成法使用了 java.math.BigDecimal. 可php没有对应的函数,自带的随机数也不好用。这里用的自己写的随机数生成方法 (获取6位的随机数字,然后除以它们的位数,就得到类似于 0.608948的随机数) 2.每个红包的份额设置了一天的过期时间,这是为了实现红包过期的功能。

redis里的结果(单位为分):

10元分配15个

100元分配7个:

50元分配25个:

可以看到基本实现了随机分配,也兼顾了手气最佳的要求。

使用也简单,打开红包获取份额的时候,使用这个list左边一个个出栈就行了。

红包地图

应用场景:查看周围发布的红包

这个实现的关键之处就是周边的坐标算法。首先,前提条件是创建红包时要获取到经纬度坐标,这个交由前端实现,我们只记录即可。

然后在调用这个接口时,把用户当前的经纬度传过来。根据这个经纬度计算出周边范围,然后查找表中在这个周边范围的记录即可。

代码如下:

/**
*
* @param double $lng 经度
* @param double $lat 纬度
* @param integer $radius 范围
* @return array
*/
public function run($lng, $lat, $radius = 500)
{
    $coordinates = $this->getAroundByCoordinates($lng, $lat, $radius);
    $field = 'id,lat,lng';
    $data = (new Query())
            ->select($field)
            ->from('{{app_redpack}}')
            ->where(sprintf("`lat` BETWEEN %f AND %f AND `lng` BETWEEN %f AND %f AND `ishandle` = 1 AND `isexpire` = 0", $coordinates[0], $coordinates[2], $coordinates[1], $coordinates[3]))
            ->all();
    return ResponseUtil::getOutputArrayByCodeAndData(Api::SUCCESS, $data);
}

/**
* 地球的圆周是24901英里。
* 24,901/360度 = 69.17 英里 / 度
* @param double $longitude 经度
* @param double $latitude 纬度
* @param integer $raidus 范围。单位米。
* @return array
*/
public function getAroundByCoordinates($longitude, $latitude, $raidus)
{
    (double) $degree = (24901 * 1609) / 360.0;
    (double) $dpmLat = 1 / $degree;
    (double) $radiusLat = $dpmLat * $raidus;
    (double) $minLat = $latitude - $radiusLat;
    (double) $maxLat = $latitude + $radiusLat;
    (double) $mpdLng = $degree * cos($latitude * (pi() / 180));
    (double) $dpmLng = 1 / $mpdLng;
    (double) $radiusLng = $dpmLng * $raidus;
    (double) $minLng = $longitude - $radiusLng;
    (double) $maxLng = $longitude + $radiusLng;
    return [$minLat, $minLng, $maxLat, $maxLng];
}

关键就是getAroundByCoordinates 这个算法,它根据输入的经纬度及范围大小,计算出左上,左下,右上,右下四个角的坐标,在地图上标出来的话就是 一个长方形的范围。

有兴趣的可以根据 http://lbs.qq.com/tool/getpoint/ 这个工具,随意点取一个坐标,根据以上的方法算出四个角,看看是不是刚好是$raidus指定的范围。

需要说明的是这个方法不是我写的,但是我实在不记得出处在哪了。我只是记得把java的实现方法改成了php。对原作者说声抱歉。

觉得本文对你有帮助?请分享给更多人。

原文发布于微信公众号 - 程序员宝库(chengxuyuanbaoku)

原文发表时间:2018-01-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏芋道源码1024

Dubbo源码解析 —— Zookeeper 订阅

前言 上周写完了服务暴露总结之后发现遗漏了一个很重要的点,在dubbo源码解析-zookeeper连接中我们对面试高频题 dubbo中zookeeper做注册...

32770
来自专栏MySQL

分库分表后如何部署上线?

不要惊讶,写这篇文章前,我特意去网上看了下分库分表的文章,很神奇的是,都在讲怎么进行分库分表,却不说分完以后,怎么部署上线的。这样在面试的时候就比较尴尬了。

32310
来自专栏玉树芝兰

如何把思维导图秒变成幻灯?

(由于微信公众号外部链接的限制,文中的部分链接可能无法正确打开。如有需要,请点击文末的“阅读原文”按钮,访问可以正常显示外链的版本。)

16220
来自专栏北京马哥教育

10年 Windows 与 Linux 程序员的区别

如果一个程序员从来没有在Linux、Unix下开发过程序,一直在Windows下面开发程序,同样是工作10年,大部分情况下与在Linux、unix下面开发10年...

20700
来自专栏腾讯技术工程官方号的专栏

TDSQL 全时态数据库系统--核心技术

35030
来自专栏Golang语言社区

关于多核编程的一点想法

Nim语言有很多语言上先进的特性和接近Python的语法,Rust定位成C++的直接竞争者。 但是请认真思考:这两个语言从一出生开始,都没有解决,而且以后也很难...

46950
来自专栏数据和云

深入剖析:not exists对外层查询的影响

何剑敏 Oracle ACS华南区售后团队,首席技术工程师。供职于Oracle ACS华南区售后团队,首席技术工程师。多年从事第一线的数据库运维工作,有丰富项...

31650
来自专栏Coding01

有机器人要菜单没什么用

微信开发,从未如此简单 每一个功能设计,都经过精心打磨,只为了提供更好的开发体验 https://www.easywechat.com/

9720
来自专栏数据之美

浅谈用户行为分析之用户身份识别:cookie 知多少?

对于数据统计分析或者数据挖掘而言,用户是个非常重要的维度,也是统计分析能落地的基础。一般而言,咱们追踪或者识别一个用户的首选方案是 userID,大多数公司的产...

1.4K60
来自专栏程序人生

分布式系统中的监工:Overseer

最近从无趣的工作中发现了有趣的事情,工作和业余时间都扑了些精力上去,本待上周末最终的成果出来后再写文章的,无奈事情太多,代码还没写完,二月上旬已过,再不写文章春...

34370

扫码关注云+社区

领取腾讯云代金券