本文目录导读:

我来帮你设计一个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);
关键功能说明
实时性实现方式
- 轮询(Polling):客户端定时请求新消息
- 可扩展方案:WebSocket(如Ratchet、Swoole)
- 第三方服务:Pusher、Firebase、Socket.io等
性能优化建议
- 使用Redis缓存在线状态和最近消息
- 对消息表进行分区
- 使用消息队列处理高并发发送
- 定期归档旧消息
安全建议
- 使用PDO预处理语句防SQL注入
- 进行XSS过滤
- 实施CSRF防护
- 使用HTTPS传输
这套方案可以直接部署使用,如果你想使用WebSocket或想了解其他功能(如群聊、文件上传等),我可以进一步补充。