prepare( 'select `_uid` from `t_user` where `_uid` = ? or `_mobile` = ? or `_name` = ? limit 1' ); $statement->execute([$value, $value, $value]); $uid = $statement->fetchColumn(); return is_string($uid) && $uid !== '' ? $uid : $value; } function messageTimestampMs(string $messageUid): int { return (((int)$messageUid) >> 21) + 1514736000000; } function nowTimestampMs(): int { return (int)floor(microtime(true) * 1000); } function recallLimitMinutes(): float { $statement = imAppDb()->prepare( 'select `conf_value` from `system_conf`' . ' where `conf_name` = ? and `is_valid` = 1 limit 1' ); $statement->execute(['RetractMessage']); $value = trim((string)($statement->fetchColumn() ?: '0')); if ($value === '' || !is_numeric($value)) { return 0.0; } return max(0.0, (float)$value); } function assertRecallWithinLimit(string $messageUid): void { $limitMinutes = recallLimitMinutes(); if ($limitMinutes <= 0) { return; } $messageAgeMs = nowTimestampMs() - messageTimestampMs($messageUid); $limitMs = (int)round($limitMinutes * 60 * 1000); if ($messageAgeMs > $limitMs) { throw new UserActionException('已超过可撤回时间'); } } function fetchActionMessageRow(string $messageUid): array { $table = imMessageTable($messageUid); $statement = imDb()->prepare( 'select `_mid`, `_from`, `_content_type`, `_searchable_key` from `' . $table . '` where `_mid` = ? limit 1' ); $statement->execute([$messageUid]); $row = $statement->fetch(); return is_array($row) ? $row : []; } function ensureMessageEditsTable(): void { imDb()->exec( 'create table if not exists `t_web_message_edits` (' . '`message_uid` varchar(32) not null,' . '`editor_uid` varchar(64) not null,' . '`edited_text` text null,' . '`edited_at_ms` bigint not null,' . '`created_at` datetime not null default current_timestamp,' . '`updated_at` datetime null default current_timestamp on update current_timestamp,' . 'primary key (`message_uid`),' . 'key `idx_editor_uid` (`editor_uid`),' . 'key `idx_edited_at_ms` (`edited_at_ms`)' . ') engine=InnoDB default charset=utf8mb4' ); } function editTextMessage(string $operatorId, string $messageUid, string $text): array { $row = fetchActionMessageRow($messageUid); if (!$row) { return editWebGroupMessage($operatorId, $messageUid, $text); } if ((string)$row['_from'] !== $operatorId) { throw new UserActionException('只能编辑自己发送的消息'); } if ((int)$row['_content_type'] !== 1) { throw new UserActionException('只能编辑文字消息'); } $editedAt = nowTimestampMs(); $table = imMessageTable($messageUid); $update = imDb()->prepare('update `' . $table . '` set `_searchable_key` = ? where `_mid` = ?'); $update->execute([$text, $messageUid]); ensureMessageEditsTable(); $upsert = imDb()->prepare( 'insert into `t_web_message_edits` (`message_uid`, `editor_uid`, `edited_text`, `edited_at_ms`)' . ' values (?, ?, ?, ?)' . ' on duplicate key update' . ' `editor_uid` = values(`editor_uid`),' . ' `edited_text` = values(`edited_text`),' . ' `edited_at_ms` = values(`edited_at_ms`),' . ' `updated_at` = current_timestamp' ); $upsert->execute([$messageUid, $operatorId, $text, $editedAt]); return [ 'messageUid' => $messageUid, 'text' => $text, 'editedAt' => $editedAt, ]; } function fetchWebGroupActionMessage(string $messageUid): array { ensureWebImAppTables(); $statement = imAppDb()->prepare( 'select m.*, g.`category` from `t_web_group_messages` m' . ' join `t_web_groups` g on g.`group_id` = m.`group_id` and g.`deleted` = 0' . ' where m.`message_uid` = ? limit 1' ); $statement->execute([$messageUid]); $row = $statement->fetch(); return is_array($row) ? $row : []; } function editWebGroupMessage(string $operatorId, string $messageUid, string $text): array { $row = fetchWebGroupActionMessage($messageUid); if (!$row) { throw new UserActionException('消息不存在'); } if ((string)$row['sender_uid'] !== $operatorId) { throw new UserActionException('只能编辑自己发送的消息'); } if ((int)($row['recalled'] ?? 0) !== 0) { throw new UserActionException('已撤回的消息请重新编辑发送'); } $editedAt = nowTimestampMs(); $update = imAppDb()->prepare( 'update `t_web_group_messages` set `text` = ?, `edited_at_ms` = ? where `message_uid` = ?' ); $update->execute([$text, $editedAt, $messageUid]); $groupUpdate = imAppDb()->prepare('update `t_web_groups` set `updated_at_ms` = ? where `group_id` = ?'); $groupUpdate->execute([$editedAt, (string)$row['group_id']]); return [ 'messageUid' => $messageUid, 'text' => $text, 'editedAt' => $editedAt, ]; } function webGroupMemberRoleForAction(string $groupId, string $userId): string { $statement = imAppDb()->prepare( 'select `role` from `t_web_group_members`' . ' where `group_id` = ? and `user_id` = ? and `status` = 0 limit 1' ); $statement->execute([$groupId, $userId]); $role = $statement->fetchColumn(); return is_string($role) ? $role : ''; } function recallWebGroupMessage(string $operatorId, string $messageUid): bool { $row = fetchWebGroupActionMessage($messageUid); if (!$row) { return false; } $isSender = (string)$row['sender_uid'] === $operatorId; if (!$isSender) { $role = webGroupMemberRoleForAction((string)$row['group_id'], $operatorId); if (!in_array($role, ['owner', 'admin'], true)) { throw new UserActionException('只有群主或管理员可以撤回他人消息'); } } assertRecallWithinLimit($messageUid); $now = nowTimestampMs(); $update = imAppDb()->prepare( 'update `t_web_group_messages` set `recalled` = 1 where `message_uid` = ?' ); $update->execute([$messageUid]); $groupUpdate = imAppDb()->prepare('update `t_web_groups` set `updated_at_ms` = ? where `group_id` = ?'); $groupUpdate->execute([$now, (string)$row['group_id']]); return true; } $payload = readJsonPayload(); $action = trim((string)($payload['action'] ?? '')); $userId = trim((string)($payload['userId'] ?? '')); $messageUidText = trim((string)($payload['messageUid'] ?? '')); $messageUid = ctype_digit($messageUidText) ? (int)$messageUidText : 0; if ($action === '' || $messageUid <= 0) { respond(400, '缺少操作类型或消息ID'); } try { if ($action === 'recall') { if ($userId === '') { respond(400, '缺少操作者ID'); } $userId = resolveActionUserId($userId); $row = fetchActionMessageRow($messageUidText); if (!$row && recallWebGroupMessage($userId, $messageUidText)) { respond(0, 'success', [ 'messageUid' => (string)$messageUid, 'action' => $action, ]); } if (!$row) { throw new UserActionException('消息不存在'); } assertRecallWithinLimit($messageUidText); requireImSuccess(imAdminPost('/admin/message/recall', [ 'operator' => $userId, 'messageUid' => $messageUid, 'userRecall' => true, 'isUserRecall' => true, ]), '撤回失败'); respond(0, 'success', [ 'messageUid' => (string)$messageUid, 'action' => $action, ]); } if ($action === 'edit') { if ($userId === '') { respond(400, '缺少操作者ID'); } $text = trim((string)($payload['text'] ?? '')); if ($text === '') { respond(400, '编辑内容不能为空'); } if (mb_strlen($text, 'UTF-8') > 5000) { respond(400, '编辑内容过长'); } $userId = resolveActionUserId($userId); respond(0, 'success', [ 'message' => editTextMessage($userId, $messageUidText, $text), 'action' => $action, ]); } if ($action === 'delete') { requireImSuccess(imAdminPost('/admin/message/delete', [ 'messageUid' => $messageUid, ]), '删除失败'); respond(0, 'success', [ 'messageUid' => (string)$messageUid, 'action' => $action, ]); } respond(400, '不支持的消息操作'); } catch (Throwable $error) { respond($error instanceof UserActionException ? 400 : 500, $error->getMessage() ?: '消息操作失败'); }