利用客服消息和模板消息实现微信群发(突破群发接口的上限)

一、关于群发接口和消息接口

关于群发接口
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

相关文章
相关标签/搜索