目录
需求来源
实现思路
1、邀请指定人员成为客服角色;
2、用户在公众号发送信息,通过关键字触发事件:通知客服;
3、客服角色接入会话,与用户对话沟通交流解决疑问,完成对话;
代码实现(基于Laravel框架前后端混合)
路由文件
控制器
模型
数据表(请根据实际业务定义表结构)
使用步骤
感谢阅读,欢迎交流
需求来源
在工作时间,用户可以在微信公众号与客服对话,咨询有关公众号的各种问题与寻求帮助;
实现思路
1、邀请指定人员成为客服角色;
功能上线时必须有客服帐号,调用接口:添加客服帐号(获取客服基本信息 | 微信开放文档)、邀请绑定客服帐号(获取客服基本信息 | 微信开放文档)
2、用户在公众号发送信息,通过关键字触发事件:通知客服;
用户在微信公众号发送信息,微信服务器推送文本事件的xml数据包给开发者,开发者通过判断文本是否为接入客服的关键词,是则通知客服帐号与转发给多客服系统;
3、客服角色接入会话,与用户对话沟通交流解决疑问,完成对话;
代码实现(基于Laravel框架前后端混合)
路由文件
<?phpuse Illuminate\Http\Request;use Illuminate\Routing\Router;Route::group(['prefix' => 'wechat/'], function (Router $router) {// 接收微信推送信息,完成验证消息真实性$router->get('receive_push', "WechatController@receivePush");// 接收微信推送信息,完成接收普通消息并回复$router->post('receive_push', "WechatController@receivePush");// 对话服务-基础支持-获取access_token$router->get('get_access_token', 'WechatController@getAccessToken');// 微信公众号集成功能$router->group(['prefix' => '/func/'], function(Router $router) {// 微信公众号集成功能--对话服务--发送消息--客服接口--邀请并绑定客服帐号$router->get('customservice_kf_invite_bind', 'WechatController@customserviceKfInviteBind');});});
控制器
<?phpnamespace App\Http\Controllers\Wechat;use App\Models\WechatKfAccount;use App\Models\WechatUserSession;use Illuminate\Http\Request;class WechatController extends BaseController{// todo 接收微信推送--明文模式public function receivePush(Request $request){file_log("进入访问" . $request->getUri(), "wechat");try {switch ($_SERVER["REQUEST_METHOD"]) {case "GET":file_log("验证消息真实性" . $request->getUri(), "wechat");$params = $request->all();$params['token'] = config("wechat_develop.Token");// 校验签名是否正确$isMatch = $this->verifySignature($params);if (!$isMatch) {exit(json_encode(["errcode" => -40001, "errmsg" => "signature sha1 error"], JSON_UNESCAPED_UNICODE));}echo $params["echostr"];exit;break;case "POST":file_log("接收公众号发来的信息" . $request->getUri(), "wechat");// 接收微信公众号传送的xml信息包$this->postXml = isset($GLOBALS["HTTP_RAW_POST_DATA"]) ? $GLOBALS["HTTP_RAW_POST_DATA"] : file_get_contents("php://input");file_log("公众号发来的xml数据包" . $this->postXml, "wechat");// xml转换成array$postArray = json_decode(json_encode(simplexml_load_string($this->postXml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);// 按照实际业务处理不同类型的信息,并返回xml包$this->receive = $postArray;@ob_clean();echo $this->reply();exit;break;default:file_log("【非验证微信服务器信息】或【非微信服务器发送的信息】" . $request->getUri(), "wechat");exit;}} catch (\Exception $e) {exception_file_log($e, 'wechat');exit($e->getMessage() . $e->getTraceAsString());}}private function verifySignature($getParam){$tmpArr = array($getParam["token"], $getParam["timestamp"], $getParam["nonce"]);sort($tmpArr, SORT_STRING);$tmpStr = implode($tmpArr);$tmpStr = sha1($tmpStr);return $tmpStr == $getParam['signature'];}private function reply(){$this->message = ["MsgType" => $this->receive["MsgType"],"CreateTime" => time(),"ToUserName" => $this->receive["FromUserName"],"FromUserName" => $this->receive["ToUserName"],];switch ($this->receive["MsgType"]) {case "event":$this->event();break;case "text":$this->text();break;default:return $this->postXml;break;}$replyXml = self::arr2xml($this->message);file_log("开发者发送的信息:" . $replyXml, "wechat");return $replyXml;}public static function arr2xml($data){return "<xml>" . self::_arr2xml($data) . "</xml>";}private static function _arr2xml($data, $xmlContent = ''){foreach ($data as $key => $val) {is_numeric($key) && $key = 'item';$xmlContent .= "<{$key}>";if (is_array($val) || is_object($val)) {$xmlContent .= self::_arr2xml($val);} elseif (is_string($val)) {$xmlContent .= '<![CDATA[' . preg_replace("/[\\x00-\\x08\\x0b-\\x0c\\x0e-\\x1f]/", '', $val) . ']]>';} else {$xmlContent .= $val;}$xmlContent .= "</{$key}>";}return $xmlContent;}# text类型数据回复private function text(){// 判断用户发送的消息文本switch ($this->receive['Content']) {// 接入客服case "客服":try {$this->integrationKf();} catch (\Exception $e) {exception_file_log($e, "wechat");$this->message['MsgType'] = "text";$this->message['Content'] = "目前暂未接入客服系统";}break;default:// 默认情况的回复信息$this->message["Content"] = '您可输入关键词语"客服"';break;}}# event类型private function event(){$this->message["MsgType"] = "text";$this->message["Content"] = "遇到未知推送事件";file_log("推送事件名称:" . strtolower($this->receive["Event"]), "wechat");switch (strtolower($this->receive["Event"])) {case "kf_create_session": // 客服会话创建时推送$this->message_custom_send(['touser' => $this->message["ToUserName"],'msgtype' => "text","text" => ["content" => "您好,请问有什么可以帮到您?"]]);// 更新当前用户会话状态$currUserSession = WechatUserSession::get_one(['where' => [['openid','=', $this->message['ToUserName']],['status','=',WechatUserSession::Waiting]],'orderBy' => [['id','desc']],'field' => ['id']]);if ($currUserSession) {$where = [['id','=',$currUserSession['id']]];$data = ['status' => WechatUserSession::Going, 'kf_account' => $this->receive['KfAccount'], 'start_time' => time()];WechatUserSession::update_by_where(['where' => $where, 'data' => $data]);}break;case "kf_close_session": // 客服会话关闭时推送$this->message_custom_send(['touser' => $this->message["ToUserName"],'msgtype' => "text","text" => ["content" => "当前会话已关闭,如需在线咨询,请回复“客服”"]]);// 更新聊天会话的客服帐号与会话状态$currUserSession = WechatUserSession::get_one(['where' => [['openid','=', $this->message['ToUserName']],['status','=',WechatUserSession::Going]],'orderBy' => [['id','desc']],'field' => ['id']]);if ($currUserSession) {$where = [['id','=',$currUserSession['id']]];$data = ['status' => WechatUserSession::End, 'end_kf_account' => $this->receive['KfAccount'], 'end_time' => time()];WechatUserSession::update_by_where(['where' => $where, 'data' => $data]);}break;default:break;}}# 接入客服系统private function integrationKf(){// 判断是否在9:00-18:00这个工作时间$hour = date('H');if ($hour < 9 || $hour >= 18) {$this->message['Content'] = "您好,在线客服工作时间9:00~18:00,请您在客服服务时间内进行咨询,非工作时间请您留言咨询问题或添加人工微信客服:xxx,万能的客服xx会为您解答所有疑问喔~。";return true;}// 1.判断当前用户是否存在未结束的客服会话$userSession = WechatUserSession::getUserSessionByStatus([WechatUserSession::Waiting, WechatUserSession::Going],$this->message['ToUserName']);// 1-1.会话不存在或者已过期的等待会话if (!$userSession || ($userSession['status'] == WechatUserSession::Waiting && $userSession['invalidtime'] < time())) {// 查询数据库所有有效的客服帐号, 发送通知$kfAccountList = WechatKfAccount::getKfAccountByStatus([WechatKfAccount::Agree]);if ($kfAccountList) {foreach ($kfAccountList as $kfAccount) {$this->message_custom_send(["touser" => $kfAccount["kf_openid"],"msgtype" => "text","text" => ["content" => "客服大大,目前有找您咨询的客户,请您立马登录客服系统进行服务。"]]);} unset($kfAccountList,$kfAccount);}//插入用户会话记录WechatUserSession::insert(new WechatUserSession(), ['openid' => $this->message['ToUserName'],'invalidtime' => time() + 60*30,]);// 统计等待接入的用户数量$waitUserSessionCount = WechatUserSession::getWaitUserSessionCount();$this->message_custom_send(["touser" => $this->message['ToUserName'],"msgtype" => "text","text" => ["content" => "目前共有{$waitUserSessionCount}人在排队,您在第{$waitUserSessionCount}位请您耐心等待。"]]);// 通知微信多客服端接收会话,等待客服接入$this->message['MsgType'] = "transfer_customer_service";} else {// 1-2.会话存在并且正在进行中的会话if (empty($userSession['kf_account'])) {// 通知用户再排队队列的位置$waitUserSessionList = WechatUserSession::getWaitUserSessionList();$nowPosition = 1;$waitUserSessionCount = count($waitUserSessionList);if ($waitUserSessionCount > 1) {foreach($waitUserSessionList as $key => $value){if($value['openid'] == $this->message['ToUserName']){$nowPosition = $key + 1;}}}$this->message_custom_send(["touser" => $this->message['ToUserName'],"msgtype" => "text","text" => ["content" => "目前共有{$waitUserSessionCount}人在排队,您在第{$nowPosition}位,请您耐心等待。"]]);// 通知微信多客服端接收会话,等待客服接入$this->message['MsgType'] = "transfer_customer_service";} else {$this->message['MsgType'] = "transfer_customer_service";$this->message['TransInfo']['KfAccount'] = $userSession['kf_account'];}}}/*** todo 微信公众号集成功能--对话服务--发送消息--客服接口--邀请并绑定客服帐号*/public function customserviceKfInviteBind(Request $request){try {check_result_isexists(!is_null($kf_account = $request->kf_account), '客服帐号不存在');check_result_isexists(!is_null($nickname = $request->nickname), '客服昵称不存在');check_result_isexists(!is_null($wx_account = $request->wx_account), '客服微信号不存在');// 判断添加的客服帐号是否已存在$kfAccount = WechatKfAccount::get_one(['where' => [['kf_account', '=', $kf_account]]]);if ($kfAccount) return return_info(500, '客服帐号已存在,请勿重复添加');// 判断添加的客服微信号是否已存在$wxAccount = WechatKfAccount::get_one(['where' =>[['wx_account', '=', $wx_account]]]);if ($wxAccount) return return_info(500, '客服微信号已存在,请勿重复添加');// 发起请求添加微信客服$this->customservice_kfaccount_add(["kf_account" => $kf_account,"nickname" => $nickname,]);// 发起请求为微信客服帐号绑定微信号$this->customservice_kfaccount_invite_worker(["kf_account" => $kf_account,"invite_wx" => $wx_account]);// 客服数据入库WechatKfAccount::insert(new WechatKfAccount(), ['nickname' => $nickname,'kf_account' => $kf_account,'wx_account' => $wx_account,'create_time' => time()]);} catch (\Exception $e) {exception_file_log($e, "wechat");exit("【customserviceKfInviteBind】邀请并绑定客服帐号失败,请到wechat日志文件查看详情");}return return_info(200, "邀请绑定已发送");}/*** todo 客服接口-发消息*/public function message_custom_send($data = []){$url = "https://api./cgi-bin/message/custom/send?access_token={$this->getAccessToken()}";return $this->post_http_request($url, $data);}/*** todo 添加客服帐号*/public function customservice_kfaccount_add($accountData = []){$url = "https://api./customservice/kfaccount/add?access_token={$this->getAccessToken()}";return $this->post_http_request($url, $accountData);}/*** todo 邀请绑定客服帐号*/public function customservice_kfaccount_invite_worker($accountData = []){$url = "https://api./customservice/kfaccount/inviteworker?access_token={$this->getAccessToken()}";return $this->post_http_request($url, $accountData);}/*** todo 发起post请求*/public function post_http_request($url, $postData){$resultArr = json_decode($this->http_url($url, json_encode($postData, JSON_UNESCAPED_UNICODE)), true);if (isset($resultArr["errcode"]) && $resultArr["errcode"] != 0) {file_log($resultArr, $this->logFile ?: "HttpRequest");throw new \Exception($resultArr['errmsg']);}return $resultArr;}/*** todo 自定义请求接口函数,$data为空时发起get请求,$data有值时发起post请求* @param $url 请求地址* @param null $data 请求参数* @return mixed*/public function http_url($url, $data = null){$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $url);curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);if (!empty($data)) {curl_setopt($ch, CURLOPT_POST, 1);curl_setopt($ch, CURLOPT_POSTFIELDS, $data);}$res = curl_exec($ch);if (curl_errno($ch)) {throw new \Exception("error:" . curl_error($ch));}curl_close($ch);return $res;}}
模型
<?phpnamespace App\Models;class WechatUserSession extends Model{protected $table = 'wechat_user_sessions';const Waiting = 0; // todo 会话等待中const Going = 1; // todo 会话执行中const End = 2; // todo 会话已结束/*** todo 根据会话状态获取会话数据* @param array $status* @param $openId* @param null $field* @return bool*/public static function getUserSessionByStatus($status = [], $openId, $field = null){$param = [];$param['whereIn'] = [['status', $status]];$param['where'] = [['openid', '=', $openId], ['invalidtime', '>', time()]];if ($field) $param['field'] = $field;return self::get_one($param);}/*** todo 根据会话状态获取等待接入的会话数量*/public static function getWaitUserSessionCount(){$param = [];$param['whereIn'] = [['status', [self::Waiting]]];$param['where'] = [['invalidtime', '>', time()]];$param['field'] = 'id';$param['opera'] = 'count';return self::number_opera($param);}/*** todo 根据会话状态获取等待接入的会话列表*/public static function getWaitUserSessionList($field = null){$param = [];$param['whereIn'] = [['status', [self::Waiting]]];$param['where'] = [['invalidtime', '>', time()]];$field ? $param['field'] = $field : true;$list = self::get_all($param);return $list ? $list : [];}}
<?phpnamespace App\Models;class WechatKfAccount extends Model{public $timestamps = false;protected $table = 'wechat_kf_accounts';const Waiting = 0; // todo 邀请等待中const Agree = 1; // todo 邀请已同意const Reject = 2; // todo 邀请已拒绝/*** todo 根据客服帐号状态获取客服帐号数据* @param array $status* @param $openId* @param null $field* @return bool*/public static function getKfAccountByStatus($status = [], $field = null){$param = [];$param['whereIn'] = [['status', $status]];if ($field) $param['field'] = $field;return self::get_all($param);}}
数据表(请根据实际业务定义表结构)
CREATE TABLE `wechat_kf_accounts` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT,`nickname` varchar(50) DEFAULT '' COMMENT '客服昵称',`kf_account` varchar(50) DEFAULT '' COMMENT '客服帐号',`kf_openid` varchar(50) DEFAULT '' COMMENT '客服openid(需要手动更新,微信公众号不支持自动获取)',`wx_account` varchar(50) DEFAULT '' COMMENT '客服的微信号(发起客服绑定邀请时需要填入)',`status` tinyint(1) unsigned DEFAULT '0' COMMENT '邀请状态 0-邀请中 1-已接受 2-已拒绝',`create_time` int(10) unsigned DEFAULT '0' COMMENT '创建时间',`update_time` int(10) unsigned DEFAULT '0' COMMENT '更新时间',`delete_time` int(10) unsigned DEFAULT '0' COMMENT '删除时间',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='微信客服帐号信息表';CREATE TABLE `wechat_user_sessions` (`id` int(10) unsigned NOT NULL AUTO_INCREMENT,`openid` varchar(50) DEFAULT '' COMMENT '用户openid',`invalidtime` int(10) unsigned DEFAULT '0' COMMENT '会话过期时间',`status` tinyint(1) unsigned DEFAULT '0' COMMENT '会话状态 0-未接入 1-已接入 2-已结束',`kf_account` varchar(50) DEFAULT '' COMMENT '开始对接会话的客服帐号',`end_kf_account` varchar(50) DEFAULT '' COMMENT '结束对接会话的客服帐号',`start_time` int(10) unsigned DEFAULT '0' COMMENT '客服开始接入会话时间',`end_time` int(10) unsigned DEFAULT '0' COMMENT '客服结束接入会话时间',`created_at` datetime DEFAULT NULL COMMENT '创建时间',`updated_at` datetime DEFAULT NULL COMMENT '更新时间',`delete_at` datetime DEFAULT NULL COMMENT '删除时间',PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COMMENT='微信客服会话记录表';
使用步骤
1、调用接口邀请帐号成为客服帐号:/wechat/func/customservice_kf_invite_bind?kf_account=xxx@公众号微信号&wx_account=微信号&nickname=昵称;
2、用户在微信公众号发送信息,推送信息给客服帐号;
3、客服帐号在微信公众平台客服功能web页面登录(微信公众平台客服功能登录),接入会话;
4、用户与客服会话直到结束会话;