sdkAppId = $config['sdkAppId']; $this->secretKey = $config['secretKey']; \think\facade\Log::info('TencentImService初始化 - sdkAppId: ' . $this->sdkAppId . ', secretKey长度: ' . strlen($this->secretKey)); } /** * @notes 生成UserSig(使用腾讯云官方算法) * @param string $userId * @param int $expire * @return string|false */ private function generateUserSig(string $userId, int $expire = 86400) { try { $api = new TLSSigAPIv2($this->sdkAppId, $this->secretKey); $userSig = $api->genUserSig($userId, $expire); \think\facade\Log::info('UserSig生成成功 - userId: ' . $userId . ', sig长度: ' . strlen($userSig)); return $userSig; } catch (\Exception $e) { \think\facade\Log::error('生成UserSig失败: ' . $e->getMessage()); return false; } } /** * @notes 导入单个账号到IM * @param string $userId 用户ID * @param string $nick 昵称(可选) * @param string $faceUrl 头像URL(可选) * @return array|false */ public function importAccount(string $userId, string $nick = '', string $faceUrl = '') { try { \think\facade\Log::info('开始导入IM账号 - userId: ' . $userId . ', nick: ' . $nick . ', sdkAppId: ' . $this->sdkAppId); // 生成管理员UserSig $adminUserSig = $this->generateUserSig($this->adminIdentifier); if (!$adminUserSig) { throw new \Exception('生成管理员UserSig失败'); } \think\facade\Log::info('管理员UserSig生成成功'); // 构建请求URL $random = rand(0, 4294967295); $url = sprintf( 'https://console.tim.qq.com/v4/im_open_login_svc/account_import?sdkappid=%s&identifier=%s&usersig=%s&random=%s&contenttype=json', $this->sdkAppId, $this->adminIdentifier, urlencode($adminUserSig), $random ); \think\facade\Log::info('请求URL构建完成'); // 构建请求体 $data = [ 'UserID' => $userId ]; if ($nick) { $data['Nick'] = $nick; } if ($faceUrl) { $data['FaceUrl'] = $faceUrl; } \think\facade\Log::info('请求数据: ' . json_encode($data, JSON_UNESCAPED_UNICODE)); // 发送请求 $result = $this->httpPost($url, json_encode($data)); \think\facade\Log::info('HTTP响应: ' . substr($result, 0, 500)); if (!$result) { throw new \Exception('请求失败:无响应'); } $response = json_decode($result, true); if (!$response) { throw new \Exception('响应解析失败:' . substr($result, 0, 200)); } \think\facade\Log::info('响应解析成功: ' . json_encode($response, JSON_UNESCAPED_UNICODE)); // 检查响应状态 if ($response['ActionStatus'] !== 'OK') { $errorMsg = sprintf( '导入账号失败 - ErrorCode: %s, ErrorInfo: %s', $response['ErrorCode'] ?? 'unknown', $response['ErrorInfo'] ?? '未知错误' ); throw new \Exception($errorMsg); } return [ 'success' => true, 'userId' => $userId, 'message' => '账号导入成功' ]; } catch (\Exception $e) { $errorMsg = sprintf( '导入IM账号失败 - userId: %s, error: %s', $userId, $e->getMessage() ); \think\facade\Log::error($errorMsg); return [ 'success' => false, 'userId' => $userId, 'message' => $e->getMessage() ]; } } /** * @notes 批量导入账号 * @param array $accounts 账号列表 [['userId' => 'xxx', 'nick' => 'xxx', 'faceUrl' => 'xxx'], ...] * @return array */ public function batchImportAccounts(array $accounts): array { $results = []; foreach ($accounts as $account) { $userId = $account['userId'] ?? ''; $nick = $account['nick'] ?? ''; $faceUrl = $account['faceUrl'] ?? ''; if (!$userId) { continue; } $result = $this->importAccount($userId, $nick, $faceUrl); $results[] = $result; } return $results; } /** * @notes 删除账号 * @param array $userIds 用户ID列表 * @return array|false */ public function deleteAccounts(array $userIds) { try { // 生成管理员UserSig $adminUserSig = $this->generateUserSig($this->adminIdentifier); if (!$adminUserSig) { throw new \Exception('生成管理员UserSig失败'); } // 构建请求URL $random = rand(0, 4294967295); $url = sprintf( 'https://console.tim.qq.com/v4/im_open_login_svc/account_delete?sdkappid=%s&identifier=%s&usersig=%s&random=%s&contenttype=json', $this->sdkAppId, $this->adminIdentifier, urlencode($adminUserSig), $random ); // 构建请求体 $data = [ 'DeleteItem' => array_map(function($userId) { return ['UserID' => $userId]; }, $userIds) ]; // 发送请求 $result = $this->httpPost($url, json_encode($data)); if (!$result) { throw new \Exception('请求失败'); } $response = json_decode($result, true); if (!$response) { throw new \Exception('响应解析失败'); } // 检查响应状态 if ($response['ActionStatus'] !== 'OK') { throw new \Exception($response['ErrorInfo'] ?? '删除账号失败'); } return [ 'success' => true, 'message' => '账号删除成功' ]; } catch (\Exception $e) { \think\facade\Log::error('删除IM账号失败', [ 'userIds' => $userIds, 'error' => $e->getMessage() ]); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * 拉取单聊(C2C)漫游消息 * @see https://cloud.tencent.com/document/product/269/2739 * * @param string $operatorAccount 会话一方 UserID(如 doctor_1) * @param string $peerAccount 会话另一方 UserID(如 patient_2) * @return array{success:bool,msgList:array,complete:int,lastMsgKey:?string,lastMsgTime:?int,error:string,rawErrorCode:int} */ public function adminGetRoamMsg( string $operatorAccount, string $peerAccount, int $maxCnt = 100, int $minTime = 0, int $maxTime = 4294967295, ?string $lastMsgKey = null, ?int $lastMsgTime = null ): array { $empty = [ 'success' => false, 'msgList' => [], 'complete' => 1, 'lastMsgKey' => null, 'lastMsgTime' => null, 'error' => '', 'rawErrorCode' => 0, ]; try { $adminUserSig = $this->generateUserSig($this->adminIdentifier); if (!$adminUserSig) { $empty['error'] = '生成管理员UserSig失败'; return $empty; } $random = rand(0, 4294967295); $url = sprintf( 'https://console.tim.qq.com/v4/openim/admin_getroammsg?sdkappid=%s&identifier=%s&usersig=%s&random=%s&contenttype=json', $this->sdkAppId, $this->adminIdentifier, urlencode($adminUserSig), $random ); $data = [ 'Operator_Account' => $operatorAccount, 'Peer_Account' => $peerAccount, 'MaxCnt' => $maxCnt, 'MinTime' => $minTime, 'MaxTime' => $maxTime, ]; if ($lastMsgKey !== null && $lastMsgKey !== '') { $data['LastMsgKey'] = $lastMsgKey; } if ($lastMsgTime !== null && $lastMsgTime > 0) { $data['LastMsgTime'] = $lastMsgTime; } $result = $this->httpPost($url, json_encode($data), 30); if (!$result) { $empty['error'] = 'IM接口无响应'; return $empty; } $response = json_decode($result, true); if (!$response) { $empty['error'] = 'IM响应解析失败'; return $empty; } $code = (int)($response['ErrorCode'] ?? -1); $empty['rawErrorCode'] = $code; if (($response['ActionStatus'] ?? '') !== 'OK') { $empty['error'] = $response['ErrorInfo'] ?? ('ErrorCode ' . $code); return $empty; } $msgList = $response['MsgList'] ?? []; if (!is_array($msgList)) { $msgList = []; } return [ 'success' => true, 'msgList' => $msgList, 'complete' => (int)($response['Complete'] ?? 1), 'lastMsgKey' => $response['LastMsgKey'] ?? null, 'lastMsgTime' => isset($response['LastMsgTime']) ? (int)$response['LastMsgTime'] : null, 'error' => '', 'rawErrorCode' => $code, ]; } catch (\Exception $e) { $empty['error'] = $e->getMessage(); return $empty; } } /** * @notes 发送HTTP POST请求 * @param string $url * @param string $data * @return string|false */ private function httpPost(string $url, string $data, int $timeout = 10) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Content-Length: ' . strlen($data) ]); $result = curl_exec($ch); $error = curl_error($ch); curl_close($ch); if ($error) { \think\facade\Log::error('HTTP请求失败', [ 'url' => $url, 'error' => $error ]); return false; } return $result; } }