PHP项目消息已读未读状态如何标记

wen PHP项目 29

本文目录导读:

PHP项目消息已读未读状态如何标记

  1. 基础方案:用户-消息关联表
  2. 使用Redis优化性能
  3. 使用位图存储状态(适合大量用户)
  4. 前端实现技巧
  5. 性能优化建议
  6. 选择建议

在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) + 消息队列 + 分布式缓存

根据你的项目规模和实时性要求选择合适的方案。

抱歉,评论功能暂时关闭!