如何作一個小程序口令紅包功能

在作小程序後端支持的過程當中遇到很多有意思的功能,有些比較考你的思惟散發及解決問題的實際能力,這裏摘錄一下記錄下來,是爲拋磚引玉、如能幫到別人,天然是最好不過了。php

先放幾張設計圖看下大概功能:前端

1

2

3

4

5

6

7

8

大概即是如此。java

經過圖片能夠看到,涉及到的稍微複雜一點的功能點有:語音文字識別、紅包分配算法,周邊紅包算法等等。 其他的都是些簡單的CRUD操做。我CODING+TESTING用了差很少一週,如下說下各個功能點的大概實現思路及方法。git

語音識別:

該功能的應用場景是:A用戶設置了一箇中文的口令紅包,接收到該紅包的B用戶須要用語音說出該口令,徹底匹配的話則獲取該紅包的某個比例金額。github

錄音天然是調用小程序提供的原生接口,不過這裏比較坑的是微信的錄音格式是 .silk。網上搜索的方法是先將.silk格式轉成wav或者MP3格式,而後再調用各大雲服務平臺的接口實現語音識別功能。redis

這裏使用了 https://github.com/kn007/silk... 提供的庫用來轉成wav格式,而後使用百度的語音識別開放接口 https://ai.baidu.com/tech/spe... 來識別語音結果。算法

業務實現步驟以下:shell

1.前端實現錄音功能
2.upload接口上傳.silk語音文件,入庫
3.觸發語音識別task,返回成功給前端(異步)
4.前端輪詢識別結果。json

由於從上傳到識別到返回結果是一個耗時操做,因此識別過程最好是異步操做。(第三步)小程序

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/questio... 找到。

說實話,看完這個答案仍是學到了一些東西的,如微信紅包的架構實現,分配寫法等等。

由於咱們的應用沒有微信的量級,天然不須要考慮太多(負載,併發等),產品的要求也只是說金額這方面要實現類微信紅包的分配方法便可。所以,考慮到擴展及性能以及時間,分配寫法我直接採用了 陳鵬 的答案裏的寫法,不過是變成了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。對原做者說聲抱歉。

相關文章
相關標籤/搜索