This commit is contained in:
Your Name
2026-09-09 15:47:48 +08:00
parent bd5d5c5f08
commit cb10e75ead
98 changed files with 7031 additions and 804 deletions
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace app\api\controller;
use app\adminapi\logic\tcm\DiagnosisLogic;
use app\common\service\ImCallbackSignature;
use JsonException;
use think\facade\Log;
use think\response\Json;
use Throwable;
/** 腾讯 IM 单聊消息归档回调;使用腾讯签名认证,不使用患者登录会话。 */
class ImController extends BaseApiController
{
public array $notNeedLogin = ['messageNotify'];
private const MAX_BODY_BYTES = 1048576;
private const AFTER_SEND_COMMAND = 'C2C.CallbackAfterSendMsg';
public function messageNotify(): Json
{
if (strtoupper($this->request->method(true)) !== 'POST') {
return $this->callbackFailure(405, 'method not allowed')->header(['Allow' => 'POST']);
}
$declaredLength = $this->request->header('content-length', '');
if (is_scalar($declaredLength) && is_numeric($declaredLength) && (float) $declaredLength > self::MAX_BODY_BYTES) {
return $this->callbackFailure(413, 'request body too large');
}
$token = (string) config('im.callback_token', '');
$sdkAppId = (string) config('project.trtc.sdkAppId', '');
if (trim($token) === '' || preg_match('/^[1-9][0-9]*$/D', $sdkAppId) !== 1) {
return $this->callbackFailure(503, 'callback authentication unavailable');
}
$query = $this->request->get();
$actualSdkAppId = $query['SdkAppid'] ?? null;
if ((!is_string($actualSdkAppId) && !is_int($actualSdkAppId)) || (string) $actualSdkAppId !== $sdkAppId
|| !ImCallbackSignature::verify($token, $query['RequestTime'] ?? null, $query['Sign'] ?? null)) {
return $this->callbackFailure(403, 'callback authentication failed');
}
$raw = $this->request->getContent();
if (strlen($raw) > self::MAX_BODY_BYTES) {
return $this->callbackFailure(413, 'request body too large');
}
try {
$decoded = json_decode($raw, false, 64, JSON_THROW_ON_ERROR);
if (!$decoded instanceof \stdClass) {
return $this->callbackFailure(400, 'invalid callback JSON object');
}
$payload = json_decode($raw, true, 64, JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $this->callbackFailure(400, 'invalid callback JSON object');
}
$command = $payload['CallbackCommand'] ?? null;
if (!is_string($command) || $command === '' || strlen($command) > 128
|| (array_key_exists('CallbackCommand', $query) && $query['CallbackCommand'] !== $command)) {
return $this->callbackFailure(400, 'callback command mismatch');
}
if ($command !== self::AFTER_SEND_COMMAND) {
return $this->callbackSuccess();
}
try {
DiagnosisLogic::archiveImCallbackMessage($payload);
} catch (Throwable) {
// 异常信息可能包含 SQL 或患者消息,日志只保留固定事件标记。
try {
Log::error('IM callback archive failed');
} catch (Throwable) {
// 日志存储不可用也必须保留腾讯失败回包。
}
return $this->callbackFailure(500, 'message archive failed');
}
return $this->callbackSuccess();
}
private function callbackSuccess(): Json
{
return json(['ActionStatus' => 'OK', 'ErrorCode' => 0, 'ErrorInfo' => '']);
}
private function callbackFailure(int $status, string $message): Json
{
return json(['ActionStatus' => 'FAIL', 'ErrorCode' => $status, 'ErrorInfo' => $message], $status);
}
}