tp5框架实现推送消息到企业微信机器人(从需求分析、方案设计、研发阐述)

需求

目前需要接入消息推送的模块是任务中心,原因是任务中心是多人协作处理某个特定小组内的所有分析任务,在整个分析的过程中,一个任务的生命周期会经历若干个关键的状态节点,

当分析师或审批人触发了相关动作,去改变当前任务的状态转移到特定的新状态时,需要业务层生成一条消息发送到企业微信群内,通知相关责任人第一时间关注这个任务的状态信息。

从而分析师或审批人能在第一时间内,看到消息后,去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和@具体人的业务场景需求也还暂时不清楚。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值