上传文件plugin:upload_column 比 file_column强得多!

本文详细介绍了UploadColumn插件的功能特点,包括自定义错误消息、图片处理接口、文件名定制等,并对比了与file_column的区别。

优于file_column是显而易见的。

本人发现的几处:

1、验证出错时,upload_column提供了自定义错误消息接口。file_column的消息是固定英文的,虽说可以改其源文件,但直接改plugin总是别扭些,也影响update。

2、upload_column提供了更全面的图片处理接口。不仅可以选择处理器,而且对于图片的处理也提供了单独调用处理器的方式。

  如:使用file_column时,我们在对图片进行缩放时,只能使用其提供的简单缩放模式,如果想调用magick对图片实现更多效果的图片处理,就没有这种接口了。

  而使用upload_column就不一样了,可以随意的调用图片处理器来对图片进行处理。

 

  upload_column :uploaded_data, :versions => [ :thumb ], :store_dir => proc{|record, file| "attachment/#{record.id}"}

  def uploaded_data_after_assign
    uploaded_data.thumb.process! do |img|
      img.resize_to_fill(200, 150) # javaeye的图片缩放就是用的resize_to_fill方式
    end
  end

 3、file_column的验证有问题。当对图片进行了缩放后,validates_filesize_of竟然是对缩放后的原图进行文件大小进行判断,而实际上我想要的是对原文件大小进行判断。

4、upload_column提供了文件名接口,可以把文件名改为自己所需要的名称。如可以使用uuid生成文件名。

  uuid_filename = UUID.new.generate
  upload_column :logo, :filename => proc{|inst, orig, ext| "#{uuid_filename}.#{ext}"}, :versions => [ :thumb ]

 5、upload_column的versions接口真棒。不再象file_column那样固定几个图片处理模式。upload_column可以任意定义自己的特别处理模式。只要加几个版本名,然后写个回调,就可以随心所欲的把图片处理成各种自定义的版本。

6、upload_column生成的版本文件名的样式是:XXXXX-version.ext,如4fe2e341-3bce-012c-5908-0016d32d937e-thumb.jpg,而不是象file_column会生成thumb这样的版本文件夹。

 

以上六个优点足以让我选择upload_column,而放弃file_column。

 

upload_column下载及说明:http://github.com/toy/upload_column/tree/master

 

下文再转载网上一篇翻译自upload_column的readme的文章。

原文地址:http://viviworld.blogbus.com/logs/28505967.html

作者:张伟

写道
UploadColumn - [翻译]
Tag:翻译
张伟点评:
这是UploadColumn 插件自带的README,我看了N遍,总是发现上次会遗漏某些东东。老外讲个东西,向来不是按照一二三四、常常泛泛而谈,看他们写的书,更是这样!中国人习惯了例子。
----------------------------------------------------
= UploadColumn

UploadColumn 是专为Ruby on Rails 框架打造的插件,支持上传文件、特别是图片。
假如你有一个用户列表,想给每个人关联一个图片。你应该把图片上传到数据库,或者借助 UploadColumn 在文件系统中实现简单存储。
如果一个名为 User 的 model 有一列'picture',类型为String,你只需在该 model 增加:

class User < ActiveRecord::Base
upload_column :picture
end

好了!你可以试着上传文件啦。当然, UploadColumn 有很多不同选项,供你自定义上传方式。
没有用户接口的文件上传可不好玩,做出如下设置:
在表单里增加一个 upload_column_field ,比如:

<p><label for="user_picture">Picture</label><br/>
<%= upload_column_field 'user', 'picture' %></p>
你应该用 upload_column_field 代替Rails的 file_field,以便在诸如验证失败导致表单重新呈现时,它仍然管用。不幸的是,file_field 不支持上述情形。

现在还有一点问题,由于它发送的是文件而非字符串。大可不必担心,如果我们设置表单的 encoding 为 multipart ,它就OK了。UploadColumn 甚至提供了一些好的 helper 来回避丑陋的 multipart 语法。形如:

<%= upload_form_tag( :action => 'create' ) %>

好了!你的上传已经做好了(但愿如此),你现在应该能够给用户增加图片了。疯狂的地方当然还在后面!

==存储路径

你并不想总是把图片存储在 upload_column 默认的路径,这不是问题,改变目录简直不值得一提。你给 upload_column 传一个 :store_dir 键,就会覆写默认机制,而启用该目录。

upload_column :picture, :store_dir => "pictures"

这完全符合当前情景。注意,所有文件将被存放于相同的目录。

如果需要对路径进行修改(或许按照关联id存储),请用 proc 。proc看起来如下所示:

upload_column :picture, :store_dir => proc{|record, file| "images/#{record.category.name}/#{record.id}/#{file.extension}"}

proc 被传给两个参数,第一个是模型类的当前实例,第二个是属性名称(这里,attr指 :picture)。

同样,你也可以修改 :tmp_dir 。

== 文件名

UploadColumn 默认将保留文件原始名称,有时显得不太方便。你可以给 upload_column 声明传递一个 :filename 指令:

upload_column :picture, :filename => "donkey.png"

这样所有文件将被命名为 donkey.png。如果文件是jpeg格式的就不是我们所期望的了。通常传一个Proc 给 :filename 比较明智。

upload_column :picture, :filename => proc{|record, file| "avatar#{record.id}.#{file.extension}"}

该 Proc 被传两个参数,当前实例和文件本身。

== Manipulators

UploadColumn 允许你通过 manipulators 操作文件,按某种格式转换文件,或实现任何种类的操作。目前绑定了两个 manipulators, RMagick manipulator 和 ImageScience manipulator,不过,自己写也很容易。网站上有进一步的说明。

== 用 RMagick 操作图片

比如你想(无论什么原因)让用户图片有一个时髦的曝光效果。upload_column 操作图片既可以在运行时、也可以在图片保存之后进行,有如下可能:

class User < ActiveRecord::Base
upload_column :picture, :manipulator => UploadColumn::Manipulators::RMagick

def picture_after_assign

picture.process! do |img|
img.solarize
end

end
end
你也能使用 :process 指令,当一个新图片被上传后,它会自动执行操作。如果你想把图片缩放到最大值为 800 * 600 的规格,可以这样做:

class User < ActiveRecord::Base
upload_column :picture, :process => '800x600', :manipulator => UploadColumn::Manipulators::RMagick
end

先前曝光的例子可以简写为:

class User < ActiveRecord::Base
upload_column :picture, :process => proc{|img| img.solarize }, :manipulator => UploadColumn::Manipulators::RMagick
end

或者我们想要图片的不同版本,就这样指定:

class User < ActiveRecord::Base
upload_column :picture, :versions => [ :solarized, :sepiatoned ], :manipulator => UploadColumn::Manipulators::RMagick

def picture_after_assign
picture.solarized.process! do |img|
img.solarize
end
picture.sepiatoned.process! do |img|
img.sepiatone
end
end
end

也可以为版本指定hash ,并传一个 dimension 或 proc:

class User < ActiveRecord::Base
upload_column :picture, :versions => { :thumb => "c100x100", :large => "200x300", :sepiatoned => proc{ |img| img.sepiatone } }, :manipulator => UploadColumn::Manipulators::RMagick
end

注意,thumb 的尺寸前的‘c’,它将把图片裁剪到精确尺寸。虽然有点啰嗦,它并不会检查文件到底是不是图片。曝光最新的 GreenDay 歌曲多少有些不太好听。鉴于此,UploadColumn 提供了 image_column 函数:

class User < ActiveRecord::Base
image_column :picture, :versions => { :thumb => "c100x100", :large => "200x300", :sepiatoned => proc{ |img| img.sepiatone } }
end

这也把图片放在了 public/images 而不是 public,多么整洁!

== 运行时渲染

你能在运行时操作图片(它非常耗性能)。在你的 controller 增加一个 action ,使用 UploadColumnRenderHelper.render_image.

def sepiatone
@user = User.find(parms[:id])
render_image @user.picture do |img|
img.sepiatone
end
end

就好了!

在 view 层,UploadColumnHelper.image 很容易为你的 action 产生一个 image 标签:

<%= image :action => "sepiatone", :id => 5 %>

== Views

如果上传文件是图片,那么你很可能想在 view 层显示出来,如果它是另一种文件,你将会做个超链接。借助 UploadColumn::BaseUploadedFile.url 会非常容易实现。

<%= link_to "Guitar Tablature", @song.tab.url %>

<%= image_tag @user.picture.url %>

== 魔术列

UploadColumn 允许你为 model 增加 'magic' 列,它将被合适数据自动填充。仅仅通过 migration 增加该列,例如:

add_column :users, :picture_content_type

如果我们的 model 为:

class User < ActiveRecord::Base
upload_column :picture
end

列 picture_content_type 将自动填充为文件的 content-type(至少是 UploadColumn 做的最好猜测)。

你能够使用 UploadColumn::UploadedFile 的任何方法,比如 size,url,store_dir 等等。

也可使用 picture_exif_date_time 或 picture_exif_model 等。当然,它仅仅适用于一个 JPEG 图片------唯一拥有 exif 数据的文件类型。这需要 EXIFR 类库,通过 gem 来安装:gem install exifr。

== 验证

UploadColumn 有它自己的验证方法,validates_integrity_of 。该方法将确保仅符合白名单上扩展名的文件才会被上传。这会阻止黑客上传可执行文件(像 .rb,.pl 或 .cgi 等)或限制某种文件允许被上传,例如仅限于图片。你能够用 :extensions 参数自定义白名单。

如果你仅仅允许 XHTML 和 XML 文件,你可以借助 XSLT 来控制:

upload_column :xml, :extensions => %w(xml html htm), :manipulator => MyXSLTProcessor

validates_integrity_of :xml

还可以在 UploadColumn 中使用一些 Rails 验证器。

validates_presence_of 和 validates_size_of 被证实管用。

validates_size_of :image, :maximum => 200000, :message => "is too big, must be smaller than 200kB!"

切记修改错误信息,UploadColumn 默认的看起来有点傻。

validates_uniqueness_of 不管用,这是由于 validates_uniqueness_of 发送(你的上传列)而不是请求实例变量,这样它会得到一个 UploadFile 对象,该对象不是真的和数据库中的其他值比较,不扰乱 Rails 内部机制而正常使用的确有点困难(如果你搞定了,请告诉我呀)。同时,你可以

validates_each :your_upload_column do |record, attr, value|
record.errors.add attr, 'already exists!' if YourModel.find( :first, :conditions => ["#{attr.to_s} = ?", value ] )
end

我知道,这不太优雅,但应该能用。
[root@yfw ~]# cd /www/wwwroot/yfw_szrengjing_com [root@yfw yfw_szrengjing_com]# chmod +x yfw-im-deploy.sh [root@yfw yfw_szrengjing_com]# sudo ./yfw-im-deploy.sh [INFO] 开始环境检查... [INFO] 创建插件目录结构... [INFO] 生成插件核心文件... [INFO] 安装 Ratchet WebSocket 依赖... [INFO] 初始化数据库表... [WARN] WP-CLI 未安装,需手动在 WordPress 后台激活插件以创建表 [SUCCESS] WebSocket 服务已启用并启动 [SUCCESS] ✅ YFW IM 插件部署完成! ================================================== 📌 使用方式: 1. 登录 WordPress 后台 → 插件 → 激活 'YFW 跨平台IM' 2. 在文章/页面中插入短代码: [yfw_im_chat] 3. 确保防火墙开放端口: 8080 firewall-cmd --add-port=8080/tcp --permanent && firewall-cmd --reload 4. 生产环境建议配置 Nginx 反向代理以启用 WSS 5. 查看日志: journalctl -u yfw-im-websocket -f ================================================== [root@yfw yfw_szrengjing_com]# 无法启用插件,因为它引起了一个致命错误(fatal error)。#!/bin/bash #============================================ # YFW 跨平台 IM 插件 —— 一键自动化部署脚本 # 功能:创建目录、生成代码、安装依赖、配置服务、支持 WSS 反向代理提示 # 支持功能:单聊、文本/图片/文件消息、已读回执、心跳保活、短代码嵌入 #============================================ # 颜色输出函数 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 "开始环境检查..." # 检查 PHP 版本 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 # 检查 Composer 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 # 检查 bc(用于浮点比较) 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 mkdir -p "$PLUGIN_DIR/includes" 2>/dev/null 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', 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())), $WS_PORT ); \$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: receiverId, 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. 初始化数据库(优先使用 WP-CLI) #=============================== 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 "==================================================" 请修复生成完整版源文件
最新发布
11-15
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值