Files
zyt/server/app/common/service/ImRoamMessagePager.php
T
2026-09-09 15:47:48 +08:00

120 lines
5.3 KiB
PHP

<?php
declare(strict_types=1);
namespace app\common\service;
/** 单次请求一页;调用者只有在消息持久化后才保存返回的游标。 */
class ImRoamMessagePager
{
private const MAX_TIME = 4294967295;
/**
* @return array{msgList:array,completed:bool,cursor:array{max_time:int,last_key:?string,min_time:int,seen_keys:array}}
* @throws \RuntimeException 云端错误码通过异常 code 保留。
*/
public static function nextPage(TencentImService $svc, string $operator, string $peer, array $cursor = []): array
{
if (trim($operator) === '' || trim($peer) === '' || $operator === $peer) {
throw new \RuntimeException('IM会话双方账号无效');
}
$cursor = self::normalizeCursor($cursor);
$response = $svc->adminGetRoamMsg(
$operator,
$peer,
100,
$cursor['min_time'],
$cursor['max_time'],
$cursor['last_key']
);
if (($response['success'] ?? null) !== true) {
$error = is_string($response['error'] ?? null) ? trim($response['error']) : '';
$code = is_int($response['rawErrorCode'] ?? null) ? $response['rawErrorCode'] : 0;
throw new \RuntimeException($error !== '' ? $error : 'IM漫游消息拉取失败', $code);
}
if (!in_array($response['complete'] ?? null, [0, 1], true)) {
throw new \RuntimeException('IM响应分页状态 Complete 非法');
}
$messages = $response['msgList'] ?? null;
if (!is_array($messages) || array_values($messages) !== $messages) {
throw new \RuntimeException('IM响应消息列表 MsgList 非法');
}
foreach ($messages as $message) {
self::validateMessage($message, $operator, $peer, $cursor);
}
$completed = $response['complete'] === 1;
$lastTime = $response['lastMsgTime'] ?? null;
$lastKey = $response['lastMsgKey'] ?? null;
$hasCursor = ($lastTime !== null && $lastTime !== 0) || ($lastKey !== null && $lastKey !== '');
if (!$completed || $hasCursor) {
if (!is_int($lastTime) || $lastTime <= 0 || !is_string($lastKey) || trim($lastKey) === '') {
throw new \RuntimeException('IM响应缺少有效续页游标 LastMsgTime/LastMsgKey');
}
if ($lastTime < $cursor['min_time'] || $lastTime > $cursor['max_time']) {
throw new \RuntimeException('IM响应续页时间超出请求范围');
}
$seenKeys = $lastTime === $cursor['max_time'] ? $cursor['seen_keys'] : [];
if (in_array($lastKey, $seenKeys, true)) {
throw new \RuntimeException('IM响应续页游标重复,分页未向前推进');
}
$seenKeys[] = $lastKey;
$cursor['max_time'] = $lastTime;
$cursor['last_key'] = $lastKey;
$cursor['seen_keys'] = $seenKeys;
}
return ['msgList' => $messages, 'completed' => $completed, 'cursor' => $cursor];
}
/** 不以本地归档最新时间作下界,初次扫描覆盖云端仍保留的全部记录。 */
private static function normalizeCursor(array $cursor): array
{
$maxTime = $cursor['max_time'] ?? self::MAX_TIME;
$minTime = $cursor['min_time'] ?? 0;
$lastKey = $cursor['last_key'] ?? null;
$seenKeys = $cursor['seen_keys'] ?? [];
if (!is_int($minTime) || !is_int($maxTime) || $minTime < 0 || $maxTime > self::MAX_TIME || $minTime > $maxTime) {
throw new \RuntimeException('IM请求时间游标无效');
}
if ($lastKey === '') {
$lastKey = null;
}
if ($lastKey !== null && (!is_string($lastKey) || trim($lastKey) === '')) {
throw new \RuntimeException('IM请求消息游标无效');
}
if (!is_array($seenKeys) || array_values($seenKeys) !== $seenKeys) {
throw new \RuntimeException('IM请求历史游标无效');
}
foreach ($seenKeys as $key) {
if (!is_string($key) || trim($key) === '') {
throw new \RuntimeException('IM请求历史游标无效');
}
}
if ($lastKey !== null && !in_array($lastKey, $seenKeys, true)) {
$seenKeys[] = $lastKey;
}
return ['max_time' => $maxTime, 'last_key' => $lastKey, 'min_time' => $minTime, 'seen_keys' => $seenKeys];
}
private static function validateMessage($message, string $operator, string $peer, array $cursor): void
{
if (!is_array($message)) {
throw new \RuntimeException('IM响应包含非法消息');
}
$from = $message['From_Account'] ?? null;
$to = $message['To_Account'] ?? null;
if (!(($from === $operator && $to === $peer) || ($from === $peer && $to === $operator))) {
throw new \RuntimeException('IM响应消息不属于当前会话,已停止同步');
}
$key = $message['MsgKey'] ?? null;
$time = $message['MsgTimeStamp'] ?? null;
if (!is_string($key) || trim($key) === '' || !is_int($time)
|| $time < $cursor['min_time'] || $time > $cursor['max_time']
|| !is_array($message['MsgBody'] ?? null)) {
throw new \RuntimeException('IM响应消息标识、时间或内容格式非法');
}
}
}