'https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code', // 授权跳转地址 'authorize'=>'https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=%s#wechat_redirect', // 获取token 'getToken'=>'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s', // 获取二维码 'getQrcode'=>'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s&scene=%s&page=%s&env_version=%s', // 生成小程序分享链接 'getSchemeLink'=>'https://api.weixin.qq.com/wxa/generatescheme?access_token=%s', // 短链接 'getShortLink'=>'https://api.weixin.qq.com/wxa/genwxashortlink?access_token=%s', // 获取用户信息 'getUserInfo'=>'https://api.weixin.qq.com/sns/jscode2session', // 获取手机号 'getPhoneNumber'=>'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=%s', // 获取运力公司列表 'getDeliveryCompanyList'=>'https://api.weixin.qq.com/product/delivery/get_company_list?access_token=%s', // 获取支持的快递公司列表 'getDelivery'=>'https://api.weixin.qq.com/cgi-bin/express/local/business/delivery/getall?access_token=%s', // 上传发货信息,同步发货状态 'deliverySend'=>'https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token=%s', ]; public function __construct() { $this->mpAppid = ConfigService::make()->getConfigByCode('wechat_mp_appid'); $this->mpAppSecret = ConfigService::make()->getConfigByCode('wechat_mp_appsecret'); } /** * 静态入口 * @return static|null */ public static function make() { if (!self::$instance) { self::$instance = new static(); } return self::$instance; } /** * web 授权 * @param $code * @return array|false|mixed|string|string[] */ public function auth($code) { try { if(empty($this->mpAppid) || empty($this->mpAppSecret)){ $this->error = '小程序参数未配置'; return false; } // 没有code参数 if(empty($code)){ $uri=urlencode(env("WEB_URL")."/v1/mpAuth"); $state=get_random_code(16); $url= sprintf($this->apiUrls['authorize'], $this->mpAppid, $this->mpAppSecret, $uri, $state); return redirect($url); } $cacheKey = "caches:mpApp:mp_{$this->mpAppid}:"; $url = sprintf($this->apiUrls['auth'],$this->mpAppid, $this->mpAppSecret, $code); $result = httpRequest($url, '', 'get','',5); $this->saveLog($cacheKey.'auth:request', ['url'=>$url,'code'=>$code,'result'=>$result,'date'=>date('Y-m-d H:i:s')]); if(empty($result)){ $this->error = '授权登录失败'; return ""; } return $result; }catch (\Exception $e){ $this->error = $e->getMessage(); $this->saveLog($cacheKey.'auth:error', ['code'=>$code,'error'=>$this->error,'trace'=>$e->getTrace(),'date'=>date('Y-m-d H:i:s')]); return false; } } /** * 获取 access_token * @param false $refresh * @return false|mixed|string */ public function getAccessToken($refresh = false) { try { if(empty($this->mpAppid) || empty($this->mpAppSecret)){ $this->error = '小程序参数未配置'; return false; } $cacheKey = "caches:mpApp:mp_{$this->mpAppid}:"; $tokenData = RedisService::get($cacheKey.'access_token'); $token = isset($tokenData['access_token'])? $tokenData['access_token'] : ''; if($token && !$refresh){ return $token; } $url = sprintf($this->apiUrls['getToken'], $this->mpAppid, $this->mpAppSecret); $result = httpRequest($url,'', 'get','',5); $this->saveLog($cacheKey.'tokens:request', ['url'=>$url,'result'=>$result,'date'=>date('Y-m-d H:i:s')]); $token = isset($result['access_token'])? $result['access_token'] : ''; if(empty($result) || empty($token)){ $this->error = '获取小程序TOKEN失败'; return false; } $result['date'] = date('Y-m-d H:i:s'); RedisService::set($cacheKey.'access_token', $result, 7000); return $token; }catch (\Exception $e){ $this->error = $e->getMessage(); $this->saveLog($cacheKey.'tokens:error', ['error'=>$this->error,'trace'=>$e->getTrace(),'date'=>date('Y-m-d H:i:s')]); return false; } } /** * 小程序二维码 * @param $page 页面 * @param $scene 场景参数 * @param string $version 类型:release-永久 * @param false $refresh * @return false|string */ public function getMiniQrcode($page, $scene, $version='release', $refresh=false) { if (!in_array($version,['release','trial','develop'])) { $version='release'; } try { if(empty($page) || empty($scene)){ $this->error = '缺少二维码参数'; return false; } if(empty($this->mpAppid) || empty($this->mpAppSecret)){ $this->error = '小程序参数未配置'; return false; } if(!$token = $this->getAccessToken()) { $this->error = '获取token失败'; return false; } $cacheKey = "caches:members:mp_{$this->mpAppid}:"; $filePath = base_path('public/uploads'); $qrFile = '/qrcodes/mp_'.date("YmdHis")."_".md5($page.$scene).".png"; $qrKey = md5(date("Ym").$page.$scene); if(RedisService::get($cacheKey.$qrKey) && file_exists($filePath.'/'.$qrFile) && !$refresh){ return $qrFile; } if(!is_dir($filePath.'/qrcodes/')){ @mkdirs($filePath.'/qrcodes/'); } $data=['page' => $page,'scene'=>$scene,'check_path'=>false,'env_version'=>$version]; $url = sprintf($this->apiUrls['getQrcode'], $token, $scene, $page, $version); $result = curl_post($url, json_encode($data)); $datas = $result? json_decode($result, true) : []; $this->saveLog($cacheKey.'qrcode:request', ['page'=>$page,'scene'=>$scene,'url'=>$url,'result'=>$result,'date'=>date('Y-m-d H:i:s')]); $errcode = isset($datas['errcode'])? $datas['errcode'] : ''; $errmsg = isset($datas['errmsg'])? $datas['errmsg'] : ''; if($errcode){ $this->error = $errmsg? $errmsg : '获取二维码失败'; return false; } file_put_contents($filePath.'/'.$qrFile, $result); if(!file_exists($filePath.'/'.$qrFile)){ $this->error = '生成二维码失败'; return false; } RedisService::set($cacheKey.$qrKey, ['page'=>$page,'scene'=>$scene,'qrcode'=>$qrFile,'date'=>date('Y-m-d H:i:s')], 30 * 86400); return $qrFile; }catch (\Exception $e){ $this->error = $e->getMessage(); $this->saveLog($cacheKey.'qrcode:error', ['page'=>$page,'scene'=>$scene,'error'=>$this->error,'trace'=>$e->getTrace(),'date'=>date('Y-m-d H:i:s')]); return false; } } /** * 小程序分享链接 * @param $path 页面路径 * @param $query 地址参数 * @param false $refresh * @return false|string */ public function getMiniShareLink($path, $query,$isExpire=false,$linkType=2, $refresh=false) { try { if(empty($path) || empty($query)){ $this->error = '缺少链接参数'; return false; } if(empty($this->mpAppid) || empty($this->mpAppSecret)){ $this->error = '小程序参数未配置'; return false; } if(!$token = $this->getAccessToken()) { $this->error = '获取token失败'; return false; } $cacheKey = "caches:members:mpShare_{$this->mpAppid}:{$linkType}_".md5($path.$query); $link = RedisService::get($cacheKey); if($link && !$refresh){ return $link; } if($linkType == 1){ $data = [ 'jump_wxa'=>[ 'path'=>$path, 'query'=>$query, ], 'is_expire'=> $isExpire, ]; }else if($linkType ==2){ $data=[ 'page_url'=> $path, 'page_title'=> $query, 'is_permanent'=>$isExpire ]; } $linkTypes = [1=>'getSchemeLink',2=>'getShortLink']; $name = isset($linkTypes[$linkType])?$linkTypes[$linkType]: 'getShortLink'; $url = sprintf($this->apiUrls[$name], $token); $result = curl_post($url, json_encode($data)); $datas = $result? json_decode($result, true) : []; $this->saveLog($cacheKey.'_result', ['path'=>$path,'query'=>$query,'linkType'=>$linkType,'url'=>$url,'result'=>$result,'date'=>date('Y-m-d H:i:s')]); $errcode = isset($datas['errcode'])? $datas['errcode'] : ''; $errmsg = isset($datas['errmsg'])? $datas['errmsg'] : ''; $link =isset($datas['link'])? $datas['link'] : ''; if($errcode || empty($link)){ $this->error = $errmsg? '获取失败:'.$errmsg : '获取链接失败'; return false; } RedisService::set($cacheKey,$link, rand(3600,7200)); return $link; }catch (\Exception $e){ $this->error = $e->getMessage(); $this->saveLog($cacheKey.'_error', ['path'=>$path,'query'=>$query,'linkType'=>$linkType,'error'=>$this->error,'trace'=>$e->getTrace(),'date'=>date('Y-m-d H:i:s')]); return false; } } /** * 获取用户信息 * @param $code * @return array|false|mixed|string[] */ public function getUserinfo($code) { try { if(empty($code)){ $this->error = '缺少授权参数'; return false; } if(empty($this->mpAppid) || empty($this->mpAppSecret)){ $this->error = '小程序参数未配置'; return false; } $data=[ 'appid' => $this->mpAppid, 'secret'=> $this->mpAppSecret, 'js_code'=>$code, 'grant_type'=>'authorization_code' ]; $cacheKey = "caches:mpApp:mp_{$this->mpAppid}:"; $url = $this->apiUrls['getUserInfo']; $result = httpRequest($url, $data,'get','',5); $this->saveLog($cacheKey.'userInfo:request', ['code'=>$code,'url'=>$url,'query'=>$data,'result'=>$result,'date'=>date('Y-m-d H:i:s')]); if(empty($result)){ $this->error = '获取用户信息失败'; return false; } return $result; }catch (\Exception $e){ $this->error = $e->getMessage(); $this->saveLog($cacheKey.'userInfo:error', ['code'=>$code,'error'=>$this->error,'trace'=>$e->getTrace(),'date'=>date('Y-m-d H:i:s')]); return false; } } /** * 获取用户手机号码 * @param $code * @return array|false|mixed|string[] */ public function getPhoneNumber($code) { try { if(empty($code)){ $this->error = '缺少授权参数'; return false; } if(empty($this->mpAppid) || empty($this->mpAppSecret)){ $this->error = '小程序参数未配置'; return false; } if(!$token = $this->getAccessToken()) { $this->error = '获取token失败'; return false; } $cacheKey = "caches:mpApp:mp_{$this->mpAppid}:"; $url = sprintf($this->apiUrls['getPhoneNumber'], $token); $result = httpRequest($url, json_encode(['code'=>$code],256),'post','',5); $this->saveLog($cacheKey.'phone:request', ['code'=>$code,'url'=>$url,'result'=>$result,'date'=>date('Y-m-d H:i:s')]); if(empty($result)){ $this->error = '获取用户手机号失败'; return false; } return $result; }catch (\Exception $e){ $this->error = $e->getMessage(); $this->saveLog($cacheKey.'phone:error', ['code'=>$code,'error'=>$this->error,'trace'=>$e->getTrace(),'date'=>date('Y-m-d H:i:s')]); return false; } } /** * 只带 access_token API 请求 * @param string $apiName 接口名称 * @param $data 接口数据 * @param $requestType 请求方式:post或get * @return false|mixed|string */ public function requestApi(string $apiName, $data=[], $requestType = 'post') { try { if(empty($this->mpAppid) || empty($this->mpAppSecret)){ $this->error = '小程序参数未配置'; return false; } if(!$token = $this->getAccessToken()) { $this->error = '获取token失败'; return false; } $cacheKey = "caches:mpApp:mp_{$this->mpAppid}:"; $url = sprintf($this->apiUrls[$apiName], $token); $result = httpRequest($url,$data?json_encode($data,256):'', $requestType,'',5); $this->saveLog($cacheKey."{$apiName}:request", ['url'=>$url,'data'=>$data,'result'=>$result,'date'=>date('Y-m-d H:i:s')]); return $result; }catch (\Exception $e){ $this->error = $e->getMessage(); $this->saveLog($cacheKey."{$apiName}:error", ['data'=>$data,'error'=>$this->error,'trace'=>$e->getTrace(),'date'=>date('Y-m-d H:i:s')]); return false; } } /** * 获取 客服接口 access_token * @param false $refresh * @return false|mixed|string */ public function getServiceToken($refresh = false) { try { if(empty($this->mpAppid) || empty($this->mpAppSecret)){ $this->error = '小程序参数未配置'; return false; } $cacheKey = "caches:mpApp:mp_{$this->mpAppid}:"; $tokenData = RedisService::get($cacheKey.'kf_access_token'); $token = isset($tokenData['access_token'])? $tokenData['access_token'] : ''; if($token && !$refresh){ return $token; } $url = sprintf($this->apiUrls['getServiceToken'], $this->mpAppid, $this->mpAppSecret); $result = httpRequest($url,'', 'get','',5); $this->saveLog($cacheKey.'kfTokens:request', ['url'=>$url,'result'=>$result,'date'=>date('Y-m-d H:i:s')]); $token = isset($result['access_token'])? $result['access_token'] : ''; if(empty($result) || empty($token)){ $this->error = '获取小程序客服TOKEN失败'; return false; } $result['date'] = date('Y-m-d H:i:s'); RedisService::set($cacheKey.'kf_access_token', $result, 7000); return $token; }catch (\Exception $e){ $this->error = $e->getMessage(); $this->saveLog($cacheKey.'kfTokens:error', ['error'=>$this->error,'trace'=>$e->getTrace(),'date'=>date('Y-m-d H:i:s')]); return false; } } /** * 获取客服地址 * @return array|false|mixed|string[] */ public function getServiceUrl() { try { if(empty($this->mpAppid) || empty($this->mpAppSecret)){ $this->error = '小程序参数未配置'; return false; } if(!$token = $this->getServiceToken()) { $this->error = '获取token失败'; return false; } $cacheKey = "caches:mpApp:mp_{$this->mpAppid}:"; $url = sprintf($this->apiUrls['getServiceUrl'], $token); $result = httpRequest($url, '','post','',5); $this->saveLog($cacheKey.'service:request', ['url'=>$url,'result'=>$result,'date'=>date('Y-m-d H:i:s')]); if(empty($result)){ $this->error = '获取小程序客服地址失败'; return false; } return $result; }catch (\Exception $e){ $this->error = $e->getMessage(); $this->saveLog($cacheKey.'service:error', ['error'=>$this->error,'trace'=>$e->getTrace(),'date'=>date('Y-m-d H:i:s')]); return false; } } /** * 检验数据的真实性,并且获取解密后的明文. * @param $encryptedData string 加密的用户数据 * @param $iv string 与用户数据一同返回的初始向量 * @param $sessionKey string 解密会话KEY * * @return int 成功0,失败返回对应的错误码 */ public function decryptData($encryptedData, $iv, $sessionKey) { if (strlen($sessionKey) != 24) { $this->error = -41001; return false; } $aesKey=base64_decode($sessionKey); if (strlen($iv) != 24) { $this->error = -41002; return false; } $aesIV=base64_decode($iv); $aesCipher=base64_decode($encryptedData); $result=openssl_decrypt( $aesCipher, "AES-128-CBC", $aesKey, 1, $aesIV); $dataObj=json_decode( $result); if( $dataObj == NULL ) { $this->error = -41003; return false; } if( $dataObj->watermark->appid != $this->mpAppid) { $this->error = -41003; return false; } return $dataObj; } /** * 保存日志 * @param $cackekey * @param $data * @param $time */ public function saveLog($cackekey, $data, $time=0) { if($this->debug){ RedisService::set($cackekey, $data, $time?$time : $this->expireTime); } } }