本文目录导读:

在PHP项目中标记消息的已读/未读状态,通常有以下几种常见的实现方案:
基础方案:用户-消息关联表
这是最常用的方案,创建关联表记录每个用户的消息阅读状态。
数据库设计
-- 消息表
CREATE TABLE messages (
id INT PRIMARY KEY AUTO_INCREMENT,
sender_id INT,VARCHAR(255),
content TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 用户消息状态表
CREATE TABLE user_message_status (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
message_id INT,
is_read TINYINT(1) DEFAULT 0, -- 0:未读 1:已读
read_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (message_id) REFERENCES messages(id),
FOREIGN KEY (user_id) REFERENCES users(id),
UNIQUE KEY unique_user_message (user_id, message_id)
);
PHP 实现代码
<?php
class MessageService {
private $db;
public function __construct($db) {
$this->db = $db;
}
// 发送消息给多个用户
public function sendMessage($senderId, $title, $content, $recipients) {
try {
$this->db->beginTransaction();
// 插入消息
$sql = "INSERT INTO messages (sender_id, title, content) VALUES (?, ?, ?)";
$stmt = $this->db->prepare($sql);
$stmt->execute([$senderId, $title, $content]);
$messageId = $this->db->lastInsertId();
// 为每个接收者创建状态记录
$sql = "INSERT INTO user_message_status (user_id, message_id) VALUES (?, ?)";
$stmt = $this->db->prepare($sql);
foreach ($recipients as $userId) {
$stmt->execute([$userId, $messageId]);
}
$this->db->commit();
return $messageId;
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
// 标记消息为已读
public function markAsRead($userId, $messageId) {
$sql = "UPDATE user_message_status
SET is_read = 1, read_at = NOW()
WHERE user_id = ? AND message_id = ? AND is_read = 0";
$stmt = $this->db->prepare($sql);
return $stmt->execute([$userId, $messageId]);
}
// 批量标记已读
public function markMultipleAsRead($userId, $messageIds) {
if (empty($messageIds)) return false;
$placeholders = implode(',', array_fill(0, count($messageIds), '?'));
$sql = "UPDATE user_message_status
SET is_read = 1, read_at = NOW()
WHERE user_id = ? AND message_id IN ($placeholders) AND is_read = 0";
$params = array_merge([$userId], $messageIds);
$stmt = $this->db->prepare($sql);
return $stmt->execute($params);
}
// 获取用户未读消息数量
public function getUnreadCount($userId) {
$sql = "SELECT COUNT(*) FROM user_message_status
WHERE user_id = ? AND is_read = 0";
$stmt = $this->db->prepare($sql);
$stmt->execute([$userId]);
return $stmt->fetchColumn();
}
// 获取用户消息列表(带状态)
public function getUserMessages($userId, $page = 1, $pageSize = 20) {
$offset = ($page - 1) * $pageSize;
$sql = "SELECT m.*, ums.is_read, ums.read_at
FROM messages m
JOIN user_message_status ums ON m.id = ums.message_id
WHERE ums.user_id = ?
ORDER BY m.created_at DESC
LIMIT ? OFFSET ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$userId, $pageSize, $offset]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}
使用Redis优化性能
对于高并发场景,可以使用Redis存储未读状态。
<?php
class RedisMessageService {
private $redis;
private $db;
public function __construct($redis, $db) {
$this->redis = $redis;
$this->db = $db;
}
// 标记消息已读(Redis + 异步更新数据库)
public function markAsRead($userId, $messageId) {
// Redis中标记
$key = "user:{$userId}:unread";
$this->redis->sRem($key, $messageId);
// 异步任务更新数据库
$this->pushToQueue('mark_message_read', [
'user_id' => $userId,
'message_id' => $messageId
]);
}
// 获取未读消息数(从Redis)
public function getUnreadCount($userId) {
$key = "user:{$userId}:unread";
return $this->redis->sCard($key);
}
// 发送消息时同步到Redis
public function sendMessage($senderId, $recipients, $title, $content) {
$messageId = $this->saveToDatabase($senderId, $title, $content);
foreach ($recipients as $userId) {
// 将消息ID加入用户的未读集合
$key = "user:{$userId}:unread";
$this->redis->sAdd($key, $messageId);
}
return $messageId;
}
}
使用位图存储状态(适合大量用户)
<?php
class BitmapMessageService {
private $redis;
// 使用位图标记用户消息状态
// 假设消息ID从1开始顺序增长
public function markAsRead($userId, $messageId) {
$key = "msg_read:{$userId}";
// 在第 messageId 位设置为1
$this->redis->setBit($key, $messageId, 1);
}
public function isRead($userId, $messageId) {
$key = "msg_read:{$userId}";
return $this->redis->getBit($key, $messageId) == 1;
}
public function getUnreadCount($userId, $maxMessageId) {
$key = "msg_read:{$userId}";
$readCount = $this->redis->bitCount($key);
return $maxMessageId - $readCount;
}
}
前端实现技巧
自动标记已读
// 使用Intersection Observer自动标记已读
class MessageReader {
constructor() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const messageId = entry.target.dataset.messageId;
this.markAsRead(messageId);
}
});
},
{ threshold: 0.5 } // 50%可见时触发
);
}
observeMessage(element) {
this.observer.observe(element);
}
async markAsRead(messageId) {
try {
await fetch('/api/messages/read', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message_id: messageId })
});
} catch (error) {
console.error('Mark as read failed:', error);
}
}
}
// 使用示例
const reader = new MessageReader();
document.querySelectorAll('.message-item').forEach(el => {
reader.observeMessage(el);
});
性能优化建议
// 1. 批量处理
public function batchMarkRead($userId, $messageIds) {
// 分批处理,避免SQL过长
$chunks = array_chunk($messageIds, 100);
foreach ($chunks as $chunk) {
$placeholders = implode(',', array_fill(0, count($chunk), '?'));
$sql = "UPDATE user_message_status
SET is_read = 1, read_at = NOW()
WHERE user_id = ? AND message_id IN ($placeholders)";
$params = array_merge([$userId], $chunk);
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
}
}
// 2. 使用索引
// 在 user_message_status 表上创建复合索引
// ALTER TABLE user_message_status ADD INDEX idx_user_read (user_id, is_read);
// 3. 缓存常用数据
public function getUnreadCountCached($userId) {
$cacheKey = "unread_count:{$userId}";
$count = $this->cache->get($cacheKey);
if ($count === false) {
$count = $this->getUnreadCount($userId);
$this->cache->set($cacheKey, $count, 300); // 缓存5分钟
}
return $count;
}
选择建议
- 小型项目:使用基础的关系型数据库方案
- 中型项目:Redis + 数据库异步写入
- 大型项目:使用专门的推送服务(如WebSocket) + 消息队列 + 分布式缓存
根据你的项目规模和实时性要求选择合适的方案。