实战经验:在服务器层面复制 Woo Multi Currency 插件并同步配置(WordPress / 宝塔 / Bash)
一、背景与问题来源
在使用宝塔 Linux 面板管理多个 WordPress 站点的过程中,我遇到一个非常常见但官方流程无法覆盖的需求:
- 服务器上有多个独立 WordPress 站点;
- 其中一个站点(
abc.shop)已经安装并深度定制了一个插件; - 该插件为 Woo Multi Currency(VillaTheme),用于 WooCommerce 的多货币展示与结算;
- 我对插件源码做过修改,因此不能通过 WordPress 后台安装 / 激活的方式在其他站点复用;
- 目标是将该插件完整复制到其他站点(
bcd.com、cde.site),并保证功能与配置生效。
这就引出了几个核心问题:
- 这个插件除了
/wp-content/plugins/woo-multi-currency目录外,还依赖哪些文件? - 它在数据库中写入了哪些内容?是否需要同步?
- 如何用脚本化、可重复、可回溯的方式完成整个复制过程?
二、插件简介:Woo Multi Currency 是什么?
Woo Multi Currency(VillaTheme) 是一个用于 WooCommerce 的多货币插件,核心功能包括:
- 多币种价格展示(基于汇率换算);
- 自动识别用户地区(可选);
- 货币切换器(前端组件 / Shortcode);
- 订单记录原始货币信息;
- 与 WooCommerce 价格计算、结账流程深度绑定。
从实现方式上看,它属于典型的:
“插件目录 + wp_options 配置驱动型插件”
这对后续复制方案至关重要。
三、第一性原理分析:复制插件到底要复制什么?
从 WordPress 插件的运行机制出发,一个插件要“生效”,通常依赖三类东西:
1️⃣ 插件代码(文件系统)
-
插件主目录:
wp-content/plugins/woo-multi-currency/ -
如果改过源码,这一部分必须原样复制。
2️⃣ 插件配置(数据库)
绝大多数 WordPress 插件的配置,都会存储在:
wp_options
表中,而不是写入代码。
3️⃣ 运行时数据(缓存 / uploads / meta)
例如:
wp-content/uploads/xxxwp_postmetawp_usermetawp_woocommerce_order_itemmeta
是否需要复制,取决于插件实现方式。
四、实际排查结果(以 adc.shop 为例)
1. 数据库中与插件相关的内容
执行:
SELECT * FROM wp_options WHERE option_name LIKE '%woo_multi_currency%';
得到关键结果:
-
woo_multi_currency_params- 一个
serialize的数组 - 包含:默认货币、启用币种、汇率、小数位数、UI 配色、自动识别等
- 属于插件的核心配置
- 一个
-
villatheme_woo-multi-currency_start_use- 值为
1 - 本质是一个“插件已使用 / 已初始化”的标记
- 值为
而以下表查询均为空:
wp_postmeta
wp_usermeta
wp_woocommerce_order_itemmeta
说明:
该插件在当前站点中 不依赖内容级 / 用户级 / 订单级额外元数据。
2. 文件系统相关目录
检查结果:
/wp-content/uploads/woo-multi-currency❌ 不存在/wp-content/languages/plugins/❌ 不存在
结论非常清晰:
只需要复制插件目录 + 同步 wp_options 中的两条记录即可。
五、为什么不能“直接导出 SQL 再导入”?
在多站点环境中,每个站点可能:
- 使用不同的数据库;
- 使用不同的
$table_prefix; - DB 用户、密码、主机不同;
如果手写 SQL,很容易出错,也不利于后期复用。
因此,我选择:
用 Bash 脚本自动解析
wp-config.php,动态处理数据库信息。
六、核心实现思路(脚本设计)
整个脚本做了以下事情:
1️⃣ 自动解析 wp-config.php
从每个站点的:
/www/wwwroot/{site}/wp-config.php
中提取:
DB_NAMEDB_USERDB_PASSWORDDB_HOST$table_prefix
⚠️ 实战中发现一个坑:
define() 使用的是 双引号,最初的正则只支持单引号,导致解析失败。
最终版本同时兼容 ' 和 "。
2️⃣ 导出源站点插件配置(精确到 option)
mysqldump ... wp_options
--where="option_name='woo_multi_currency_params'
OR option_name='villatheme_woo-multi-currency_start_use'"
只导出 必要配置,而不是整个 wp_options。
3️⃣ 替换表前缀并导入目标站点
- 自动将
wp_options→{target_prefix}options - 导入前先删除目标站点已有同名 option(防止重复)
- 导入前对目标站点配置做 备份
4️⃣ 同步插件源码(rsync)
rsync -a --delete
abc/wp-content/plugins/woo-multi-currency/
target/wp-content/plugins/woo-multi-currency/
确保:
- 覆盖旧文件;
- 保留你修改过的源码;
- 不依赖 WordPress 后台安装。
七、执行方式(后台 + 可追溯)
把下面完整脚本保存为 /root/copy_woo_multi_currency.sh(以 root 或有权限用户执行):
#!/bin/bash
# copy_woo_multi_currency.sh
# 功能说明:
# 1. 从源站点 abc.shop 复制已经被修改过的 woo-multi-currency 插件文件
# 2. 同步指定的插件配置项(options)到多个目标站点数据库
#
# 设计用途:
# 源站点路径:/www/wwwroot/abc.shop
# 目标站点路径:
# - /www/wwwroot/bcd.com
# - /www/wwwroot/cde.site
#
# 日志文件:/root/copy_woo_multi_currency.log
#
# 使用方式:
# 1. 将脚本放在 /root 目录
# 2. chmod +x copy_woo_multi_currency.sh
# 3. 后台运行:
# nohup bash /root/copy_woo_multi_currency.sh > /root/copy_woo_multi_currency.log 2>&1 &
# 遇到任何错误立即退出;未定义变量视为错误;管道中任一命令失败则整体失败
set -euo pipefail
# 设置 IFS,避免因为空格导致循环或变量解析异常
IFS=$'
'
# 定义日志文件路径
LOG="/root/copy_woo_multi_currency.log"
# 将 stdout 和 stderr 同时输出到终端并追加写入日志文件
exec > >(tee -a "$LOG") 2>&1
echo "===== $(date '+%F %T') START copy_woo_multi_currency ====="
# -----------------------------
# 基础路径与站点配置
# -----------------------------
# 源站点域名
SRC_SITE="abc.shop"
# 源站点 WordPress 根目录
SRC_PATH="/www/wwwroot/${SRC_SITE}"
# 插件相对路径
PLUGIN_DIR_REL="wp-content/plugins/woo-multi-currency"
# 源站点插件完整路径
PLUGIN_SRC="${SRC_PATH}/${PLUGIN_DIR_REL}"
# 目标站点域名数组
TARGET_SITES=("bcd.com" "cde.site")
# 所有站点的统一根目录
WWW_ROOT="/www/wwwroot"
# -----------------------------
# 需要同步的 WordPress option 名称
# -----------------------------
OPTIONS_TO_COPY=(
"woo_multi_currency_params"
"villatheme_woo-multi-currency_start_use"
)
# -----------------------------
# 基础工具依赖检查
# -----------------------------
for cmd in rsync mysql mysqldump grep sed awk cat; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "ERROR: required command '$cmd' not found. Install it and retry."
exit 1
fi
done
# -----------------------------
# 工具函数:解析 wp-config.php
# 功能:
# - 提取数据库名、用户名、密码、主机
# - 提取表前缀 $table_prefix
# 参数:
# - WordPress 根目录路径
# 返回:
# - DB_NAME|DB_USER|DB_PASSWORD|DB_HOST|TABLE_PREFIX
# -----------------------------
parse_wp_config() {
local wp_root="$1"
local cfg="${wp_root}/wp-config.php"
# 如果 wp-config.php 不存在,直接报错
if [ ! -f "$cfg" ]; then
echo "ERR::no-wp-config"
return 1
fi
# 使用 grep + sed 提取 define('DB_XXX', 'value') 中的值
local DB_NAME DB_USER DB_PASSWORD DB_HOST TABLE_PREFIX
DB_NAME=$(grep -E "define(s*'DB_NAME'" "$cfg" | sed -E "s/.*'DB_NAME's*,s*'([^']+)'.*//")
DB_USER=$(grep -E "define(s*'DB_USER'" "$cfg" | sed -E "s/.*'DB_USER's*,s*'([^']+)'.*//")
DB_PASSWORD=$(grep -E "define(s*'DB_PASSWORD'" "$cfg" | sed -E "s/.*'DB_PASSWORD's*,s*'([^']+)'.*//")
DB_HOST=$(grep -E "define(s*'DB_HOST'" "$cfg" | sed -E "s/.*'DB_HOST's*,s*'([^']+)'.*//")
# 提取 $table_prefix 变量
TABLE_PREFIX=$(grep -E "^s*$table_prefix" "$cfg" | sed -E "s/.*$table_prefixs*=s*'([^']+)'.*//")
# 如果没有找到表前缀,默认使用 wp_
if [ -z "$TABLE_PREFIX" ]; then
TABLE_PREFIX="wp_"
fi
# 通过 | 分隔输出,便于后续 awk 解析
printf "%s|%s|%s|%s|%s" "$DB_NAME" "$DB_USER" "$DB_PASSWORD" "$DB_HOST" "$TABLE_PREFIX"
return 0
}
# -----------------------------
# 校验源插件目录是否存在
# -----------------------------
if [ ! -d "$PLUGIN_SRC" ]; then
echo "ERROR: plugin source directory not found: $PLUGIN_SRC"
exit 1
fi
# -----------------------------
# 解析源站点数据库配置
# -----------------------------
echo "Parsing source wp-config.php for ${SRC_SITE}..."
SRC_CFG_LINE=$(parse_wp_config "$SRC_PATH" || true)
if [[ "$SRC_CFG_LINE" == "ERR::no-wp-config" || -z "$SRC_CFG_LINE" ]]; then
echo "ERROR: cannot parse wp-config.php for source site ${SRC_PATH}"
exit 1
fi
# 拆分数据库信息
SRC_DB_NAME=$(echo "$SRC_CFG_LINE" | awk -F'|' '{print $1}')
SRC_DB_USER=$(echo "$SRC_CFG_LINE" | awk -F'|' '{print $2}')
SRC_DB_PASS=$(echo "$SRC_CFG_LINE" | awk -F'|' '{print $3}')
SRC_DB_HOST=$(echo "$SRC_CFG_LINE" | awk -F'|' '{print $4}')
SRC_TABLE_PREFIX=$(echo "$SRC_CFG_LINE" | awk -F'|' '{print $5}')
# options 表名
SRC_OPTIONS_TABLE="${SRC_TABLE_PREFIX}options"
echo "Source DB: ${SRC_DB_NAME}@${SRC_DB_HOST} (user: ${SRC_DB_USER}), table_prefix: ${SRC_TABLE_PREFIX}"
# -----------------------------
# 从源数据库导出指定 option
# -----------------------------
TMP_DUMP="/tmp/woo_multi_currency_options_${SRC_SITE}_$$.sql"
echo "Dumping options from source DB into $TMP_DUMP ..."
# 构造 WHERE 条件:option_name='a' OR option_name='b'
WHERE_CLAUSE=""
for opt in "${OPTIONS_TO_COPY[@]}"; do
if [ -z "$WHERE_CLAUSE" ]; then
WHERE_CLAUSE="option_name='${opt}'"
else
WHERE_CLAUSE="${WHERE_CLAUSE} OR option_name='${opt}'"
fi
done
# 使用 mysqldump 只导出数据,不导出表结构
mysqldump --skip-add-drop-table --no-create-info --complete-insert
-h "${SRC_DB_HOST}" -u"${SRC_DB_USER}" -p"${SRC_DB_PASS}" "${SRC_DB_NAME}"
"${SRC_OPTIONS_TABLE}" --where="$WHERE_CLAUSE" > "$TMP_DUMP"
if [ ! -s "$TMP_DUMP" ]; then
echo "WARNING: dump file is empty. Source options may not exist or mysqldump failed."
fi
# -----------------------------
# 逐个处理目标站点
# -----------------------------
for site in "${TARGET_SITES[@]}"; do
echo "------"
echo "Processing target site: ${site}"
TGT_PATH="${WWW_ROOT}/${site}"
if [ ! -d "$TGT_PATH" ]; then
echo "ERROR: target path not found: ${TGT_PATH} -- skipping"
continue
fi
# 使用 rsync 同步插件文件(保持权限,删除多余文件)
echo "Copying plugin to ${TGT_PATH}/${PLUGIN_DIR_REL} ..."
mkdir -p "${TGT_PATH}/wp-content/plugins"
rsync -a --delete "${PLUGIN_SRC%/}/" "${TGT_PATH}/wp-content/plugins/woo-multi-currency/"
# 如果插件在 uploads 中有数据,也一并同步
SRC_UPLOAD_DIR="${SRC_PATH}/wp-content/uploads/woo-multi-currency"
if [ -d "$SRC_UPLOAD_DIR" ]; then
echo "Copying uploads data..."
mkdir -p "${TGT_PATH}/wp-content/uploads"
rsync -a "${SRC_UPLOAD_DIR%/}/" "${TGT_PATH}/wp-content/uploads/woo-multi-currency/"
else
echo "No source uploads directory found: ${SRC_UPLOAD_DIR} (skipping uploads copy)."
fi
# 解析目标站点数据库配置
echo "Parsing wp-config.php for target site..."
TGT_CFG_LINE=$(parse_wp_config "$TGT_PATH" || true)
if [[ "$TGT_CFG_LINE" == "ERR::no-wp-config" || -z "$TGT_CFG_LINE" ]]; then
echo "ERROR: cannot parse wp-config.php for target site ${TGT_PATH} -- skipping DB sync"
continue
fi
TGT_DB_NAME=$(echo "$TGT_CFG_LINE" | awk -F'|' '{print $1}')
TGT_DB_USER=$(echo "$TGT_CFG_LINE" | awk -F'|' '{print $2}')
TGT_DB_PASS=$(echo "$TGT_CFG_LINE" | awk -F'|' '{print $3}')
TGT_DB_HOST=$(echo "$TGT_CFG_LINE" | awk -F'|' '{print $4}')
TGT_TABLE_PREFIX=$(echo "$TGT_CFG_LINE" | awk -F'|' '{print $5}')
TGT_OPTIONS_TABLE="${TGT_TABLE_PREFIX}options"
echo "Target DB: ${TGT_DB_NAME}@${TGT_DB_HOST} (user: ${TGT_DB_USER}), table_prefix: ${TGT_TABLE_PREFIX}"
# 备份目标站点中已有的对应 option
BACKUP_SQL="/root/backup_${site}_woo_options_$(date '+%F_%T').sql"
echo "Backing up existing target options (if any) to ${BACKUP_SQL} ..."
mysqldump --skip-add-drop-table --no-create-info --complete-insert
-h "${TGT_DB_HOST}" -u"${TGT_DB_USER}" -p"${TGT_DB_PASS}" "${TGT_DB_NAME}"
"${TGT_OPTIONS_TABLE}" --where="$WHERE_CLAUSE" > "${BACKUP_SQL}" || true
# 删除目标数据库中旧的 option,避免主键冲突
echo "Deleting existing matching options from target DB..."
DELETE_SQL="DELETE FROM `${TGT_OPTIONS_TABLE}` WHERE ${WHERE_CLAUSE};"
mysql -h "${TGT_DB_HOST}" -u"${TGT_DB_USER}" -p"${TGT_DB_PASS}" "${TGT_DB_NAME}" -e "$DELETE_SQL"
# 准备导入 SQL:替换表名和数据库名
if [ -s "$TMP_DUMP" ]; then
TMP_IMPORT="/tmp/woo_multi_currency_import_${site}_$$.sql"
SRC_TABLE_ESCAPED="`${SRC_OPTIONS_TABLE}`"
TGT_TABLE_ESCAPED="`${TGT_OPTIONS_TABLE}`"
sed "s/${SRC_TABLE_ESCAPED}/${TGT_TABLE_ESCAPED}/g" "$TMP_DUMP" > "$TMP_IMPORT"
sed -i "s/`${SRC_DB_NAME}`/`${TGT_DB_NAME}`/g" "$TMP_IMPORT" || true
echo "Importing plugin options into target DB..."
mysql -h "${TGT_DB_HOST}" -u"${TGT_DB_USER}" -p"${TGT_DB_PASS}" "${TGT_DB_NAME}" < "$TMP_IMPORT" || {
echo "ERROR: import to target DB failed for ${site}. See log."
}
rm -f "$TMP_IMPORT"
echo "Imported options for ${site}."
else
echo "WARNING: source dump file ${TMP_DUMP} empty; nothing to import for ${site}."
fi
echo "Completed site: ${site}"
done
# 清理临时文件
rm -f "$TMP_DUMP"
echo "===== $(date '+%F %T') FINISH copy_woo_multi_currency ====="
如何部署与执行(快速步骤)
1.将上面脚本完整粘贴到 /root/copy_woo_multi_currency.sh:
sudo nano /root/copy_woo_multi_currency.sh
# 粘贴并保存
sudo chmod +x /root/copy_woo_multi_currency.sh
2.执行(宝塔Linux面板终端中执行):
nohup bash /root/copy_woo_multi_currency.sh > /root/copy_woo_multi_currency.log 2>&1 &
3.查看日志(实时):
tail -f /root/copy_woo_multi_currency.log
所有过程都有日志,可回放、可排错。
八、最终结论
通过这次实践,我总结出几个非常通用的经验:
-
不要从“插件”出发,而要从 WordPress 的运行模型出发
-
大多数插件:
- 核心逻辑在代码;
- 核心配置在
wp_options;
-
源码被修改过的插件:
- ❌ 不适合后台安装;
- ✅ 适合文件级复制 + 精确配置同步;
-
Bash +
wp-config.php自动解析,是多站点运维的利器; -
把一次性操作做成脚本,是对未来自己最友好的做法。
九、适用场景总结
本方案适用于:
- 宝塔 / 纯 Linux 管理 WordPress;
- 多个独立站点(非 WordPress Multisite);
- 插件被二次开发或修改源码;
- 希望可重复、可审计、可回滚的部署方式。










