在作小程序后端支持的过程当中遇到很多有意思的功能,有些比较考你的思惟散发及解决问题的实际能力,这里摘录一下记录下来,是为抛砖引玉、如能帮到别人,天然是最好不过了。php
先放几张设计图看下大概功能:前端
大概即是如此。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。对原做者说声抱歉。