本文目录导读:

我来详细说明PHP项目实现用户私信功能的完整方案:
数据库设计
私信相关表结构
-- 私信会话表
CREATE TABLE `conversations` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`subject` VARCHAR(200) DEFAULT NULL COMMENT '私信主题',
`initiator_id` INT NOT NULL COMMENT '发起人ID',
`last_message_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '最后消息时间',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX `idx_user` (`initiator_id`),
INDEX `idx_last_message` (`last_message_at`)
);
-- 会话参与者表
CREATE TABLE `conversation_participants` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`conversation_id` INT NOT NULL,
`user_id` INT NOT NULL,
`is_read` TINYINT(1) DEFAULT 0 COMMENT '是否已读',
`last_read_at` TIMESTAMP NULL COMMENT '最后阅读时间',
FOREIGN KEY (`conversation_id`) REFERENCES `conversations`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`),
UNIQUE KEY `unique_conversation_user` (`conversation_id`, `user_id`)
);
-- 消息表
CREATE TABLE `messages` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`conversation_id` INT NOT NULL,
`sender_id` INT NOT NULL,
`message_type` ENUM('text', 'image', 'file', 'system') DEFAULT 'text',
`content` TEXT NOT NULL,
`is_deleted` TINYINT(1) DEFAULT 0 COMMENT '是否删除',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`conversation_id`) REFERENCES `conversations`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`sender_id`) REFERENCES `users`(`id`),
INDEX `idx_conversation` (`conversation_id`),
INDEX `idx_created_at` (`created_at`)
);
PHP核心功能实现
私信核心类
<?php
class MessageSystem {
private $db;
public function __construct($db) {
$this->db = $db;
}
// 创建新会话
public function createConversation($initiatorId, $recipientId, $subject = null) {
try {
// 检查是否已有会话
$existingConvo = $this->findExistingConversation($initiatorId, $recipientId);
if ($existingConvo) {
return $existingConvo;
}
$this->db->beginTransaction();
// 创建会话
$stmt = $this->db->prepare("
INSERT INTO conversations (initiator_id, subject)
VALUES (?, ?)
");
$stmt->execute([$initiatorId, $subject]);
$conversationId = $this->db->lastInsertId();
// 添加参与者
$participants = [$initiatorId, $recipientId];
$stmt = $this->db->prepare("
INSERT INTO conversation_participants
(conversation_id, user_id) VALUES (?, ?)
");
foreach ($participants as $userId) {
$stmt->execute([$conversationId, $userId]);
}
$this->db->commit();
return $conversationId;
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
// 发送消息
public function sendMessage($conversationId, $senderId, $content, $type = 'text') {
// 验证发送者是参与者
if (!$this->isParticipant($conversationId, $senderId)) {
throw new Exception("您不是该会话的参与者");
}
try {
$this->db->beginTransaction();
// 插入消息
$stmt = $this->db->prepare("
INSERT INTO messages
(conversation_id, sender_id, message_type, content)
VALUES (?, ?, ?, ?)
");
$stmt->execute([$conversationId, $senderId, $type, $content]);
// 更新会话最后消息时间
$stmt = $this->db->prepare("
UPDATE conversations
SET last_message_at = NOW()
WHERE id = ?
");
$stmt->execute([$conversationId]);
// 更新其他参与者的未读状态
$stmt = $this->db->prepare("
UPDATE conversation_participants
SET is_read = 0
WHERE conversation_id = ? AND user_id != ?
");
$stmt->execute([$conversationId, $senderId]);
$this->db->commit();
// 触发通知(可选)
$this->notifyParticipants($conversationId, $senderId, $content);
return true;
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
// 获取用户会话列表
public function getUserConversations($userId) {
$stmt = $this->db->prepare("
SELECT
c.*,
cp.is_read,
cp.last_read_at,
u.username as other_username,
u.avatar as other_avatar,
m.content as last_message_content,
m.created_at as last_message_time
FROM conversations c
INNER JOIN conversation_participants cp ON c.id = cp.conversation_id
INNER JOIN conversation_participants cp2 ON c.id = cp2.conversation_id
AND cp2.user_id != ?
INNER JOIN users u ON cp2.user_id = u.id
LEFT JOIN messages m ON c.id = m.id
WHERE cp.user_id = ?
ORDER BY c.last_message_at DESC
");
$stmt->execute([$userId, $userId]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 获取会话消息
public function getConversationMessages($conversationId, $userId, $offset = 0, $limit = 20) {
// 验证用户是参与者
if (!$this->isParticipant($conversationId, $userId)) {
return [];
}
// 标记为已读
$this->markAsRead($conversationId, $userId);
$stmt = $this->db->prepare("
SELECT
m.*,
u.username as sender_name,
u.avatar as sender_avatar
FROM messages m
INNER JOIN users u ON m.sender_id = u.id
WHERE m.conversation_id = ? AND m.is_deleted = 0
ORDER BY m.created_at ASC
LIMIT ? OFFSET ?
");
$stmt->execute([$conversationId, $limit, $offset]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 获取未读消息数量
public function getUnreadCount($userId) {
$stmt = $this->db->prepare("
SELECT COUNT(*) as count
FROM conversation_participants cp
WHERE cp.user_id = ? AND cp.is_read = 0
");
$stmt->execute([$userId]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result['count'];
}
// 检查用户是否是参与者
private function isParticipant($conversationId, $userId) {
$stmt = $this->db->prepare("
SELECT id FROM conversation_participants
WHERE conversation_id = ? AND user_id = ?
");
$stmt->execute([$conversationId, $userId]);
return $stmt->fetch(PDO::FETCH_ASSOC) !== false;
}
// 标记为已读
private function markAsRead($conversationId, $userId) {
$stmt = $this->db->prepare("
UPDATE conversation_participants
SET is_read = 1, last_read_at = NOW()
WHERE conversation_id = ? AND user_id = ?
");
$stmt->execute([$conversationId, $userId]);
}
// 查找已有会话
private function findExistingConversation($userId1, $userId2) {
$stmt = $this->db->prepare("
SELECT c.id
FROM conversations c
INNER JOIN conversation_participants cp1 ON c.id = cp1.conversation_id
INNER JOIN conversation_participants cp2 ON c.id = cp2.conversation_id
WHERE cp1.user_id = ? AND cp2.user_id = ?
AND (SELECT COUNT(*) FROM conversation_participants WHERE conversation_id = c.id) = 2
LIMIT 1
");
$stmt->execute([$userId1, $userId2]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
return $result ? $result['id'] : false;
}
// 通知参与者(可扩展)
private function notifyParticipants($conversationId, $senderId, $content) {
// 实现通知逻辑,如推送、邮件等
// 获取其他参与者并发送通知
}
}
前后端交互实现
API 接口示例
<?php
// send_message.php
header('Content-Type: application/json');
require_once 'config.php';
require_once 'MessageSystem.php';
$msgSystem = new MessageSystem($db);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$conversationId = $_POST['conversation_id'];
$content = $_POST['content'];
$senderId = $_SESSION['user_id'];
try {
$result = $msgSystem->sendMessage($conversationId, $senderId, $content);
echo json_encode(['success' => true, 'message' => '消息发送成功']);
} catch (Exception $e) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}
前端JavaScript示例
// message.js
class MessageManager {
constructor() {
this.conversationId = null;
this.pollInterval = null;
}
// 发送消息
async sendMessage(conversationId, content) {
try {
const response = await fetch('send_message.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `conversation_id=${conversationId}&content=${encodeURIComponent(content)}`
});
const data = await response.json();
if (data.success) {
this.appendMessage(content, 'self');
}
return data;
} catch (error) {
console.error('发送消息失败:', error);
}
}
// 加载消息
async loadMessages(conversationId) {
const response = await fetch(`get_messages.php?conversation_id=${conversationId}`);
const messages = await response.json();
this.renderMessages(messages);
}
// 轮询新消息
startPolling(conversationId) {
this.conversationId = conversationId;
this.pollInterval = setInterval(() => {
this.checkNewMessages();
}, 3000); // 3秒轮询一次
}
// WebSocket连接(推荐替代轮询)
connectWebSocket(conversationId) {
const ws = new WebSocket('wss://your-server.com/chat');
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'join',
conversation_id: conversationId,
user_id: currentUserId
}));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
this.handleNewMessage(message);
};
return ws;
}
// 渲染消息
renderMessages(messages) {
const container = document.getElementById('message-container');
container.innerHTML = '';
messages.forEach(msg => {
const messageEl = this.createMessageElement(msg);
container.appendChild(messageEl);
});
container.scrollTop = container.scrollHeight;
}
}
安全性与优化建议
安全措施
// 消息过滤
function sanitizeMessage($content) {
// XSS防护
$content = htmlspecialchars($content, ENT_QUOTES, 'UTF-8');
// 敏感词过滤
$forbiddenWords = ['敏感词1', '敏感词2'];
$content = str_replace($forbiddenWords, '***', $content);
// SQL注入防护(使用预处理语句)
return $content;
}
// 权限验证中间件
function checkMessagePermission($conversationId, $userId) {
// 验证用户权限
if (!isParticipant($conversationId, $userId)) {
http_response_code(403);
die(json_encode(['error' => '无权访问该会话']));
}
// 检查黑名单
if (isBlocked($userId, $otherUserId)) {
http_response_code(403);
die(json_encode(['error' => '对方已将你拉黑']));
}
}
性能优化
// 1. 分页加载
$limit = 20;
$offset = isset($_GET['page']) ? ($_GET['page'] - 1) * $limit : 0;
// 2. 缓存未读计数
function getCachedUnreadCount($userId) {
$cacheKey = "unread_count_{$userId}";
$count = apcu_fetch($cacheKey);
if ($count === false) {
$count = $msgSystem->getUnreadCount($userId);
apcu_store($cacheKey, $count, 300); // 缓存5分钟
}
return $count;
}
// 3. 数据库索引优化
// 确保已创建适当的索引
$indexSql = "
CREATE INDEX idx_conversation_user ON conversation_participants(conversation_id, user_id);
CREATE INDEX idx_conversation_created ON messages(conversation_id, created_at);
";
消息实时推送方案
Pusher (推荐)
// 使用Pusher实现实时消息
require_once 'vendor/autoload.php';
$pusher = new Pusher\Pusher(
'your-app-key',
'your-app-secret',
'your-app-id',
['cluster' => 'your-cluster']
);
// 发送消息时触发事件
$pusher->trigger("conversation-{$conversationId}", 'new-message', [
'message' => $message,
'sender_id' => $senderId
]);
服务器推送(SSE)
// server_sent_events.php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
$lastMessageId = $_GET['last_id'] ?? 0;
while (true) {
$newMessages = getNewMessages($lastMessageId);
if ($newMessages) {
echo "data: " . json_encode($newMessages) . "\n\n";
$lastMessageId = end($newMessages)['id'];
}
ob_flush();
flush();
sleep(3); // 3秒检查一次
}
这个实现方案涵盖了私信功能的核心需求,你可以根据实际项目要求进行调整,记得在生产环境中做好错误处理、日志记录和性能监控。