Files
zyt/server/app/common/service/TencentImService.php
2026-05-11 17:49:38 +08:00

363 lines
12 KiB
PHP
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common\service;
/**
* 腾讯云IM服务类
* Class TencentImService
* @package app\common\service
*/
class TencentImService
{
private $sdkAppId;
private $secretKey;
private $adminIdentifier = 'administrator'; // 管理员账号
public function __construct()
{
$config = config('project.trtc');
$this->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;
}
// 增加超时时间到 60 秒
$result = $this->httpPost($url, json_encode($data), 60);
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;
}
}