本文目录导读:

在 PHP 项目中实现“@提及”功能,通常涉及几个关键环节:触发检测、用户列表查询与展示、数据存储与解析。
下面详细拆解实现步骤,从简单的前端模拟到完整的后端存储。
核心流程梳理
- 输入阶段:用户输入 字符。
- 搜索阶段:前端弹出下拉框,显示匹配的用户列表,后端接收搜索请求。
- 选择阶段:用户点击某个用户,前端将该用户信息(如 ID)嵌入到文本中。
- 提交与存储:将包含特殊标记的文本存入数据库。
- 展示阶段:从数据库读取文本时,将
@用户ID替换为可点击的链接或高亮文字。
前端实现(关键交互)
前端负责监听输入、弹出列表、高亮选择,可以使用原生 JS 或现成的库(如 At.js, jquery-textcomplete, Tribute.js 等)来节省时间。
简单原生实现思路 (无库):
// 在 textarea 或 contenteditable div 上监听
const input = document.getElementById('comment-input');
const mentionList = document.getElementById('mention-list');
input.addEventListener('input', function(e) {
const cursorPos = this.selectionStart;
const text = this.value;
// 定位当前输入位置前的最后一个 @ 符号
const lastAtIndex = text.lastIndexOf('@', cursorPos - 1);
// 检查 @ 符号前是否为空或空格 (避免匹配到 email 如 aa@bb.com)
if (lastAtIndex !== -1 && (lastAtIndex === 0 || text[lastAtIndex - 1] === ' ')) {
const query = text.substring(lastAtIndex + 1, cursorPos);
// 防止查询空白
if (query !== '' && !query.includes(' ')) {
// 异步查询后端 API
fetchMentions(query).then(users => {
// 渲染下拉列表
renderMentionList(users, lastAtIndex, cursorPos);
});
}
} else {
mentionList.style.display = 'none'; // 隐藏下拉框
}
});
function fetchMentions(query) {
return fetch(`/api/users/search?q=${encodeURIComponent(query)}`)
.then(res => res.json());
}
function selectUser(user) {
// 1. 替换输入框中的 @query 为 @user:displayName (或自定义格式)
const beforeAt = input.value.substring(0, lastAtIndex);
const afterAt = input.value.substring(cursorPos);
input.value = beforeAt + `@[${user.id}:${user.name}] ` + afterAt;
// 2. 隐藏下拉列表
mentionList.style.display = 'none';
}
重要:在 contenteditable 中实现更复杂,建议使用 Tribute.js 这类成熟库。
后端 API 接口 (搜索用户)
提供一个搜索接口,实时返回匹配用户。
// /api/users/search.php
header('Content-Type: application/json');
$query = $_GET['q'] ?? '';
$query = trim($query);
if (mb_strlen($query) < 1) {
echo json_encode([]);
exit;
}
// 假设从数据库查询
$db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$stmt = $db->prepare("SELECT id, username, avatar_url FROM users WHERE username LIKE :q LIMIT 10");
$stmt->execute([':q' => $query . '%']);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode($users);
优化建议:
- 权限过滤:只返回当前用户可提及的用户(如好友、群成员)。
- 缓存:使用 Redis 缓存热门用户搜索,减少数据库压力。
- 防 SQL 注入:使用参数绑定,如上面的
q。
数据存储设计 (存储提及)
存储时不能直接存 @张三,因为用户名可能会变,最好存储用户 ID。
推荐格式:
- 带标记的文本:
这篇文档需要@[user:123]来处理。 - JSON 结构化:
{ "text": "这篇文档需要 @user 来处理。", "mentions": [{"user_id": 123, "offset": 8, "length": 5}] }但开发复杂度较高,大部分项目使用格式1。
数据库存储示例:
-- 存储文本内容 INSERT INTO comments (post_id, content, mentions_count) VALUES (:post_id, :content, :mentions_count); -- 同时存储提及关系(用于通知查询) INSERT INTO mention_notifications (comment_id, mentioned_user_id, post_id) VALUES ( :comment_id, :mentioned_user_id, :post_id );
PHP 解析与提取提及的用户 ID:
// 假设前端提交的文本格式为: "Hello @[user:123:John] how are you?"
$content = $_POST['content'];
$pattern = '/@\[user:(\d+):([^\]]+)\]/';
preg_match_all($pattern, $content, $matches);
// $matches[1] 是所有被提及的用户 ID 数组
$mentionedUserIds = array_unique($matches[1]);
// 这里可以触发发送通知的逻辑
foreach ($mentionedUserIds as $userId) {
// sendNotification($userId, $commentId);
}
展示渲染 (显示提及)
从数据库读取文本后,将 @[user:ID:Name] 替换为带链接的 HTML。
function renderContent($content) {
// 替换 @[user:123:张三] 为 <a href="/user/123" class="mention">@张三</a>
$pattern = '/@\[user:(\d+):([^\]]+)\]/';
$replacement = '<a href="/user/$1" class="mention" data-user-id="$1">@$2</a>';
return preg_replace($pattern, $replacement, htmlspecialchars($content));
}
// 使用
echo renderContent($row['content']);
安全提醒:htmlspecialchars 必不可少,防止 XSS 攻击。
完整代码示例 (极简版)
前端 (HTML + JS):监听 输入,展示下拉框,替换文本。 后端搜索 (search.php):模糊查询并返回 JSON。 提交处理 (submit.php):
$rawContent = $_POST['content']; // "你好 @[user:5:Lisi] 辛苦了"
// 1. 提取被 @ 的用户 ID
preg_match_all('/@\[user:(\d+):[^\]]+\]/', $rawContent, $matches);
$mentionedIds = array_unique($matches[1]);
// 2. 保存内容到数据库
$stmt = $db->prepare("INSERT INTO comments (content, user_id, created_at) VALUES (?, ?, NOW())");
$stmt->execute([$rawContent, $currentUserId]);
$commentId = $db->lastInsertId();
// 3. 生成通知记录
foreach ($mentionedIds as $toUserId) {
$stmt = $db->prepare("INSERT INTO notifications (type, from_user_id, to_user_id, comment_id) VALUES ('mention', ?, ?, ?)");
$stmt->execute([$currentUserId, $toUserId, $commentId]);
}
展示 (view.php):
$comment = $db->query("SELECT content FROM comments WHERE id = 1")->fetch();
$displayContent = preg_replace(
'/@\[user:(\d+):([^\]]+)\]/',
'<a href="/profile/$1" class="mention">@$2</a>',
htmlspecialchars($comment['content'])
);
echo $displayContent;
进阶优化
- 实时性:使用 WebSocket 推送 通知。
- 富文本编辑器:在
contenteditable中实现,建议使用Froala Editor,Quill等支持 Mention 插件的编辑器。 - 权限控制:用户只能提及当前模块(如该群聊)中的成员。
- 历史记录:存储
mentions表,方便统计和搜索被 @ 过的内容。
- 前端:监听输入 -> 请求搜索 -> 替换文本。
- 后端:搜索接口 (LIKE) -> 存储格式 (
@[user:ID:Name]) -> 解析存入通知表 -> 展示时渲染链接。 - 关键: 使用 用户 ID 而非用户名来标记“@提及”,防止改名后失效。
如果需要进一步了解某个具体环节(例如如何实现 contenteditable 中插入不可编辑的 mention 标签),请随时提出。