本文目录导读:

PHP项目站内信系统从零搭建:完整方案与核心代码解析
目录导读
- 站内信系统的基础设计思路
- 数据库表结构设计(关键)
- PHP核心功能实现步骤
- 消息通知与未读状态管理
- 常见性能优化与安全防护
- FAQ:开发者最常问的5个问题
站内信系统的基础设计思路
在PHP项目中实现站内信,本质是构建一个用户间或系统对用户的异步消息通道,与即时通讯(WebSocket实时推送)不同,站内信采用“存储+拉取”模型:用户登录后主动查询未读消息,这种设计对服务器压力更小,适合电商、社区、OA系统等非实时场景。
核心流程:
- 发送方 → 写入消息表(标记接收方ID、内容、时间)
- 接收方 → 请求时查询“收件箱”并标记已读
- 系统处理 → 支持批量发送、站内弹窗提示、消息归档
数据库表结构设计(关键)
站内信的性能基石是表设计,以下是经过大量项目验证的3表方案:
表1:messages(消息主表)
CREATE TABLE `messages` ( `id` int(11) NOT NULL AUTO_INCREMENT, `sender_id` int(11) DEFAULT '0' COMMENT '发送者ID,系统消息填0', varchar(100) NOT NULL DEFAULT '' COMMENT '消息标题/#39;, `content` text NOT NULL COMMENT '消息正文(支持HTML或文本)', `msg_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '类型:1私信 2系统通知 3业务提醒', `is_deleted` tinyint(1) NOT NULL DEFAULT '0', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), INDEX `idx_type` (`msg_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
表2:message_receivers(收件人关联表)——核心优化表
CREATE TABLE `message_receivers` ( `id` int(11) NOT NULL AUTO_INCREMENT, `msg_id` int(11) NOT NULL COMMENT '关联messages.id', `receiver_id` int(11) NOT NULL COMMENT '收件用户ID', `folder` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1收件箱 2草稿箱 3归档', `is_read` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0未读 1已读', `read_at` datetime DEFAULT NULL, `is_deleted` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), INDEX `idx_receiver_folder` (`receiver_id`, `folder`, `is_read`), // 查询未读数量时极速 INDEX `idx_receiver_read` (`receiver_id`, `is_read`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设计要点:
- 为什么不用单表存所有字段? 用户量10万+时,单表where receiver_id查询极慢,关联表让每个用户仅查询自己的行记录。
- 未读统计:
SELECT COUNT(*) FROM message_receivers WHERE receiver_id=100 AND is_read=0 AND is_deleted=0
PHP核心功能实现步骤
步骤1:发送消息(示例代码)
function sendMessage($senderId, $receiverIds, $title, $content, $type=1) {
$pdo = getDB(); // 伪代码,实际用PDO或ORM
// 1. 插入消息主表
$stmt = $pdo->prepare("INSERT INTO messages (sender_id, title, content, msg_type) VALUES (?, ?, ?, ?)");
$stmt->execute([$senderId, $title, $content, $type]);
$msgId = $pdo->lastInsertId();
// 2. 批量插入收件人记录(使用事务提升效率)
$sql = "INSERT INTO message_receivers (msg_id, receiver_id) VALUES (?, ?)";
$stmt = $pdo->prepare($sql);
$pdo->beginTransaction();
foreach ($receiverIds as $uid) {
$stmt->execute([$msgId, $uid]);
}
$pdo->commit();
// 3. 可触发缓存清理:在Redis中标记用户有新消息
// 如:$redis->sAdd("user:unread:{$uid}", $msgId);
return $msgId;
}
步骤2:获取用户收件箱列表
// 分页获取未删除消息,只查message_receivers + join messages
function getInbox($userId, $page=1, $size=20) {
$offset = ($page-1) * $size;
$sql = "SELECT m.id, m.title, m.created_at, m.sender_id,
r.is_read, r.id as receiver_record_id
FROM message_receivers r
JOIN messages m ON r.msg_id = m.id AND m.is_deleted = 0
WHERE r.receiver_id = ? AND r.folder = 1 AND r.is_deleted = 0
ORDER BY m.created_at DESC
LIMIT ?, ?";
// 注意LIMIT参数绑定需要整型
return query($sql, [$userId, $offset, $size]);
}
步骤3:标记已读
function markAsRead($receiverRecordIds, $userId) {
// 防止越权:必须同时验证receiver_id等于当前用户
$pdo->prepare("UPDATE message_receivers
SET is_read=1, read_at=NOW()
WHERE id IN (".implode(',', array_fill(0, count($receiverRecordIds), '?')).")
AND receiver_id = ?");
// 参数绑定省略...
}
消息通知与未读状态管理
最佳实践方案:
不要每次请求都查询数据库统计未读数,可以使用Redis缓存:
- 用户登录/页面加载时:
$unreadCount = $redis->scard("user:unread:{$uid}"); - 发送消息时:
$redis->sadd("user:unread:{$receiverId}", $msgId); - 用户点开消息时(批量标记已读后):
$redis->del("user:unread:{$uid}");
这样即使10万用户同时在线,未读统计响应速度<1ms。
前端轮询方案:
使用JavaScript的setInterval或Web Worker,每30-60秒发一次AJAX请求:
setInterval(function() {
fetch('/api/message/unread-count')
.then(res => res.json())
.then(data => {
if(data.count > 0) {
showBadge(data.count); // 小红点提示
}
});
}, 30000);
常见性能优化与安全防护
优化点:
- 避免全表扫描:收件箱查询一定走
idx_receiver_folder索引。 - 批量删除/归档:使用
UPDATE message_receivers SET is_deleted=1 WHERE ...而非硬删除。 - 历史消息归档:超过90天的旧消息可迁移到
messages_archive表。
安全防护:
- SQL注入:所有查询必须使用参数绑定(PDO或预处理)。
- 越权检查:任何对消息的修改(标记已读、删除)都必须验证当前用户是否为
receiver_id。 - XSS过滤:存储消息前用
htmlspecialchars()或HTML Purifier过滤内容。 - 频率限制:防止恶意批量发送消息骚扰用户(每用户每分钟最多发20条)。
FAQ:开发者最常问的5个问题
Q1:系统消息(给所有用户发)怎么实现?
A:不要循环插入千万条记录!使用“广播标记法”:在message_receivers表中用receiver_id=0代表全部用户,查询时使用WHERE receiver_id=当前用户ID OR receiver_id=0,配合缓存标记用户是否已读取系统消息,如果非要存储每个用户的记录,采用队列异步写入(如Laravel Job)。
Q2:站内信和WebSocket实时推送怎么结合? A:站内信负责持久化存储,实时推送仅作为“提醒”,发送消息后,通过Redis订阅(如$redis->publish('new_msg:'.$userId, $msgId)),WebSocket服务器监听该频道,推送给在线用户。
Q3:如何支持带附件或图片的站内信?设计为富文本(HTML)+ 附件字段(JSON格式,存储文件路径),前端用编辑器上传图片到阿里云OSS/本地存储,再插入消息内容。
Q4:用户删除消息后,发送者还能看到吗? A:设计上分离“发送者视角”和“接收者视角”,接收者删除只改动message_receivers的is_deleted,不影响messages主表,发送者可选择删除“已发送”记录。
Q5:几十万用户群发消息怎么提高性能?
A:先生成一条messages主记录,然后分批次(每次1000条)写入message_receivers,使用MySQL的insert into ... values (...), (...), (...) 语法,或使用批量插入ORM(ThinkPHP的saveAll,Laravel的insert),写入完成后执行OPTIMIZE TABLE message_receivers(非必须,维护时做)。
通过以上设计,一个日均十万次消息交互的PHP系统可以稳定运行,重点在于表结构优化(关联表+索引)、缓存层提升未读查询速度,以及始终遵循“验证用户身份”的安全原则,实际开发中,建议使用成熟的框架如Laravel、ThinkPHP,它们自带消息队列和事件系统,能进一步简化实现。