mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-10-29 08:58:22 +08:00
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>
This commit is contained in:
@@ -153,13 +153,13 @@ CREATE DATABASE IF NOT EXISTS ulthon_admin_test
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
第二步,建表 + 种子(测试库已建后执行):
|
||||
第二步,建表 + 初始化数据(测试库已建后执行):
|
||||
|
||||
```bash
|
||||
php tests/setup_test_db.php
|
||||
```
|
||||
|
||||
这个脚本会在同进程内依次跑 `migrate:run`(系统表)、`scheme:sync`(业务表)、`seed:run`(参考数据),并自查 `information_schema`。成功退出码 0。脚本幂等,可安全重跑。
|
||||
这个脚本会在同进程内依次跑 `migrate:run`(系统表 + 全部初始数据)、`scheme:sync`(业务表),并自查 `information_schema`。成功退出码 0。脚本幂等,可安全重跑。
|
||||
|
||||
### 日常运行
|
||||
|
||||
@@ -347,7 +347,7 @@ class XxxServiceTest extends TestCase
|
||||
|
||||
1. **装依赖**:`composer install`(`phpunit/phpunit: ^10` 已在 require-dev)
|
||||
2. **建测试库**:用 root 连接执行 `CREATE DATABASE {env.database}_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`(库名 = .env 的 `DATABASE` + `_test`)
|
||||
3. **建表 + 种子**:`php tests/setup_test_db.php`
|
||||
3. **建表 + 初始化数据**:`php tests/setup_test_db.php`
|
||||
|
||||
为什么建表必须跑两套机制(这是框架最重要的特点之一,缺一不可):
|
||||
|
||||
@@ -356,7 +356,7 @@ class XxxServiceTest extends TestCase
|
||||
| Phinx 迁移 | `migrate:run` | `database/migrations/` | 系统表(system_admin / system_menu / system_auth_node / system_config / system_host / system_timer_* 等) |
|
||||
| Scheme 同步 | `scheme:sync` | `app/admin/scheme/` | 业务表(各项目自己用 PHP 8 Attribute 声明的表) |
|
||||
|
||||
只跑 migrate 拿不到业务表,只跑 scheme 拿不到系统表(框架无法登录鉴权)。`setup_test_db.php` 在同进程内按顺序调这三条命令(migrate:run / scheme:sync / seed:run),并对每条带 `--force-force` 跳过交互确认(非 TTY 下 `scheme:sync` 会因 confirm 抛 Aborted)。
|
||||
只跑 migrate 拿不到业务表,只跑 scheme 拿不到系统表(框架无法登录鉴权)。`setup_test_db.php` 在同进程内按顺序调这两条命令(migrate:run / scheme:sync),并对每条带 `--force-force` 跳过交互确认(非 TTY 下 `scheme:sync` 会因 confirm 抛 Aborted)。
|
||||
|
||||
为什么用 config 级覆盖而不是环境变量:ThinkPHP 的 Env 加载 `.env`,且 `.env` 值优先于 OS 环境变量。`putenv('DATABASE=...')`、`$env:DATABASE=...`、bash 前缀变量都无法覆盖 `.env` 里已有的连接键。唯一可靠覆盖点是 config 级,在 `App::initialize()` 之后改写 `config/database.php` 解析出的连接数组。`tests/bootstrap.php` 和 `tests/setup_test_db.php` 都用这个模式。
|
||||
|
||||
@@ -397,7 +397,7 @@ php tests/setup_test_db.php
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `tests/bootstrap.php` | PHPUnit 引导:autoload + 容器初始化 + config 级覆盖到测试库 |
|
||||
| `tests/setup_test_db.php` | 测试库建表脚本:migrate:run + scheme:sync + seed:run + 自验证 |
|
||||
| `tests/setup_test_db.php` | 测试库建表脚本:migrate:run + scheme:sync + 自验证 |
|
||||
| `tests/setup_test_db.ps1` / `.sh` | Windows / Linux 包装器 |
|
||||
| `tests/README.md` | 测试库建库与建表的完整说明 |
|
||||
| `phpunit.xml.dist` | PHPUnit 配置:testsuite 定义,bootstrap 指向 tests/bootstrap.php |
|
||||
|
||||
@@ -43,12 +43,9 @@ DEBUG=true
|
||||
PREFIX=ul_
|
||||
|
||||
|
||||
Step 4, install the database
|
||||
Step 4, install the database (create tables + initialize data)
|
||||
php think migrate:run
|
||||
|
||||
Step 5: Initialize database data
|
||||
php think seed:run
|
||||
|
||||
Finally, run locally temporarily
|
||||
php think run
|
||||
|
||||
|
||||
@@ -45,12 +45,9 @@ DEBUG=true
|
||||
PREFIX=ul_
|
||||
|
||||
|
||||
第四步, 安装数据库
|
||||
第四步, 安装数据库(建表 + 初始化数据)
|
||||
php think migrate:run
|
||||
|
||||
第五步,初始化数据库数据
|
||||
php think seed:run
|
||||
|
||||
最后,本地临时运行
|
||||
php think run
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\service;
|
||||
|
||||
use base\admin\service\AdminInitServiceBase;
|
||||
|
||||
class AdminInitService extends AdminInitServiceBase
|
||||
{
|
||||
}
|
||||
16
app/admin/service/InitDataAlignService.php
Normal file
16
app/admin/service/InitDataAlignService.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\service;
|
||||
|
||||
use base\admin\service\InitDataAlignServiceBase;
|
||||
|
||||
/**
|
||||
* 初始化数据对齐服务(app 层入口).
|
||||
*
|
||||
* 六大数据域(超管/角色/权限节点/配置/快捷入口/商城演示)按快照对齐:
|
||||
* 由数据型 migration 调用,把 extend/base/admin/service/adminInitData/
|
||||
* 下的快照对齐到当前库。需要定制对齐行为时覆盖父类方法,保持方法签名一致。
|
||||
*/
|
||||
class InitDataAlignService extends InitDataAlignServiceBase
|
||||
{
|
||||
}
|
||||
36
database/migrations/20260909130000_system_init_data.php
Normal file
36
database/migrations/20260909130000_system_init_data.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use app\admin\service\InitDataAlignService;
|
||||
use think\console\Output;
|
||||
use think\migration\Migrator;
|
||||
|
||||
/**
|
||||
* 系统初始化数据对齐 - 种子退役后初始数据的唯一安装途径
|
||||
*
|
||||
* 背景:seed:run 退役,migrate:run 成为唯一安装命令(建表 + 全部初始数据)。
|
||||
* 本迁移调用 InitDataAlignService 把 adminInitData/ 快照对齐到当前库:
|
||||
* - 超管 id=1 只插不改(密码永不覆盖,软删行不复活)
|
||||
* - 角色 id=1/id=6 逐行只插
|
||||
* - 权限节点跳过(注解动态扫描)
|
||||
* - 配置按 (group, name) 只插缺失,插后清 sysconfig 缓存
|
||||
* - 快捷入口按 href 只插缺失(软删行不复活)
|
||||
* - 商城演示分表守卫(cate/goods/tag 各自空则插)
|
||||
*
|
||||
* 幂等:phinxlog 按文件记账保证只执行一次;重放由服务内的存在性检查兜底。
|
||||
* down:数据对齐迁移不提供对称回滚(无法精确还原对齐前的用户态数据),
|
||||
* 如需回退参考 adminInitData/ 下各快照手工处理。
|
||||
*/
|
||||
class SystemInitData extends Migrator
|
||||
{
|
||||
public function up()
|
||||
{
|
||||
$service = new InitDataAlignService(new Output());
|
||||
|
||||
$service->align();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
// 有意留空:见类注释 down 说明
|
||||
}
|
||||
}
|
||||
1
database/seeds/.gitkeep
Normal file
1
database/seeds/.gitkeep
Normal file
@@ -0,0 +1 @@
|
||||
# 种子已退役:migrate:run 是唯一安装命令;下游业务仍可放自己的种子文件
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
use app\admin\model\SystemConfig;
|
||||
use app\admin\service\AdminInitService;
|
||||
use app\admin\service\TriggerService;
|
||||
use think\migration\Seeder;
|
||||
|
||||
class InitBaseAdminData extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run Method.
|
||||
*
|
||||
* Write your database seeder using this method.
|
||||
*
|
||||
* More information on writing seeders is available here:
|
||||
* http://docs.phinx.org/en/latest/seeding.html
|
||||
*/
|
||||
public function run():void
|
||||
{
|
||||
$output = $this->getOutput();
|
||||
|
||||
TriggerService::updateSysconfig();
|
||||
|
||||
$install_lock = sysconfig('base_admin_install', true, 0);
|
||||
|
||||
if ($install_lock == 1) {
|
||||
$output->writeln('系统已初始化,跳过当前程序');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$model_config = SystemConfig::where('group', 'system')
|
||||
->where('name', 'base_admin_install')
|
||||
->find();
|
||||
|
||||
if (empty($model_config)) {
|
||||
$model_config = new SystemConfig();
|
||||
|
||||
$model_config->group = 'system';
|
||||
$model_config->name = 'base_admin_install';
|
||||
$model_config->remark = '系统初始化锁定,请勿修改,避免插入重复数据';
|
||||
}
|
||||
|
||||
$model_config->value = 1;
|
||||
|
||||
$model_config->save();
|
||||
|
||||
TriggerService::updateSysconfig();
|
||||
|
||||
(new AdminInitService($output))->init();
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace base\admin\service;
|
||||
|
||||
use app\admin\model\MallCate;
|
||||
use app\admin\model\MallGoods;
|
||||
use app\admin\model\MallTag;
|
||||
use app\admin\model\SystemAdmin;
|
||||
use app\admin\model\SystemAuth;
|
||||
use app\admin\model\SystemAuthNode;
|
||||
use app\admin\model\SystemConfig;
|
||||
use app\admin\model\SystemMenu;
|
||||
use app\admin\model\SystemQuick;
|
||||
use app\common\constants\AdminConstant;
|
||||
|
||||
class AdminInitServiceBase
|
||||
{
|
||||
protected $output = null;
|
||||
|
||||
public function __construct($output)
|
||||
{
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
public function init()
|
||||
{
|
||||
$this->initAdmin();
|
||||
$this->initAuth();
|
||||
$this->initConfig();
|
||||
$this->initMenu();
|
||||
$this->initQuick();
|
||||
$this->initMall();
|
||||
}
|
||||
|
||||
public function initMall()
|
||||
{
|
||||
$output = $this->output;
|
||||
|
||||
$output->writeln('开始初始化商城案例');
|
||||
|
||||
$list_cate = $this->requireData('MallCate');
|
||||
|
||||
$list_goods = $this->requireData('MallGoods');
|
||||
|
||||
foreach ($list_cate as $data_cate) {
|
||||
$model_cate = MallCate::create($data_cate);
|
||||
|
||||
foreach ($list_goods as $data_goods) {
|
||||
$data_goods['cate_id'] = $model_cate->id;
|
||||
MallGoods::create($data_goods);
|
||||
}
|
||||
}
|
||||
|
||||
$list_tag = $this->requireData('MallTag');
|
||||
|
||||
$this->installData(MallTag::class, $list_tag);
|
||||
}
|
||||
|
||||
public function initQuick()
|
||||
{
|
||||
$output = $this->output;
|
||||
|
||||
$output->writeln('开始初始化快捷入口');
|
||||
|
||||
$list_quick = $this->requireData('SystemQuick');
|
||||
|
||||
$this->installData(SystemQuick::class, $list_quick);
|
||||
}
|
||||
|
||||
public function initMenu()
|
||||
{
|
||||
$output = $this->output;
|
||||
|
||||
$output->writeln('开始初始化系统菜单');
|
||||
|
||||
$list_menu = $this->requireData('SystemMenu');
|
||||
|
||||
$this->installData(SystemMenu::class, $list_menu);
|
||||
}
|
||||
|
||||
public function initConfig()
|
||||
{
|
||||
$output = $this->output;
|
||||
$output->writeln('开始初始化系统设置');
|
||||
|
||||
$list_config = $this->requireData('SystemConfig');
|
||||
|
||||
$this->installData(SystemConfig::class, $list_config);
|
||||
}
|
||||
|
||||
public function initAdmin()
|
||||
{
|
||||
$output = $this->output;
|
||||
|
||||
$output->writeln('创建超级管理员');
|
||||
|
||||
$model_admin = SystemAdmin::find(AdminConstant::SUPER_ADMIN_ID);
|
||||
|
||||
if (empty($model_admin)) {
|
||||
$model_admin = new SystemAdmin();
|
||||
$model_admin->id = AdminConstant::SUPER_ADMIN_ID;
|
||||
$model_admin->head_img = '/static/admin/images/head.jpg';
|
||||
$model_admin->username = 'admin';
|
||||
$model_admin->password = password(123456);
|
||||
$model_admin->status = 1;
|
||||
$model_admin->save();
|
||||
$output->writeln('创建超级管理员成功');
|
||||
} else {
|
||||
$output->writeln('超级管理员已存在,无需初始化');
|
||||
}
|
||||
}
|
||||
|
||||
public function initAuth()
|
||||
{
|
||||
$output = $this->output;
|
||||
|
||||
$output->writeln('开始初始化权限');
|
||||
|
||||
$list_auth = $this->requireData('SystemAuth');
|
||||
|
||||
$this->installData(SystemAuth::class, $list_auth);
|
||||
|
||||
$output->writeln('开始初始化权限节点');
|
||||
|
||||
$list_auth_node = $this->requireData('SystemAuthNode');
|
||||
|
||||
$this->installData(SystemAuthNode::class, $list_auth_node);
|
||||
}
|
||||
|
||||
protected function installData($model_name, $list)
|
||||
{
|
||||
foreach ($list as $key => $value) {
|
||||
$model_name::create($value);
|
||||
}
|
||||
}
|
||||
|
||||
protected function requireData($table_name)
|
||||
{
|
||||
return include __DIR__ . '/adminInitData/' . $table_name . '.php';
|
||||
}
|
||||
}
|
||||
262
extend/base/admin/service/InitDataAlignServiceBase.php
Normal file
262
extend/base/admin/service/InitDataAlignServiceBase.php
Normal file
@@ -0,0 +1,262 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @internal-framework
|
||||
*
|
||||
* 此文件为框架核心服务
|
||||
*
|
||||
* 用途:初始化数据对齐服务(六大数据域按快照对齐,migrate:run 唯一安装路径)
|
||||
* 维护者:框架维护者
|
||||
*
|
||||
* 背景:种子机制退役后,adminInitData/*.php 快照是初始数据的唯一数据源,
|
||||
* 由本服务随数据型 migration 安装(migrate:run 是文档化安装命令,phinxlog
|
||||
* 保证幂等)。取代原 AdminInitServiceBase 的种子初始化。
|
||||
*
|
||||
* 对齐语义(保守,尊重用户自定义):
|
||||
* - 超管 :id=1 不存在才插入;密码永不覆盖(软删行存在同样视为已存在,不复活)
|
||||
* - 角色 :按显式 id 逐行只插(含游客权限 id=6)
|
||||
* - 权限节点:快照为空数组,直接跳过(该表由 auth 注解动态扫描生成)
|
||||
* - 配置 :按 (group, name) check-then-insert(表无唯一键,纯应用层判重),
|
||||
* 只插缺失不覆盖 value;发生过插入则清一次 sysconfig 缓存
|
||||
* - 快捷入口:按 href 只插缺失;软删行存在视为已存在,不复活
|
||||
* - 商城演示:分表守卫(cate 空则插分类;goods 空则插商品并注入分类 id;
|
||||
* tag 空则插标签),三守卫相互独立
|
||||
*
|
||||
* 硬性实现约束:auth/quick/admin 三表均有 delete_time,存在性检查必须用
|
||||
* Db::name 原生查询绕过软删除作用域——否则软删行被判「缺失」→ INSERT →
|
||||
* 主键冲突崩溃或复活用户删除的行。沿用 MenuAlignServiceBase 已验证模式。
|
||||
*/
|
||||
|
||||
namespace base\admin\service;
|
||||
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
class InitDataAlignServiceBase
|
||||
{
|
||||
/**
|
||||
* @var Output|null 命令行输出(migration 场景传入,便于追踪对齐过程)
|
||||
*/
|
||||
protected $output = null;
|
||||
|
||||
public function __construct($output = null)
|
||||
{
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按快照对齐全部初始数据域(幂等,可重复执行).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function align(): void
|
||||
{
|
||||
$this->writeln('开始对齐初始化数据(超管/角色/权限节点/配置/快捷入口/商城演示)');
|
||||
|
||||
$this->alignAdmin();
|
||||
$this->alignAuth();
|
||||
$this->alignAuthNode();
|
||||
$this->alignConfig();
|
||||
$this->alignQuick();
|
||||
$this->alignMall();
|
||||
|
||||
$this->writeln('初始化数据对齐完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 超管对齐:id=1 不存在才插入,密码永不覆盖.
|
||||
*/
|
||||
protected function alignAdmin(): void
|
||||
{
|
||||
$snapshot = include __DIR__ . '/adminInitData/SystemAdmin.php';
|
||||
$now = time();
|
||||
|
||||
foreach ($snapshot as $row) {
|
||||
// 原生查询不过滤软删除:软删的超管行仍占用主键,视为已存在,不复活不覆盖
|
||||
$exists = Db::name('system_admin')->where('id', $row['id'])->find();
|
||||
|
||||
if (!empty($exists)) {
|
||||
$this->writeln(' - 超管 id=' . $row['id'] . ' 已存在,跳过(密码永不覆盖)');
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $row;
|
||||
$data['create_time'] = $now;
|
||||
$data['update_time'] = $now;
|
||||
$data['delete_time'] = 0;
|
||||
Db::name('system_admin')->insert($data);
|
||||
$this->writeln(' - 新增超管 id=' . $row['id'] . ' ' . $row['username']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色对齐:按显式 id 逐行只插(快照含管理员 id=1 与游客权限 id=6).
|
||||
*/
|
||||
protected function alignAuth(): void
|
||||
{
|
||||
$snapshot = include __DIR__ . '/adminInitData/SystemAuth.php';
|
||||
$now = time();
|
||||
|
||||
foreach ($snapshot as $row) {
|
||||
// 原生查询不过滤软删除:软删的角色行仍占用主键,视为已存在,不复活
|
||||
$exists = Db::name('system_auth')->where('id', $row['id'])->find();
|
||||
|
||||
if (!empty($exists)) {
|
||||
$this->writeln(' - 角色 id=' . $row['id'] . ' ' . $row['title'] . ' 已存在,跳过');
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $row;
|
||||
$data['create_time'] = $now;
|
||||
$data['update_time'] = $now;
|
||||
$data['delete_time'] = 0;
|
||||
Db::name('system_auth')->insert($data);
|
||||
$this->writeln(' - 新增角色 id=' . $row['id'] . ' ' . $row['title']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限节点对齐:快照为空数组,直接跳过(该表由 auth 注解动态扫描生成).
|
||||
*/
|
||||
protected function alignAuthNode(): void
|
||||
{
|
||||
$this->writeln(' - 权限节点由注解动态扫描生成,跳过快照对齐');
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置对齐:按 (group, name) check-then-insert,只插缺失不覆盖 value;
|
||||
* 表无唯一键,判重纯靠应用层;发生过插入则清一次 sysconfig 缓存.
|
||||
*/
|
||||
protected function alignConfig(): void
|
||||
{
|
||||
$snapshot = include __DIR__ . '/adminInitData/SystemConfig.php';
|
||||
$now = time();
|
||||
$inserted = 0;
|
||||
|
||||
foreach ($snapshot as $row) {
|
||||
$exists = Db::name('system_config')
|
||||
->where('group', $row['group'])
|
||||
->where('name', $row['name'])
|
||||
->find();
|
||||
|
||||
if (!empty($exists)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $row;
|
||||
$data['create_time'] = $now;
|
||||
$data['update_time'] = $now;
|
||||
Db::name('system_config')->insert($data);
|
||||
++$inserted;
|
||||
}
|
||||
|
||||
$this->writeln(' - 系统配置新增 ' . $inserted . ' 条(缺失补齐,不覆盖已有 value)');
|
||||
|
||||
if ($inserted > 0) {
|
||||
TriggerServiceBase::updateSysconfig();
|
||||
$this->writeln(' - 已刷新系统设置缓存');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷入口对齐:按 href 只插缺失;软删行存在视为已存在,不复活.
|
||||
*/
|
||||
protected function alignQuick(): void
|
||||
{
|
||||
$snapshot = include __DIR__ . '/adminInitData/SystemQuick.php';
|
||||
$now = time();
|
||||
$inserted = 0;
|
||||
|
||||
foreach ($snapshot as $row) {
|
||||
// 原生查询不过滤软删除:软删的快捷入口仍占用 href 语义,视为已存在,不复活
|
||||
$exists = Db::name('system_quick')->where('href', $row['href'])->find();
|
||||
|
||||
if (!empty($exists)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $row;
|
||||
$data['create_time'] = $now;
|
||||
$data['update_time'] = $now;
|
||||
$data['delete_time'] = 0;
|
||||
Db::name('system_quick')->insert($data);
|
||||
++$inserted;
|
||||
}
|
||||
|
||||
$this->writeln(' - 快捷入口新增 ' . $inserted . ' 条(按 href 判重)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 商城演示对齐:分表守卫(三守卫相互独立):
|
||||
* - cate 空则插分类(快照无显式 id,捕获自增 id)
|
||||
* - goods 空则插商品,注入分类 id(复刻旧 initMall 逻辑)
|
||||
* - tag 空则插标签(快照含显式 id 1-4)
|
||||
*/
|
||||
protected function alignMall(): void
|
||||
{
|
||||
$now = time();
|
||||
|
||||
// 分类表守卫:空则插入快照分类,捕获新自增 id 供商品注入
|
||||
$cateId = 0;
|
||||
if ($this->isTableEmpty('mall_cate')) {
|
||||
$snapshotCate = include __DIR__ . '/adminInitData/MallCate.php';
|
||||
foreach ($snapshotCate as $row) {
|
||||
$data = $row;
|
||||
$data['create_time'] = $now;
|
||||
$data['update_time'] = $now;
|
||||
$data['delete_time'] = 0;
|
||||
Db::name('mall_cate')->insert($data);
|
||||
$cateId = (int) Db::name('mall_cate')->getLastInsID();
|
||||
$this->writeln(' - 新增商品分类 id=' . $cateId . ' ' . $row['title']);
|
||||
}
|
||||
} else {
|
||||
$this->writeln(' - 商品分类表已有数据,跳过');
|
||||
}
|
||||
|
||||
// 商品表守卫:空则插入快照商品,cate_id 注入分类 id(无快照分类时取库内首个分类)
|
||||
if ($this->isTableEmpty('mall_goods')) {
|
||||
if ($cateId === 0) {
|
||||
$cateId = (int) Db::name('mall_cate')->order('id', 'asc')->value('id');
|
||||
}
|
||||
|
||||
$snapshotGoods = include __DIR__ . '/adminInitData/MallGoods.php';
|
||||
foreach ($snapshotGoods as $row) {
|
||||
$data = $row;
|
||||
$data['cate_id'] = $cateId;
|
||||
$data['create_time'] = $now;
|
||||
$data['update_time'] = $now;
|
||||
$data['delete_time'] = 0;
|
||||
Db::name('mall_goods')->insert($data);
|
||||
$this->writeln(' - 新增商品 ' . $row['title'] . ' (cate_id=' . $cateId . ')');
|
||||
}
|
||||
} else {
|
||||
$this->writeln(' - 商品表已有数据,跳过');
|
||||
}
|
||||
|
||||
// 标签表守卫:空则插入快照标签(显式 id,快照行自带时间戳与 delete_time)
|
||||
if ($this->isTableEmpty('mall_tag')) {
|
||||
$snapshotTag = include __DIR__ . '/adminInitData/MallTag.php';
|
||||
foreach ($snapshotTag as $row) {
|
||||
Db::name('mall_tag')->insert($row);
|
||||
$this->writeln(' - 新增商品标签 id=' . $row['id'] . ' ' . $row['title']);
|
||||
}
|
||||
} else {
|
||||
$this->writeln(' - 商品标签表已有数据,跳过');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断表是否为空(原生查询,不受任何作用域影响).
|
||||
*/
|
||||
protected function isTableEmpty(string $table): bool
|
||||
{
|
||||
return Db::name($table)->count() == 0;
|
||||
}
|
||||
|
||||
protected function writeln(string $message): void
|
||||
{
|
||||
if ($this->output !== null) {
|
||||
$this->output->writeln($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
extend/base/admin/service/adminInitData/SystemAdmin.php
Normal file
24
extend/base/admin/service/adminInitData/SystemAdmin.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* @internal-framework
|
||||
*
|
||||
* 此文件为框架核心初始化数据
|
||||
*
|
||||
* 用途:超级管理员初始化(password 为 password('123456') 的 40 位字面量哈希,
|
||||
* 不得调用 password() 生成,保证迁移上下文可用且结果确定)
|
||||
* 维护者:框架维护者
|
||||
*
|
||||
* 注意:此文件属于框架内核,业务开发者不应修改
|
||||
*/
|
||||
|
||||
$ul_system_admin = array(
|
||||
array(
|
||||
"id" => 1,
|
||||
"head_img" => "/static/admin/images/head.jpg",
|
||||
"username" => "admin",
|
||||
"password" => "ac1e198122f74121a69b7bc9048561973a433daa",
|
||||
"status" => 1,
|
||||
)
|
||||
);
|
||||
|
||||
return $ul_system_admin;
|
||||
@@ -38,7 +38,7 @@ use think\facade\Db;
|
||||
* Mcp-Protocol-Version 用 initialize 返回的协商版本。
|
||||
*
|
||||
* ===== 超管创建者 =====
|
||||
* 不造 admin id=1(seed 已存在);用全新管理员 + 全新角色(避开 SystemAuthNode 60s
|
||||
* 不造 admin id=1(初始化迁移已创建);用全新管理员 + 全新角色(避开 SystemAuthNode 60s
|
||||
* 缓存与 adminInfo autoCache 窗口,learnings Task 4/6 的既定模式)。
|
||||
*
|
||||
* ===== 403 分支 =====
|
||||
|
||||
@@ -212,7 +212,7 @@ class McpServiceTest extends TestCase
|
||||
|
||||
public function test_drift_super_admin_creator_gets_full_whitelist(): void
|
||||
{
|
||||
// 超管(seed 的 id=1,auth_ids 空):判定链超管直通,白名单全保留
|
||||
// 超管(初始化迁移创建的 id=1,auth_ids 空):判定链超管直通,白名单全保留
|
||||
// (含未注册节点也放行——超管对全量节点可见)
|
||||
$fakeNode = 'nonexist.fake_node/action';
|
||||
$key = $this->createKey(1, [self::NODE_A, $fakeNode, self::NODE_B]);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# 测试数据库初始化
|
||||
|
||||
本目录提供测试数据库(默认 `ulthon_admin_test`)的初始化脚本。运行一次即可在与 `.env`
|
||||
同实例的 MySQL 上建出框架运行所需的**全部表**(系统表 + 业务表),并插入最小参考数据。
|
||||
同实例的 MySQL 上建出框架运行所需的**全部表**(系统表 + 业务表),并写入全部初始数据
|
||||
(超管、角色、系统配置、快捷入口、菜单等,由数据迁移随 `migrate:run` 一并交付)。
|
||||
|
||||
## 测试库策略:参数化,无需专用 Docker 容器
|
||||
|
||||
@@ -23,7 +24,7 @@ ulthon 是框架仓库,测试库策略**与衍生项目(wkbox)不同**:
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `setup_test_db.php` | 真正的运行器(PHP)。启动 ThinkPHP 容器、读 `.env`、PDO 建测试库、config 级覆盖连接、按序调用 `migrate:run` / `scheme:sync` / `seed:run`、查 `information_schema` 自验证。 |
|
||||
| `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 跑的测试库与本脚本初始化的不是同一个。 |
|
||||
@@ -61,10 +62,12 @@ ulthon 的建表有**两个独立的来源**,二者不能互相替代:
|
||||
- **只跑 `scheme:sync`**:得到业务表,但没有系统表,框架无法登录、无法鉴权。
|
||||
- **必须两个都跑**,业务表通常引用系统表(如外键关联 `system_admin`)。
|
||||
|
||||
`seed:run` 负责最小参考数据(超级管理员、权限节点、系统设置、菜单、快捷入口等),由
|
||||
`database/seeds/InitBaseAdminData.php` 提供,自带 install-lock,重复执行安全跳过。
|
||||
初始数据不再走 seed:`migrate:run` 建表的同时由数据迁移(`20260909130000_system_init_data.php`)
|
||||
写入全部初始数据(超级管理员、角色、系统配置、快捷入口、商城演示、菜单等),数据源自
|
||||
`extend/base/admin/service/adminInitData/` 快照。seed 机制已退役,`seed:run` 为空操作,
|
||||
仅保留给下游业务项目放自己的种子。
|
||||
|
||||
本脚本在**同一进程**内依次调用这三条命令(见下节),确保它们都指向测试库。
|
||||
本脚本在**同一进程**内依次调用这两条命令(见下节),确保它们都指向测试库。
|
||||
|
||||
## 技术细节:为什么用 config 级覆盖,而不是环境变量
|
||||
|
||||
@@ -79,7 +82,7 @@ ThinkPHP 的 `Env` 组件加载 `.env` 文件,且 **`.env` 的值优先于 OS
|
||||
|
||||
**唯一可靠**的覆盖点是 **config 级**:在 `App::initialize()` 之后,直接改写
|
||||
`config/database.php` 解析出的连接配置数组。因为 `Db` facade 运行时读 config,
|
||||
config 被改写后,后续所有 DB 操作(包括 `migrate:run` / `scheme:sync` / `seed:run`
|
||||
config 被改写后,后续所有 DB 操作(包括 `migrate:run` / `scheme:sync`
|
||||
内部)都连到测试库。
|
||||
|
||||
`setup_test_db.php` 的核心步骤:
|
||||
@@ -122,7 +125,6 @@ if (stripos(\think\facade\Config::get('database.connections.main.database'), 'te
|
||||
// 同进程内调用 think 命令,config 覆盖对它们同样生效
|
||||
\think\facade\Console::call('migrate:run', ['--force-force']);
|
||||
\think\facade\Console::call('scheme:sync', ['--force-force']); // --force-force 跳过交互确认
|
||||
\think\facade\Console::call('seed:run', ['--force-force']);
|
||||
```
|
||||
|
||||
### 关于 `--force-force`(`-ff`)
|
||||
@@ -131,7 +133,7 @@ if (stripos(\think\facade\Config::get('database.connections.main.database'), 'te
|
||||
数据库吗?")。非 TTY 环境(脚本、CI)下 `think\console\Output::confirm` 会抛
|
||||
"Aborted" 而失败。框架提供了全局 `--force-force` / `-ff` 选项(见
|
||||
`extend/base/common/provider/ConsoleBase.php`、`extend/base/common/console/OutputBase.php`),
|
||||
命中时 `confirm()` 直接返回默认值,跳过所有交互确认。本脚本对三条命令统一带上该参数,
|
||||
命中时 `confirm()` 直接返回默认值,跳过所有交互确认。本脚本对两条命令统一带上该参数,
|
||||
保证非交互可运行、可重跑。
|
||||
|
||||
### 关于默认连接名 `main`
|
||||
@@ -144,9 +146,8 @@ if (stripos(\think\facade\Config::get('database.connections.main.database'), 'te
|
||||
|
||||
脚本可安全重复执行:
|
||||
|
||||
- `migrate:run`:已执行的迁移记录在 `ul_migrations`(phinxlog),二次运行直接 "All Done" 无操作。
|
||||
- `migrate:run`:已执行的迁移记录在 `ul_migrations`(phinxlog),二次运行直接 "All Done" 无操作(初始数据由迁移交付,同样只执行一次且幂等)。
|
||||
- `scheme:sync`:无差异时打印 "未检测到 Scheme 变更";有差异时先备份原表(`ul_backup_<时间戳>_<表名>`)再改,不报错。
|
||||
- `seed:run`:`InitBaseAdminData` 自带 install-lock(`base_admin_install` 配置项),二次运行打印 "系统已初始化,跳过当前程序"。
|
||||
- `CREATE DATABASE IF NOT EXISTS`:库已存在时无操作。
|
||||
|
||||
> 注:`scheme:sync` 每次遇到差异都会生成 `ul_backup_*` 备份表,这是框架的设计行为
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
* - 不 require 全局 common.php(App::initialize() + autoload 已够引导 facade,
|
||||
* helper.php 全局函数测试基建用不到)
|
||||
* - 不开事务(事务隔离由 TestCaseBase.setUp 负责)
|
||||
* - 不跑 migrate/scheme/seed(由独立的 setup 脚本负责)
|
||||
* - 不跑 migrate/scheme(由独立的 setup 脚本负责)
|
||||
* - 不销毁容器(容器跨测试复用,tearDown 只回滚事务)
|
||||
*/
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
/**
|
||||
* 测试数据库初始化脚本(参数化版).
|
||||
*
|
||||
* 在与 .env 同实例的 MySQL 上建出 ulthon_admin_test 库,并初始化全部表:
|
||||
* 1. migrate:run 建系统/框架表(database/migrations/,如 ul_system_admin / ul_system_menu)
|
||||
* 在与 .env 同实例的 MySQL 上建出 ulthon_admin_test 库,并初始化全部表与初始数据:
|
||||
* 1. migrate:run 建系统/框架表(database/migrations/,如 ul_system_admin / ul_system_menu),
|
||||
* 并写入全部初始数据(超管、配置、菜单等,数据迁移随迁移一同执行)
|
||||
* 2. scheme:sync 建业务表(app/admin/scheme/)
|
||||
* 3. seed:run 插入最小参考数据(database/seeds/InitBaseAdminData)
|
||||
*
|
||||
* 与 wkbox 衍生项目的区别:
|
||||
* - 完全参数化:连接参数(hostname/hostport/username/password/charset/prefix/database)
|
||||
@@ -19,7 +19,7 @@
|
||||
* 都无法覆盖已存在的 .env 键,唯一可靠覆盖点是 config 级。
|
||||
* - think\Config::set 的第二参数只支持单段一级名,不能传 'database.connections.main'。
|
||||
*
|
||||
* 幂等:migrate:run / scheme:sync / seed:run 均可安全重复执行(详见 README)。
|
||||
* 幂等:migrate:run / scheme:sync 均可安全重复执行(详见 README)。
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
@@ -135,12 +135,10 @@ if (!is_string($resolvedDb) || stripos($resolvedDb, 'test') === false) {
|
||||
// scheme:sync 在非 TTY 下不带它会被 parent::confirm 抛 "Aborted"。
|
||||
// ----------------------------------------------------------------------------
|
||||
$commands = [
|
||||
// 系统/框架表(Phinx 迁移):ul_system_admin / ul_system_menu / ul_system_auth_node / ul_system_config / ...
|
||||
['migrate:run', '系统/框架表(database/migrations/)'],
|
||||
// 系统/框架表 + 全部初始数据(Phinx 迁移):ul_system_admin / ul_system_menu / ul_system_config / ...
|
||||
['migrate:run', '系统/框架表 + 初始数据(database/migrations/)'],
|
||||
// 业务表(Scheme 同步):app/admin/scheme/ 下声明的表
|
||||
['scheme:sync', '业务表(app/admin/scheme/)'],
|
||||
// 参考数据(seed):InitBaseAdminData,自带 install-lock 跳过,可安全重跑
|
||||
['seed:run', '参考数据(database/seeds/)'],
|
||||
];
|
||||
|
||||
$handleProp = (new \ReflectionProperty(\think\console\Output::class, 'handle'));
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
(e.g. ulthon_admin -> ulthon_admin_test)
|
||||
3. CREATE DATABASE IF NOT EXISTS on the same MySQL instance (no Docker)
|
||||
4. config-level override connections.main -> test DB
|
||||
5. php think migrate:run -> system/framework tables (database/migrations/)
|
||||
5. php think migrate:run -> system/framework tables + all initial data
|
||||
(database/migrations/)
|
||||
6. php think scheme:sync -> business tables (app/admin/scheme/)
|
||||
7. php think seed:run -> reference data (database/seeds/)
|
||||
8. queries information_schema to verify the tables landed in the test DB
|
||||
7. queries information_schema to verify the tables landed in the test DB
|
||||
|
||||
Fully parameterized: no hardcoded host/port/credentials. The MySQL instance
|
||||
is whatever .env points at (typically a single local MySQL).
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
# 2. 派生测试库名 = {env.database} + '_test'(如 ulthon_admin -> ulthon_admin_test)
|
||||
# 3. 在同一 MySQL 实例上 CREATE DATABASE IF NOT EXISTS(无需专用 Docker 容器)
|
||||
# 4. config 级覆盖 connections.main 指向测试库
|
||||
# 5. php think migrate:run 建系统/框架表(database/migrations/)
|
||||
# 5. php think migrate:run 建系统/框架表 + 全部初始数据(database/migrations/)
|
||||
# 6. php think scheme:sync 建业务表(app/admin/scheme/)
|
||||
# 7. php think seed:run 插参考数据(database/seeds/)
|
||||
# 8. 查 information_schema 验证表已落到测试库
|
||||
# 7. 查 information_schema 验证表已落到测试库
|
||||
#
|
||||
# 完全参数化:不硬编码 host/port/credentials。MySQL 实例 = .env 指向的那个。
|
||||
# 详见 tests/README.md。
|
||||
|
||||
Reference in New Issue
Block a user