feat(migrations): 引入版本化迁移链,跨版本更新按游标顺序执行
9 个文件变更
+351
-15
13172193298@163.com
| •MPHX/github_updater.php | +3 -1 |
| •MPHX/migrations.php | +285 -0 |
| •admin/api/gg.php | +20 -9 |
| •docs/guide/changelog.md | +11 -1 |
| •install/install.api.php | +13 -2 |
| •install/install.sql | +9 -0 |
| •install/repair_tables.sql | +8 -0 |
| •templates/default/admin/update.php | +1 -1 |
| •templates/tdesign/spa/src/admin/views/UpdateView.vue | +1 -1 |
变更内容
diff --git a/MPHX/github_updater.php b/MPHX/github_updater.php
index ec45b98..7f57b95 100644
--- a/MPHX/github_updater.php
+++ b/MPHX/github_updater.php
@@ -944,7 +944,9 @@ function mnbt_updater_guard_shutdown()
}
/**
- * 执行升级 SQL(只执行 update/update.sql,逻辑与旧版一致:按分号逐句跑)
+ * V1.85 迁移链(MPHX/migrations.php / MN_dbver 游标表)的旧式兼容路径:
+ * 仅用于执行发布包里遗留的单文件 update/update.sql,按分号逐句跑,不做 DELIMITER 切分。
+ * 新版本请改放 update/update_v<3位或4位>_<slug>.sql,走 mnbt_migrations_run。
* 注意参数顺序是 mysqli(host, user, pwd, dbname[, port]),旧代码把 dbname 和 pwd 传反了,
* 导致连接总是失败,这里按正确顺序传
* @return string 空串表示成功,否则为错误说明
diff --git a/MPHX/migrations.php b/MPHX/migrations.php
new file mode 100644
index 0000000..6735b1f
--- /dev/null
+++ b/MPHX/migrations.php
@@ -0,0 +1,285 @@
+<?php
+if (!defined('IN_CRONLITE')) exit();
+/**
+ * 版本化迁移链引擎(V1.85 起)
+ *
+ * 目标:更新包里 update/ 目录按版本各放一个小增量 SQL(update_v183_xxx.sql、
+ * update_1860_xxx.sql…),执行器读库里的"已应用版本"游标,只把游标之后、
+ * 目标版本之前的迁移按升序依次跑一遍,做到任意老版本一步跳到目标版本。
+ *
+ * 为什么这么设计:
+ * - 游标用独立表 MN_dbver(自举建表),不塞 MN_config,避免和业务配置耦合、
+ * 也留下每个版本何时应用的审计。
+ * - 语句切分支持 DELIMITER,因为幂等的"存储过程守护列存在"写法(v183/v184)
+ * 必须整段(含过程体内的分号)作为一个语句执行,朴素按 ; 切会崩。
+ * - 容忍"对象已存在"类错误码,配合迁移文件本身的幂等写法,存量老站从头重跑也安全。
+ * 只定义函数,不产生任何输出;入参用 $dbconfig 自建连接,调用方无需传 DB 层。
+ */
+
+/** 迁移重跑时"对象/列/索引已存在"或"已删除"类错误码,视为幂等跳过;其余按致命错误停止 */
+function mnbt_migrations_tolerant_errno()
+{
+ // 1050 表已存在 1060 列重复 1061 键名重复(MySQL) 1826 索引重复(MariaDB) 1091 无法删除不存在列/索引
+ return array(1050, 1060, 1061, 1826, 1091);
+}
+
+/**
+ * 迁移文件名 → 版本整数(与 $WEBQB / tag_to_version 同一套:主版本*1000+次版本*10+补丁)
+ * 支持两种写法:3 位 update_v183_xxx.sql(=>1830)、4 位 update_1860_xxx.sql / update_v1860_xxx.sql(=>1860)
+ * 不符合命名(如旧的单文件 update.sql)返回 0,交由链忽略
+ */
+function mnbt_migration_version_from_name($path)
+{
+ $name = basename((string)$path);
+ if (!preg_match('/^update_v?(\d{3,4})_.*\.sql$/i', $name, $m)) return 0;
+ $ds = $m[1];
+ if (strlen($ds) >= 4) {
+ $major = (int)$ds[0];
+ $minor = (int)substr($ds, 1, 2);
+ $patch = (int)substr($ds, 3, 1);
+ } else { // 3 位:形如 183 => 主1 次83
+ $major = (int)$ds[0];
+ $minor = (int)substr($ds, 1, 2);
+ $patch = 0;
+ }
+ return $major * 1000 + $minor * 10 + min(9, $patch);
+}
+
+/**
+ * DELIMITER-aware 语句切分:
+ * - 识别 `DELIMITER xxx` 指令行切换结束符
+ * - 跳过 -- / # 行注释与 /* */ 块注释(注释后的换行保留,避免相邻行粘连)
+ * - 单引号/双引号/反引号内的分隔符与注释不参与切分,反斜杠转义(反引号除外)
+ * @return array 去掉空白与纯注释后的完整语句列表
+ */
+function mnbt_sql_statements($sql)
+{
+ $sql = (string)$sql;
+ $stmts = array();
+ $delim = ';';
+ $buf = '';
+ $len = strlen($sql);
+ $i = 0;
+ $q = ''; // 当前引号:'' 无,或 ' " `
+ $line_start = true; // 是否处于行首(允许出现 DELIMITER 指令)
+ while ($i < $len) {
+ $c = $sql[$i];
+ if ($q !== '') { // 引号内:只关心转义与闭合,其余原样进 buf
+ if ($c === '\\' && $q !== '`') {
+ $buf .= $c;
+ if ($i + 1 < $len) $buf .= $sql[$i + 1];
+ $i += 2;
+ continue;
+ }
+ if ($c === $q) { $buf .= $c; $q = ''; $i++; $line_start = false; continue; }
+ $buf .= $c;
+ if ($c === "\n" || $c === "\r") $line_start = true;
+ $i++;
+ continue;
+ }
+ // 引号外:先处理注释
+ if ($c === '-' && $i + 1 < $len && $sql[$i + 1] === '-') { // -- 行注释
+ $j = $i + 2;
+ while ($j < $len && $sql[$j] !== "\n") $j++;
+ $i = $j; // 停在换行上,交给下面的普通分支保留换行
+ continue;
+ }
+ if ($c === '#') { // # 行注释
+ $j = $i + 1;
+ while ($j < $len && $sql[$j] !== "\n") $j++;
+ $i = $j;
+ continue;
+ }
+ if ($c === '/' && $i + 1 < $len && $sql[$i + 1] === '*') { // 块注释
+ $j = $i + 2;
+ while ($j < $len && !($sql[$j] === '*' && $j + 1 < $len && $sql[$j + 1] === '/')) $j++;
+ $i = ($j < $len) ? $j + 2 : $j;
+ continue;
+ }
+ // 引号开始
+ if ($c === "'" || $c === '"' || $c === '`') {
+ $q = $c; $buf .= $c; $i++; $line_start = false;
+ continue;
+ }
+ // DELIMITER 指令(必须位于行首且当前语句缓冲为空);结束符可能是 ';',故用 \S+ 捕获
+ if ($line_start && trim($buf) === '' && preg_match('/^[ \t]*DELIMITER[ \t]+(\S+)/i', substr($sql, $i), $md)) {
+ $delim = $md[1];
+ $j = $i;
+ while ($j < $len && $sql[$j] !== "\n") $j++;
+ $i = $j; $buf = '';
+ continue;
+ }
+ // 结束符匹配
+ $dl = strlen($delim);
+ if ($dl > 0 && substr($sql, $i, $dl) === $delim) {
+ $s = trim($buf);
+ if ($s !== '') $stmts[] = $s;
+ $buf = ''; $i += $dl; $line_start = false;
+ continue;
+ }
+ // 普通字符入缓冲
+ if ($c === "\n" || $c === "\r") { $line_start = true; }
+ elseif ($c === ' ' || $c === "\t") { /* 维持 line_start */ }
+ else { $line_start = false; }
+ $buf .= $c;
+ $i++;
+ }
+ $s = trim($buf);
+ if ($s !== '') $stmts[] = $s;
+ return $stmts;
+}
+
+/** 用 $dbconfig 建一个 mysqli(关闭异常模式,保持 query 返回 false 的旧语义);失败写 $err 返回 null */
+function mnbt_migrations_db($dbconfig, &$err)
+{
+ $err = '';
+ if (!class_exists('mysqli')) {
+ $err = '服务器未启用 mysqli 扩展,无法执行升级 SQL';
+ return null;
+ }
+ if (function_exists('mysqli_report')) @mysqli_report(MYSQLI_REPORT_OFF);
+ $port = !empty($dbconfig['port']) ? (int)$dbconfig['port'] : 3306;
+ try {
+ $db = @new mysqli($dbconfig['host'], $dbconfig['user'], $dbconfig['pwd'], $dbconfig['dbname'], $port);
+ } catch (Throwable $e) {
+ $err = '连接数据库出错:' . $e->getMessage();
+ return null;
+ }
+ if (mysqli_connect_errno()) {
+ $err = '连接数据库出错:' . mysqli_connect_error();
+ return null;
+ }
+ $db->set_charset('utf8');
+ $db->query("set sql_mode = ''");
+ return $db;
+}
+
+/** 建游标表(自举,幂等) */
+function mnbt_migrations_ensure($db, &$err)
+{
+ $sql = "CREATE TABLE IF NOT EXISTS `MN_dbver` ("
+ . "`version` int(11) NOT NULL,"
+ . "`file` varchar(191) NOT NULL DEFAULT '',"
+ . "`applied_at` datetime NOT NULL,"
+ . "PRIMARY KEY (`version`)) ENGINE=MyISAM DEFAULT CHARSET=utf8";
+ if ($db->query($sql) === false) {
+ $err = '创建迁移记录表 MN_dbver 失败:' . $db->error;
+ return false;
+ }
+ return true;
+}
+
+/** 当前库已应用到的版本(高水位游标);无记录返回 0 */
+function mnbt_migrations_cursor($db)
+{
+ $r = $db->query("SELECT MAX(`version`) AS v FROM `MN_dbver`");
+ if (!$r) return 0;
+ $row = $r->fetch_assoc();
+ if (is_object($r)) $r->free();
+ return isset($row['v']) ? (int)$row['v'] : 0;
+}
+
+/** 记录某版本已应用(REPLACE 防主键冲突) */
+function mnbt_migrations_record($db, $version, $file)
+{
+ $version = (int)$version;
+ $now = date('Y-m-d H:i:s');
+ $stmt = $db->prepare("REPLACE INTO `MN_dbver`(`version`,`file`,`applied_at`) VALUES(?,?,?)");
+ if ($stmt) {
+ $stmt->bind_param('iss', $version, $file, $now);
+ $stmt->execute();
+ $stmt->close();
+ return;
+ }
+ // 兜底:预处理不可用时走转义拼接
+ $safe_file = $db->real_escape_string($file);
+ $db->query("REPLACE INTO `MN_dbver`(`version`,`file`,`applied_at`) VALUES($version,'$safe_file','$now')");
+}
+
+/** 顺序执行一组语句;容忍"已存在"类错误,其余即视为致命并停止 */
+function mnbt_migrations_exec($db, $stmts, &$err)
+{
+ $tol = mnbt_migrations_tolerant_errno();
+ foreach ($stmts as $st) {
+ if (trim($st) === '') continue;
+ $res = $db->query($st);
+ if ($res === false) {
+ if (in_array($db->errno, $tol, true)) { continue; }
+ $preview = function_exists('mb_substr') ? mb_substr($st, 0, 140) : substr($st, 0, 420);
+ $err = 'SQL 错误 ' . $db->errno . ':' . $db->error . '(语句:' . $preview . '…)';
+ return false;
+ }
+ if (is_object($res)) $res->free();
+ }
+ return true;
+}
+
+/** 扫描迁移目录,返回按版本升序的迁移项 [{version,name,file}] */
+function mnbt_migrations_find($dir)
+{
+ $out = array();
+ $paths = glob(rtrim($dir, '/') . '/update_*.sql');
+ if (!is_array($paths)) return $out;
+ foreach ($paths as $p) {
+ $v = mnbt_migration_version_from_name($p);
+ if ($v <= 0) continue; // 不符合命名规范(如遗留单文件 update.sql)忽略
+ $out[] = array('version' => $v, 'name' => basename($p), 'file' => $p);
+ }
+ usort($out, function ($a, $b) {
+ if ($a['version'] === $b['version']) return strcmp($a['name'], $b['name']);
+ return $a['version'] - $b['version'];
+ });
+ return $out;
+}
+
+/**
+ * 执行迁移链:只跑「游标 < 版本 <= 目标」的迁移,按升序,成功一个记一个。
+ * 中途失败即停止并保留后续文件(游标停在最后成功版本,修复后重跑可续)。
+ * @return array 已成功应用的迁移文件名列表
+ */
+function mnbt_migrations_run($dbconfig, $dir, $target, &$err)
+{
+ $applied = array();
+ $err = '';
+ $files = mnbt_migrations_find($dir);
+ if (!$files) return $applied; // 无迁移文件视为无需变更
+ $db = mnbt_migrations_db($dbconfig, $conn_err);
+ if ($db === null) { $err = $conn_err; return $applied; }
+ if (!mnbt_migrations_ensure($db, $err)) { $db->close(); return $applied; }
+ $cursor = mnbt_migrations_cursor($db);
+ foreach ($files as $f) {
+ if ($f['version'] > $target) break; // 已按版本升序,后续都超目标
+ if ($f['version'] <= $cursor) continue; // 游标之前的高水位已应用
+ $sql = @file_get_contents($f['file']);
+ if ($sql === false) { $err = '无法读取迁移文件 ' . $f['name']; break; }
+ $stmts = mnbt_sql_statements($sql);
+ if (!$stmts && trim($sql) !== '') { $err = '迁移文件未解析出任何语句:' . $f['name']; break; }
+ if (!mnbt_migrations_exec($db, $stmts, $err)) { $err = '迁移 ' . $f['name'] . '(版本 ' . $f['version'] . ')失败:' . $err; break; }
+ mnbt_migrations_record($db, $f['version'], $f['name']);
+ $applied[] = $f['name'];
+ }
+ $db->close();
+ if ($err !== '') return array(); // 有错:整链视为失败,交由上层保留文件、提示手工处理
+ return $applied;
+}
+
+/**
+ * 基线播种:全新安装 / 安装向导升级后调用,把游标抬到目标版本,
+ * 之后在线更新只跑更高版本的增量,不会重放已到位的历史迁移。
+ * 已到位的表/列由安装流程本身保证,这里只是记账。
+ */
+function mnbt_migrations_seed($dbconfig, $version, &$err)
+{
+ $version = (int)$version;
+ $err = '';
+ if ($version <= 0) { $err = '基线版本号无效'; return false; }
+ $db = mnbt_migrations_db($dbconfig, $conn_err);
+ if ($db === null) { $err = $conn_err; return false; }
+ if (!mnbt_migrations_ensure($db, $err)) { $db->close(); return false; }
+ if (mnbt_migrations_cursor($db) < $version) {
+ mnbt_migrations_record($db, $version, '__baseline__');
+ }
+ $db->close();
+ return true;
+}
+?>
diff --git a/admin/api/gg.php b/admin/api/gg.php
index 2f97c7d..8988c50 100644
--- a/admin/api/gg.php
+++ b/admin/api/gg.php
@@ -11,6 +11,7 @@ if($egn=='update') {
@ignore_user_abort(true);
include("../MPHX/BL.php");
include_once("../MPHX/github_updater.php");
+ include_once("../MPHX/migrations.php");
include("../cf_up.php");
mnbt_updater_guard_start();
@@ -103,16 +104,26 @@ if($egn=='update') {
json_exit_error('解压覆盖失败:'.$ex_err);
}
- // 8. 升级 SQL:只执行包里带的 update/update.sql,位置在剥顶层目录后不变
+ // 8. 升级 SQL:把包里 update/ 的版本化迁移文件按游标顺序跑一遍(支持跨版本一步跳)
$sql_msg = '';
- $sql_file = $root.'/update/update.sql';
- if(is_file($sql_file)) {
- $sql_msg = mnbt_updater_run_sql($sql_file,$dbconfig);
- if($sql_msg==='') {
- // 执行成功才删,出错时保留文件让管理员可以手工再跑一次
- @unlink($sql_file);
- @rmdir($root.'/update/');
+ $upd_dir = $root.'/update/';
+ $applied_files = array();
+ if(is_dir($upd_dir)) {
+ $applied_files = mnbt_migrations_run($dbconfig,$upd_dir,(int)$chk['version'],$mig_err);
+ if($mig_err!=='') $sql_msg = $mig_err;
+ // 遗留单文件 update.sql(旧式发布)继续兼容:链成功后一次性执行并删除
+ if($sql_msg==='' && is_file($upd_dir.'update.sql')) {
+ $sql_msg = mnbt_updater_run_sql($upd_dir.'update.sql',$dbconfig);
+ if($sql_msg==='') {
+ @unlink($upd_dir.'update.sql');
+ @rmdir($upd_dir);
+ }
+ } elseif($sql_msg==='') {
+ // 版本链全部成功:删掉已应用的迁移文件,保持 update/ 目录整洁(游标表已记账,重跑无副作用)
+ foreach($applied_files as $fn) @unlink($upd_dir.$fn);
+ @rmdir($upd_dir);
}
+ // 失败:保留 update/ 目录所有文件,游标停在最后成功版本,修复后可再次点击继续
}
// 9. 还原本地配置与安装锁、把后台目录改回原名、清临时文件
@@ -124,7 +135,7 @@ if($egn=='update') {
logjl($user ?? '', '系统更新', '更新到 '.$chk['latest'].'('.$used['label'].',来源 '.$via_label.')', $sql_msg===''?'更新成功':'更新成功但升级SQL有误', $DB);
}
if($sql_msg!=='') {
- json_exit_error('文件已更新,但升级 SQL 执行失败:'.$sql_msg.'(update/update.sql 已保留,可手工执行)', [
+ json_exit_error('文件已更新,但升级 SQL 执行失败:'.$sql_msg.'(update/ 迁移文件已保留,游标停在最后成功的版本,可修复后重跑或手工执行)', [
'ver' => $chk['latest'], 'tag' => $chk['tag'],
'source' => $used['label'], 'via' => $via_label, 'strip' => $za['strip'], 'files' => $za['files'],
]);
diff --git a/docs/guide/changelog.md b/docs/guide/changelog.md
index 66a8223..5882ca0 100644
--- a/docs/guide/changelog.md
+++ b/docs/guide/changelog.md
@@ -41,7 +41,7 @@ description: MNBT 版本更新记录(V1.85 ~ V1.60)
- 解压前扫描 ZipArchive 条目名,拒绝绝对路径、盘符、`..` 段
- 覆盖前备份并在完成后还原 `config.php`、`cf_up.php`、`MPHX/SQ.php`、`install/install.lock`、`api/cookie/`;包自带的 `install.lock` 若站点原本没有会被删除,避免被判定为未安装
- `set_time_limit(600)` + `ignore_user_abort` + `register_shutdown_function` 兜底:任何中断路径都会还原改名后的后台目录、还原本地配置并清理临时包
-- 升级 SQL 仅执行包内 `update/update.sql`(通过 `$dbconfig` 连接),执行成功后删除该文件与 `update/` 目录
+- 升级 SQL 走版本化迁移链:`update/update_v<3 位或 4 位>_<slug>.sql` 按版本号升序执行,仅跑游标之后、目标版本以内的增量(详见下节)
**发版运维约定**
@@ -53,6 +53,16 @@ description: MNBT 版本更新记录(V1.85 ~ V1.60)
- 安装向导(`install/index.php` 的徽章/升级文案、`install/install.api.php` 的接口 `vs` 字段与升级完成提示)、后台版本显示(`admin/api/bt.php`)全部改为从 `$WEBQB` 计算,不再写死字面量;发版改产品内版本只需动 `BL.php` 一处
- README 与 docs 首页的 shields.io 徽章是静态 Markdown(PHP 渲染不到),由 `tools/sync-version.sh` 从 `$WEBQB` 同步;发版流程:改 `BL.php` → `bash tools/sync-version.sh` → 追加 changelog 段落
+**版本化迁移链**
+
+- 新增 `MPHX/migrations.php`:按文件版本号排序、以游标表驱动依次执行的迁移引擎;任意老版本一键跳到目标版本,无需人工拼 SQL
+- 游标表 `MN_dbver`(`version` PK / `file` / `applied_at`)独立记录每个版本何时应用,不塞进 `MN_config`,也留下审计;安装向导与 `install.sql`、`repair_tables.sql` 一并建表
+- 迁移文件命名:`update/update_v<3位或4位>_<slug>.sql`;3 位取 `183`→`1830`,4 位取 `1860`→`1860`;不符合此命名的旧式单文件 `update.sql` 走一次性兼容路径执行后删除
+- 语句切分支持 `DELIMITER $$` 与存储过程守护写法(v183/v184 的幂等「列存在则跳过」),引号/注释内的分隔符不参与切分
+- 幂等容忍错误码:1050 表已存在、1060 列重复、1061/1826 索引重复(MySQL/MariaDB)、1091 无法删除不存在列/索引;其它错误码致命并停链,`update/` 目录保留、游标停在最后成功的版本,修复后可再次点击继续
+- 基线播种:全新安装/升级完成后调用 `mnbt_migrations_seed($dbconfig, $WEBQB)` 把游标抬到当前版本,之后在线更新只跑更高版本的增量,不会重放历史迁移
+- 发版约定:每个版本的增量放独立小 SQL 追加到 `update/`,每条迁移保持幂等;跨版本升级由客户端自动顺序执行游标之后的所有文件
+
## V1.83
**Docker 容器托管**
diff --git a/install/install.api.php b/install/install.api.php
index 0a94c27..216c158 100644
--- a/install/install.api.php
+++ b/install/install.api.php
@@ -57,14 +57,14 @@ function mnbt_table_columns($table) {
return $cols;
}
-// 标准表清单(含 V1.83 Docker 集成四表)
+// 标准表清单(含 V1.83 Docker 集成四表、V1.85 迁移游标表 MN_dbver)
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');
+ 'MN_docker_node','MN_docker_user','MN_docker_plan','MN_docker_order','MN_dbver');
}
// 各版本增量字段定义(随发版追加,勿在此写死当前版本号):表 => (列名 => ALTER 语句)
@@ -529,6 +529,17 @@ switch ($action) {
exit(Res(0, '站点配置写入失败:' . DB::error()));
}
+ // V1.85 迁移链基线播种:全新安装 / 覆盖升级已把库结构带到当前版本,
+ // 据此把游标 MN_dbver 抬到 $WEBQB,之后在线更新只跑更高版本的增量、不重放历史迁移。
+ // skip 模式(只改配置不动表结构)库版本未知,故不播种;播种失败也不阻断安装。
+ if ($install_mode !== 'skip') {
+ include_once __DIR__ . '/../MPHX/migrations.php';
+ if (function_exists('mnbt_migrations_seed')) {
+ $seed_err = '';
+ mnbt_migrations_seed($dbconfig, (int)$WEBQB, $seed_err);
+ }
+ }
+
@file_put_contents("install.lock", '安装锁');
if ($install_mode === 'upgrade') {
exit(Res(1, '升级完成!已保留原有数据,成功更新至 ' . mnbt_version()));
diff --git a/install/install.sql b/install/install.sql
index 6a6f33f..d68c326 100644
--- a/install/install.sql
+++ b/install/install.sql
@@ -423,3 +423,12 @@ CREATE TABLE `MN_plugin_option` (
PRIMARY KEY (`id`),
UNIQUE KEY `uk_plugin_k` (`plugin_slug`,`k`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+-- 迁移链版本游标(V1.85 起,在线更新按此表记录已应用的版本决定后续增量)
+DROP TABLE IF EXISTS `MN_dbver`;
+CREATE TABLE `MN_dbver` (
+ `version` int(11) NOT NULL, -- 已应用的版本整数(同 $WEBQB 体系,如 1850)
+ `file` varchar(191) NOT NULL DEFAULT '', -- 应用的迁移文件名
+ `applied_at` datetime NOT NULL, -- 应用时间
+ PRIMARY KEY (`version`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
diff --git a/install/repair_tables.sql b/install/repair_tables.sql
index 4bdf6ff..08e14b0 100644
--- a/install/repair_tables.sql
+++ b/install/repair_tables.sql
@@ -327,3 +327,11 @@ CREATE TABLE IF NOT EXISTS `MN_docker_order` (
`date` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+-- V1.85 迁移链版本游标表(在线更新据此判断已应用版本)
+CREATE TABLE IF NOT EXISTS `MN_dbver` (
+ `version` int(11) NOT NULL,
+ `file` varchar(191) NOT NULL DEFAULT '',
+ `applied_at` datetime NOT NULL,
+ PRIMARY KEY (`version`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
diff --git a/templates/default/admin/update.php b/templates/default/admin/update.php
index df20f1a..3244ba9 100644
--- a/templates/default/admin/update.php
+++ b/templates/default/admin/update.php
@@ -146,7 +146,7 @@ $upd_token_set = !empty($mnbt_update['has_token']);
<div class="mn-upd-note" style="margin-top:16px;">
<b>更新说明:</b>更新会直接用 GitHub Release 的包覆盖站点文件,覆盖前会备份并还原
<code>config.php</code>、<code>cf_up.php</code>、<code>MPHX/SQ.php</code>、<code>install/install.lock</code>、<code>api/cookie/</code>;
- 包里若带 <code>update/update.sql</code> 会自动执行。请在维护时段操作,更新过程中不要关闭页面。
+ 包里 <code>update/update_v*_*.sql</code> 的版本化迁移会按版本号依次执行(仅跑游标 <code>MN_dbver</code> 之后的增量),旧式单文件 <code>update/update.sql</code> 仍兼容。请在维护时段操作,更新过程中不要关闭页面。
</div>
</div>
</div>
diff --git a/templates/tdesign/spa/src/admin/views/UpdateView.vue b/templates/tdesign/spa/src/admin/views/UpdateView.vue
index 5f1f0dd..625e0a6 100644
--- a/templates/tdesign/spa/src/admin/views/UpdateView.vue
+++ b/templates/tdesign/spa/src/admin/views/UpdateView.vue
@@ -163,7 +163,7 @@
<p class="upd-note">
更新会直接用 GitHub Release 的包覆盖站点文件,覆盖前会备份并还原
<code>config.php</code>、<code>cf_up.php</code>、<code>MPHX/SQ.php</code>、<code>install/install.lock</code>、<code>api/cookie/</code>;
- 包里若带 <code>update/update.sql</code> 会自动执行。请在维护时段操作。
+ 包里 <code>update/update_v*_*.sql</code> 的版本化迁移会按版本号依次执行(仅跑游标 <code>MN_dbver</code> 之后的增量),旧式单文件 <code>update/update.sql</code> 仍兼容。请在维护时段操作。
</p>
</div>
</div>