利用客服消息和模板消息實現微信羣發(突破羣發接口的上限)

一、關於羣發接口和消息接口

關於羣發接口
1.訂閱號天天能夠羣發消息一條,服務號每個月(天然月)四條的羣發權限。開發者模式下,能夠經過高級羣發接口,實現更靈活的羣發能力。
2.注意
● 對於認證訂閱號,羣發接口天天可成功調用1次,這次羣發可選擇發送給所有用戶或某個標籤;
● 對於認證服務號雖然開發者使用高級羣發接口的每日調用限制爲100次,可是用戶每個月只能接收4條,不管在公衆平臺網站上,仍是使用接口羣發,用戶每個月只能接收4條羣發消息,多於4條的羣發將對該用戶發送失敗;
● 具有微信支付權限的公衆號,在使用羣發接口上傳、羣發圖文消息類型時,可以使用a標籤加入外鏈;
關於客服消息和模板消息接口
當用戶和公衆號產生特定動做的交互時(具體動做列表請見下方說明),微信將會把消息數據推送給開發者,開發者能夠在一段時間內(目前修改成48小時)調用客服接口,經過POST一個JSON數據包來發送消息給普通用戶。此接口主要用於客服等有人工消息處理環節的功能,方便開發者爲用戶提供更加優質的服務。
模板消息僅用於公衆號向用戶發送重要的服務通知,只能用於符合其要求的服務場景中,如信用卡刷卡通知,商品購買成功通知等。不支持廣告等營銷類消息以及其它全部可能對用戶形成騷擾的消息。php

二、背景

簡單的描述一下業務的背景,目前是作一個供求的微信公衆號,每當用戶付費發送一次供求的信息,咱們須要將本條消息推送給全部的用戶,這樣可讓用戶及時收到消息,實現消息的實時性和保證消息的有效性。可是羣發的接口根本不能知足咱們的需求,因而咱們利用模板消息和客服消息的接口來實現咱們的需求,在此,有的人可能會發問,爲何客服消息的接收要求這麼變態還要利用它呢,實際上是由於,羣發的消息在某些設備上收到的時候,就像微信好友發送的一條信息,更加吸引用戶去關注。
另外,咱們還須要知道,假如咱們的用戶用成千上萬的人話,那麼羣發的方式是十分的耗時的,微信支付提供了notify的異步通知,以前我也一直嘗試利用這個現場的異步通知來實現羣發,可是根據實際的使用,這個異步IO的閾值差很少在6分鐘左右,但是實際上發送一次模板的網絡耗時加上數據庫的IO其實仍是時間仍是挺長的,所以咱們不得不使用異步多線程來實現咱們的羣發的功能這裏我利用的是swoole的擴展來完成這一需求的。
swoole的異步郵件羣發的demo,你們能夠參照個人另外一篇文章thinkphp5+swoole實現異步郵件羣發(SMTP方式)這裏能夠較爲詳細的瞭解服務端和客戶端的構建。html

三、環境說明

centos7
swoole2.0+
tp5.0+
郵件發送
crontab定時任務mysql

四、實現

4.1建立服務端

我這裏就直接貼代碼了,須要注意的地方都寫在了相關代碼的註釋,注意看一下sql

/**
     * description:服務端
     */
    public function asyncSend()
    {
        $serv = new \swoole_server('0.0.0.0', 9090);
        $serv->set(array(
            'task_worker_num' => 60,
            'daemonize' => true,
            'log_file' => '/var/www/html/myswl/tp/swoole.log'
            )
        );
        $serv->on('receive', function ($serv, $fd, $from_id, $data) {
            $task_id = $serv->task($data);
            echo "開始投遞異步任務 id=$task_id\n";
        });

        $serv->on('task', function ($serv, $task_id, $from_id, $data) {
            echo "接收異步任務[id=$task_id]" . PHP_EOL;
            $data = json_decode($data,true);

//這裏要特別的說明,我這裏用到了數據庫的遠程連接,由於支付的功能在另外一臺window虛擬雲主機上的,因此不得不利用遠程訪問的方式。Db::connect($conn,true);的第二個參數給給予關注,由於咱們沒發送一次其實都會去進行一次遠程數據庫鏈接,因此頻繁的鏈接中,確定會有鏈接失敗的狀況,所以咱們須要作好斷線重連的配置
            $conn = 'mysql://用戶名:數據庫遠程連接地址:3306/密碼#utf8';
            $db = Db::connect($conn,true);

            $now = date('Y-m-d H:i:s');
            $users = $db
                ->table('tp_receiver')
                ->field('openid')
                ->where("(`expire_time` > '{$now}' OR `send_status` = 1 ) AND `receive_status` = 1 ")
                ->limit($data['flag']*500,500)
                ->select();

            echo $db->getLastSql();

            echo 'send start--' . date('H:i:s') . PHP_EOL;
            foreach ($users as $user) {
                $token = $db->table('tp_accesstoken')->field('accesstoken')->find();
//這裏是客服消息
                $templData2 = array(
                    'touser' => $user['openid'],
                    'msgtype' => 'text',
                     'text' => array('content' => $data['content']."\n\n點擊下方「信息查詢」查看更多求購信息。"."\n回覆0取消接收信息,回覆1從新接收信息")
                );

                $res = $this->sendCustomMessage($templData2,$token['accesstoken']);
                $res = json_decode($res, true);
//判斷客服消息是否成功發送
                if($res['errcode'] == 45047 || $res['errcode'] == 45015) {
                    $templData = array(
                        'touser' => $user['openid'],
                        'template_id' => '模板消息',
                        'url' => '',
                        'data' => array(
                            'first' => array('value' => '您好,您收到一條新的提醒', 'color' => '#173177'),
                            'keyword1' => array('value' => '求購信息', 'color' => '#173177'),
                            'keyword2' => array('value' => $data['content'], 'color' => '#173177'),
                            'keyword3' => array('value' => $data['phone'], 'color' => '#173177'),
                            'keyword4' => array('value' => date('Y-m-d H:i:s'), 'color' => '#173177'),
                            'remark' => array('value' => "點擊下方「信息查詢」查看更多求購信息。"."回覆0取消接收信息,回覆1從新接收信息", 'color' => '#173177')
                        ),
                    );
                    $res = $this->sendTemplateMessage($templData, $token['accesstoken']);
                    Log::write('send to'.$user['openid'].$res . PHP_EOL);
                }
            }
            echo 'send end--' . date('H:i:s') . PHP_EOL;

            $serv->finish('');
        });

        $serv->on('finish', function ($serv, $task_id, $data) {
            echo 'finish time--' . microtime(true) . PHP_EOL;
            echo "異步任務[id=$task_id]完成" . PHP_EOL;
        });

        $serv->start();
    }

而後咱們在CLI模式下進入項目的根目錄,執行thinkphp

php public/index.php demo/wechat/asyncSend

這樣咱們的服務端就以守護進程的模式一直運行來咱們的後臺了,經過ps -aux | grep asyncSend
能夠看見,已經有62個進程在處於S(睡眠待喚醒)的狀態了,除了60個task進程還用一個master和一個woker進程。shell

process.png

4.2構建客戶端

代碼以下,須要注意的地方都寫在了相關代碼的註釋,注意看一下數據庫

/**
     * description:客戶端
     */
    public function index()
    {
//由於這個羣發比較敏感,咱們須要作一個token的機制,我這邊就用最簡單的發送方和接收方都以明文的方式來作了。
            $token = $_GET['token'];
            if($token != 'test'){
                exit;
            }
//content是發送的內容,由於不可預估裏面的東西,因此進行加解密
            $content = rawurldecode($_GET['content']);
            $flag = $_GET['flag'];
            $id = $_GET['id'];
            $phone = $_GET['phone'];

            $data['content'] = $content;
            $data['flag'] = $flag;
            $data['phone'] = $phone;

            Log::write(self::json_encode($data));

            $insert = [
                'flag'=>$flag,
                'miaomu_id'=>$id,
                'status'=>1,
                'createtime'=>date('Y-m-d H:i:s'),
                'content'=>$content
            ];
            Db::table('task')->insert($insert);

//異步客戶端
            $client = new \swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_SYNC);
            $ret = $client->connect("127.0.0.1", 9090);
//當沒法鏈接的時候,發送告警郵件
            if (empty($ret)) {
                SendMail::postmail('kongwentao@zuihuibao.com','警告','error!connect to swoole_server failed');
                SendMail::postmail('937069176@qq.com','警告','error!connect to swoole_server failed');
                Log::write('error!connect to swoole_server failed');
            } else {
                $client->send(self::json_encode($data));
            }
    }

4.3端口監控

這個羣發已經涉及到金額了,因此咱們更要關係服務的運行穩定了,這裏咱們簡單的利用crontab定時任務和Php的一些shell相關函數來實現端口的監控。
本次用到的定時任務json

*/1 * * * * curl http://你的域名/index.php/demo/Jrmm/checkPortStatus?token=test

就是實現一個每分鐘去執行咱們下面php代碼的一個任務,這裏我沒有直接用shell來操做,緣由有3點,1是我不是很熟悉shell命令,2是咱們不太熟悉shell命令,3是寫在php裏面更方便我去寫相關代碼和利用已有的一些方法,好比郵件發送。這樣雖然多了一點網絡資源的消耗,可是也還划算。
具體的監控代碼,這邊實現的時候會出現不少權限的問題,我就很少說了,遇到的時候自行百度。
/**centos

* description:8082服務端口監控
 */
public function checkPortStatus(){
    if (!isset($_GET['token']) || $_GET['token'] != 'test'){
        exit();
    }
    $res1 = exec('sudo netstat -lpn | grep 9090');
    Log::write($res1);
    if($res1 == ''){
        Log::write('9090stop');
        SendMail::postmail('kongwentao@zuihuibao.com','警告','9090端口服務錯誤');
        SendMail::postmail('937069176@qq.com','警告','9090端口服務錯誤');

//重啓咱們的服務端,這裏須要注意的是,我沒有用到swoole提供的平滑重啓的功能,極可能會形成數據的丟失,這別額外的須要注意微信

exec('sudo php /var/www/html/myswl/tp/public/index.php demo/jrmm/asyncSend');
        $res = exec('sudo netstat -lpn | grep 8082');
        if($res != ''){
            Log::write('9090restart success');
            SendMail::postmail('937069176@qq.com','警告解除','9090端口重啓成功');
            SendMail::postmail('kongwentao@zuihuibao.com','警告解除','9090端口重啓成功');
        }
    }
}

4.4實現

咱們利用PHP curl函數來模擬一次支付成功後調用咱們羣發的功能。

$content = 'test';
        for ($j=0;$j<3;$j++){
             $url = 'http://你的域名/index.php/demo/Jrmm/index'.'?flag='.$j.'&id=1.'&token=test'.'&phone='110&content='.rawurlencode($content);
            $this->http_post($url);
        }
function http_post($url){
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_HEADER, 0);
    $res= curl_exec($ch);
    curl_close($ch);
    return $res;
}

爲何循環三次呢,由於咱們經過測試發送發送1500次消息的時候,耗時差很少6分鐘,可是咱們的項目的併發很低,那麼就沒法充分利用咱們開啓的60個task進程,因此咱們將1500分紅三次去發送那麼實際上咱們消耗了幾乎能夠忽略不計的網絡消耗,讓咱們的發送的性能提升了三倍多,實際的項目中,發送1500多條實際耗時只要不到兩分鐘。固然當併發量更大的時候,咱們還能夠採用隊列的方式來處理,這樣須要咱們隊task進程管理更加的熟練。

五、截圖

收到的推送消息

jieshou.png

告警
gaojing.png

發送的日誌

sengdLog.png

六、項目完整代碼下載

http://pan.baidu.com/s/1c2q7ik4

相關文章
相關標籤/搜索