企业微信外部联系人回调事件

企业微信外部联系人回调事件

说明:

1) 下列"外部联系人" 和 "客户联系" 其实都是一个意思,都是指顾客,但是由于企业微信开发文档中叫"外部联系人",管理后台叫"客户联系", 为方便操作,故本文名称跟企业微信保持一致
2) 下列两段代码实例,默认使用者已经有PHP-SDK, 原生代码案例可以自行前往git下载PHP-SDK(地址见官方文档),ThinkPHP5.*版本案例,可以参考本人下载资源中的PHP-SDK,或者根据官方提供的sdk自行修改命名空间; 由于时间问题,原生代码部分由TP5版本代码修改而成,仅供参考代码,暂未实测; ThinkPHP5.*版本代码亲测有效,有异议欢迎提出讨论

1.作用

		企业成员 添加/删除外部联系人 时,可在企业后台接收添加/删除的外部人数据,及时更新企业后台数据

2.运行原理

	1) 开发者验证回调事件url有效性,验证通过后,可在企业微信管理员后台,配置回调所需的3个参数: 回调事件url, Token , EncodingAESKey
	2) 企业微信管理后台给相关企业成员配置"客户联系"权限
	3) 当企业成员(需要配置客户联系操作权限) 添加/删除外部联系人时,企业微信服务器会向外部联系人回调事件url 推送一段加密字符串(xml格式) ,具体事件格式可参照开发文档, 而且务必保证正确处理数据,

3.开发者操作步骤

	1) 管理后台配置外部联系人回调事件url地址,并验证该url有效性
			A) 先调用调试工具,验证回调事件url有效性,具体参见:
			 ![验证回调事件url的详情图片](https://img-blog.csdnimg.cn/20190221220329870.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2R1cmluZ25vbmU=,size_16,color_FFFFFF,t_70)
			B) 除了A)操作,还需要在该url中,返回解密后的加密消息内容,首先两点;
				a) 调用PHP-SDK中的XMLparse类中的VerifyUrl方法(Verify方法可验证回调事件url); PHP-SDK可下载本人下载资源中的SDK(此SDK根据ThinkPHP5.0进行了命名空间的封装),或者参考企业微信开发文档自行编写
				b) **坑一**: 在回调事件中必须 return 企业微信调试工具发送的Get请求结果(ps:企业微信文档中说的是正确响应,经和其技术沟通,并亲测为使用return关键字即可,示例代码如下:
						$params = $_GET;
						$obj = new XMLparse();
						$callbackRes = $obj->VerifyUrl($params);	//调用SDK中的VerifyUrl方法,返回值为解密后的消息,即步骤A)图中的 "测试消息123"
						return $callbackRes;	// 相当于直接返回明文消息: return	"测试消息123";
					)
				
				c) **坑二**: 验证回调事件url有效性时,即调用verifyUrl方法时,必须先urldecode('echoStr参数'),否则会抛出异常; 若使用原生代码示例,务必自行添加urldecode,若使用框架可自行输出参数查看;(本人使用ThinkPHP5.*框架,ThinkPHp5.*框架中做了urldecode的处理,可以不必开发者手动urldecode;)
			
	2) url通过验证后,添加/删除外部联系人时,会想该url推送指定格式的xml数据(加密字符)
		A)  **坑三**: 接收xml数据时,确保接收的数据事原生的,最好使用 $params = file_get_contents('php://input');	(ps: 本人就是因为在框架配置了htmlspecialchars(html标签过滤函数)这个函数将<>转义了,导致SDK中的DOMDocument类的loadXML方法, 无法读取正确的xml数据,抛出异常,记得将数据恢复成xml格式数据,若使用了htmlspecialchars,记得htmlspecialchars_decode一次)
		B)  **坑四**: 因为涉及的字符串长度过长(400~750个字节),使用var_dump,echo,print_r均无法正常输出字符内容,建议使用将字符写入文件中(使用函数fopen(),fwrite(),或者file_put_content())
		C)  具体代码过程参见下列两个版本,以ThinkPHP5.*版本为准,如有异议,欢迎提出讨论
		
	3) 详情参见企业微信api开发文档,
		A)文档地址:
			https://work.weixin.qq.com/api/doc#90000/90135/90664
		B) 接口调试工具地址: 
			https://work.weixin.qq.com/api/devtools/devtool.php		

3.注意:

	1) 上述有提及四个坑点,请各位开发者多留意 (可结合下列示例代码理解)
	2) 必须先登录企业管理后台(管理员身份),配置回调事件的url相关的参数,并给相关企业成员配置"客户联系"权限

4.PHP原生代码示例 (使用前提,需要先获取SDK)

 '',	//企业ID
    ];	
    protected $_externalCallbackEvent = [	//回调事件参数
        'url'	=> 'http://www.test.com/External/callbackEvent',
        'token' => '',
        'encodingAESKey' => ''
    ];	
    protected $_callbackObj;    // 回调事件对象(外部联系人添加/删除)
	
    protected $_callbackErrorMsgArr = [	//企业微信(添加/删除外部联系人)回调事件错误码+错误信息
        '0'=> 'success',
        '-40001'=> '签名验证错误',
        '-40002'=> 'xml解析失败',
        '-40003'=> 'sha加密生成签名失败',
        '-40004'=> 'encodingAesKey 非法',
        '-40005'=> 'corpid 校验错误',
        '-40006'=> 'aes 加密失败',
        '-40007'=> 'aes 解密失败',
        '-40008'=> '解密后得到的buffer非法',
        '-40009'=> 'base64加密失败',
        '-40010'=> 'base64解密失败',
        '-40011'=> '生成xml失败',
    ];
    
    public function __construct() {
        # 回调事件对象
        $this->_callbackObj = new \weworkapi\callback\WXBizMsgCrypt($this->_externalCallbackEvent['token'], $this->_externalCallbackEvent['encodingAESKey'], $this->_weworkConfig['corpId']); //企业应用回调, 消息加密/解密类WXBizMsgCrypt
        
    }
	
	/**
     * 验证(添加/删除外部联系人)回调事件url有效性
     */
    public function verifyUrl($data) {
        $params = $_GET;
        $params2 = $_POST;
        $params = array_merge($params,$params2);    // get数据+post数据
        try {
            if (empty($params['msg_signature'])) {
                throw new \Exception('msg_signature不得为空');
            }
            if (empty($params['timestamp'])) {
                throw new \Exception('timestamp不得为空');
            }
            if (empty($params['nonce'])) {
                throw new \Exception('nonce不得为空');
            }
            if (empty($params['echostr'])) {
                throw new \Exception('echostr不得为空');
            }
            $sReplyEchoStr = "";
            $verifyRes = $this->_callbackObj->VerifyURL($data['msg_signature'], $data['timestamp'], $data['nonce'], urldecode($data['echostr']),$sReplyEchoStr);   // 此处需要urldecode($data['echoStr'])
            0 !== $verifyRes && exception('errCode: '.$verifyRes .', errMsg: '.$this->_callbackErrorMsgArr[$verifyRes]);
            return $sReplyEchoStr;
        } catch (\Exception $ex) {
            return ['errCode'=>'0084','errMsg'=>$ex->getMessage()];
        }
        return ['errCode'=>'0','errMsg'=>'success','data'=>$verifyRes];
    }
	
	/**
     * 测试企业微信的外部联系人事件回调
     */
    public function callbackEvent() {
        $params = $_GET; //get参数
        $params['xmlContent'] = file_get_contents('php://input');  //post的xml数据
        # 记录入参
        $params['date'] = date('Y-m-d H:i:s');
        $paramsStr = json_encode($params);
        $fp = fopen('./externalCallbackEvent_params.log', 'w');
        fwrite($fp, $paramsStr);
        try {
             //若回调url验证通过,处理回调的xml消息
            if (!empty($params['xmlContent']) && empty($params['echostr'])) {   
                $dealRes = $this->dealCallbackEvent($params);
                if ('success' !== $dealRes['desc']) {
                    throw new \Exception($dealRes['desc']);
                }
                # 记录调用结果
                $callbakcRes = json_encode(['date'=>date('Y-m-d H:i:s'),'result'=>$dealRes['data']]);
                $fp2 = fopen('./externalCallbackEvent_result.log', 'w');
                fwrite($fp2, $callbakcRes);
                return $dealRes['data'];
            } else {     //验证url有效性
                $verifyRes = $this->verifyUrl($params);
                if ('success' !== $verifyRes['desc']) {
                    throw new \Exception($verifyRes['desc']);
                }
                return $verifyRes['data'];  // 解密后的消息内容(务必原文输出),否则报错
            }
        } catch (\Exception $ex) {
            return ['errCode'=>'0083','errMsg'=>$ex->getMessage()];
        }
    }
	
	/**
     * 验证(添加/删除外部联系人)回调事件消息加密
     */
    public function dealCallbackEvent($data) {
        try {
            # 解密
            $decryptMsg = $this->decryptMsg($data);
            $decryptMsgArr = $this->XMLString2Array($decryptMsg);
            switch ($decryptMsgArr['ChangeType']) {
                case 'add_external_contact':    // 添加外部联系人回调事件
                    $dealRes = $this->addExternalCallbackEvent($decryptMsgArr);
                    break;
                case 'del_external_contact':    // 删除外部联系人回调事件
                    $dealRes = $this->delExternalCallbackEvent($decryptMsgArr);
                    break;
                default:
                    throw new \Exception($type . '回调事件类型不合法');
                    break;
            }
            return $dealRes;
        }catch (\Exception $ex) {
             throw new \Exception($ex->getMessage());
        }
    }
	
	/**
     * 消息加密
     */
    public function encryptMsg($data) {
        try {
	//        $sReqTimeStamp = "1409659813";
	//        $sReqNonce = "1372623149";
	//        $sRespData = "13488318601234567890123456128";
            $sReqTimeStamp = $data['timestamp'];    //时间戳
            $sReqNonce = $data['nonce'];    //随机字符串
            $content = $data['content'];    //被加密的消息内容
            // 需要发送的明文
            $sRespData = "".$sReqNonce."1234567890123456".Config::get('WEWORK_AGENT_ID')."";
            $sEncryptMsg = ""; //xml格式的密文
            $errCode = $this->_callbackObj->EncryptMsg($sRespData, $sReqTimeStamp, $sReqNonce, $sEncryptMsg);
            if (0 !== $errCode) {
                throw new \Exceptionexception('errCode: '.$errCode .', errMsg: '.$this->_callbackErrorMsgArr[$errCode]);
            }
            return $sEncryptMsg;
        } catch (\Exception $ex) {
            throw new \Exception($ex->getMessage());
        }
    }
    
    /**
     * 消息解密
     */
    public function decryptMsg($data) {
        try {
    //        $sReqMsgSig = "477715d11cdb4164915debcba66cb864d751f3e6";
    //        $sReqTimeStamp = "1409659813";
    //        $sReqNonce = "1372623149";
    //        $sReqData = "";
            $sReqMsgSig = $data['msg_signature'];
            $sReqTimeStamp = $data['timestamp'];
            $sReqNonce = $data['nonce'];
            $sReqData = $data['xmlContent'];    // post请求的密文数据
            $decryptMsg = "";  // 解析之后的明文
            $errCode = $this->_callbackObj->DecryptMsg($sReqMsgSig, $sReqTimeStamp, $sReqNonce, $sReqData, $decryptMsg);
            if (0 !== $errCode) {
                throw new \Exception('errCode: '.$errCode .', errMsg: '.$_callbackErrorMsgArr[$errCode]);
            }
            return $decryptMsg;
        } catch (\Exception $ex) {
            throw new \Exception($ex->getMessage());
        }
    }
	
	/**
     * 提取xml数据中的指定参数
     * (转换原理:xml字符串->xml对象->json对象->数组)
     * @param string xml格式数据
     * @return array array格式数据
     */
    public function XMLString2Array($xmlStr) {
        try {
            //xml字符转换为xml对象
            $xmlObj = simplexml_load_string($xmlStr,'SimpleXMLElement', LIBXML_NOCDATA);
            $jsonObj = json_encode($xmlObj);
            return json_decode($jsonObj,true);
        } catch (\Exception $ex) {
            throw new Exception($ex->getMessage());
        }
    }
	
	 /**
     * 添加外部联系人回调处理
     * 原理:
     *  1) 获取外部联系人userid(异步处理时,userid从队列中获取)
     *  2) 获取外部联系人详情
     *  3) 将外部联系人插入数据库
     * @param array 
     */
    public function addExternalCallbackEvent($data) {
        
    }
    
    /**
     * 删除外部联系人回调处理
     * 原理:
     *   1) 获取外部联系人userid(异步处理时,userid从队列中获取)
     *   2) 删除本地数据库中外部联系人记录
     */
    public function delExternalCallbackEvent($data) {
           
    }
	
}

5. ThinkPHP5.*实例(先获取PHP-SDK)

 '',	//企业ID
    ];	
    protected $_externalCallbackEvent = [	//回调事件参数
        'url'	=> 'http://www.test.com/External/callbackEvent',
        'token' => '',
        'encodingAESKey' => ''
    ];	
    protected $_callbackObj;    // 回调事件对象(外部联系人添加/删除)
    protected $_callbackErrorMsgArr = [	//企业微信(添加/删除外部联系人)回调事件错误码+错误信息
        '0'=> 'success',
        '-40001'=> '签名验证错误',
        '-40002'=> 'xml解析失败',
        '-40003'=> 'sha加密生成签名失败',
        '-40004'=> 'encodingAesKey 非法',
        '-40005'=> 'corpid 校验错误',
        '-40006'=> 'aes 加密失败',
        '-40007'=> 'aes 解密失败',
        '-40008'=> '解密后得到的buffer非法',
        '-40009'=> 'base64加密失败',
        '-40010'=> 'base64解密失败',
        '-40011'=> '生成xml失败',
    ];
    
    public function __construct() {
        # 回调事件对象
        $this->_callbackObj = new \weworkapi\callback\WXBizMsgCrypt($this->_externalCallbackEvent['token'], $this->_externalCallbackEvent['encodingAESKey'], $this->_weworkConfig['corpId']); //企业应用回调,消息加密/解密类WXBizMsgCrypt
    }
    /**
     * 测试企业微信的外部联系人事件回调
     */
    public function callbackEvent() {
        $params = Request::instance()->param(); //get参数
        $params['xmlContent'] = file_get_contents('php://input');  //post的xml数据
        # 记录入参
        $params['date'] = date('Y-m-d H:i:s');
        $paramsStr = json_encode($params);
        $fp = fopen('./externalCallbackEvent_params.log', 'w');
        fwrite($fp, $paramsStr);
        try {
             //若回调url验证通过,处理回调的xml消息
            if (!empty($params['xmlContent']) && empty($params['echostr'])) {   
                $dealRes = $this->dealCallbackEvent($params);
                'success' !== $dealRes['desc'] && exception($dealRes['desc']);
                # 记录调用结果
                $callbakcRes = json_encode(['date'=>date('Y-m-d H:i:s'),'result'=>$dealRes['data']]);
                $fp2 = fopen('./externalCallbackEvent_result.log', 'w');
                fwrite($fp2, $callbakcRes);
                return $dealRes['data'];
            } else {     //验证url有效性
                $verifyRes = $this->verifyUrl($params);
                'success' !== $verifyRes['desc'] && exception($verifyRes['desc']);
                return $verifyRes['data'];  // 解密后的消息内容(务必原文输出),否则报错
            }
        } catch (\Exception $ex) {
            $this->result([],'0083',$ex->getMessage(),'json');
        }
    }
    
     /**
     * 验证(添加/删除外部联系人)回调事件消息加密
     */
    public function dealCallbackEvent($data) {
        try {
            $rules = [
                'msg_signature' => 'require',
                'timestamp' => 'require',
                'nonce' => 'require',
                'xmlContent' => 'require',  //接企业微信服务器发送的回调的xml消息内容
            ];
            $validate = new Validate($rules);
            !$validate->check($params) && exception($validate->getError());
//            $params['xmlContent'] = htmlspecialchars_decode($params['xmlContent']); //若配置中使用了htmlspecialchars函数,转义了<,>等符号
            
            # 解密
            $decryptMsg = $this->decryptMsg($data);
            $decryptMsgArr = $this->XMLString2Array($decryptMsg);
            switch ($decryptMsgArr['ChangeType']) {
                case 'add_external_contact':    // 添加外部联系人回调事件
                    $dealRes = $this->addExternalCallbackEvent($decryptMsgArr);
                    break;
                case 'del_external_contact':    // 删除外部联系人回调事件
                    $dealRes = $this->delExternalCallbackEvent($decryptMsgArr);
                    break;
                default:
                    throw new \Exception($type . '回调事件类型不合法');
                    break;
            }
            return $dealRes;
        }catch (\Exception $ex) {
             throw new \Exception($ex->getMessage());
        }
    }
    
    /**
     * 验证URL有效性
     * @param array 参数
     * @return int 错误码,0-正常,否则出错
     */
    public function verifyUrl($data) {
        try {
             $rules = [
                'msg_signature' => 'require',
                'timestamp' => 'require',
                'nonce' => 'require',
                'echostr' => 'require',
            ];
            $validate = new Validate($rules);
            !$validate->check($params) && exception($validate->getError());
            
            $sReplyEchoStr = "";
            $verifyRes = $this->_callbackObj->VerifyURL($data['msg_signature'], $data['timestamp'], $data['nonce'], $data['echostr'],$sReplyEchoStr);   // 此处不需要urldecode($data['echoStr']),初步猜测是TP5的Request类已经做过了urldecode处理
            0 !== $verifyRes && exception('errCode: '.$verifyRes .', errMsg: '.$this->_callbackErrorMsgArr[$verifyRes]);
            return $sReplyEchoStr;
        } catch (\Exception $ex) {
            throw new \Exception($ex->getMessage());
        }
    }
    
    /**
     * 消息加密
     */
    public function encryptMsg($data) {
        try {
//        $sReqTimeStamp = "1409659813";
//        $sReqNonce = "1372623149";
//        $sRespData = "13488318601234567890123456128";
            $sReqTimeStamp = $data['timestamp'];    //时间戳
            $sReqNonce = $data['nonce'];    //随机字符串
            $content = $data['content'];    //被加密的消息内容
            // 需要发送的明文
            $sRespData = "".$sReqNonce."1234567890123456".Config::get('WEWORK_AGENT_ID')."";
            $sEncryptMsg = ""; //xml格式的密文
            $errCode = $this->_callbackObj->EncryptMsg($sRespData, $sReqTimeStamp, $sReqNonce, $sEncryptMsg);
            0 !== $errCode && exception('errCode: '.$errCode .', errMsg: '.$this->_callbackErrorMsgArr[$errCode]);
            return $sEncryptMsg;
        } catch (\Exception $ex) {
            throw new \Exception($ex->getMessage());
        }

    }
    
    /**
     * 消息解密
     */
    public function decryptMsg($data) {
        try {
    //        $sReqMsgSig = "477715d11cdb4164915debcba66cb864d751f3e6";
    //        $sReqTimeStamp = "1409659813";
    //        $sReqNonce = "1372623149";
    //        $sReqData = "";
            $sReqMsgSig = $data['msg_signature'];
            $sReqTimeStamp = $data['timestamp'];
            $sReqNonce = $data['nonce'];
            $sReqData = $data['xmlContent'];    // post请求的密文数据
            $decryptMsg = "";  // 解析之后的明文
            $errCode = $this->_callbackObj->DecryptMsg($sReqMsgSig, $sReqTimeStamp, $sReqNonce, $sReqData, $decryptMsg);
             0 !== $errCode && exception('errCode: '.$errCode .', errMsg: '.$this->_callbackErrorMsgArr[$errCode]);
            return $decryptMsg;
        } catch (\Exception $ex) {
            throw new \Exception($ex->getMessage());
        }
    }
    
    /**
     * 提取xml数据中的指定参数
     * (转换原理:xml字符串->xml对象->json对象->数组)
     * @param string xml格式数据
     * @return array array格式数据
     */
    public function XMLString2Array($xmlStr) {
        try {
            //xml字符转换为xml对象
            $xmlObj = simplexml_load_string($xmlStr,'SimpleXMLElement', LIBXML_NOCDATA);
            $jsonObj = json_encode($xmlObj);
            return json_decode($jsonObj,true);
        } catch (\Exception $ex) {
            throw new Exception($ex->getMessage());
        }
    }
	
	 /**
     * 添加外部联系人回调处理
     * 原理:
     *  1) 获取外部联系人userid(异步处理时,userid从队列中获取)
     *  2) 获取外部联系人详情
     *  3) 将外部联系人插入数据库
     * @param array 
     */
    public function addExternalCallbackEvent($data) {
        
    }
    
    /**
     * 删除外部联系人回调处理
     * 原理:
     *   1) 获取外部联系人userid(异步处理时,userid从队列中获取)
     *   2) 删除本地数据库中外部联系人记录
     */
    public function delExternalCallbackEvent($data) {
           
    }
}

你可能感兴趣的:(实践类,企业微信,外部联系人回调事件,ThinkPHP5.*,企业微信PHP-SDK,企业微信客户联系)