怎样在PHP项目中实现私信系统?

wen java案例 4

本文目录导读:

怎样在PHP项目中实现私信系统?

  1. 数据库设计
  2. PHP后端实现
  3. 前端实现
  4. 安全考虑和优化建议

我来介绍如何在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));
}

优化建议

  1. 使用WebSocket:对于实时消息,考虑使用WebSocket替代轮询
  2. 消息队列:使用RabbitMQ或Redis处理大量消息
  3. 缓存:缓存会话列表和未读计数
  4. 限制频率:实现消息发送频率限制
  5. 文件上传:支持图片和文件需要单独处理上传逻辑
  6. 消息状态:添加已送达、已读等状态追踪

数据库优化索引

-- 添加更多索引
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);

这个实现包含了私信系统的核心功能:发送、接收、显示会话列表、标记已读、删除消息等,你可以根据具体需求进行扩展,比如添加表情、图片上传、消息撤回等功能。

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