#!/bin/bash
# YFW跨平台IM插件自动化部署脚本
# 部署路径: yfw_szrengjing_com/wp-content/plugins
# 数据库配置: 名称=yfw_szrengjing_c, 用户=yfw_szrengjing_c, 密码=GAm2jPL4Dm
# 配置参数
PLUGIN_DIR="yfw_szrengjing_com/wp-content/plugins/yfw-im"
DB_NAME="yfw_szrengjing_c"
DB_USER="yfw_szrengjing_c"
DB_PASS="GAm2jPL4Dm"
WS_PORT=8080
PHP_VERSION_REQUIRED="7.4"
RATCHET_VERSION="0.4.4"
# 颜色输出函数
info() { echo -e "\033[34m[INFO] $1\033[0m"; }
success() { echo -e "\033[32m[SUCCESS] $1\033[0m"; }
error() { echo -e "\033[31m[ERROR] $1\033[0m" && exit 1; }
# 1. 环境检查
info "开始环境检查..."
# 检查PHP版本
PHP_VERSION=$(php -r "echo PHP_MAJOR_VERSION.'.'.PHP_MINOR_VERSION;")
if [[ $(echo "$PHP_VERSION < $PHP_VERSION_REQUIRED" | bc -l) -eq 1 ]]; then
error "需要PHP $PHP_VERSION_REQUIRED及以上版本,当前版本: $PHP_VERSION"
fi
# 检查Composer
if ! command -v composer &> /dev/null; then
error "未安装Composer,请先安装Composer"
fi
# 检查目录是否存在
if [ ! -d "yfw_szrengjing_com/wp-content/plugins" ]; then
error "目标路径不存在: yfw_szrengjing_com/wp-content/plugins"
fi
# 2. 创建插件目录
info "创建插件目录结构..."
mkdir -p $PLUGIN_DIR/{includes,assets/{js,css}} || error "创建目录失败"
# 3. 生成核心文件
info "生成插件文件..."
# 3.1 插件入口文件 (yfw-im.php)
cat > $PLUGIN_DIR/yfw-im.php << 'EOF'
<?php
/*
Plugin Name: YFW 跨平台IM
Description: 基于WebSocket的跨平台即时通讯插件
Version: 1.0
Author: Your Name
*/
if (!defined('ABSPATH')) exit;
// 加载核心文件
require_once plugin_dir_path(__FILE__) . 'includes/class-yfw-im.php';
require_once plugin_dir_path(__FILE__) . 'includes/db.php';
require_once plugin_dir_path(__FILE__) . 'config.php';
// 初始化插件
function yfw_im_init() {
$yfw_im = new YFW_IM();
$yfw_im->init();
}
add_action('plugins_loaded', 'yfw_im_init');
// 激活插件时创建数据库表
register_activation_hook(__FILE__, 'yfw_im_install');
function yfw_im_install() {
require_once plugin_dir_path(__FILE__) . 'includes/db.php';
yfw_im_create_tables();
}
EOF
# 3.2 数据库操作文件 (includes/db.php)
cat > $PLUGIN_DIR/includes/db.php << EOF
<?php
global \$wpdb;
\$table_prefix = \$wpdb->prefix . 'yfw_im_';
// 数据库配置(自动填充)
define('YFW_IM_DB_NAME', '$DB_NAME');
define('YFW_IM_DB_USER', '$DB_USER');
define('YFW_IM_DB_PASS', '$DB_PASS');
// 创建消息表和会话表
function yfw_im_create_tables() {
global \$wpdb, \$table_prefix;
\$charset_collate = \$wpdb->get_charset_collate();
// 会话表
\$sql = "CREATE TABLE IF NOT EXISTS {\$table_prefix}conversations (
id INT AUTO_INCREMENT PRIMARY KEY,
user1_id BIGINT NOT NULL,
user2_id BIGINT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY user_pair (user1_id, user2_id)
) \$charset_collate;";
// 消息表
\$sql .= "CREATE TABLE IF NOT EXISTS {\$table_prefix}messages (
id INT AUTO_INCREMENT PRIMARY KEY,
conversation_id INT NOT NULL,
sender_id BIGINT NOT NULL,
content TEXT NOT NULL,
type ENUM('text', 'image', 'file') DEFAULT 'text',
status ENUM('sent', 'delivered', 'read') DEFAULT 'sent',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (conversation_id) REFERENCES {\$table_prefix}conversations(id) ON DELETE CASCADE
) \$charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta(\$sql);
}
// 存储消息
function yfw_im_save_message(\$conversation_id, \$sender_id, \$content, \$type = 'text') {
global \$wpdb, \$table_prefix;
return \$wpdb->insert(
"{\$table_prefix}messages",
[
'conversation_id' => \$conversation_id,
'sender_id' => \$sender_id,
'content' => \$content,
'type' => \$type
]
);
}
EOF
# 3.3 WebSocket服务端 (includes/websocket.php)
cat > $PLUGIN_DIR/includes/websocket.php << 'EOF'
<?php
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
require_once plugin_dir_path(__FILE__) . '../../vendor/autoload.php';
class YFW_IM_Socket implements MessageComponentInterface {
protected $clients;
protected $userConnections; // 用户ID与连接的映射
public function __construct() {
$this->clients = new \SplObjectStorage;
$this->userConnections = [];
}
// 新连接建立
public function onOpen(ConnectionInterface $conn) {
$this->clients->attach($conn);
error_log("新连接: {$conn->resourceId}");
}
// 接收消息
public function onMessage(ConnectionInterface $from, $msg) {
$data = json_decode($msg, true);
if (!$data) return;
// 处理用户认证(首次连接时)
if ($data['action'] === 'auth') {
$user_id = $data['user_id'];
$this->userConnections[$user_id] = $from;
$from->send(json_encode(['status' => 'success', 'msg' => '认证成功']));
return;
}
// 处理消息发送
if ($data['action'] === 'send_message') {
$this->handleMessage($data);
}
}
// 处理消息转发
private function handleMessage($data) {
$sender_id = $data['sender_id'];
$receiver_id = $data['receiver_id'];
$content = $data['content'];
// 1. 查找或创建会话
$conversation_id = $this->getOrCreateConversation($sender_id, $receiver_id);
// 2. 保存消息到数据库
yfw_im_save_message($conversation_id, $sender_id, $content);
// 3. 转发消息给接收者
if (isset($this->userConnections[$receiver_id])) {
$receiverConn = $this->userConnections[$receiver_id];
$receiverConn->send(json_encode([
'action' => 'new_message',
'sender_id' => $sender_id,
'content' => $content,
'time' => date('Y-m-d H:i:s')
]));
}
}
// 获取或创建会话
private function getOrCreateConversation($user1, $user2) {
global $wpdb, $table_prefix;
$min = min($user1, $user2);
$max = max($user1, $user2);
$conv = $wpdb->get_row(
$wpdb->prepare(
"SELECT id FROM {$table_prefix}conversations WHERE user1_id = %d AND user2_id = %d",
$min, $max
)
);
if ($conv) return $conv->id;
// 创建新会话
$wpdb->insert(
"{$table_prefix}conversations",
['user1_id' => $min, 'user2_id' => $max]
);
return $wpdb->insert_id;
}
public function onClose(ConnectionInterface $conn) {
$this->clients->detach($conn);
// 移除用户连接映射
$this->userConnections = array_filter($this->userConnections, function($c) use ($conn) {
return $c !== $conn;
});
error_log("连接关闭: {$conn->resourceId}");
}
public function onError(ConnectionInterface $conn, \Exception $e) {
error_log("错误: {$e->getMessage()}");
$conn->close();
}
}
// 启动WebSocket服务(需通过命令行运行)
if (php_sapi_name() === 'cli') {
$server = IoServer::factory(
new HttpServer(
new WsServer(
new YFW_IM_Socket()
)
),
8080 // 端口号
);
$server->run();
}
EOF
# 3.4 核心类文件 (includes/class-yfw-im.php)
cat > $PLUGIN_DIR/includes/class-yfw-im.php << 'EOF'
<?php
class YFW_IM {
public function init() {
// 注册AJAX接口(加载历史消息)
add_action('wp_ajax_yfw_im_load_messages', [$this, 'load_messages']);
add_action('wp_ajax_nopriv_yfw_im_load_messages', [$this, 'load_messages_nopriv']);
}
// 加载历史消息(登录用户)
public function load_messages() {
check_ajax_referer('yfw_im_nonce', 'nonce');
$current_user = wp_get_current_user();
if (!$current_user->ID) {
wp_send_json_error('未登录');
}
$receiver_id = intval($_POST['receiver_id']);
$messages = $this->get_message_history($current_user->ID, $receiver_id);
wp_send_json_success(['messages' => $messages]);
}
// 未登录用户处理
public function load_messages_nopriv() {
wp_send_json_error('请先登录');
}
// 获取消息历史
private function get_message_history($user1, $user2) {
global $wpdb, $table_prefix;
$min = min($user1, $user2);
$max = max($user1, $user2);
$conv = $wpdb->get_row(
$wpdb->prepare(
"SELECT id FROM {$table_prefix}conversations WHERE user1_id = %d AND user2_id = %d",
$min, $max
)
);
if (!$conv) return [];
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$table_prefix}messages WHERE conversation_id = %d ORDER BY created_at ASC",
$conv->id
),
ARRAY_A
);
}
}
EOF
# 3.5 前端WebSocket客户端 (assets/js/socket.js)
cat > $PLUGIN_DIR/assets/js/socket.js << 'EOF'
class YFW_IM_Client {
constructor() {
this.socket = null;
this.userId = yfw_im_config.user_id;
this.connect();
}
// 连接WebSocket服务
connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = yfw_im_config.ws_host || `${protocol}//${window.location.hostname}:8080`;
this.socket = new WebSocket(host);
this.socket.onopen = () => {
console.log('WebSocket连接成功');
this.auth();
};
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleMessage(data);
};
this.socket.onerror = (error) => {
console.error('WebSocket错误:', error);
};
this.socket.onclose = () => {
console.log('连接关闭,尝试重连...');
setTimeout(() => this.connect(), 3000);
};
}
// 认证用户
auth() {
this.send({
action: 'auth',
user_id: this.userId
});
}
// 发送消息
sendMessage(receiverId, content) {
this.send({
action: 'send_message',
sender_id: this.userId,
receiver_id: receiverId,
content: content
});
}
// 通用发送方法
send(data) {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
} else {
console.error('连接未就绪');
}
}
// 处理接收的消息
handleMessage(data) {
if (data.action === 'new_message') {
window.dispatchEvent(new CustomEvent('yfw_im_new_message', { detail: data }));
}
}
}
EOF
# 3.6 前端聊天界面逻辑 (assets/js/frontend.js)
cat > $PLUGIN_DIR/assets/js/frontend.js << 'EOF'
// 初始化聊天客户端
document.addEventListener('DOMContentLoaded', () => {
const imClient = new YFW_IM_Client();
const chatWindow = document.getElementById('yfw-im-chat');
const messageInput = document.getElementById('yfw-im-input');
const sendBtn = document.getElementById('yfw-im-send');
let currentReceiver = null;
// 设置当前聊天对象
document.querySelectorAll('.yfw-im-contact').forEach(contact => {
contact.addEventListener('click', () => {
currentReceiver = contact.dataset.userId;
chatWindow.innerHTML = '';
loadMessages(currentReceiver);
});
});
// 发送消息
sendBtn.addEventListener('click', () => {
const content = messageInput.value.trim();
if (content && currentReceiver) {
imClient.sendMessage(currentReceiver, content);
addMessageToUI(content, 'outgoing', yfw_im_config.user_id);
messageInput.value = '';
}
});
// 回车发送
messageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendBtn.click();
});
// 接收新消息
window.addEventListener('yfw_im_new_message', (e) => {
const data = e.detail;
addMessageToUI(data.content, 'incoming', data.sender_id);
});
// 加载历史消息
function loadMessages(receiverId) {
fetch(yfw_im_config.ajax_url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
action: 'yfw_im_load_messages',
receiver_id: receiverId,
nonce: yfw_im_config.nonce
})
})
.then(res => res.json())
.then(data => {
if (data.success) {
data.data.messages.forEach(msg => {
const type = msg.sender_id == yfw_im_config.user_id ? 'outgoing' : 'incoming';
addMessageToUI(msg.content, type, msg.sender_id);
});
}
});
}
// 添加消息到界面
function addMessageToUI(content, type, senderId) {
const msgEl = document.createElement('div');
msgEl.className = `yfw-im-message ${type}`;
msgEl.innerHTML = `<div class="content">${content}</div>`;
chatWindow.appendChild(msgEl);
chatWindow.scrollTop = chatWindow.scrollHeight;
}
});
EOF
# 3.7 样式文件 (assets/css/style.css)
cat > $PLUGIN_DIR/assets/css/style.css << 'EOF'
.yfw-im-chat {
width: 400px;
height: 500px;
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.yfw-im-messages {
flex: 1;
padding: 10px;
overflow-y: auto;
background-color: #f9f9f9;
}
.yfw-im-message {
margin: 5px 0;
max-width: 70%;
padding: 8px 12px;
border-radius: 18px;
clear: both;
}
.yfw-im-message.incoming {
background-color: #e9e9eb;
float: left;
}
.yfw-im-message.outgoing {
background-color: #0078d7;
color: white;
float: right;
}
.yfw-im-input-area {
display: flex;
padding: 10px;
border-top: 1px solid #ccc;
}
#yfw-im-input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 20px;
outline: none;
}
#yfw-im-send {
margin-left: 10px;
padding: 8px 16px;
background-color: #0078d7;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
}
.yfw-im-contacts {
padding: 10px;
border-bottom: 1px solid #ccc;
}
.yfw-im-contact {
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
margin-bottom: 5px;
}
.yfw-im-contact:hover {
background-color: #f0f0f0;
}
EOF
# 3.8 配置文件 (config.php)
cat > $PLUGIN_DIR/config.php << 'EOF'
<?php
// 跨平台配置
define('YFW_IM_WS_HOST', 'ws://' . $_SERVER['HTTP_HOST'] . ':8080');
define('YFW_IM_ALLOWED_ORIGINS', [
'https://yfw_szrengjing_com',
'http://yfw_szrengjing_com',
'http://localhost:3000'
]);
// 前端配置(通过WP本地化脚本传递)
function yfw_im_enqueue_scripts() {
wp_enqueue_script('yfw-im-socket', plugins_url('assets/js/socket.js', dirname(__FILE__)), [], '1.0', true);
wp_enqueue_script('yfw-im-frontend', plugins_url('assets/js/frontend.js', dirname(__FILE__)), ['yfw-im-socket'], '1.0', true);
wp_enqueue_style('yfw-im-style', plugins_url('assets/css/style.css', dirname(__FILE__)));
// 传递配置到前端
wp_localize_script('yfw-im-frontend', 'yfw_im_config', [
'user_id' => get_current_user_id(),
'ws_host' => YFW_IM_WS_HOST,
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('yfw_im_nonce')
]);
}
add_action('wp_enqueue_scripts', 'yfw_im_enqueue_scripts');
EOF
# 4. 安装依赖
info "安装WebSocket依赖..."
cd $PLUGIN_DIR || error "进入插件目录失败"
composer require cboden/ratchet:$RATCHET_VERSION > /dev/null 2>&1 || error "安装Ratchet失败"
# 5. 数据库初始化(通过WordPress CLI触发,若没有WP-CLI则提示手动激活)
info "初始化数据库..."
if command -v wp &> /dev/null; then
# 若安装了WP-CLI,直接触发激活钩子
wp plugin activate yfw-im > /dev/null 2>&1
success "数据库表创建成功"
else
info "未检测到WP-CLI,请在WordPress后台手动激活插件以创建数据库表"
fi
# 6. 配置WebSocket服务自启动(使用systemd)
info "配置WebSocket服务自启动..."
SERVICE_FILE="/etc/systemd/system/yfw-im-websocket.service"
if [ -w "/etc/systemd/system/" ]; then
cat > $SERVICE_FILE << EOF
[Unit]
Description=YFW IM WebSocket Service
After=network.target
[Service]
User=$(whoami)
WorkingDirectory=$PLUGIN_DIR/includes
ExecStart=$(which php) websocket.php
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
EOF
# 启动服务
systemctl daemon-reload
systemctl start yfw-im-websocket
systemctl enable yfw-im-websocket > /dev/null 2>&1
success "WebSocket服务已启动并设置开机自启"
else
info "无权限配置systemd,手动启动命令: php $PLUGIN_DIR/includes/websocket.php"
fi
# 7. 完成部署
success "部署完成!"
echo "======================================"
echo "插件路径: $PLUGIN_DIR"
echo "WebSocket端口: $WS_PORT"
echo "使用说明:"
echo "1. 在WordPress后台激活 'YFW 跨平台IM' 插件"
echo "2. 在需要显示聊天的页面添加聊天组件(可通过短代码实现)"
echo "3. 确保服务器开放$WS_PORT端口的防火墙权限"
echo "======================================" [root@yfw ~]# cd /www/wwwroot/yfw_szrengjing_com/wp-content/plugins
[root@yfw plugins]# ./deploy_yfw_im.sh
[INFO] 开始环境检查...
[ERROR] 目标路径不存在: yfw_szrengjing_com/wp-content/plugins
[root@yfw plugins]#