如何做一個小程式口令紅包功能
作者:張先生 原文: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。對原作者說聲抱歉。
覺得本文對你有幫助?請分享給更多人。