仰望星辰工作室

fix(install): 支持 1.81 直接升级到 1.84

Y yang1145 提交于 2026-09-13 22:30 · f354519 ·父提交 3c3b24b
fix(install): 支持 1.81 直接升级到 1.84

升级模式此前存在硬伤,无法完成 1.81→1.84 升级:
1. 引用的 upgrade_179to181.sql 文件不存在(实际为 1.79To1.81.sql),
   PHP8 下 file_get_contents 返回 false 触发 explode TypeError 致命错误
2. V1.83 Docker 四表与 V1.84 home_* 字段不在升级链中,
   update/ 下的增量脚本也没有被任何代码引用

修复:
- 修正脚本文件名引用并加存在性判断,升级不再中断
- repair_tables.sql 补入 V1.83 Docker 四表(IF NOT EXISTS)
- 抽取 mnbt_upgrade_columns 共享字段定义(V1.81 修复字段 +
  V1.83.1 Docker 磁盘配额字段 + V1.84 主页 home_* 字段),
  升级与修复两条路径共用,全部按 information_schema/SHOW COLUMNS
  判重后添加,兼容 MySQL 5.6/5.7
- 升级检测接口同步纳入 Docker 四表与 home_enable 检测,
  1.84 库正确显示"当前已是 V1.84,无需升级"
- 1.79To1.81.sql 幂等化(CREATE TABLE IF NOT EXISTS),
  在 1.81 库上重跑安全
- update_v183_docker.sql 的 MariaDB 专属 ADD COLUMN IF NOT EXISTS
  改为存储过程判重写法,MySQL 5.7 手动执行同样生效
- 升级成功消息由 V1.83 修正为 V1.84
4 个文件变更 +247 -143 13172193298@163.com
•install/1.79To1.81.sql +11 -10
•install/install.api.php +145 -129
•install/repair_tables.sql +68 -0
•update/update_v183_docker.sql +23 -4
变更内容
diff --git a/install/1.79To1.81.sql b/install/1.79To1.81.sql
index 474b818..fdc06b2 100644
--- a/install/1.79To1.81.sql
+++ b/install/1.79To1.81.sql
@@ -1,5 +1,6 @@
 -- MNBT V1.79 → V1.81 升级迁移脚本
 -- 保留所有已有数据,仅添加 V1.81 新增的表和字段
+-- 表使用 IF NOT EXISTS、字段由安装器判重后添加,可重复执行(1.81 库上重跑安全)
 
 -- V1.81: MN_bt 新增 mrbts_php 字段(节点默认 PHP 版本)
 ALTER TABLE `MN_bt`
@@ -11,7 +12,7 @@ ALTER TABLE `MN_config`
 
 
 DROP TABLE IF EXISTS `MN_monitor_task`; -- 用户端URL监控任务
-CREATE TABLE `MN_monitor_task`
+CREATE TABLE IF NOT EXISTS `MN_monitor_task`
 (
     `id`                 int(11)       NOT NULL AUTO_INCREMENT,
     `user`               varchar(250)  NOT NULL,
@@ -45,7 +46,7 @@ CREATE TABLE `MN_monitor_task`
   DEFAULT CHARSET = utf8;
 
 DROP TABLE IF EXISTS `MN_monitor_log`; -- 用户端URL监控检测日志
-CREATE TABLE `MN_monitor_log`
+CREATE TABLE IF NOT EXISTS `MN_monitor_log`
 (
     `id`               int(11)       NOT NULL AUTO_INCREMENT,
     `task_id`          int(11)       NOT NULL,
@@ -65,7 +66,7 @@ CREATE TABLE `MN_monitor_log`
   DEFAULT CHARSET = utf8;
 
 DROP TABLE IF EXISTS `MN_notice_log`; -- 用户端通知日志
-CREATE TABLE `MN_notice_log`
+CREATE TABLE IF NOT EXISTS `MN_notice_log`
 (
     `id`         int(11)      NOT NULL AUTO_INCREMENT,
     `user`       varchar(250) NOT NULL,
@@ -81,7 +82,7 @@ CREATE TABLE `MN_notice_log`
   DEFAULT CHARSET = utf8;
 
 DROP TABLE IF EXISTS `MN_node`; -- MNBT节点插件
-CREATE TABLE `MN_node`
+CREATE TABLE IF NOT EXISTS `MN_node`
 (
     `id`             int(11)      NOT NULL AUTO_INCREMENT,
     `bt_id`          int(11)      NOT NULL DEFAULT 0,
@@ -103,7 +104,7 @@ CREATE TABLE `MN_node`
   DEFAULT CHARSET = utf8;
 
 DROP TABLE IF EXISTS `MN_node_task`; -- MNBT节点任务
-CREATE TABLE `MN_node_task`
+CREATE TABLE IF NOT EXISTS `MN_node_task`
 (
     `id`          int(11)     NOT NULL AUTO_INCREMENT,
     `task_id`     varchar(64) NOT NULL,
@@ -124,7 +125,7 @@ CREATE TABLE `MN_node_task`
   DEFAULT CHARSET = utf8;
 
 DROP TABLE IF EXISTS `MN_node_nonce`; -- MNBT节点防重放
-CREATE TABLE `MN_node_nonce`
+CREATE TABLE IF NOT EXISTS `MN_node_nonce`
 (
     `id`         int(11)     NOT NULL AUTO_INCREMENT,
     `node_id`    varchar(64) NOT NULL,
@@ -136,7 +137,7 @@ CREATE TABLE `MN_node_nonce`
   DEFAULT CHARSET = utf8;
 
 DROP TABLE IF EXISTS `MN_forbidden_scan`; -- 违禁词扫描任务
-CREATE TABLE `MN_forbidden_scan`
+CREATE TABLE IF NOT EXISTS `MN_forbidden_scan`
 (
     `id`            int(11)      NOT NULL AUTO_INCREMENT,
     `task_id`       varchar(64)  NOT NULL,
@@ -156,7 +157,7 @@ CREATE TABLE `MN_forbidden_scan`
   DEFAULT CHARSET = utf8;
 
 DROP TABLE IF EXISTS `MN_forbidden_match`; -- 违禁词命中记录
-CREATE TABLE `MN_forbidden_match`
+CREATE TABLE IF NOT EXISTS `MN_forbidden_match`
 (
     `id`         int(11)       NOT NULL AUTO_INCREMENT,
     `task_id`    varchar(64)   NOT NULL,
@@ -199,7 +200,7 @@ ALTER TABLE `MN_config`
 
 -- V1.81 插件系统
 DROP TABLE IF EXISTS `MN_plugin`;
-CREATE TABLE `MN_plugin`
+CREATE TABLE IF NOT EXISTS `MN_plugin`
 (
     `id`           int(11)      NOT NULL AUTO_INCREMENT,
     `slug`         varchar(64)  NOT NULL,
@@ -215,7 +216,7 @@ CREATE TABLE `MN_plugin`
   DEFAULT CHARSET = utf8;
 
 DROP TABLE IF EXISTS `MN_plugin_option`;
-CREATE TABLE `MN_plugin_option`
+CREATE TABLE IF NOT EXISTS `MN_plugin_option`
 (
     `id`          int(11)      NOT NULL AUTO_INCREMENT,
     `plugin_slug` varchar(64)  NOT NULL,
diff --git a/install/install.api.php b/install/install.api.php
index 2e281c7..053cc94 100644
--- a/install/install.api.php
+++ b/install/install.api.php
@@ -44,6 +44,91 @@ function table_case_map($canonical_list) {
     return $map;
 }
 
+// 读取表的全部列名(小写),表不存在时返回空数组
+function mnbt_table_columns($table) {
+    $cols = array();
+    if (!table_exists_case($table)) return $cols;
+    $result = DB::query("SHOW COLUMNS FROM `{$table}`");
+    if ($result) {
+        while ($row = DB::fetch($result)) {
+            $cols[] = strtolower($row['Field']);
+        }
+    }
+    return $cols;
+}
+
+// 标准表清单(含 V1.83 Docker 集成四表)
+function mnbt_standard_tables() {
+    return array('MN_config','MN_log','MN_bt','MN_zj','MN_bs','MN_ym','MN_dd',
+        'MN_monitor_task','MN_monitor_log','MN_notice_log',
+        'MN_node','MN_node_task','MN_node_nonce',
+        'MN_forbidden_scan','MN_forbidden_match',
+        'MN_plugin','MN_plugin_option',
+        'MN_docker_node','MN_docker_user','MN_docker_plan','MN_docker_order');
+}
+
+// V1.81~V1.84 增量字段定义:表 => (列名 => ALTER 语句)
+// 实际表名由调用方传入(兼容大小写迁移场景);仅添加缺失列
+function mnbt_upgrade_columns($actual_config, $actual_zj, $actual_bt, $actual_dplan, $actual_duser) {
+    return array(
+        // V1.81 修复字段
+        $actual_config => array(
+            'mailhost' => "ALTER TABLE `{$actual_config}` ADD `mailhost` VARCHAR(50) NULL DEFAULT NULL",
+            'mailuser' => "ALTER TABLE `{$actual_config}` ADD `mailuser` VARCHAR(50) NULL DEFAULT NULL",
+            'mailpassword' => "ALTER TABLE `{$actual_config}` ADD `mailpassword` VARCHAR(50) NULL DEFAULT NULL",
+            'mailport' => "ALTER TABLE `{$actual_config}` ADD `mailport` VARCHAR(20) NOT NULL DEFAULT '465'",
+            'ymjkkg' => "ALTER TABLE `{$actual_config}` ADD `ymjkkg` VARCHAR(20) NOT NULL DEFAULT 'false'",
+            'mtyxfskg' => "ALTER TABLE `{$actual_config}` ADD `mtyxfskg` VARCHAR(20) NOT NULL DEFAULT 'false'",
+            'ymjktsyz' => "ALTER TABLE `{$actual_config}` ADD `ymjktsyz` VARCHAR(20) NOT NULL DEFAULT '7'",
+            'wjjkkg' => "ALTER TABLE `{$actual_config}` ADD `wjjkkg` VARCHAR(20) NOT NULL DEFAULT 'false'",
+            'mtwjfskg' => "ALTER TABLE `{$actual_config}` ADD `mtwjfskg` VARCHAR(50) NOT NULL DEFAULT 'false'",
+            'wjjktsyz' => "ALTER TABLE `{$actual_config}` ADD `wjjktsyz` VARCHAR(20) NOT NULL DEFAULT '7'",
+            'optionzc' => "ALTER TABLE `{$actual_config}` ADD `optionzc` VARCHAR(20) NOT NULL DEFAULT 'stop'",
+            'zjyxbd' => "ALTER TABLE `{$actual_config}` ADD `zjyxbd` VARCHAR(20) NOT NULL DEFAULT 'true'",
+            'wjsckg' => "ALTER TABLE `{$actual_config}` ADD `wjsckg` VARCHAR(20) NOT NULL DEFAULT 'false'",
+            'wjsccnr' => "ALTER TABLE `{$actual_config}` ADD `wjsccnr` TEXT NULL DEFAULT NULL",
+            'wjsckgqbfx' => "ALTER TABLE `{$actual_config}` ADD `wjsckgqbfx` VARCHAR(10) NOT NULL DEFAULT 'true'",
+            'wjscml' => "ALTER TABLE `{$actual_config}` ADD `wjscml` VARCHAR(500) NOT NULL DEFAULT '/www/wwwroot'",
+            'wjstqml' => "ALTER TABLE `{$actual_config}` ADD `wjstqml` TEXT NULL DEFAULT NULL",
+            'wjstqhz' => "ALTER TABLE `{$actual_config}` ADD `wjstqhz` TEXT NULL DEFAULT NULL",
+            'wjscdzmax' => "ALTER TABLE `{$actual_config}` ADD `wjscdzmax` INT(11) NOT NULL DEFAULT 5242880",
+            'wjscdhmax' => "ALTER TABLE `{$actual_config}` ADD `wjscdhmax` INT(11) NOT NULL DEFAULT 1000",
+            'wjscqzcs' => "ALTER TABLE `{$actual_config}` ADD `wjscqzcs` VARCHAR(50) NOT NULL DEFAULT '0 3 * * *'",
+            'wjscqzcskg' => "ALTER TABLE `{$actual_config}` ADD `wjscqzcskg` VARCHAR(20) NOT NULL DEFAULT 'true'",
+            'pay_methods' => "ALTER TABLE `{$actual_config}` ADD `pay_methods` TEXT NOT NULL DEFAULT ''",
+            // V1.84 独立主页系统(与 update/update_v184_home.sql 保持一致)
+            'home_enable' => "ALTER TABLE `{$actual_config}` ADD `home_enable` varchar(10) NOT NULL DEFAULT 'true'",
+            'home_theme' => "ALTER TABLE `{$actual_config}` ADD `home_theme` varchar(50) NOT NULL DEFAULT ''",
+            'home_theme_settings' => "ALTER TABLE `{$actual_config}` ADD `home_theme_settings` text NOT NULL",
+            'home_title' => "ALTER TABLE `{$actual_config}` ADD `home_title` text NOT NULL",
+            'home_hero' => "ALTER TABLE `{$actual_config}` ADD `home_hero` text NOT NULL",
+            'home_primary' => "ALTER TABLE `{$actual_config}` ADD `home_primary` varchar(10) NOT NULL DEFAULT '#4f46e5'",
+            'home_logo' => "ALTER TABLE `{$actual_config}` ADD `home_logo` text NOT NULL",
+            'home_favicon' => "ALTER TABLE `{$actual_config}` ADD `home_favicon` text NOT NULL",
+            'home_footer' => "ALTER TABLE `{$actual_config}` ADD `home_footer` text NOT NULL",
+            'home_show_notice' => "ALTER TABLE `{$actual_config}` ADD `home_show_notice` varchar(10) NOT NULL DEFAULT 'true'",
+            'home_show_plans' => "ALTER TABLE `{$actual_config}` ADD `home_show_plans` varchar(10) NOT NULL DEFAULT 'true'",
+        ),
+        $actual_zj => array(
+            'backup' => "ALTER TABLE `{$actual_zj}` ADD `backup` VARCHAR(50) NOT NULL DEFAULT '{\"max\":\"3\",\"dq\":0}'",
+            'mailuser' => "ALTER TABLE `{$actual_zj}` ADD `mailuser` VARCHAR(50) NULL DEFAULT NULL",
+        ),
+        $actual_bt => array(
+            'ftpdz' => "ALTER TABLE `{$actual_bt}` ADD `ftpdz` VARCHAR(50) NOT NULL DEFAULT 'false'",
+            'mrbts_php' => "ALTER TABLE `{$actual_bt}` ADD `mrbts_php` VARCHAR(10) NOT NULL DEFAULT ''",
+        ),
+        // V1.83.1 Docker 磁盘配额字段(表已在 repair_tables.sql 中补齐)
+        $actual_dplan => array(
+            'disk_max' => "ALTER TABLE `{$actual_dplan}` ADD `disk_max` varchar(20) NOT NULL DEFAULT '0' COMMENT '磁盘配额 MB 上限(0=不限制)'",
+            'proxy_max' => "ALTER TABLE `{$actual_dplan}` ADD `proxy_max` varchar(20) NOT NULL DEFAULT '0' COMMENT '反向代理数量上限(0=不限制)'",
+        ),
+        $actual_duser => array(
+            'disk_usage' => "ALTER TABLE `{$actual_duser}` ADD `disk_usage` bigint(20) NOT NULL DEFAULT '0' COMMENT '最近磁盘用量(字节)'",
+            'disk_usage_at' => "ALTER TABLE `{$actual_duser}` ADD `disk_usage_at` varchar(50) DEFAULT NULL COMMENT '磁盘用量采集时间'",
+        ),
+    );
+}
+
 if (file_exists('install.lock')) exit(Res(1, '已安装', ['vs' => $vs,'is_install'=>true], 1));
 
 function send_post()
@@ -201,13 +286,7 @@ switch ($action) {
             }
 
             // 检查所有标准表
-            $all_tables = array(
-                'MN_log', 'MN_bt', 'MN_zj', 'MN_bs', 'MN_ym', 'MN_dd',
-                'MN_monitor_task', 'MN_monitor_log', 'MN_notice_log',
-                'MN_node', 'MN_node_task', 'MN_node_nonce',
-                'MN_forbidden_scan', 'MN_forbidden_match',
-                'MN_plugin', 'MN_plugin_option'
-            );
+            $all_tables = mnbt_standard_tables();
             foreach ($all_tables as $tbl) {
                 if (!in_array(strtolower($tbl), $existing_tables, true)) {
                     $result['need_upgrade'] = true;
@@ -227,6 +306,13 @@ switch ($action) {
                 $result['need_upgrade'] = true;
                 $result['missing_columns'][] = 'MN_bt.mrbts_php';
             }
+
+            // 检查 V1.84 新增字段(独立主页系统)
+            $col_home = DB::get_row("SHOW COLUMNS FROM `MN_config` LIKE 'home_enable'");
+            if (!$col_home) {
+                $result['need_upgrade'] = true;
+                $result['missing_columns'][] = 'MN_config.home_enable';
+            }
         }
 
         echo Res(1, '升级检测完成', $result);
@@ -248,12 +334,7 @@ switch ($action) {
         $r_t = 0; $r_e = 0;
 
         // 获取规范名→实际名映射(兼容跨系统迁移后大小写不一致)
-        $all_tables = array('MN_config','MN_log','MN_bt','MN_zj','MN_bs','MN_ym','MN_dd',
-            'MN_monitor_task','MN_monitor_log','MN_notice_log',
-            'MN_node','MN_node_task','MN_node_nonce',
-            'MN_forbidden_scan','MN_forbidden_match',
-            'MN_plugin','MN_plugin_option');
-        $tbl_map = table_case_map($all_tables);
+        $tbl_map = table_case_map(mnbt_standard_tables());
 
         // 1. 补齐缺失的表(跳过已存在的,不管大小写)
         $sql = file_get_contents("repair_tables.sql");
@@ -272,70 +353,25 @@ switch ($action) {
             }
         }
 
-        // 2. 补齐字段(使用实际表名)
+        // 2. 补齐字段(使用实际表名,仅添加缺失列)
         $actual_config = isset($tbl_map['MN_config']) ? $tbl_map['MN_config'] : 'MN_config';
         $actual_zj     = isset($tbl_map['MN_zj']) ? $tbl_map['MN_zj'] : 'MN_zj';
         $actual_bt     = isset($tbl_map['MN_bt']) ? $tbl_map['MN_bt'] : 'MN_bt';
-
-        $config_cols = array(
-            'mailhost','mailuser','mailpassword','mailport',
-            'ymjkkg','mtyxfskg','ymjktsyz','wjjkkg','mtwjfskg','wjjktsyz',
-            'optionzc','zjyxbd',
-            'wjsckg','wjsccnr','wjsckgqbfx','wjscml','wjstqml','wjstqhz','wjscdzmax','wjscdhmax','wjscqzcs','wjscqzcskg',
-            'pay_methods',
-        );
-
-        // 先获取已有列名
-        $existing_cols = array();
-        $col_result = DB::query("SHOW COLUMNS FROM `{$actual_config}`");
-        if ($col_result) {
-            while ($row = DB::fetch($col_result)) {
-                $existing_cols[] = strtolower($row['Field']);
-            }
-        }
-
-        $alter_sqls = array(
-            'mailhost' => "ALTER TABLE `{$actual_config}` ADD `mailhost` VARCHAR(50) NULL DEFAULT NULL",
-            'mailuser' => "ALTER TABLE `{$actual_config}` ADD `mailuser` VARCHAR(50) NULL DEFAULT NULL",
-            'mailpassword' => "ALTER TABLE `{$actual_config}` ADD `mailpassword` VARCHAR(50) NULL DEFAULT NULL",
-            'mailport' => "ALTER TABLE `{$actual_config}` ADD `mailport` VARCHAR(20) NOT NULL DEFAULT '465'",
-            'ymjkkg' => "ALTER TABLE `{$actual_config}` ADD `ymjkkg` VARCHAR(20) NOT NULL DEFAULT 'false'",
-            'mtyxfskg' => "ALTER TABLE `{$actual_config}` ADD `mtyxfskg` VARCHAR(20) NOT NULL DEFAULT 'false'",
-            'ymjktsyz' => "ALTER TABLE `{$actual_config}` ADD `ymjktsyz` VARCHAR(20) NOT NULL DEFAULT '7'",
-            'wjjkkg' => "ALTER TABLE `{$actual_config}` ADD `wjjkkg` VARCHAR(20) NOT NULL DEFAULT 'false'",
-            'mtwjfskg' => "ALTER TABLE `{$actual_config}` ADD `mtwjfskg` VARCHAR(50) NOT NULL DEFAULT 'false'",
-            'wjjktsyz' => "ALTER TABLE `{$actual_config}` ADD `wjjktsyz` VARCHAR(20) NOT NULL DEFAULT '7'",
-            'optionzc' => "ALTER TABLE `{$actual_config}` ADD `optionzc` VARCHAR(20) NOT NULL DEFAULT 'stop'",
-            'zjyxbd' => "ALTER TABLE `{$actual_config}` ADD `zjyxbd` VARCHAR(20) NOT NULL DEFAULT 'true'",
-            'wjsckg' => "ALTER TABLE `{$actual_config}` ADD `wjsckg` VARCHAR(20) NOT NULL DEFAULT 'false'",
-            'wjsccnr' => "ALTER TABLE `{$actual_config}` ADD `wjsccnr` TEXT NULL DEFAULT NULL",
-            'wjsckgqbfx' => "ALTER TABLE `{$actual_config}` ADD `wjsckgqbfx` VARCHAR(10) NOT NULL DEFAULT 'true'",
-            'wjscml' => "ALTER TABLE `{$actual_config}` ADD `wjscml` VARCHAR(500) NOT NULL DEFAULT '/www/wwwroot'",
-            'wjstqml' => "ALTER TABLE `{$actual_config}` ADD `wjstqml` TEXT NULL DEFAULT NULL",
-            'wjstqhz' => "ALTER TABLE `{$actual_config}` ADD `wjstqhz` TEXT NULL DEFAULT NULL",
-            'wjscdzmax' => "ALTER TABLE `{$actual_config}` ADD `wjscdzmax` INT(11) NOT NULL DEFAULT 5242880",
-            'wjscdhmax' => "ALTER TABLE `{$actual_config}` ADD `wjscdhmax` INT(11) NOT NULL DEFAULT 1000",
-            'wjscqzcs' => "ALTER TABLE `{$actual_config}` ADD `wjscqzcs` VARCHAR(50) NOT NULL DEFAULT '0 3 * * *'",
-            'wjscqzcskg' => "ALTER TABLE `{$actual_config}` ADD `wjscqzcskg` VARCHAR(20) NOT NULL DEFAULT 'true'",
-            'pay_methods' => "ALTER TABLE `{$actual_config}` ADD `pay_methods` TEXT NOT NULL DEFAULT ''",
-            // MN_zj
-            'backup' => "ALTER TABLE `{$actual_zj}` ADD `backup` VARCHAR(50) NOT NULL DEFAULT '{\"max\":\"3\",\"dq\":0}'",
-            'mailuser_zj' => "ALTER TABLE `{$actual_zj}` ADD `mailuser` VARCHAR(50) NULL DEFAULT NULL",
-            // MN_bt
-            'ftpdz' => "ALTER TABLE `{$actual_bt}` ADD `ftpdz` VARCHAR(50) NOT NULL DEFAULT 'false'",
-            'mrbts_php' => "ALTER TABLE `{$actual_bt}` ADD `mrbts_php` VARCHAR(10) NOT NULL DEFAULT ''",
-        );
-
-        foreach ($alter_sqls as $col_name => $alter_sql) {
-            // 修复列名键(去掉 _zj 后缀)
-            $check_name = $col_name;
-            if ($check_name === 'mailuser_zj') $check_name = 'mailuser';
-
-            if (!in_array(strtolower($check_name), $existing_cols, true)) {
-                if (DB::query($alter_sql)) {
-                    ++$r_t;
-                } else {
-                    ++$r_e;
+        $actual_dplan  = isset($tbl_map['MN_docker_plan']) ? $tbl_map['MN_docker_plan'] : 'MN_docker_plan';
+        $actual_duser  = isset($tbl_map['MN_docker_user']) ? $tbl_map['MN_docker_user'] : 'MN_docker_user';
+
+        $alter_sqls = mnbt_upgrade_columns($actual_config, $actual_zj, $actual_bt, $actual_dplan, $actual_duser);
+
+        foreach ($alter_sqls as $table_name => $cols) {
+            $existing_cols = mnbt_table_columns($table_name);
+            if (empty($existing_cols)) continue; // 表不存在,repair_tables 已尝试创建
+            foreach ($cols as $col_name => $alter_sql) {
+                if (!in_array(strtolower($col_name), $existing_cols, true)) {
+                    if (DB::query($alter_sql)) {
+                        ++$r_t;
+                    } else {
+                        ++$r_e;
+                    }
                 }
             }
         }
@@ -392,32 +428,32 @@ switch ($action) {
         $skip_sql = ($install_mode === 'skip');
 
         if ($install_mode === 'upgrade') {
-            // 获取规范名→实际名映射
-            $all_tables = array('MN_config','MN_log','MN_bt','MN_zj','MN_bs','MN_ym','MN_dd',
-                'MN_monitor_task','MN_monitor_log','MN_notice_log',
-                'MN_node','MN_node_task','MN_node_nonce',
-                'MN_forbidden_scan','MN_forbidden_match',
-                'MN_plugin','MN_plugin_option');
-            $tbl_map = table_case_map($all_tables);
+            // 获取规范名→实际名映射(含 V1.83 Docker 集成四表)
+            $tbl_map = table_case_map(mnbt_standard_tables());
             $actual_config = isset($tbl_map['MN_config']) ? $tbl_map['MN_config'] : 'MN_config';
             $actual_zj     = isset($tbl_map['MN_zj']) ? $tbl_map['MN_zj'] : 'MN_zj';
             $actual_bt     = isset($tbl_map['MN_bt']) ? $tbl_map['MN_bt'] : 'MN_bt';
+            $actual_dplan  = isset($tbl_map['MN_docker_plan']) ? $tbl_map['MN_docker_plan'] : 'MN_docker_plan';
+            $actual_duser  = isset($tbl_map['MN_docker_user']) ? $tbl_map['MN_docker_user'] : 'MN_docker_user';
 
-            // 1. 运行 V1.79→V1.81 升级脚本
-            $sql = file_get_contents("upgrade_179to181.sql");
-            $sql = explode(';', $sql);
-            for ($i = 0; $i < count($sql); $i++) {
-                $q = trim($sql[$i]);
-                if ($q === '') continue;
-                if (DB::query($q)) {
-                    ++$t;
-                } else {
-                    ++$e;
-                    $error .= DB::error() . '<br/>';
+            // 1. 运行 V1.79→V1.81 升级脚本(脚本已幂等化,1.81 库上重跑安全)
+            $upgrade_sql_file = __DIR__ . '/1.79To1.81.sql';
+            if (file_exists($upgrade_sql_file)) {
+                $sql = file_get_contents($upgrade_sql_file);
+                $sql = explode(';', $sql);
+                for ($i = 0; $i < count($sql); $i++) {
+                    $q = trim($sql[$i]);
+                    if ($q === '') continue;
+                    if (DB::query($q)) {
+                        ++$t;
+                    } else {
+                        ++$e;
+                        $error .= DB::error() . '<br/>';
+                    }
                 }
             }
-            // 2. 补齐缺失的表(跳过已存在的)
-            $sql = file_get_contents("repair_tables.sql");
+            // 2. 补齐缺失的表(含 V1.83 Docker 四表,跳过已存在的)
+            $sql = file_get_contents(__DIR__ . '/repair_tables.sql');
             $sql = explode(';', $sql);
             for ($i = 0; $i < count($sql); $i++) {
                 $q = trim($sql[$i]);
@@ -432,40 +468,20 @@ switch ($action) {
                     $error .= DB::error() . '<br/>';
                 }
             }
-            // 3. 补齐缺失字段(使用实际表名)
-            $repair_cols = array(
-                "ALTER TABLE `{$actual_config}` ADD `mailhost` VARCHAR(50) NULL DEFAULT NULL",
-                "ALTER TABLE `{$actual_config}` ADD `mailuser` VARCHAR(50) NULL DEFAULT NULL",
-                "ALTER TABLE `{$actual_config}` ADD `mailpassword` VARCHAR(50) NULL DEFAULT NULL",
-                "ALTER TABLE `{$actual_config}` ADD `mailport` VARCHAR(20) NOT NULL DEFAULT '465'",
-                "ALTER TABLE `{$actual_config}` ADD `ymjkkg` VARCHAR(20) NOT NULL DEFAULT 'false'",
-                "ALTER TABLE `{$actual_config}` ADD `mtyxfskg` VARCHAR(20) NOT NULL DEFAULT 'false'",
-                "ALTER TABLE `{$actual_config}` ADD `ymjktsyz` VARCHAR(20) NOT NULL DEFAULT '7'",
-                "ALTER TABLE `{$actual_config}` ADD `wjjkkg` VARCHAR(20) NOT NULL DEFAULT 'false'",
-                "ALTER TABLE `{$actual_config}` ADD `mtwjfskg` VARCHAR(50) NOT NULL DEFAULT 'false'",
-                "ALTER TABLE `{$actual_config}` ADD `wjjktsyz` VARCHAR(20) NOT NULL DEFAULT '7'",
-                "ALTER TABLE `{$actual_config}` ADD `optionzc` VARCHAR(20) NOT NULL DEFAULT 'stop'",
-                "ALTER TABLE `{$actual_config}` ADD `zjyxbd` VARCHAR(20) NOT NULL DEFAULT 'true'",
-                "ALTER TABLE `{$actual_config}` ADD `wjsckg` VARCHAR(20) NOT NULL DEFAULT 'false'",
-                "ALTER TABLE `{$actual_config}` ADD `wjsccnr` TEXT NULL DEFAULT NULL",
-                "ALTER TABLE `{$actual_config}` ADD `wjsckgqbfx` VARCHAR(10) NOT NULL DEFAULT 'true'",
-                "ALTER TABLE `{$actual_config}` ADD `wjscml` VARCHAR(500) NOT NULL DEFAULT '/www/wwwroot'",
-                "ALTER TABLE `{$actual_config}` ADD `wjstqml` TEXT NULL DEFAULT NULL",
-                "ALTER TABLE `{$actual_config}` ADD `wjstqhz` TEXT NULL DEFAULT NULL",
-                "ALTER TABLE `{$actual_config}` ADD `wjscdzmax` INT(11) NOT NULL DEFAULT 5242880",
-                "ALTER TABLE `{$actual_config}` ADD `wjscdhmax` INT(11) NOT NULL DEFAULT 1000",
-                "ALTER TABLE `{$actual_config}` ADD `wjscqzcs` VARCHAR(50) NOT NULL DEFAULT '0 3 * * *'",
-                "ALTER TABLE `{$actual_config}` ADD `wjscqzcskg` VARCHAR(20) NOT NULL DEFAULT 'true'",
-                "ALTER TABLE `{$actual_config}` ADD `pay_methods` TEXT NOT NULL DEFAULT ''",
-                "ALTER TABLE `{$actual_zj}` ADD `backup` VARCHAR(50) NOT NULL DEFAULT '{\"max\":\"3\",\"dq\":0}'",
-                "ALTER TABLE `{$actual_zj}` ADD `mailuser` VARCHAR(50) NULL DEFAULT NULL",
-                "ALTER TABLE `{$actual_bt}` ADD `mrbts_php` VARCHAR(10) NOT NULL DEFAULT ''",
-            );
-            foreach ($repair_cols as $col_sql) {
-                if (DB::query($col_sql)) {
-                    ++$t;
-                } else {
-                    ++$e;
+            // 3. 补齐缺失字段(V1.81 修复字段 + V1.83 Docker 配额字段 + V1.84 主页字段,判重后添加)
+            $alter_sqls = mnbt_upgrade_columns($actual_config, $actual_zj, $actual_bt, $actual_dplan, $actual_duser);
+            foreach ($alter_sqls as $table_name => $cols) {
+                $existing_cols = mnbt_table_columns($table_name);
+                if (empty($existing_cols)) continue; // 表不存在,第 2 步已尝试创建
+                foreach ($cols as $col_name => $alter_sql) {
+                    if (!in_array(strtolower($col_name), $existing_cols, true)) {
+                        if (DB::query($alter_sql)) {
+                            ++$t;
+                        } else {
+                            ++$e;
+                            $error .= DB::error() . '<br/>';
+                        }
+                    }
                 }
             }
         } elseif (!$skip_sql) {
@@ -506,7 +522,7 @@ switch ($action) {
 
         @file_put_contents("install.lock", '安装锁');
         if ($install_mode === 'upgrade') {
-            exit(Res(1, '升级完成!已保留原有数据,成功更新至 V1.83'));
+            exit(Res(1, '升级完成!已保留原有数据,成功更新至 V1.84'));
         }
         if ($skip_sql) {
             exit(Res(1, '安装完成(保留原表并更新站点/管理员配置)'));
diff --git a/install/repair_tables.sql b/install/repair_tables.sql
index eb9d88d..4bdf6ff 100644
--- a/install/repair_tables.sql
+++ b/install/repair_tables.sql
@@ -259,3 +259,71 @@ CREATE TABLE IF NOT EXISTS `MN_plugin_option` (
   PRIMARY KEY (`id`),
   UNIQUE KEY `uk_plugin_k` (`plugin_slug`,`k`)
 ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+-- ======================== Docker 集成(V1.83)========================
+-- MN_docker_node / MN_docker_user / MN_docker_plan / MN_docker_order
+-- 与 update/update_v183_docker.sql 保持一致,仅在表不存在时创建
+CREATE TABLE IF NOT EXISTS `MN_docker_node` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(64) NOT NULL,
+  `btip` varchar(128) NOT NULL,
+  `btdk` varchar(10) NOT NULL DEFAULT '8888',
+  `ptl` varchar(10) NOT NULL DEFAULT 'false',
+  `btmy` varchar(255) NOT NULL,
+  `ktmy` varchar(255) NOT NULL,
+  `qmk` varchar(255) NOT NULL DEFAULT '',
+  `qk` varchar(10) NOT NULL DEFAULT 'true',
+  `date` varchar(50) NOT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS `MN_docker_user` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `username` varchar(64) NOT NULL,
+  `password_hash` varchar(255) NOT NULL,
+  `email` varchar(128) DEFAULT NULL,
+  `ssbt` int(11) NOT NULL DEFAULT '0',
+  `data` varchar(50) NOT NULL,
+  `datae` varchar(50) NOT NULL,
+  `qk` varchar(20) NOT NULL DEFAULT 'active',
+  `plan_id` int(11) DEFAULT NULL,
+  `container_id` varchar(64) DEFAULT NULL,
+  `service_name` varchar(64) DEFAULT NULL,
+  `app_name` varchar(64) DEFAULT NULL,
+  `container_spec` text,
+  `container_status` varchar(20) DEFAULT 'none',
+  `disk_usage` bigint(20) NOT NULL DEFAULT '0',
+  `disk_usage_at` varchar(50) DEFAULT NULL,
+  `expired_at` varchar(50) DEFAULT NULL,
+  `prune_due` varchar(50) DEFAULT NULL,
+  `extra` text,
+  `created_at` varchar(50) NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_username` (`username`),
+  KEY `idx_ssbt` (`ssbt`),
+  KEY `idx_qk` (`qk`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS `MN_docker_plan` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `name` varchar(64) NOT NULL,
+  `jc` text,
+  `cpu_max` varchar(20) NOT NULL DEFAULT '1',
+  `mem_max` varchar(20) NOT NULL DEFAULT '512',
+  `disk_max` varchar(20) NOT NULL DEFAULT '0',
+  `proxy_max` varchar(20) NOT NULL DEFAULT '0',
+  `jg` varchar(50) NOT NULL,
+  `qk` varchar(10) NOT NULL DEFAULT 'true',
+  `date` varchar(50) NOT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS `MN_docker_order` (
+  `id` int(11) NOT NULL AUTO_INCREMENT,
+  `username` varchar(64) NOT NULL,
+  `plan_id` int(11) NOT NULL,
+  `rmb` varchar(50) NOT NULL,
+  `qk` varchar(10) NOT NULL DEFAULT 'false',
+  `date` varchar(50) NOT NULL,
+  PRIMARY KEY (`id`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
diff --git a/update/update_v183_docker.sql b/update/update_v183_docker.sql
index 7a4d6e7..465d3d5 100644
--- a/update/update_v183_docker.sql
+++ b/update/update_v183_docker.sql
@@ -74,7 +74,26 @@ CREATE TABLE IF NOT EXISTS `MN_docker_order` (
 ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 
 -- v1.83.1 磁盘配额:为已有表添加 disk_max / disk_usage / disk_usage_at 字段(若不存在)
-ALTER TABLE `MN_docker_plan` ADD COLUMN IF NOT EXISTS `disk_max` varchar(20) NOT NULL DEFAULT '0' COMMENT '磁盘配额 MB 上限(0=不限制)';
-ALTER TABLE `MN_docker_plan` ADD COLUMN IF NOT EXISTS `proxy_max` varchar(20) NOT NULL DEFAULT '0' COMMENT '反向代理数量上限(0=不限制)';
-ALTER TABLE `MN_docker_user` ADD COLUMN IF NOT EXISTS `disk_usage` bigint(20) NOT NULL DEFAULT '0' COMMENT '最近磁盘用量(字节)';
-ALTER TABLE `MN_docker_user` ADD COLUMN IF NOT EXISTS `disk_usage_at` varchar(50) DEFAULT NULL COMMENT '磁盘用量采集时间';
+-- 使用存储过程实现列存在检查(兼容 MySQL 5.5+,原 ADD COLUMN IF NOT EXISTS 为 MariaDB 专属语法)
+-- 可重复执行,已有列会跳过
+DELIMITER $$
+
+DROP PROCEDURE IF EXISTS mnbt_docker_add_col$$
+CREATE PROCEDURE mnbt_docker_add_col(IN t VARCHAR(64), IN c VARCHAR(64), IN d VARCHAR(255))
+BEGIN
+  IF (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE table_schema = DATABASE() AND table_name = t AND column_name = c) = 0 THEN
+    SET @s = CONCAT('ALTER TABLE `', t, '` ADD COLUMN `', c, '` ', d);
+    PREPARE st FROM @s;
+    EXECUTE st;
+    DEALLOCATE PREPARE st;
+  END IF;
+END$$
+
+DELIMITER ;
+
+CALL mnbt_docker_add_col('MN_docker_plan', 'disk_max',      "varchar(20) NOT NULL DEFAULT '0' COMMENT '磁盘配额 MB 上限(0=不限制)'");
+CALL mnbt_docker_add_col('MN_docker_plan', 'proxy_max',     "varchar(20) NOT NULL DEFAULT '0' COMMENT '反向代理数量上限(0=不限制)'");
+CALL mnbt_docker_add_col('MN_docker_user', 'disk_usage',    "bigint(20) NOT NULL DEFAULT '0' COMMENT '最近磁盘用量(字节)'");
+CALL mnbt_docker_add_col('MN_docker_user', 'disk_usage_at', "varchar(50) DEFAULT NULL COMMENT '磁盘用量采集时间'");
+
+DROP PROCEDURE IF EXISTS mnbt_docker_add_col;