#!/bin/bash
#============================================
# YFW 跨平台 IM 插件 —— 一键自动化部署脚本(修复版)
# 修复:PHP 代码中 $ 变量未正确转义导致的 Fatal Error
#============================================
# 颜色输出函数
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; }
warn() { echo -e "\033[33m[WARN] $1\033[0m"; }
#===============================
# 🔧 配置参数
#===============================
PLUGIN_DIR="/www/wwwroot/yfw_szrengjing_com/wp-content/plugins/yfw-im"
WORDPRESS_ROOT="/www/wwwroot/yfw_szrengjing_com"
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"
SERVICE_NAME="yfw-im-websocket"
#===============================
# 1. 环境检查(保持原样)
#===============================
info "开始环境检查..."
if ! command -v php &> /dev/null; then
error "PHP 未安装,请先安装 PHP >= $PHP_VERSION_REQUIRED"
fi
PHP_VERSION=$(php -r "echo PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION;")
if (( $(echo "$PHP_VERSION < $PHP_VERSION_REQUIRED" | bc -l) )); then
error "PHP 版本过低!需要 $PHP_VERSION_REQUIRED+,当前: $PHP_VERSION"
fi
if ! command -v composer &> /dev/null; then
warn "Composer 未安装,尝试自动安装..."
curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer || \
error "Composer 安装失败,请手动安装后再试。"
fi
if [ ! -d "$WORDPRESS_ROOT/wp-content/plugins" ]; then
error "WordPress 插件目录不存在: $WORDPRESS_ROOT/wp-content/plugins"
fi
if ! command -v bc &> /dev/null; then
warn "未安装 'bc' 工具,正在尝试安装..."
if command -v apt &> /dev/null; then
sudo apt update && sudo apt install -y bc || error "无法安装 bc"
elif command -v yum &> /dev/null; then
sudo yum install -y bc || error "无法安装 bc"
else
error "请手动安装 bc 工具(用于数值比较)"
fi
fi
#===============================
# 2. 创建目录结构
#===============================
info "创建插件目录结构..."
mkdir -p "$PLUGIN_DIR"/{includes,assets/{js,css}} || error "创建目录失败"
cd "$PLUGIN_DIR" || error "无法进入插件目录"
#===============================
# 3. 生成核心文件(⚠️ 关键修复区)
#===============================
info "生成插件核心文件..."
# 3.1 主插件文件 yfw-im.php
cat > "$PLUGIN_DIR/yfw-im.php" << 'EOF'
<?php
/**
* Plugin Name: YFW 跨平台IM
* Description: 基于WebSocket的即时通讯系统 | 支持文本/图片/文件 | 单聊 | 已读回执
* Version: 1.1
* Author: Dev Team
*/
if (!defined('ABSPATH')) exit;
define('YFW_IM_PLUGIN_DIR', plugin_dir_path(__FILE__));
// 加载组件
require_once YFW_IM_PLUGIN_DIR . 'includes/class-yfw-im.php';
require_once YFW_IM_PLUGIN_DIR . 'includes/db.php';
require_once YFW_IM_PLUGIN_DIR . '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 YFW_IM_PLUGIN_DIR . '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_';
// 注意:这些常量目前只是定义,数据库连接由 WordPress 处理
define('YFW_IM_DB_NAME', 'yfw_szrengjing_c'); // 不建议在此硬编码,WordPress 自动处理 DB
define('YFW_IM_DB_USER', 'yfw_szrengjing_c');
define('YFW_IM_DB_PASS', 'GAm2jPL4Dm');
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',
read_at DATETIME NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_conversation (conversation_id),
INDEX idx_status (status),
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,
'status' => 'sent'
],
['%d', '%d', '%s', '%s', '%s']
);
}
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 __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/db.php';
class YFW_IM_Socket implements MessageComponentInterface {
protected $clients;
protected $userConnections;
public function __construct() {
$this->clients = new \SplObjectStorage();
$this->userConnections = [];
}
public function onOpen(ConnectionInterface $conn) {
$this->clients->attach($conn);
error_log("[WS] 新连接: RID={$conn->resourceId}");
}
public function onMessage(ConnectionInterface $from, $msg) {
$data = json_decode($msg, true);
if (!$data) return;
switch ($data['action']) {
case 'auth':
$user_id = intval($data['user_id']);
$this->userConnections[$user_id] = $from;
$from->send(json_encode(['status' => 'success', 'msg' => '认证成功']));
break;
case 'send_message':
$this->handleMessage($data);
break;
case 'message_read':
$this->markAsRead($data['message_id'], $data['receiver_id']);
break;
case 'ping':
$from->send(json_encode(['action' => 'pong']));
break;
}
}
private function handleMessage($data) {
$sender_id = intval($data['sender_id']);
$receiver_id = intval($data['receiver_id']);
$content = sanitize_text_field($data['content']);
$type = in_array($data['type'], ['text','image','file']) ? $data['type'] : 'text';
$conversation_id = $this->getOrCreateConversation($sender_id, $receiver_id);
yfw_im_save_message($conversation_id, $sender_id, $content, $type);
if (isset($this->userConnections[$receiver_id])) {
$receiverConn = $this->userConnections[$receiver_id];
$receiverConn->send(json_encode([
'action' => 'new_message',
'sender_id' => $sender_id,
'content' => $content,
'type' => $type,
'time' => current_time('mysql')
]));
}
}
private function getOrCreateConversation($u1, $u2) {
global $wpdb, $table_prefix;
$min = min($u1, $u2);
$max = max($u1, $u2);
$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;
}
private function markAsRead($msgId, $receiverId) {
global $wpdb, $table_prefix;
$wpdb->update(
"{$table_prefix}messages",
['status' => 'read', 'read_at' => current_time('mysql')],
['id' => $msgId],
['%s', '%s'],
['%d']
);
}
public function onClose(ConnectionInterface $conn) {
$this->clients->detach($conn);
foreach ($this->userConnections as $uid => $c) {
if ($c === $conn) unset($this->userConnections[$uid]);
}
error_log("[WS] 连接关闭: RID={$conn->resourceId}");
}
public function onError(ConnectionInterface $conn, \Exception $e) {
error_log("[WS] 错误: " . $e->getMessage());
$conn->close();
}
}
// CLI 启动服务
if (php_sapi_name() === 'cli') {
$server = IoServer::factory(
new HttpServer(new WsServer(new YFW_IM_Socket())),
8080
);
$server->run();
}
EOF
# 3.4 核心类 class-yfw-im.php
cat > "$PLUGIN_DIR/includes/class-yfw-im.php" << 'EOF'
<?php
class YFW_IM {
public function init() {
add_action('wp_ajax_yfw_im_load_messages', [$this, 'load_messages']);
add_action('wp_ajax_nopriv_yfw_im_load_messages', [$this, 'load_messages_nopriv']);
add_action('wp_ajax_yfw_im_upload_file', [$this, 'upload_file']);
add_action('wp_ajax_nopriv_yfw_im_upload_file', [$this, 'upload_file']);
add_shortcode('yfw_im_chat', [$this, 'render_chat_html']);
}
public function load_messages() {
check_ajax_referer('yfw_im_nonce', 'nonce');
$user = wp_get_current_user();
if (!$user->ID) wp_send_json_error('未登录');
$receiver_id = intval($_POST['receiver_id']);
$msgs = $this->get_message_history($user->ID, $receiver_id);
wp_send_json_success(['messages' => $msgs]);
}
public function load_messages_nopriv() {
wp_send_json_error('请登录后查看');
}
private function get_message_history($u1, $u2) {
global $wpdb, $table_prefix;
$min = min($u1, $u2);
$max = max($u1, $u2);
$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);
}
public function upload_file() {
check_ajax_referer('yfw_im_nonce', 'nonce');
$user = wp_get_current_user();
if (!$user->ID) wp_send_json_error('未登录');
if (empty($_FILES['file'])) wp_send_json_error('无文件上传');
$file = $_FILES['file'];
$upload = wp_handle_upload($file, ['test_form' => false]);
if (isset($upload['error'])) {
wp_send_json_error($upload['error']);
}
$ext = strtolower(pathinfo($upload['file'], PATHINFO_EXTENSION));
$type = in_array($ext, ['jpg','jpeg','png','gif']) ? 'image' : 'file';
wp_send_json_success([
'url' => $upload['url'],
'type' => $type
]);
}
public function render_chat_html() {
ob_start();
?>
<div class="yfw-im-container">
<div class="yfw-im-contacts">
<div class="yfw-im-contact" data-user-id="2">客服小张</div>
<div class="yfw-im-contact" data-user-id="3">技术小李</div>
</div>
<div class="yfw-im-chat">
<div class="yfw-im-messages" id="yfw-im-chat"></div>
<div class="yfw-im-input-area">
<input type="text" id="yfw-im-input" placeholder="输入消息..." />
<input type="file" id="yfw-im-file" style="display:none;" accept="image/*,.pdf,.doc,.zip" />
<button id="yfw-im-upload">📎</button>
<button id="yfw-im-send">发送</button>
</div>
</div>
</div>
<?php
return ob_get_clean();
}
}
EOF
# 3.5 前端客户端 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();
}
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();
setInterval(() => this.ping(), 30000); // 心跳
};
this.socket.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleMessage(data);
};
this.socket.onerror = (err) => console.error('WebSocket 错误:', err);
this.socket.onclose = () => {
console.log('连接断开,3秒后重连...');
setTimeout(() => this.connect(), 3000);
};
}
auth() {
this.send({ action: 'auth', user_id: this.userId });
}
sendMessage(receiverId, content, type = 'text') {
this.send({
action: 'send_message',
sender_id: this.userId,
receiver_id: receiverId,
content: content,
type: type
});
}
send(data) {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify(data));
}
}
ping() {
this.send({ action: 'ping' });
}
handleMessage(data) {
if (data.action === 'new_message') {
window.dispatchEvent(new CustomEvent('yfw_im_new_message', { detail: data }));
}
}
}
EOF
# 3.6 前端逻辑 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');
const fileInput = document.getElementById('yfw-im-file');
const uploadBtn = document.getElementById('yfw-im-upload');
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, 'text');
addMessageToUI(content, 'outgoing', yfw_im_config.user_id, 'text');
messageInput.value = '';
}
});
messageInput.addEventListener('keypress', e => {
if (e.key === 'Enter') sendBtn.click();
});
uploadBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', () => {
const file = fileInput.files[0];
if (!file || !currentReceiver) return;
const formData = new FormData();
formData.append('action', 'yfw_im_upload_file');
formData.append('nonce', yfw_im_config.nonce);
formData.append('file', file);
fetch(yfw_im_config.ajax_url, { method: 'POST', body: formData })
.then(res => res.json())
.then(data => {
if (data.success) {
imClient.sendMessage(currentReceiver, data.data.url, data.data.type);
addMessageToUI(data.data.url, 'outgoing', yfw_im_config.user_id, data.data.type);
}
});
fileInput.value = '';
});
window.addEventListener('yfw_im_new_message', e => {
const data = e.detail;
addMessageToUI(data.content, 'incoming', data.sender_id, data.type);
});
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: receiver_id,
nonce: yfw_im_config.nonce
})
}).then(r => r.json()).then(d => {
if (d.success) d.data.messages.forEach(m => {
const type = m.sender_id == yfw_im_config.user_id ? 'outgoing' : 'incoming';
addMessageToUI(m.content, type, m.sender_id, m.type);
});
});
}
function addMessageToUI(content, type, senderId, msgType) {
const msgEl = document.createElement('div');
msgEl.className = `yfw-im-message ${type}`;
let innerHTML = '';
if (msgType === 'image') {
innerHTML = `<img src="${content}" onclick="window.open('${content}')" />`;
} else if (msgType === 'file') {
const name = content.split('/').pop();
innerHTML = `<a href="${content}" target="_blank">${name}</a>`;
} else {
innerHTML = `<div class="content">\${DOMPurify?.sanitize(content) || content}</div>`;
}
msgEl.innerHTML = innerHTML;
chatWindow.appendChild(msgEl);
chatWindow.scrollTop = chatWindow.scrollHeight;
}
});
EOF
# 3.7 样式文件 style.css
cat > "$PLUGIN_DIR/assets/css/style.css" << 'EOF'
.yfw-im-container { width: 400px; margin: 20px auto; font-family: Arial, sans-serif; }
.yfw-im-contacts { padding: 10px; background: #f0f0f0; border-radius: 8px; margin-bottom: 10px; }
.yfw-im-contact { padding: 8px; cursor: pointer; border-radius: 4px; }
.yfw-im-contact:hover { background: #ddd; }
.yfw-im-chat { height: 500px; border: 1px solid #ccc; border-radius: 8px; display: flex; flex-direction: column; }
.yfw-im-messages { flex: 1; overflow-y: auto; padding: 10px; background: #f9f9f9; }
.yfw-im-message { max-width: 70%; margin: 5px 0; padding: 8px 12px; border-radius: 18px; clear: both; }
.yfw-im-message img { max-width: 200px; border-radius: 8px; cursor: zoom-in; }
.yfw-im-message.incoming { background: #e9e9eb; float: left; }
.yfw-im-message.outgoing { background: #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; outline: none; border-radius: 20px; }
#yfw-im-upload, #yfw-im-send { margin-left: 8px; padding: 8px 12px; border: none; border-radius: 20px; cursor: pointer; }
#yfw-im-upload { background: #eee; }
#yfw-im-send { background: #0078d7; color: white; }
EOF
# 3.8 配置文件 config.php
cat > "$PLUGIN_DIR/config.php" << 'EOF'
<?php
define('YFW_IM_WS_HOST', 'ws://' . $_SERVER['HTTP_HOST'] . ':8080');
function yfw_im_enqueue_scripts() {
if (!is_admin()) {
wp_enqueue_script('dompurify', 'https://cdn.jsdelivr.net/npm/dompurify@2.4.7/dist/purify.min.js', [], '2.4.7', true);
wp_enqueue_script('yfw-im-socket', plugins_url('assets/js/socket.js', __FILE__), [], '1.1', true);
wp_enqueue_script('yfw-im-frontend', plugins_url('assets/js/frontend.js', __FILE__), ['yfw-im-socket'], '1.1', true);
wp_enqueue_style('yfw-im-style', plugins_url('assets/css/style.css', __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. 安装 Composer 依赖
#===============================
info "安装 Ratchet WebSocket 依赖..."
cd "$PLUGIN_DIR" || error "无法进入插件目录"
composer require cboden/ratchet:$RATCHET_VERSION --no-interaction --quiet || error "Composer 安装失败"
#===============================
# 5. 初始化数据库
#===============================
info "初始化数据库表..."
if command -v wp &> /dev/null; then
cd "$WORDPRESS_ROOT" && wp plugin activate yfw-im --quiet
success "插件已激活,数据表创建完成"
else
warn "WP-CLI 未安装,需手动在 WordPress 后台激活插件以创建表"
fi
#===============================
# 6. 配置 systemd 服务
#===============================
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME.service"
CURRENT_USER=$(whoami)
if [ -w "/etc/systemd/system/" ]; then
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=YFW IM WebSocket Service
After=network.target
[Service]
User=$CURRENT_USER
Group=$CURRENT_USER
WorkingDirectory=$PLUGIN_DIR/includes
ExecStart=/usr/bin/php websocket.php
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable "$SERVICE_NAME" --quiet
sudo systemctl start "$SERVICE_NAME"
success "WebSocket 服务已启用并启动"
else
warn "无权限写入 /etc/systemd/system/,请使用 root 运行或手动配置服务"
info "启动命令: php $PLUGIN_DIR/includes/websocket.php"
fi
#===============================
# 7. 完成提示
#===============================
success "✅ YFW IM 插件部署完成!"
echo "=================================================="
echo "📌 使用方式:"
echo "1. 登录 WordPress 后台 → 插件 → 激活 'YFW 跨平台IM'"
echo "2. 在文章/页面中插入短代码: [yfw_im_chat]"
echo "3. 确保防火墙开放端口: $WS_PORT"
echo " firewall-cmd --add-port=${WS_PORT}/tcp --permanent && firewall-cmd --reload"
echo "4. 生产环境建议配置 Nginx 反向代理以启用 WSS"
echo "5. 查看日志: journalctl -u $SERVICE_NAME -f"
echo "=================================================="生成完整版前后端 sh前后端源文件自动化配置 启用就可用