本文目录导读:

在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) + 消息队列 + 分布式缓存
根据你的项目规模和实时性要求选择合适的方案。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播