120 lines
5.3 KiB
PHP
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响应消息标识、时间或内容格式非法');
|
|
}
|
|
}
|
|
}
|