corpId = config('pay.wechat_work.corp_id'); $this->secret = config('pay.wechat_work.external_pay_secret'); } /** * @notes 获取access_token * @see https://developer.work.weixin.qq.com/document/path/91039 */ private function getAccessToken(): string { if ($this->accessToken) { return $this->accessToken; } // 从缓存获取 $cacheKey = 'qywx_access_token'; $token = Cache::get($cacheKey); if ($token) { $this->accessToken = $token; return $token; } // 请求新token $url = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken'; $params = [ 'corpid' => $this->corpId, 'corpsecret' => $this->secret, ]; $response = $this->httpGet($url, $params); if (isset($response['access_token'])) { $token = $response['access_token']; $expiresIn = $response['expires_in'] ?? 7200; // 缓存token,提前5分钟过期 Cache::set($cacheKey, $token, $expiresIn - 300); $this->accessToken = $token; return $token; } throw new \Exception('获取access_token失败: ' . ($response['errmsg'] ?? '未知错误')); } /** * @notes 获取部门成员列表 * @see https://developer.work.weixin.qq.com/document/path/90200 */ public function getDepartmentUserList(int $departmentId = 1, bool $fetchChild = true): array { $accessToken = $this->getAccessToken(); $url = 'https://qyapi.weixin.qq.com/cgi-bin/user/list'; $params = [ 'access_token' => $accessToken, 'department_id' => $departmentId, 'fetch_child' => $fetchChild ? 1 : 0, ]; $response = $this->httpGet($url, $params); Log::info('企业微信-获取部门成员列表响应: ' . json_encode($response, JSON_UNESCAPED_UNICODE)); if (isset($response['userlist'])) { return $response['userlist']; } Log::error('获取部门成员列表失败: ' . json_encode($response)); return []; } /** * @notes 获取成员的客户列表 * @see https://developer.work.weixin.qq.com/document/path/92113 * @param string $userId 成员ID * @return array|false 返回客户列表数组,权限错误返回false */ public function getExternalContactList(string $userId) { $accessToken = $this->getAccessToken(); $url = 'https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list'; $params = [ 'access_token' => $accessToken, 'userid' => $userId, ]; $response = $this->httpGet($url, $params); Log::info('企业微信-获取成员客户列表响应 userId=' . $userId . ': ' . json_encode($response, JSON_UNESCAPED_UNICODE)); // 检查是否是权限错误 if (isset($response['errcode']) && $response['errcode'] == 48002) { Log::error('API权限不足(48002): 请在企业微信管理后台开启"客户联系"权限'); return false; } if (isset($response['external_userid'])) { return $response['external_userid']; } // 如果没有客户,返回空数组(不记录错误) if (isset($response['errcode']) && $response['errcode'] == 0) { return []; } Log::error('获取客户列表失败: ' . json_encode($response)); return []; } /** * 批量获取指定成员添加的客户详情(分页,单次最多 100 条) * * @see https://developer.work.weixin.qq.com/document/path/92994 * @return array 含 errcode、external_contact_list、next_cursor 等 */ public function batchGetExternalContactByUser(string $userId, string $cursor = '', int $limit = 100): array { $accessToken = $this->getAccessToken(); $url = 'https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user?access_token=' . urlencode($accessToken); $body = [ 'userid_list' => [$userId], 'cursor' => $cursor, 'limit' => min(100, max(1, $limit)), ]; return $this->httpPostJson($url, $body); } /** * @notes 获取客户详情 * @see https://developer.work.weixin.qq.com/document/path/92114 */ public function getExternalContactDetail(string $externalUserId): array { $accessToken = $this->getAccessToken(); $url = 'https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get'; $params = [ 'access_token' => $accessToken, 'external_userid' => $externalUserId, ]; $response = $this->httpGet($url, $params); if (isset($response['external_contact'])) { return $response; } Log::error('获取客户详情失败: ' . json_encode($response)); return []; } /** * @notes 检查应用权限(用于诊断) * @return array 返回应用的权限信息 */ public function checkAppPermissions(): array { try { $accessToken = $this->getAccessToken(); // 尝试获取应用详情 $url = 'https://qyapi.weixin.qq.com/cgi-bin/agent/get'; $params = [ 'access_token' => $accessToken, 'agentid' => config('pay.wechat_work.agent_id', 1000002), ]; $response = $this->httpGet($url, $params); return [ 'success' => isset($response['agentid']), 'data' => $response, 'message' => isset($response['errmsg']) ? $response['errmsg'] : 'OK', ]; } catch (\Throwable $e) { return [ 'success' => false, 'data' => [], 'message' => $e->getMessage(), ]; } } /** * @notes HTTP GET请求 */ private function httpGet(string $url, array $params = []): array { if (!empty($params)) { $url .= '?' . http_build_query($params); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 30); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { Log::error('HTTP请求失败: ' . $url . ', HTTP Code: ' . $httpCode); return []; } $result = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { Log::error('JSON解析失败: ' . $response); return []; } return $result; } /** * POST JSON */ private function httpPostJson(string $url, array $body): array { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 60); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json; charset=UTF-8', ]); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200) { Log::error('HTTP POST 失败: ' . $url . ', HTTP Code: ' . $httpCode); return []; } $result = json_decode((string) $response, true); if (!is_array($result)) { Log::error('JSON 解析失败: ' . $response); return []; } return $result; } }