web = $cfg['web']; $this->mini = $cfg['mini']; $this->login = $cfg['login']; } /** * 获取微信二维码 */ public function makeQrUrl() { try { $state = bin2hex(random_bytes(16)); $ttl = (int)($this->login['state_ttl'] ?? 300); // 记录 state Redis::setEx("wx:login:state:$state", $ttl, json_encode([ 'redirect' => $this->web['redirect'], 'ts' => time(), ], JSON_UNESCAPED_UNICODE)); // 组装跳转地址 $qrUrl = sprintf( 'https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=%s&scope=%s&state=%s#wechat_redirect', $this->web['app_id'], urlencode($this->web['redirect']), 'code', 'snsapi_login', $state, ); $data = [ 'state' => $state, 'qr_url' => $qrUrl ]; return $data; } catch (\Throwable $th) { // var_dump($th->getFile().'|'.$th->getLine().'|'.$th->getMessage()); throw new BusinessException('获取微信二维码失败'); } } /** * 获取微信access_token */ public function wechatWebAccessToken(string $code, string $state): array { try { // 校验 state $stateJson = Redis::get("wx:login:state:$state"); if (!$stateJson) { throw new \RuntimeException('state 已过期或无效'); } // 拼接地址 $tokenUrl = sprintf( 'https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code', $this->web['app_id'], $this->web['app_secret'], $code ); // code 换 token $token = $this->httpGetJson($tokenUrl); if (!isset($token['access_token'], $token['openid'])) { throw new \RuntimeException('换取 access_token 失败:'. json_encode($token, JSON_UNESCAPED_UNICODE)); } return $token; } catch (\Throwable $th) { throw new BusinessException('获取 access_token 失败'); } } /** * 获取微信用户信息 * * @param array $token * @return array */ public function wechatWebUserInfo(array $accessToken): array { try { // 拉取用户信息 return $this->httpGetJson(sprintf( 'https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s', $accessToken['access_token'], $accessToken['openid'] )); } catch (\Throwable $th) { // var_dump($th->getFile().'|'.$th->getLine().'|'.$th->getMessage()); throw new BusinessException('获取用户信息失败'); } } /** * tools - httpGetJson */ protected function httpGetJson(string $url): array { $client = new Client([ 'verify' => false, // 禁用 SSL 验证 'timeout' => 10, ]); $json = $client->get($url)->getBody()->getContents(); return json_decode($json, true) ?: []; } //----------------------------小程序登录---------------------------------------- /** * 获取 openid / session_key * @param $code * @return mixed */ public function miniCode2Session($code) { $appid = $this->mini['app_id']; $secret = $this->mini['app_secret']; $url = "https://api.weixin.qq.com/sns/jscode2session?appid=$appid&secret=$secret&js_code=$code&grant_type=authorization_code"; return json_decode(file_get_contents($url), true); } /** * 获取 access_token * @return mixed */ public function getMiniAccessToken() { $appid = $this->mini['app_id']; $secret = $this->mini['app_secret']; $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$appid&secret=$secret"; return json_decode(file_get_contents($url), true); } /** * 调用微信接口取手机号 * @param $access_token * @param $phone_code * @return mixed */ public function getMiniPhoneNumber($access_token, $phone_code) { $url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={$access_token}"; $data = json_encode([ 'code' => $phone_code ]); $options = [ 'http' => [ 'header' => "Content-type: application/json", 'method' => 'POST', 'content' => $data, ], ]; return json_decode(file_get_contents($url, false, stream_context_create($options)), true); } /** * 小程序获取openid * @param string $code * @return array * @throws BusinessException */ public function xcxOpenid(string $code): array { $config = [ 'app_id' => getenv('WECHAT_MINI_APPID'), 'secret' => getenv('WECHAT_MINI_SECRET'), 'response_type' => 'array', 'log' => [ 'level' => 'debug', 'file' => runtime_path().'/mzl/wechat.log', ] ]; // 创建微信小程序对象 $wechat = new xcxApplication($config); // 获取 session_key 和 openid return $wechat->getUtils()->codeToSession($code); } /** * 小程序获取手机号 * @param string $phone_code * @return mixed * @throws BusinessException */ public function xcxPhoneNumber(string $phone_code) { $config = [ 'app_id' => getenv('WECHAT_MINI_APPID'), 'secret' => getenv('WECHAT_MINI_SECRET'), 'response_type' => 'array', 'log' => [ 'level' => 'debug', 'file' => runtime_path().'/mzl/wechat.log', ] ]; // 创建微信小程序对象 $wechat = new xcxApplication($config); // getPhoneNumber $data = ['code' => $phone_code]; $result = $wechat->getClient()->postJson('wxa/business/getuserphonenumber', $data); if (isset($result['phone_info'])) { // 业务处理 $result['phone_info']['phoneNumber'] return $result; } else { throw new BusinessException($result['errmsg']); } } /** * 文本安全检测 * @param string $content 待检测文本内容 * @param string $openid 用户openid(可选) * @param string $scene 场景值(1-资料,2-评论,3-论坛,4-社交日志) * @throws Exception */ public function checkText($content, $openid, $accessToken, $scene = 1) { // 1. 参数校验 if (empty($content)) { throw new Exception('检测内容不能为空'); } // 2. 长度限制检查(API限制为2500字节) $contentLength = strlen($content); if ($contentLength > 2500) { throw new Exception('文本内容过长,最大支持2500字节'); } // 3. 构建请求数据 $requestData = [ 'content' => $content, 'version' => 2, // 使用v2接口 'scene' => intval($scene), // 场景值 'openid' => $openid ?: '' ]; // 4. 调用微信API $apiUrl = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token={$accessToken}"; $result = $this->httpPost($apiUrl, json_encode($requestData, JSON_UNESCAPED_UNICODE)); // 5. 解析响应 $response = json_decode($result, true); if (empty($response)) { throw new Exception('API响应解析失败'); } // 6. 处理结果 if($response['errcode'] == 0){ foreach ($response['detail'] as $detail) { if (isset($detail['suggest'])&& ($detail['suggest'] == 'risky' || $detail['suggest'] == 'review')){ throw new Exception('内容含有违法违规内容'); } } return true; }elseif($response['errcode'] == 87014){ throw new Exception('内容含有违法违规内容'); } Log::info('checkText',[$response]); return false; } /** * 解析API响应结果 * @param array $response API响应 * @return array */ private function parseResult($response) { $result = [ 'success' => false, 'safe' => false, 'message' => '', 'errCode' => $response['errcode'] ?? -1, 'data' => $response ]; // 错误码处理 switch ($response['errcode']) { case 0: // 检测通过 return true; break; case 87014: // 风险内容 throw new Exception('内容含有违法违规内容'); break; case 40001: $result['message'] = 'access_token无效或已过期'; break; case 40003: $result['message'] = 'openid无效'; break; case 40097: $result['message'] = '参数错误'; break; case 48001: $result['message'] = '接口功能未授权,请确认小程序已开通此接口权限'; break; default: $result['message'] = $response['errmsg'] ?? '未知错误'; break; } // 如果有详细结果,添加到返回数据中 if (isset($response['result'])) { $result['detail'] = $response['result']; } return $result; } /** * HTTP POST请求 * @param string $url 请求地址 * @param string $data POST数据 * @return string 响应内容 * @throws Exception */ private function httpPost($url, $data) { $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $data, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Content-Length: ' . strlen($data) ] ]); $response = curl_exec($ch); $error = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($error) { throw new Exception("HTTP请求失败: {$error}"); } if ($httpCode !== 200) { throw new Exception("HTTP状态码异常: {$httpCode}"); } return $response; } }