需求
目前需要接入消息推送的模块是任务中心,原因是任务中心是多人协作处理某个特定小组内的所有分析任务,在整个分析的过程中,一个任务的生命周期会经历若干个关键的状态节点,
当分析师或审批人触发了相关动作,去改变当前任务的状态转移到特定的新状态时,需要业务层生成一条消息发送到企业微信群内,通知相关责任人第一时间关注这个任务的状态信息。
从而分析师或审批人能在第一时间内,看到消息后,去wxqb的业务系统做进一步的后续处理。
前期调研
一、怎么在业务层代码实现推送消息到企业微信?
1.首先你得有个机器人
企业微信默认是群内相关成员都有权限添加机器人,操作入口如下:
可能弹出后的效果是:
点击“添加机器人”按钮,得到:
为啥我们没有看到创建或则叫新建机器人的入库?
网上搜了一下,看看有无创建企业微信机器人的文档,搜索结果如下:
找有权限的用户拿到了创建后web hook地址:(地址里面的key很关键,不要暴露到互联网,可能会被别人利用,给你推送一些乱七八糟的消息)
2.如何推送消息到企业微信机器人
首先,想到的是企业微信的openapi应该提供了对外的api能力,让我们可以通过调用接口的方式,实现消息的推送!
文档传送门:https://open.work.weixin.qq.com/api/doc/90000/90136/91770
文档主要内容截图示意:
二、业务层接入方案设计
a.方案概述
简述为,任务中心的特定api接口,被触发后,调用已经封装好的企业微信消息推送方法,推送消息到企业微信机器人。
可以设计的细节为:
1.根据需求,整理一下当前的消息内容有哪些?
“任务id:168,任务类型:样本分析,已被张三完成。”
“任务id:168,任务类型:样本分析,已被张三提交审核。”
“任务id:168,任务类型:样本分析,分析师:张三,已被李四审核通过。”
那么,我们可以对这类消息抽象成消息模板,形如:
“A,B”
A:对任务的描述信息(用于分析师,看到消息时,能去业务系统查找对应的任务)
B:对当前任务怎么了,发生了什么事件的描述(告知特定人员发生了啥,你需要后续响应)
消息模板形如:
“任务id:${id},任务类型:${type},已被${displayname}完成。”
${id}是消息的占位符,用于实际发送消息时,替换占位符为真实信息,得到最终的消息字符串。
模板的占位符的实际参数,在发送阶段通过业务层代码去补全。;
2.关于消息推送的时机
按照现有的模板消息,基本上是一条消息对应一个任务中心的api,对于当前mvc的框架来说,就是对应到了特定module/controller/action的代码块内。
如果你不想通过把model层封装的推送消息的方法,埋点到各个module/controller/action的代码块内。
这样做的弊端:
代码太分散了,如果后期修改了消息推送的方法,特别是参数有所变化,那么埋点的代码块的调用语句,都要跟着适配修改,不利于项目的后期维护。
那么,我们是否可以在基类控制器,统一处理消息的推送逻辑?
即在基类控制器内,获取当前api请求的module、controler、action是不是涉及需要额外触发消息推送的api?
是的话,就在基类调用model层的消息推送的方法,获取当前任务信息,触发推送代码。
感觉,这个方案有点不妥,原因在于消息实际推送的时机是在任务中心的api调用成功结束后推送;
现在把消息发送前置到了基类控制器,那么可能存在当api非法调用或参数有误,或代码在执行到action代码块内出现异常时,导致消息的误发!
那么,就得想办法,在action代码块结束后,且确保api请求是成功的前提下,才触发消息推送的代码。
在当前thinkphp5的框架上,如何实现?
3.行为的定义?
我们把行为发生作用的位置称之为标签(位),当应用程序运行到这个标签的时候,就会被拦截下来,统一执行相关的行为,类似于AOP编程中的“切面”的概念,给某一个切面绑定相关行为就成了一种类AOP编程的思想。
其实,就是说在一个web api打到服务器,框架层面的处理的api是一个完整的生命周期,在这个周期内,其实就是对api的进行流水线的加工,tp5框架源码里面会存在一些预定义的行为标签,其实就是选取代码的特定位置,执行一段循环,区别轮询处理绑定的若干行为的代码。
b.Db设计
#消息模板配置表
CREATE TABLE `hl_robot_msg_tpl` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`msg_tpl` varchar(150) NOT NULL DEFAULT '' COMMENT '模板消息字符串',
`tpl_param` varchar(1000) NOT NULL COMMENT 'api请求可用于消息发送,可组装消息的原始数据(更加智能地处理消息发送的逻辑)',
`trigger_name` varchar(50) NOT NULL DEFAULT '' COMMENT '消息模板跟module/controller/action的绑定关系',
`user_id` int(11) NOT NULL DEFAULT '0' COMMENT '创建者的用户id',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最近一次的修改时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
模板示例值:
1.审核通过
INSERT INTO `ahcloneapp`.`hl_robot_msg_tpl` (
`id`,
`msg_tpl`,
`tpl_param`,
`trigger_name`,
`user_id`,
`create_time`,
`update_time`
)
VALUES
(
'4',
'任务id:${id},程序名为:${program_name},已被${current_user}审核通过,审批意见为:${approve_remark}。',
'[{\"tpl_param_type\":\"integer\",\"request_param_type\":\"integer\",\"tpl_param_name\":\"id\",\"request_param_name\":\"id\"},{\"tpl_param_type\":\"string\",\"request_param_type\":\"string\",\"tpl_param_name\":\"approve_remark\",\"request_param_name\":\"approve_remark\"},{\"tpl_param_type\":\"string\",\"request_param_type\":\"string\",\"tpl_param_name\":\"program_name\",\"request_param_name\":\"program_name\"}]',
'index/Task/taskResultApprove',
'0',
'2021-05-17 15:02:12',
'2021-05-27 17:22:49'
);
tpl是个json参数
2.批量操作的模板示例
INSERT INTO `ahcloneapp`.`hl_robot_msg_tpl` (
`id`,
`msg_tpl`,
`tpl_param`,
`trigger_name`,
`user_id`,
`create_time`,
`update_time`
)
VALUES
(
'7',
'任务id:${id},已被${current_user}批量转派给${target_user}。',
'[{\"tpl_param_type\":\"array\",\"request_param_type\":\"array\",\"tpl_param_name\":\"id\",\"request_param_name\":\"id\"},{\"tpl_param_type\":\"integer\",\"request_param_type\":\"integer\",\"tpl_param_name\":\"target_user\",\"request_param_name\":\"analysis_id\"}]',
'index/Task/batchTaskTransfer',
'0',
'2021-05-17 14:52:16',
'2021-05-27 17:39:34'
);
关于tpl里面的tpl_param_type为array时,业务逻辑有特殊处理;
至于模板为啥这么设置,更多的细节可以看看根据模板拼接消息的代码逻辑。(在上面的model代码里)
c.全部代码细节
模型层推送消息的代码:
<?php
namespace app\index\model;
use think\Request;
use think\Db;
use app\index\traits\WxRobot;
class WxRobotMsg{
use WxRobot;
private $tplTableName='hl_robot_msg_tpl';
private $logTableName='hl_robot_msg_push_log';
/**
* 公共方法:向企业微信机器人推送任务中心的事件消息
* 参数:
* 省略: $triggerName index\Task\commitTask (triggerName和模板是一对一关系,知道triggerName就可以查询模板信息)
* 省略:$taskId 68
*/
public function pushMsgToWxRobot(){
//$triggerName通过Request对象直接获取
$request = Request::instance();
$requestPath=$request->path(); //index/index/hello
//获取模板信息
$tplInfo=$this->getTplInfoByPath($requestPath);
//新的:获取task的信息
$tplMsg=$tplInfo['msg_tpl'];
$tplParam=json_decode($tplInfo['tpl_param'],true);
//根据模板信息、任务信息整合生成消息,格式为text的字符串
$msg=$this->produceMsgByTpl($tplMsg,$tplParam,$request);
// echo $msg;die();
//发送消息到企业微信
$res=$this->helpPushMsgToWxRobot($msg);
return $res;
}
//根据模板消息生成消息字符串
protected function produceMsgByTpl($tplMsg,array $tplParam,$request){
$reqInfoArr=$request->param();
$replace=[];
foreach($tplParam as $row){
$tempRqParamName = $row['request_param_name'];
if(isset($reqInfoArr[$tempRqParamName])){
$tempRqParamValue=$reqInfoArr[$tempRqParamName];
}else{
//模板里面配置了非request传递的字段名,但是这些新增的字段名都是根据主键id,查询hl_annalysis_task获取
$taskInfo=Db::table('hl_analysis_task')
->field('*')
->where('id','eq',$reqInfoArr['id'])
->find();
$tempRqParamValue=empty($taskInfo[$tempRqParamName])?'-':$taskInfo[$tempRqParamName];
}
$tempTplParamName = $row['tpl_param_name'];
$tempRqParamType=$row['request_param_type'];
$tempTplParamType=$row['tpl_param_type'];
if($tempTplParamName=='target_user'){
$tempKey='${'.$tempTplParamName.'}';
$replace[$tempKey]=$this->getUserName($tempRqParamValue);
}else{
if($tempRqParamType=='array' && $tempTplParamType=='array'){
$tempKey='${'.$tempTplParamName.'}';
$replace[$tempKey]=json_encode($tempRqParamValue);
}else{
$tempKey='${'.$tempTplParamName.'}';
$replace[$tempKey]=$tempRqParamValue;
}
}
}
// print_r($replace);die();
$realMsg=$this->strReplaceAssoc($replace,$tplMsg);
//把预定义的${current_user}替换为当前用户的用户名
$curUserName=session('manager')['displayname'];
$realMsg=str_replace('${current_user}',$curUserName,$realMsg);
return $realMsg;
}
//path和hl_robot_msg_tpl的trigger_name匹配,有一只对应的关系
protected function getTplInfoByPath($requestPath){
$tplInfo=Db::table($this->tplTableName)->field('*')
->where('trigger_name','eq',$requestPath)
->find();
return $tplInfo;
}
/**
*$replace = [
'dog' => 'cat',
'apple' => 'orange'
'chevy' => 'ford'
];
* $subject = 'I like to eat an apple with my dog in my chevy';
* 替换后返回字符串:I like to eat an orange with my cat in my ford
*/
protected function strReplaceAssoc(array $replace, $subject) {
return str_replace(array_keys($replace), array_values($replace), $subject);
}
//模板里面涉及用户名时,需要把用户id转换为用户名
protected function getUserName($userId){
$userName=Db::table('user')->where('user_id','eq',$userId)->value('displayname');
return $userName;
}
}
封装到traits的公用的推送消息到企业微信的方法:
<?php
namespace app\index\traits;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Exception\RequestException;
use think\Log;
use GuzzleHttp\Psr7;
trait WxRobot{
//guzzle 发送post api请求到企业微信的机器人的hook uri,实现消息的推送
protected function helpPushMsgToWxRobot($msg){
try{
$client = new GuzzleClient();
$uri=config('wx_robot_hook_uri');
//这里还得灵活处理,要不要@谁,要不要@全体?db设计上要改动一下?后期迭代,暂不考虑
$postJsonArr=[
'msgtype'=>'text',
'text'=>[
'content'=>$msg
]
];
//记录rest api的信息,用于写入日志
$logInfo=[
"uri"=>$uri,
'json_param'=>$postJsonArr
];
$logInfoStr=json_encode($logInfo);
//调用wx api
$res = $client->request("POST", $uri, [
// 'debug' => true, //调试阶段开启
'headers' => [
'Content-Type' => 'application/json'
],
'json' => $postJsonArr,
'timeout' => 60
]);
//验证调用是否成功,成功返回:{"errcode":0,"errmsg":"ok"}
$responseCode=$res->getStatusCode();
if($responseCode!=200){
Log::write("WxRobot->pushMsgToWx fail:response code is not 200;restapi info:{$logInfoStr}",'notice');
return false;
}
$responseBody=$res->getBody();
$jsonArr=json_decode($responseBody,true);
if(!isset($jsonArr['errmsg'])){
Log::write("WxRobot->pushMsgToWx fail:response body missing result the errmsg key;restapi info:{$logInfoStr}",'notice');
return false;
}
if($jsonArr['errmsg']!='ok'){
Log::write("WxRobot->pushMsgToWx fail:push failed;restapi info:{$logInfoStr}",'notice');
return false;
}
return true;
}catch (RequestException $e) {
Log::write("WxRobot->pushMsgToWx 异常;restapi info:{$logInfoStr}",'notice');
$requestExInfo=Psr7\str($e->getRequest());
Log::write("WxRobotMsg->pushMsgToWx,request:{$requestExInfo}","notice");
if ($e->hasResponse()) {
$reponseExInfo=Psr7\str($e->getResponse());
Log::write("WxRobot->pushMsgToWx,response:{$reponseExInfo}","notice");
}
return false;
}
}
}
具体app_end的埋点是位于框架哪里的源码?
以上就是所有的代码细节!
d.可优化的点
1..未考虑接入rabbitmq实现消息推送业务的解耦
目前这个工具用户量较小,暂时可以不考虑增加rabbtmq实现解耦。
如果要做的话,本质上就是在pushMsg的公共方法中把消息推送到队列,启动另外一个消息的消费进程去实时消费消息。(发布订阅模式)
2.没有细化消息发送的对象
即企业微信消息推送是可以,@all,@wangzhiping,或谁也不@;
规则是:
{
"msgtype": "text",
"text": {
"content": "广州今日天气:29度,大部分多云,降雨概率:60%",
"mentioned_list":["wangqing"],
}
}
发现@单个群内的人时,需要要名字的全拼;
@全体成员是用:@all ;
不带mentioned_list参数就是单纯发消息;
这块逻辑暂时没有想好,因为@all和@具体人的业务场景需求也还暂时不清楚。