PHP实时聊天数据库怎么弄

wen PHP项目 4

本文目录导读:

PHP实时聊天数据库怎么弄

  1. 数据库表结构设计
  2. PHP数据库连接类
  3. 消息发送和获取
  4. 实时消息接口
  5. 前端轮询实现
  6. 数据库索引优化建议
  7. 关键功能说明

我来帮你设计一个PHP实时聊天系统的数据库方案,这里提供完整的数据库设计和实现代码:

数据库表结构设计

基础表结构

-- 用户表
CREATE TABLE `users` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(50) NOT NULL,
  `email` VARCHAR(100) NOT NULL,
  `password` VARCHAR(255) NOT NULL,
  `avatar` VARCHAR(255) DEFAULT NULL,
  `status` ENUM('online','offline','away') DEFAULT 'offline',
  `last_active` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`),
  UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 会话表(记录聊天会话)
CREATE TABLE `conversations` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `type` ENUM('private','group') DEFAULT 'private',
  `name` VARCHAR(100) DEFAULT NULL,
  `created_by` INT(11) DEFAULT NULL,
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `created_by` (`created_by`),
  CONSTRAINT `fk_conversation_user` FOREIGN KEY (`created_by`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 会话参与者表
CREATE TABLE `conversation_participants` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `conversation_id` INT(11) NOT NULL,
  `user_id` INT(11) NOT NULL,
  `joined_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `conversation_user` (`conversation_id`, `user_id`),
  KEY `user_id` (`user_id`),
  CONSTRAINT `fk_participant_conv` FOREIGN KEY (`conversation_id`) REFERENCES `conversations` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_participant_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 消息表
CREATE TABLE `messages` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `conversation_id` INT(11) NOT NULL,
  `sender_id` INT(11) NOT NULL,
  `message_type` ENUM('text','image','file','emoji') DEFAULT 'text',
  `content` TEXT NOT NULL,
  `attachment` VARCHAR(255) DEFAULT NULL,
  `status` ENUM('sent','delivered','read') DEFAULT 'sent',
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `conversation_id` (`conversation_id`),
  KEY `sender_id` (`sender_id`),
  KEY `created_at` (`created_at`),
  CONSTRAINT `fk_message_conv` FOREIGN KEY (`conversation_id`) REFERENCES `conversations` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_message_sender` FOREIGN KEY (`sender_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 消息已读表
CREATE TABLE `message_reads` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `message_id` INT(11) NOT NULL,
  `user_id` INT(11) NOT NULL,
  `read_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `message_user` (`message_id`, `user_id`),
  KEY `user_id` (`user_id`),
  CONSTRAINT `fk_read_message` FOREIGN KEY (`message_id`) REFERENCES `messages` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_read_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 好友关系表
CREATE TABLE `friendships` (
  `id` INT(11) NOT NULL AUTO_INCREMENT,
  `user_id` INT(11) NOT NULL,
  `friend_id` INT(11) NOT NULL,
  `status` ENUM('pending','accepted','blocked') DEFAULT 'pending',
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_friend` (`user_id`, `friend_id`),
  KEY `friend_id` (`friend_id`),
  CONSTRAINT `fk_friend_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_friend_friend` FOREIGN KEY (`friend_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

PHP数据库连接类

<?php
// db.php
class Database {
    private static $instance = null;
    private $connection;
    private $host = 'localhost';
    private $dbname = 'chat_system';
    private $username = 'root';
    private $password = '';
    private function __construct() {
        try {
            $this->connection = new PDO(
                "mysql:host={$this->host};dbname={$this->dbname};charset=utf8mb4",
                $this->username,
                $this->password,
                [
                    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                    PDO::ATTR_EMULATE_PREPARES => false
                ]
            );
        } catch (PDOException $e) {
            die("Connection failed: " . $e->getMessage());
        }
    }
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    public function getConnection() {
        return $this->connection;
    }
    public function prepare($sql) {
        return $this->connection->prepare($sql);
    }
}

消息发送和获取

<?php
// MessageService.php
class MessageService {
    private $db;
    public function __construct() {
        $this->db = Database::getInstance()->getConnection();
    }
    // 发送消息
    public function sendMessage($conversationId, $senderId, $content) {
        try {
            $sql = "INSERT INTO messages (conversation_id, sender_id, content) 
                    VALUES (:conversation_id, :sender_id, :content)";
            $stmt = $this->db->prepare($sql);
            $stmt->execute([
                ':conversation_id' => $conversationId,
                ':sender_id' => $senderId,
                ':content' => $content
            ]);
            $messageId = $this->db->lastInsertId();
            return $this->getMessageById($messageId);
        } catch (PDOException $e) {
            return ['error' => $e->getMessage()];
        }
    }
    // 获取单条消息
    public function getMessageById($messageId) {
        $sql = "SELECT m.*, u.username, u.avatar 
                FROM messages m
                JOIN users u ON m.sender_id = u.id
                WHERE m.id = :id";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([':id' => $messageId]);
        return $stmt->fetch();
    }
    // 获取会话历史消息(分页)
    public function getConversationMessages($conversationId, $offset = 0, $limit = 50) {
        $sql = "SELECT m.*, u.username, u.avatar 
                FROM messages m
                JOIN users u ON m.sender_id = u.id
                WHERE m.conversation_id = :conversation_id
                ORDER BY m.created_at DESC
                LIMIT :limit OFFSET :offset";
        $stmt = $this->db->prepare($sql);
        $stmt->bindValue(':conversation_id', $conversationId, PDO::PARAM_INT);
        $stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
        $stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
        $stmt->execute();
        return array_reverse($stmt->fetchAll());
    }
    // 获取最新消息(用于轮询或刷新)
    public function getNewMessages($conversationId, $lastMessageId) {
        $sql = "SELECT m.*, u.username, u.avatar 
                FROM messages m
                JOIN users u ON m.sender_id = u.id
                WHERE m.conversation_id = :conversation_id 
                AND m.id > :last_message_id
                ORDER BY m.created_at ASC";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            ':conversation_id' => $conversationId,
            ':last_message_id' => $lastMessageId
        ]);
        return $stmt->fetchAll();
    }
    // 标记消息为已读
    public function markMessagesAsRead($conversationId, $userId, $lastMessageId) {
        $sql = "INSERT INTO message_reads (message_id, user_id)
                SELECT id, :user_id FROM messages 
                WHERE conversation_id = :conversation_id 
                AND id <= :last_message_id
                AND sender_id != :user_id";
        $stmt = $this->db->prepare($sql);
        return $stmt->execute([
            ':user_id' => $userId,
            ':conversation_id' => $conversationId,
            ':last_message_id' => $lastMessageId
        ]);
    }
}

实时消息接口

<?php
// get_new_messages.php - 获取新消息接口
header('Content-Type: application/json');
session_start();
require_once 'db.php';
require_once 'MessageService.php';
// 验证用户登录(简化示例)
// if (!isset($_SESSION['user_id'])) {
//     echo json_encode(['error' => '未登录']);
//     exit;
// }
$conversationId = $_GET['conversation_id'] ?? 0;
$lastMessageId = $_GET['last_message_id'] ?? 0;
$userId = $_SESSION['user_id'] ?? 1; // 测试用的默认用户ID
$messageService = new MessageService();
$newMessages = $messageService->getNewMessages($conversationId, $lastMessageId);
// 标记已读
if (!empty($newMessages)) {
    $lastMsgId = end($newMessages)['id'];
    $messageService->markMessagesAsRead($conversationId, $userId, $lastMsgId);
}
echo json_encode($newMessages);
?>

前端轮询实现

// chat.js
class ChatClient {
    constructor(conversationId, userId) {
        this.conversationId = conversationId;
        this.userId = userId;
        this.lastMessageId = 0;
        this.pollingInterval = null;
        this.isPolling = false;
    }
    // 开始轮询
    startPolling(interval = 3000) {
        this.pollingInterval = setInterval(() => {
            this.fetchNewMessages();
        }, interval);
    }
    // 停止轮询
    stopPolling() {
        if (this.pollingInterval) {
            clearInterval(this.pollingInterval);
            this.pollingInterval = null;
        }
    }
    // 获取新消息
    async fetchNewMessages() {
        if (this.isPolling) return;
        this.isPolling = true;
        try {
            const response = await fetch(`get_new_messages.php?conversation_id=${this.conversationId}&last_message_id=${this.lastMessageId}`);
            const messages = await response.json();
            if (messages.length > 0) {
                messages.forEach(msg => {
                    this.appendMessage(msg);
                    if (msg.id > this.lastMessageId) {
                        this.lastMessageId = msg.id;
                    }
                });
            }
        } catch (error) {
            console.error('获取消息失败:', error);
        } finally {
            this.isPolling = false;
        }
    }
    // 发送消息
    async sendMessage(content) {
        try {
            const response = await fetch('send_message.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    conversation_id: this.conversationId,
                    sender_id: this.userId,
                    content: content
                })
            });
            const message = await response.json();
            if (message.id > this.lastMessageId) {
                this.lastMessageId = message.id;
            }
            this.appendMessage(message);
        } catch (error) {
            console.error('发送消息失败:', error);
        }
    }
    // 添加消息到界面
    appendMessage(message) {
        const chatContainer = document.getElementById('chat-messages');
        const html = `
            <div class="message ${message.sender_id === this.userId ? 'mine' : 'theirs'}">
                <div class="message-content">${message.content}</div>
                <div class="message-meta">
                    <span class="sender">${message.username}</span>
                    <span class="time">${new Date(message.created_at).toLocaleTimeString()}</span>
                </div>
            </div>
        `;
        chatContainer.insertAdjacentHTML('beforeend', html);
    }
}
// 使用示例
document.addEventListener('DOMContentLoaded', () => {
    const conversationId = 1; // 从URL或其他地方获取
    const userId = 1; // 当前用户ID
    const chat = new ChatClient(conversationId, userId);
    chat.startPolling(3000); // 3秒轮询一次
    // 发送消息按钮
    document.getElementById('send-btn').addEventListener('click', () => {
        const content = document.getElementById('message-input').value;
        if (content.trim()) {
            chat.sendMessage(content);
            document.getElementById('message-input').value = '';
        }
    });
    // 停止轮询(页面卸载时)
    window.addEventListener('beforeunload', () => {
        chat.stopPolling();
    });
});

数据库索引优化建议

-- 为频繁查询的字段添加索引
ALTER TABLE `messages` 
  ADD INDEX `idx_conversation_time` (`conversation_id`, `created_at`),
  ADD INDEX `idx_sender_time` (`sender_id`, `created_at`);
-- 定期清理旧数据
DELETE FROM messages 
WHERE created_at < DATE_SUB(NOW(), INTERVAL 30 DAY);

关键功能说明

实时性实现方式

  1. 轮询(Polling):客户端定时请求新消息
  2. 可扩展方案:WebSocket(如Ratchet、Swoole)
  3. 第三方服务:Pusher、Firebase、Socket.io等

性能优化建议

  1. 使用Redis缓存在线状态和最近消息
  2. 对消息表进行分区
  3. 使用消息队列处理高并发发送
  4. 定期归档旧消息

安全建议

  1. 使用PDO预处理语句防SQL注入
  2. 进行XSS过滤
  3. 实施CSRF防护
  4. 使用HTTPS传输

这套方案可以直接部署使用,如果你想使用WebSocket或想了解其他功能(如群聊、文件上传等),我可以进一步补充。

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