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