1、关于群发接口和消息接口
关于群发接口
1.订阅号每天可以群发消息一条,服务号每月(自然月)四条的群发权限。开发者模式下,可以通过高级群发接口,实现更灵活的群发能力。
2.注意
● 对于认证订阅号,群发接口每天可成功调用1次,此次群发可选择发送给全部用户或某个标签;
● 对于认证服务号虽然开发者使用高级群发接口的每日调用限制为100次,但是用户每月只能接收4条,无论在公众平台网站上,还是使用接口群发,用户每月只能接收4条群发消息,多于4条的群发将对该用户发送失败;
● 具备微信支付权限的公众号,在使用群发接口上传、群发图文消息类型时,可使用a标签加入外链;
关于客服消息和模板消息接口
当用户和公众号产生特定动作的交互时(具体动作列表请见下方说明),微信将会把消息数据推送给开发者,开发者可以在一段时间内(目前修改为48小时)调用客服接口,通过POST一个JSON数据包来发送消息给普通用户。此接口主要用于客服等有人工消息处理环节的功能,方便开发者为用户提供更加优质的服务。
模板消息仅用于公众号向用户发送重要的服务通知,只能用于符合其要求的服务场景中,如信用卡刷卡通知,商品购买成功通知等。不支持广告等营销类消息以及其它所有可能对用户造成骚扰的消息。
2、背景
简单的描述一下业务的背景,目前是做一个供求的微信公众号,每当用户付费发送一次供求的信息,我们需要将本条消息推送给所有的用户,这样可以让用户及时收到消息,实现消息的实时性和保证消息的有效性。但是群发的接口根本不能满足我们的需求,于是我们利用模板消息和客服消息的接口来实现我们的需求,在此,有的人可能会发问,为什么客服消息的接收要求这么变态还要利用它呢,其实是因为,群发的消息在某些设备上收到的时候,就像微信好友发送的一条信息,更加吸引用户去关注。
另外,我们还需要知道,假如我们的用户用成千上万的人话,那么群发的方式是十分的耗时的,微信支付提供了notify的异步通知,之前我也一直尝试利用这个现场的异步通知来实现群发,但是根据实际的使用,这个异步IO的阈值差不多在6分钟左右,可是实际上发送一次模板的网络耗时加上数据库的IO其实还是时间还是挺长的,因此我们不得不使用异步多线程来实现我们的群发的功能这里我利用的是swoole的扩展来完成这一需求的。
swoole的异步邮件群发的demo,大家可以参照我的另一篇文章thinkphp5+swoole实现异步邮件群发(SMTP方式)这里可以较为详细的了解服务端和客户端的构建。
3、环境说明
centos7
swoole2.0+
tp5.0+
邮件发送
crontab定时任务
4、实现
4.1创建服务端
我这里就直接贴代码了,需要注意的地方都写在了相关代码的注释,注意看一下
/**
* 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模式下进入项目的根目录,执行
php public/index.php demo/wechat/asyncSend
这样我们的服务端就以守护进程的模式一直运行来我们的后台了,通过ps -aux | grep asyncSend
可以看见,已经有62个进程在处于S(睡眠待唤醒)的状态了,除了60个task进程还用一个master和一个woker进程。
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相关函数来实现端口的监控。
本次用到的定时任务
/1 * * * curl http://你的域名/index.php/demo/Jrmm/checkPortStatus?token=test
就是实现一个每分钟去执行我们下面php代码的一个任务,这里我没有直接用shell来操作,原因有3点,1是我不是很熟悉shell命令,2是我们不太熟悉shell命令,3是写在php里面更方便我去写相关代码和利用已有的一些方法,比如邮件发送。这样虽然多了一点网络资源的消耗,但是也还划算。
具体的监控代码,这边实现的时候会出现很多权限的问题,我就不多说了,遇到的时候自行百度。
/**
* 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=curlinit();curlsetopt(
ch, CURLOPT_URL,
url);curlsetopt(
ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt(
ch,CURLOPTHEADER,0);
res= curl_exec(
ch);curlclose(
ch);
return $res;
}
“`
为什么循环三次呢,因为我们通过测试发送发送1500次消息的时候,耗时差不多6分钟,但是我们的项目的并发很低,那么就无法充分利用我们开启的60个task进程,所以我们将1500分成三次去发送那么实际上我们消耗了几乎可以忽略不计的网络消耗,让我们的发送的性能提高了三倍多,实际的项目中,发送1500多条实际耗时只要不到两分钟。当然当并发量更大的时候,我们还可以采用队列的方式来处理,这样需要我们队task进程管理更加的熟练。
5、截图
收到的推送消息
告警
发送的日志