Files
ulthon_admin/tests
augushong 553047025c refactor(install): 种子退役,migrate:run 一条命令完成建表与初始数据
新增 InitDataAlignService 六域对齐(超管/角色/权限节点/配置/快捷入口/商城演示)与 20260909130000 迁移;SystemAdmin 快照以字面量哈希入库;删除种子三件与安装锁依赖,seeds 目录留 .gitkeep;README/tests/规则等 11 处说明同步。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-09 14:38:10 +00:00
..

测试数据库初始化

本目录提供测试数据库(默认 ulthon_admin_test)的初始化脚本。运行一次即可在与 .env 同实例的 MySQL 上建出框架运行所需的全部表(系统表 + 业务表),并写入全部初始数据 (超管、角色、系统配置、快捷入口、菜单等,由数据迁移随 migrate:run 一并交付)。

测试库策略:参数化,无需专用 Docker 容器

ulthon 是框架仓库,测试库策略与衍生项目(wkbox)不同:

  • 完全参数化:连接参数(hostname / hostport / username / password / charset / prefix / database)全部从 .env 读取,复用开发库同实例的 MySQL。
  • 不依赖 Docker:单机装一个 MySQL 即可,不需要为测试专起容器。
  • 测试库名派生规则:.env 的 DATABASE + _test 后缀。如 DATABASE=ulthon_admin 则测试库为 ulthon_admin_test;若 .env 改成别的项目名,测试库名会跟着变。
  • 脚本自带建库:用 PDO 直连 MySQL 执行 CREATE DATABASE IF NOT EXISTS,测试库 不存在会自动创建(utf8mb4 / utf8mb4_unicode_ci),已存在则幂等跳过。

衍生项目(如 wkbox)如果偏好专用 Docker 容器做物理隔离,可以 fork 本脚本,把 PDO 建库段换成 docker exec ... mysql -e "...",并把连接参数改成硬编码容器端口—— 但 ulthon 框架自身保持参数化、零容器假设。

文件说明

文件 作用
setup_test_db.php 真正的运行器(PHP)。启动 ThinkPHP 容器、读 .env、PDO 建测试库、config 级覆盖连接、按序调用 migrate:run / scheme:sync、查 information_schema 自验证。
setup_test_db.ps1 Windows / PowerShell 包装器:检测 php 在 PATH → 调 PHP 运行器 → 透传退出码。
setup_test_db.sh bash / CI 包装器:逻辑同上,Linux / macOS / CI 用。
bootstrap.php PHPUnit 引导脚本(已存在,独立维护)。连接覆盖逻辑与本脚本完全一致,二者必须保持同步——否则 PHPUnit 跑的测试库与本脚本初始化的不是同一个。

如何运行

# Windows / PowerShell
powershell -File tests/setup_test_db.ps1
# 或直接跑运行器
php tests/setup_test_db.php
# Linux / macOS / CI
bash tests/setup_test_db.sh
# 或
php tests/setup_test_db.php

成功后测试库(如 ulthon_admin_test)会包含框架所需的系统表(ul_system_*)以及 app/admin/scheme/ 下声明的业务表,退出码为 0。脚本自带验证段,会打印表总数并按 system / app_ / 其它分类,并断言关键系统表存在。

关键概念:migrate:run 与 scheme:sync 两套机制,缺一不可

ulthon 的建表有两个独立的来源,二者不能互相替代:

机制 命令 来源目录 建什么表 举例
Phinx 迁移 php think migrate:run database/migrations/ 系统 / 框架基础表 ul_system_admin、ul_system_menu、ul_system_auth_node、ul_system_config、ul_system_host、ul_system_timer_*、ul_debug_log 等
Scheme 同步 php think scheme:sync app/admin/scheme/ 业务表(用 PHP 8 Attribute 声明) 框架仓库本身不带业务表,使用方按需在 app/admin/scheme/ 添加
  • 只跑 migrate:run:得到系统表,但没有业务表。
  • 只跑 scheme:sync:得到业务表,但没有系统表,框架无法登录、无法鉴权。
  • 必须两个都跑,业务表通常引用系统表(如外键关联 system_admin)。

初始数据不再走 seed:migrate:run 建表的同时由数据迁移(20260909130000_system_init_data.php) 写入全部初始数据(超级管理员、角色、系统配置、快捷入口、商城演示、菜单等),数据源自 extend/base/admin/service/adminInitData/ 快照。seed 机制已退役,seed:run 为空操作, 仅保留给下游业务项目放自己的种子。

本脚本在同一进程内依次调用这两条命令(见下节),确保它们都指向测试库。

技术细节:为什么用 config 级覆盖,而不是环境变量

ThinkPHP 的 Env 组件加载 .env 文件,且 .env 的值优先于 OS 环境变量 (getenv 仅作为 .env 中不存在键的 fallback)。因此下列方式都无法覆盖 .env 里已存在的连接键(HOSTNAME / DATABASE / USERNAME / PASSWORD / HOSTPORT 都在 .env 中):

  • putenv('DATABASE=ulthon_admin_test')(PHP)
  • $env:DATABASE='ulthon_admin_test'(PowerShell)
  • DATABASE=ulthon_admin_test php think migrate:run(bash 前缀变量)

唯一可靠的覆盖点是 config 级:在 App::initialize() 之后,直接改写 config/database.php 解析出的连接配置数组。因为 Db facade 运行时读 config, config 被改写后,后续所有 DB 操作(包括 migrate:run / scheme:sync 内部)都连到测试库。

setup_test_db.php 的核心步骤:

require __DIR__ . '/../vendor/autoload.php';
$app = new \think\App();
$app->initialize();

// 从 .env 读连接参数(与 bootstrap.php 完全一致)
$hostname = \think\facade\Env::get('database.hostname');
// ...其它参数同理
$testDatabase = \think\facade\Env::get('database.database') . '_test';

// PDO 直连(不指定 database),建测试库
$pdo = new \PDO("mysql:host={$hostname};port={$hostport};charset={$charset}", $username, $password);
$pdo->exec("CREATE DATABASE IF NOT EXISTS `{$testDatabase}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");

// 把整段 'database' 配置 pull 出来,原地改 connections.main,再 set 回去。
// 注意:think\Config::set(array, $name) 的 $name 只支持单段一级名,
//       不像 Config::get 那样解析点号多级路径——不能直接传 'database.connections.main'。
$dbConfig = \think\facade\Config::get('database');
$dbConfig['default'] = 'main';
$dbConfig['connections']['main'] = array_merge(
    $dbConfig['connections']['main'] ?? [],
    [
        'hostname' => $hostname, 'hostport' => $hostport,
        'database' => $testDatabase, 'username' => $username, 'password' => $password,
        'charset' => $charset, 'prefix' => $prefix, 'fields_cache' => false,
    ],
    ['query' => \app\common\provider\db\Query::class], // 必须保留框架自定义查询类
);
\think\facade\Config::set($dbConfig, 'database');

// 安全护栏:库名必须含 'test' 才允许继续
if (stripos(\think\facade\Config::get('database.connections.main.database'), 'test') === false) {
    throw new \RuntimeException('拒绝执行:当前库不像测试库');
}

// 同进程内调用 think 命令,config 覆盖对它们同样生效
\think\facade\Console::call('migrate:run', ['--force-force']);
\think\facade\Console::call('scheme:sync',  ['--force-force']); // --force-force 跳过交互确认

关于 --force-force(-ff)

scheme:sync 在检测到 Scheme 与 DB 有差异时会交互式确认("确认要将这些变更应用到 数据库吗?")。非 TTY 环境(脚本、CI)下 think\console\Output::confirm 会抛 "Aborted" 而失败。框架提供了全局 --force-force / -ff 选项(见 extend/base/common/provider/ConsoleBase.php、extend/base/common/console/OutputBase.php), 命中时 confirm() 直接返回默认值,跳过所有交互确认。本脚本对两条命令统一带上该参数, 保证非交互可运行、可重跑。

关于默认连接名 main

本项目 config/database.php 的默认连接是 main(不是 ThinkPHP 常见的 mysql), default = Env::get('database.main'),.env 里 MAIN=main。因此覆盖目标是 connections.main,不是 connections.mysql。

幂等性

脚本可安全重复执行:

  • migrate:run:已执行的迁移记录在 ul_migrations(phinxlog),二次运行直接 "All Done" 无操作(初始数据由迁移交付,同样只执行一次且幂等)。
  • scheme:sync:无差异时打印 "未检测到 Scheme 变更";有差异时先备份原表(ul_backup_<时间戳>_<表名>)再改,不报错。
  • CREATE DATABASE IF NOT EXISTS:库已存在时无操作。

注:scheme:sync 每次遇到差异都会生成 ul_backup_* 备份表,这是框架的设计行为 (改表结构前自动备份)。若测试库中积累了大量备份表,可直接 DROP DATABASE + CREATE DATABASE 后重跑本脚本重建(见下)。

彻底重置测试库

当测试库状态污染、备份表堆积、或想从干净状态重新开始时:

# 用 .env 里的 root 账号连本机 MySQL(按实际 .env 替换 host/port/user/pass)
mysql -h127.0.0.1 -P3306 -uroot -proot \
  -e "DROP DATABASE IF EXISTS ulthon_admin_test; \
      CREATE DATABASE ulthon_admin_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# 然后重跑初始化
php tests/setup_test_db.php

PowerShell 等价:

mysql -h127.0.0.1 -P3306 -uroot -proot `
  -e "DROP DATABASE IF EXISTS ulthon_admin_test; `
      CREATE DATABASE ulthon_admin_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
php tests/setup_test_db.php