本文目录导读:

我来介绍如何在PHP项目中实现一个私信系统,我会提供一个完整的实现方案,包含数据库设计、后端API和前端交互。
数据库设计
-- 私信表
CREATE TABLE `private_messages` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`sender_id` INT UNSIGNED NOT NULL COMMENT '发送者ID',
`receiver_id` INT UNSIGNED NOT NULL COMMENT '接收者ID',
`content` TEXT NOT NULL COMMENT '消息内容',
`type` ENUM('text', 'image', 'file') DEFAULT 'text' COMMENT '消息类型',
`is_read` TINYINT(1) DEFAULT 0 COMMENT '是否已读',
`is_deleted_sender` TINYINT(1) DEFAULT 0 COMMENT '发送者是否删除',
`is_deleted_receiver` TINYINT(1) DEFAULT 0 COMMENT '接收者是否删除',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_sender_id (`sender_id`),
INDEX idx_receiver_id (`receiver_id`),
INDEX idx_created_at (`created_at`),
INDEX idx_sender_receiver (`sender_id`, `receiver_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 会话表(用于显示最近会话)
CREATE TABLE `conversations` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`user1_id` INT UNSIGNED NOT NULL,
`user2_id` INT UNSIGNED NOT NULL,
`last_message_id` BIGINT UNSIGNED,
`last_message_content` TEXT,
`last_message_time` TIMESTAMP,
`user1_unread_count` INT DEFAULT 0,
`user2_unread_count` INT DEFAULT 0,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `unique_conversation` (`user1_id`, `user2_id`),
INDEX idx_user1 (`user1_id`),
INDEX idx_user2 (`user2_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
PHP后端实现
Message类
<?php
// config/database.php
class Database {
private $host = "localhost";
private $db_name = "your_database";
private $username = "root";
private $password = "";
public $conn;
public function getConnection() {
$this->conn = null;
try {
$this->conn = new PDO(
"mysql:host=" . $this->host . ";dbname=" . $this->db_name,
$this->username,
$this->password
);
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $exception) {
echo "Connection error: " . $exception->getMessage();
}
return $this->conn;
}
}
// classes/Message.php
class Message {
private $conn;
private $table = 'private_messages';
public function __construct($db) {
$this->conn = $db;
}
// 发送私信
public function sendMessage($senderId, $receiverId, $content, $type = 'text') {
try {
$this->conn->beginTransaction();
// 插入消息
$query = "INSERT INTO {$this->table}
(sender_id, receiver_id, content, type)
VALUES (:sender_id, :receiver_id, :content, :type)";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':sender_id', $senderId);
$stmt->bindParam(':receiver_id', $receiverId);
$stmt->bindParam(':content', $content);
$stmt->bindParam(':type', $type);
$stmt->execute();
$messageId = $this->conn->lastInsertId();
// 更新会话
$this->updateConversation($senderId, $receiverId, $messageId, $content);
$this->conn->commit();
return ['success' => true, 'message_id' => $messageId];
} catch (Exception $e) {
$this->conn->rollback();
return ['success' => false, 'error' => $e->getMessage()];
}
}
// 更新会话
private function updateConversation($userId1, $userId2, $messageId, $lastMessage) {
// 确保 user1_id < user2_id 来保持一致性
$user1 = min($userId1, $userId2);
$user2 = max($userId1, $userId2);
// 检查会话是否存在
$query = "SELECT id FROM conversations
WHERE user1_id = :user1 AND user2_id = :user2";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user1', $user1);
$stmt->bindParam(':user2', $user2);
$stmt->execute();
if ($stmt->rowCount() > 0) {
// 更新现有会话
$updateQuery = "UPDATE conversations SET
last_message_id = :last_message_id,
last_message_content = :last_message_content,
last_message_time = NOW()";
// 更新未读计数
if ($userId1 == $user1) {
$updateQuery .= ", user2_unread_count = user2_unread_count + 1";
} else {
$updateQuery .= ", user1_unread_count = user1_unread_count + 1";
}
$updateQuery .= " WHERE user1_id = :user1 AND user2_id = :user2";
$stmt = $this->conn->prepare($updateQuery);
$stmt->bindParam(':last_message_id', $messageId);
$stmt->bindParam(':last_message_content', $lastMessage);
$stmt->bindParam(':user1', $user1);
$stmt->bindParam(':user2', $user2);
$stmt->execute();
} else {
// 创建新会话
$insertQuery = "INSERT INTO conversations
(user1_id, user2_id, last_message_id, last_message_content,
last_message_time, user1_unread_count, user2_unread_count)
VALUES (:user1, :user2, :last_message_id, :last_message_content,
NOW(), :unread1, :unread2)";
$unread1 = ($userId1 == $user1) ? 0 : 1;
$unread2 = ($userId1 == $user2) ? 0 : 1;
$stmt = $this->conn->prepare($insertQuery);
$stmt->bindParam(':user1', $user1);
$stmt->bindParam(':user2', $user2);
$stmt->bindParam(':last_message_id', $messageId);
$stmt->bindParam(':last_message_content', $lastMessage);
$stmt->bindParam(':unread1', $unread1);
$stmt->bindParam(':unread2', $unread2);
$stmt->execute();
}
}
// 获取会话列表
public function getConversations($userId) {
$query = "SELECT
c.*,
CASE
WHEN c.user1_id = :user_id THEN
(SELECT username FROM users WHERE id = c.user2_id)
ELSE
(SELECT username FROM users WHERE id = c.user1_id)
END as other_user_name,
CASE
WHEN c.user1_id = :user_id THEN c.user1_unread_count
ELSE c.user2_unread_count
END as unread_count
FROM conversations c
WHERE c.user1_id = :user_id OR c.user2_id = :user_id
ORDER BY c.last_message_time DESC
LIMIT 50";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user_id', $userId);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 获取聊天记录(带分页)
public function getMessages($userId, $otherUserId, $page = 1, $perPage = 20) {
$offset = ($page - 1) * $perPage;
$query = "SELECT * FROM {$this->table}
WHERE ((sender_id = :user_id AND receiver_id = :other_id AND is_deleted_sender = 0)
OR (sender_id = :other_id2 AND receiver_id = :user_id2 AND is_deleted_receiver = 0))
ORDER BY created_at DESC
LIMIT :limit OFFSET :offset";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user_id', $userId);
$stmt->bindParam(':other_id', $otherUserId);
$stmt->bindParam(':other_id2', $otherUserId);
$stmt->bindParam(':user_id2', $userId);
$stmt->bindParam(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
// 标记为已读
$this->markAsRead($userId, $otherUserId);
return array_reverse($stmt->fetchAll(PDO::FETCH_ASSOC));
}
// 标记消息为已读
public function markAsRead($userId, $otherUserId) {
// 更新消息表
$query = "UPDATE {$this->table}
SET is_read = 1
WHERE receiver_id = :user_id
AND sender_id = :other_id
AND is_read = 0";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user_id', $userId);
$stmt->bindParam(':other_id', $otherUserId);
$stmt->execute();
// 更新会话表的未读计数
$user1 = min($userId, $otherUserId);
$user2 = max($userId, $otherUserId);
if ($userId == $user1) {
$query = "UPDATE conversations SET user1_unread_count = 0
WHERE user1_id = :user1 AND user2_id = :user2";
} else {
$query = "UPDATE conversations SET user2_unread_count = 0
WHERE user1_id = :user1 AND user2_id = :user2";
}
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user1', $user1);
$stmt->bindParam(':user2', $user2);
$stmt->execute();
}
// 删除消息(软删除)
public function deleteMessage($messageId, $userId) {
$query = "UPDATE {$this->table} SET ";
// 根据用户角色设置删除标志
$checkQuery = "SELECT sender_id, receiver_id FROM {$this->table} WHERE id = :id";
$stmt = $this->conn->prepare($checkQuery);
$stmt->bindParam(':id', $messageId);
$stmt->execute();
$message = $stmt->fetch(PDO::FETCH_ASSOC);
if ($message['sender_id'] == $userId) {
$query .= "is_deleted_sender = 1";
} else if ($message['receiver_id'] == $userId) {
$query .= "is_deleted_receiver = 1";
} else {
return ['success' => false, 'error' => '无权删除此消息'];
}
$query .= " WHERE id = :id";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':id', $messageId);
if ($stmt->execute()) {
return ['success' => true];
}
return ['success' => false];
}
// 获取未读消息数
public function getUnreadCount($userId) {
$query = "SELECT COUNT(*) as count FROM {$this->table}
WHERE receiver_id = :user_id AND is_read = 0";
$stmt = $this->conn->prepare($query);
$stmt->bindParam(':user_id', $userId);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC)['count'];
}
}
API接口
<?php
// api/send_message.php
require_once '../config/database.php';
require_once '../classes/Message.php';
require_once '../middleware/auth.php';
header('Content-Type: application/json');
$database = new Database();
$db = $database->getConnection();
$message = new Message($db);
// 验证用户身份
$userId = authenticateUser();
$data = json_decode(file_get_contents("php://input"));
if (!empty($data->receiver_id) && !empty($data->content)) {
$result = $message->sendMessage(
$userId,
$data->receiver_id,
$data->content,
$data->type ?? 'text'
);
if ($result['success']) {
http_response_code(200);
echo json_encode([
"success" => true,
"message_id" => $result['message_id'],
"message" => "消息发送成功"
]);
} else {
http_response_code(500);
echo json_encode([
"success" => false,
"message" => "消息发送失败"
]);
}
} else {
http_response_code(400);
echo json_encode(["message" => "缺少必要参数"]);
}
// api/get_conversations.php
require_once '../config/database.php';
require_once '../classes/Message.php';
require_once '../middleware/auth.php';
header('Content-Type: application/json');
$database = new Database();
$db = $database->getConnection();
$message = new Message($db);
$userId = authenticateUser();
$conversations = $message->getConversations($userId);
echo json_encode([
"success" => true,
"data" => $conversations
]);
// api/get_messages.php
require_once '../config/database.php';
require_once '../classes/Message.php';
require_once '../middleware/auth.php';
header('Content-Type: application/json');
$database = new Database();
$db = $database->getConnection();
$message = new Message($db);
$userId = authenticateUser();
$otherUserId = $_GET['user_id'] ?? 0;
$page = $_GET['page'] ?? 1;
$perPage = $_GET['per_page'] ?? 20;
if ($otherUserId > 0) {
$messages = $message->getMessages($userId, $otherUserId, $page, $perPage);
echo json_encode([
"success" => true,
"data" => $messages
]);
} else {
http_response_code(400);
echo json_encode(["message" => "缺少用户ID"]);
}
前端实现
HTML/JavaScript
<!DOCTYPE html>
<html>
<head>私信系统</title>
<style>
.messages-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.conversations-list {
float: left;
width: 30%;
border: 1px solid #ddd;
border-radius: 5px;
margin-right: 2%;
}
.messages-box {
float: left;
width: 68%;
border: 1px solid #ddd;
border-radius: 5px;
}
.message {
padding: 10px;
margin: 10px;
border-radius: 10px;
clear: both;
}
.sent {
background-color: #007bff;
color: white;
float: right;
}
.received {
background-color: #f1f0f0;
float: left;
}
.message-input {
padding: 20px;
border-top: 1px solid #ddd;
}
.message-input textarea {
width: 80%;
height: 50px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.message-input button {
width: 15%;
padding: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.unread-badge {
background-color: red;
color: white;
padding: 2px 6px;
border-radius: 50%;
font-size: 12px;
}
</style>
</head>
<body>
<div class="messages-container">
<div class="conversations-list" id="conversations-list">
<h3>消息列表</h3>
<div id="conversations"></div>
</div>
<div class="messages-box">
<div id="messages-header">
<h3 id="chat-with">请选择对话</h3>
</div>
<div id="messages" style="height: 400px; overflow-y: auto;"></div>
<div class="message-input" id="message-input" style="display: none;">
<textarea id="message-content" placeholder="输入消息..."></textarea>
<button onclick="sendMessage()">发送</button>
</div>
</div>
</div>
<script>
let currentUserId = 1; // 从服务器获取当前用户ID
let currentChatUserId = null;
let conversations = [];
let pollingInterval = null;
// 加载会话列表
async function loadConversations() {
try {
const response = await fetch('/api/get_conversations.php');
const data = await response.json();
if (data.success) {
conversations = data.data;
renderConversations();
}
} catch (error) {
console.error('加载会话失败:', error);
}
}
// 渲染会话列表
function renderConversations() {
const container = document.getElementById('conversations');
container.innerHTML = '';
conversations.forEach(conv => {
const div = document.createElement('div');
div.className = 'conversation-item';
div.style.cssText = 'padding: 10px; border-bottom: 1px solid #ddd; cursor: pointer;';
const unreadBadge = conv.unread_count > 0 ?
`<span class="unread-badge">${conv.unread_count}</span>` : '';
div.innerHTML = `
<strong>${conv.other_user_name}</strong> ${unreadBadge}
<br>
<small>${conv.last_message_content}</small>
<br>
<small style="color: #999;">${conv.last_message_time}</small>
`;
div.onclick = () => openConversation(conv);
container.appendChild(div);
});
}
// 打开对话
async function openConversation(conversation) {
// 确定与当前用户对话的对方用户ID
const otherUserId = conversation.user1_id == currentUserId ?
conversation.user2_id : conversation.user1_id;
currentChatUserId = otherUserId;
document.getElementById('chat-with').textContent =
`与 ${conversation.other_user_name} 的对话`;
document.getElementById('message-input').style.display = 'block';
await loadMessages(otherUserId);
startPolling(otherUserId);
}
// 加载消息
async function loadMessages(otherUserId) {
try {
const response = await fetch(
`/api/get_messages.php?user_id=${otherUserId}`
);
const data = await response.json();
if (data.success) {
renderMessages(data.data);
}
} catch (error) {
console.error('加载消息失败:', error);
}
}
// 渲染消息
function renderMessages(messages) {
const container = document.getElementById('messages');
container.innerHTML = '';
messages.forEach(msg => {
const div = document.createElement('div');
div.className = `message ${msg.sender_id == currentUserId ? 'sent' : 'received'}`;
div.style.maxWidth = '70%';
div.innerHTML = `
<div>${msg.content}</div>
<small style="font-size: 10px;">
${new Date(msg.created_at).toLocaleString()}
</small>
`;
container.appendChild(div);
});
// 滚动到底部
container.scrollTop = container.scrollHeight;
}
// 发送消息
async function sendMessage() {
if (!currentChatUserId) {
alert('请先选择一个对话');
return;
}
const content = document.getElementById('message-content').value;
if (!content.trim()) {
alert('请输入消息内容');
return;
}
try {
const response = await fetch('/api/send_message.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
receiver_id: currentChatUserId,
content: content
})
});
const data = await response.json();
if (data.success) {
document.getElementById('message-content').value = '';
await loadMessages(currentChatUserId);
await loadConversations();
} else {
alert('发送失败');
}
} catch (error) {
console.error('发送消息失败:', error);
}
}
// 轮询新消息
function startPolling(otherUserId) {
if (pollingInterval) {
clearInterval(pollingInterval);
}
pollingInterval = setInterval(async () => {
await loadMessages(otherUserId);
await loadConversations();
}, 5000); // 每5秒检查一次
}
// 发送消息(回车键)
document.getElementById('message-content').addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// 初始化
loadConversations();
</script>
</body>
</html>
安全考虑和优化建议
安全措施
<?php
// middleware/auth.php
function authenticateUser() {
// 实现用户认证逻辑
session_start();
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(["message" => "未授权访问"]);
exit();
}
return $_SESSION['user_id'];
}
// 输入过滤
function sanitizeInput($input) {
return htmlspecialchars(strip_tags($input));
}
优化建议
- 使用WebSocket:对于实时消息,考虑使用WebSocket替代轮询
- 消息队列:使用RabbitMQ或Redis处理大量消息
- 缓存:缓存会话列表和未读计数
- 限制频率:实现消息发送频率限制
- 文件上传:支持图片和文件需要单独处理上传逻辑
- 消息状态:添加已送达、已读等状态追踪
数据库优化索引
-- 添加更多索引 ALTER TABLE private_messages ADD INDEX idx_read_status (receiver_id, is_read); ALTER TABLE private_messages ADD INDEX idx_created (sender_id, receiver_id, created_at);
这个实现包含了私信系统的核心功能:发送、接收、显示会话列表、标记已读、删除消息等,你可以根据具体需求进行扩展,比如添加表情、图片上传、消息撤回等功能。