🗄️ 插件数据库

ThinkAdmin 的插件数据库管理基于 Phinx 迁移脚本。插件通常把迁移文件放在 stc/database,安装时复制到项目 database/migrations,再由 migrate:run 执行。

📚 基础概念

🤔 基本介绍

插件数据库管理用于声明插件需要的数据表、初始化数据和全局菜单。它解决的是“安装或升级插件时,如何让目标项目获得必要数据库结构”的问题。

传统方式(不推荐)

-- ❌ 传统方式:手动执行 SQL 脚本
-- 1. 开发环境:手动执行 SQL
CREATE TABLE `system_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 2. 生产环境:再次手动执行 SQL(容易出错)
-- 可能忘记执行某些 SQL,导致数据库结构不一致

使用数据库迁移(推荐)

// ✅ 使用数据库迁移:自动管理数据库结构
// 1. 创建迁移脚本
php think xadmin:package

// 2. 执行迁移(自动执行所有未执行的脚本)
php think migrate:run

// 3. 数据库结构自动同步,无需手动操作

🤔 使用优势

问题1:数据库结构不一致

不使用数据库迁移时,不同环境的数据库结构可能不一致:

-- ❌ 开发环境有某个字段,生产环境没有
-- 开发环境:ALTER TABLE user ADD COLUMN email VARCHAR(100);
-- 生产环境:忘记执行,导致程序出错

问题2:无法追踪数据库变更

不使用数据库迁移时,无法追踪数据库结构的变更历史:

-- ❌ 不知道数据库结构什么时候改的
-- 不知道改了什么
-- 无法回滚到之前的版本

问题3:团队协作困难

不使用数据库迁移时,团队协作困难:

-- ❌ 团队成员需要手动同步数据库结构
-- 容易出错,导致数据库结构不一致

使用数据库迁移的优势

// ✅ 统一管理,自动同步
// 1. 创建迁移脚本
// 2. 提交到代码仓库
// 3. 团队成员拉取代码后,执行迁移即可
php think migrate:run

🎯 核心功能

功能1:版本控制

管理数据库结构的版本,确保结构一致性:

// 每个迁移脚本都有版本号
// 20241010000005_install_account20241010.php
// 版本号:20241010000005
// 脚本名:install_account20241010

功能2:自动迁移

自动执行数据库结构变更,无需手动操作:

// 执行所有未执行的迁移脚本
php think migrate:run
// 自动检测并执行新的迁移脚本

功能3:回滚支持

Phinx 支持回滚命令,但是否可安全回滚取决于迁移脚本是否提供可逆操作。当前 ThinkAdmin 插件迁移多以 change() 和幂等写入为主,生产环境回滚前需要先评估脚本内容:

// 回滚最后一次迁移
php think migrate:rollback
// 尝试回滚最后一次数据库结构变更

功能4:迁移与生成脚本分工

Phinx 负责执行迁移脚本,ThinkAdmin 另外提供 xadmin:package 用于从现有数据库生成迁移脚本:

# 执行已有迁移脚本
php think migrate:run

# 从现有 MySQL 数据库生成迁移脚本
php think xadmin:package

📖 相关概念

Phinx

  • PHP 数据库迁移工具
  • 基于 ThinkPHP 的 think-migration 组件
  • 支持数据库结构的版本控制

迁移脚本

  • 数据库结构变更的脚本文件
  • 位于 database/migrations/ 目录
  • 命名格式:版本号_脚本名称.php

版本号

  • 14 位数字,通常使用年月日时分秒格式
  • 例如:20241010000005
  • 确保版本号的唯一性和可排序性

脚本名称

  • 容易区别功能的组合单词
  • 例如:install_accountupdate_user_table
  • 类名首字母大写:InstallAccountUpdateUserTable

🚀 主要功能

  • 版本控制: 基于 Phinx 的数据库版本控制
  • 自动迁移: 通过 php think migrate:run 执行未执行的迁移脚本
  • 脚本生成: 通过 php think xadmin:package 从现有 MySQL 数据库生成结构与数据脚本
  • 规范管理: 统一的数据库脚本管理规范
  • 插件集成: 与 ThinkPHP 插件机制无缝对接
  • 开发简化: 简化数据库迁移过程

📋 支持的数据库

迁移执行

  • php think migrate:run 使用 think-migration/Phinx 执行迁移脚本,具体数据库支持取决于项目数据库配置、Phinx 适配器和脚本自身写法。

脚本生成

  • php think xadmin:package 会读取现有数据库结构并生成迁移文件。
  • 当前实现仅支持 MySQL;在非 MySQL 连接下会提示“只支持 MySql 数据库生成数据库脚本”。
  • 生成脚本时会用到 information_schemaSHOW INDEX 等 MySQL 结构信息。

关于数据库脚本

为了确保数据库脚本文件管理的规范性和准确性,对数据库脚本的命名规则、结构以及执行机制进行了进一步的优化。

管理规范明确规定了数据库脚本文件的命名规则。每个脚本文件都由版本号和脚本名称两部分组成,确保每个脚本文件具有唯一的标识。手写迁移脚本时,版本号通常使用 14 位数字按年月日时分秒组织;通过 xadmin:package 自动生成脚本时,版本号由工具按已有迁移文件计算,不等同于当前时间。脚本名称则取容易区别其功能的组合单词,以便于理解和维护。

每个脚本文件都是一个继承自 think\migration\Migrator 的子类,主要实现其中的 change 方法。change 方法是脚本的核心部分,包含了数据库结构的变更逻辑。开发者可以根据需要自定义该方法,以实现各种复杂的数据库迁移操作。同时也提供了更多高级用法的文档和示例,供开发者自行研究和使用。

脚本的执行信息会记录在数据表 migrations 中,以便跟踪迁移状态。如果需要重新执行脚本,不能只依赖删除 migrations 记录,还需要确认脚本本身具备幂等能力,例如先检查表、字段或菜单是否已存在。

通过一个具体的案例来解析命名规则的实际应用。例如,文件名 20241010000005_install_account20241010.php 对应的版本号是 20241010000005,脚本名称为 install_account20241010。根据命名规则,该脚本内的类名应为 InstallAccount20241010,类名首字母大写,与脚本名称保持一致,符合 PHP 的命名规范。

生成数据库脚本

目前在核心组件 ThinkLibrary 内集成了 php think xadmin:package 指令,可以将现有 MySQL 数据库打包为数据库脚本,打包好的文件存入在 database/migrations 目录。 打包好后的文件可能还需要后期修改,注意不能出现版本号重复和类名重复。xadmin:package 会按脚本类名生成文件名:扫描 database/migrations 后取现有最小版本号再减一,并删除同类名旧脚本及对应的数据目录,提交前仍应检查生成结果是否符合插件安装需要。

数据库脚本里面需要做好执行前置检查逻辑,尽量保证同一个脚本重复运行不会导致系统出错。迁移记录可以防止正常流程下重复执行,但手动清理迁移记录、复制脚本或调整版本号后仍可能再次运行。

系统也可以通过这个指令生成数据安装包,支持数据结构与指定数据表内容打包。命令每次都会先生成数据安装包脚本,再生成结构脚本;--table--backup 只控制各自的表范围,并不会关闭另一类脚本生成。可通过命令选项直接指定表,也可以通过项目的 config/phinx.php 配置控制默认范围:

  • --table-t:指定要生成结构脚本的数据表,多个表可用逗号或竖线分隔;
  • --backup-b:指定要写入数据安装包的数据表,多个表可用逗号或竖线分隔;
  • --all-a:结构和数据都按全部数据表处理;
  • --force-f:生成结构脚本时传入强制更新标记,仅影响 PhinxExtend::upgrade() 的第四个参数;
  • 配置 phinx.tables 数组,表示默认要生成结构脚本的数据表,不填写时默认读取全部表;
  • 配置 phinx.backup 数组,表示默认要写入数据安装包的数据表,不填写时默认不打包业务数据;
  • 配置 phinx.ignore 数组,表示要忽略的数据表;结构脚本始终忽略 migrations,数据打包在未配置 phinx.ignore 时还会默认忽略 system_queuesystem_oplog

经验教训: 以往我们做过很多的项目,经常忘记将数据库打包备份落地到项目仓库,项目交付经过一段时间停止运维后,线上数据库和本地开发数据库可能都已经丢失,只剩下仓库里面的项目代码,此时这个项目就失去了重复利用的价值,没有数据库项目是跑不起来的,因此我们开发项目的时候需要经常备份数据库结构并与项目保存到一起。

备份所有数据: 可以通过 php think xadmin:package --all 备份所有数据 ( 数据库表结构和数据记录 )。生成的数据包迁移脚本不会覆盖已有记录:只有目标数据表为空时才导入对应 .data 文件;系统配置、默认用户和菜单也分别在对应表为空时才初始化。恢复前仍建议使用空库或已备份库。

操作演示截图

封装插件数据库

将上面生成的数据库脚本文件复制到插件目录,建议不要与源码 src 放到一个目录,防止 Composerautoload 扫描到而导致加载规则污染。

推荐目录结构:

plugin/think-plugs-account/
├── src/                    # 插件源码
├── stc/                    # 静态资源(可选)
│   └── database/          # 数据库脚本目录,安装时复制到 database/migrations
│       └── 20241010000005_install_account20241010.php
├── composer.json
└── readme.md

安装应用插件时通过 插件安装器stc/database 复制到项目 database/migrations 目录,执行 php think migrate:run 完成数据库安装与更新。

🔧 脚本示例

创建数据表(使用 PhinxExtend)

推荐使用 PhinxExtend::upgrade() 方法创建或更新数据表。它在表不存在时创建表和索引;表已存在时,只有第四个参数为 true 才会更新已有字段、补充缺失字段并同步索引,不会自动删除多余字段:

<?php
use think\admin\extend\PhinxExtend;
use think\migration\Migrator;

class InstallAdmin20241010 extends Migrator
{
    /**
     * 获取脚本名称
     * @return string
     */
    public function getName(): string
    {
        return 'AdminPlugin';
    }

    /**
     * 数据库变更
     */
    public function change()
    {
        $this->_create_system_auth();
        $this->_create_system_user();
    }

    /**
     * 创建权限表
     */
    private function _create_system_auth()
    {
        $table = $this->table('system_auth', [
            'engine' => 'InnoDB',
            'collation' => 'utf8mb4_general_ci',
            'comment' => '系统-权限',
        ]);
        
        // 使用 PhinxExtend::upgrade() 自动创建或更新表结构
        PhinxExtend::upgrade($table, [
            ['title', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '权限名称']],
            ['utype', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => '身份权限']],
            ['desc', 'string', ['limit' => 500, 'default' => '', 'null' => true, 'comment' => '备注说明']],
            ['sort', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '排序权重']],
            ['status', 'integer', ['limit' => 1, 'default' => 1, 'null' => true, 'comment' => '权限状态(1使用,0禁用)']],
            ['create_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'null' => true, 'comment' => '创建时间']],
        ], [
            'sort', 'title', 'status',  // 索引字段
        ], true);  // true 表示表已存在时更新字段和索引;false 表示表已存在时跳过
    }

    /**
     * 创建用户表
     */
    private function _create_system_user()
    {
        $table = $this->table('system_user', [
            'engine' => 'InnoDB',
            'collation' => 'utf8mb4_general_ci',
            'comment' => '系统-用户',
        ]);
        
        PhinxExtend::upgrade($table, [
            ['username', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => '登录账号']],
            ['password', 'string', ['limit' => 32, 'default' => '', 'null' => true, 'comment' => '登录密码']],
            ['nickname', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => '用户昵称']],
            ['authorize', 'text', ['null' => true, 'comment' => '权限授权']],
            ['status', 'integer', ['limit' => 1, 'default' => 1, 'null' => true, 'comment' => '用户状态(1使用,0禁用)']],
            ['is_deleted', 'integer', ['limit' => 1, 'default' => 0, 'null' => true, 'comment' => '删除状态(0未删,1已删)']],
            ['create_at', 'timestamp', ['default' => 'CURRENT_TIMESTAMP', 'null' => true, 'comment' => '创建时间']],
        ], [
            'username', 'status', 'create_at',  // 索引字段
        ], true);
    }
}

PhinxExtend::upgrade() 方法说明:

PhinxExtend::upgrade(
    $table,           // 表对象
    $columns,         // 字段定义数组
    $indexes = [],    // 索引定义数组
    $force = false    // 表已存在时是否强制更新字段和索引
);

索引定义可以是单字段字符串、多字段数组,也可以是包含 columnsuniquelimitname 等选项的数组。未指定索引名时会自动生成 idx_uni_ 前缀的名称;表已存在且 $force = true 时,会删除同名或同列但定义不一致的索引后重新创建,不会删除多余字段。

字段定义格式:

[
    '字段名',
    '字段类型',  // string, integer, biginteger, text, timestamp, datetime 等
    [
        'limit' => 100,           // 字段长度
        'default' => '',           // 默认值
        'null' => true,            // 是否允许 NULL
        'comment' => '字段说明'    // 字段注释
    ]
]

创建数据表(传统方式)

如果不使用 PhinxExtend,也可以使用传统方式:

<?php
use think\migration\Migrator;
use think\migration\db\Column;

class InstallAccount extends Migrator
{
    /**
     * 数据库变更
     */
    public function change()
    {
        // 创建用户账号表
        $table = $this->table('account_user', [
            'engine' => 'InnoDB',
            'comment' => '用户账号表',
            'charset' => 'utf8mb4',
            'collation' => 'utf8mb4_unicode_ci'
        ]);
        
        $table->addColumn('username', 'string', ['limit' => 50, 'comment' => '用户名'])
              ->addColumn('password', 'string', ['limit' => 32, 'comment' => '密码'])
              ->addColumn('nickname', 'string', ['limit' => 50, 'null' => true, 'comment' => '昵称'])
              ->addColumn('phone', 'string', ['limit' => 20, 'null' => true, 'comment' => '手机号'])
              ->addColumn('email', 'string', ['limit' => 100, 'null' => true, 'comment' => '邮箱'])
              ->addColumn('status', 'integer', ['limit' => 1, 'default' => 1, 'comment' => '状态'])
              ->addColumn('create_at', 'datetime', ['null' => true, 'comment' => '创建时间'])
              ->addColumn('update_at', 'datetime', ['null' => true, 'comment' => '更新时间'])
              ->addIndex(['username'], ['unique' => true, 'name' => 'idx_username'])
              ->addIndex(['phone'], ['name' => 'idx_phone'])
              ->create();
    }
}

修改数据表结构

<?php
use think\migration\Migrator;
use think\migration\db\Column;

class UpdateAccountTable extends Migrator
{
    /**
     * 数据库变更
     */
    public function change()
    {
        $table = $this->table('account_user');
        
        // 添加新字段
        $table->addColumn('avatar', 'string', ['limit' => 255, 'null' => true, 'comment' => '头像'])
              ->addColumn('gender', 'integer', ['limit' => 1, 'default' => 0, 'comment' => '性别'])
              ->update();
        
        // 修改字段
        $table->changeColumn('phone', 'string', ['limit' => 20, 'null' => true, 'comment' => '手机号码'])
              ->update();
    }
}

插入初始数据

<?php
use think\migration\Migrator;
use think\admin\extend\PhinxExtend;

class InstallAccountData extends Migrator
{
    /**
     * 数据库变更
     */
    public function change()
    {
        if ($this->fetchRow('select id from account_user limit 1') === false) {
            $this->table('account_user')->insert([
                [
                    'username' => 'admin',
                    'password' => md5('admin'),
                    'nickname' => '管理员',
                    'status' => 1,
                    'create_at' => date('Y-m-d H:i:s'),
                ],
            ])->save();
        }

        PhinxExtend::write2menu([
            [
                'name' => '账号管理',
                'subs' => [
                    ['name' => '用户列表', 'node' => 'plugin-account/master/index'],
                    ['name' => '短信管理', 'node' => 'plugin-account/message/index'],
                ],
            ],
        ], [
            'url|node' => 'plugin-account/master/index',
        ]);
    }
}

PhinxExtend::write2menu() 方法说明:

该方法用于将菜单数据写入 system_menu 数据表,通常在数据库迁移脚本中调用:

// 参数 array $menus 菜单数据数组
// 参数 mixed $exists 写入前的存在性检查条件
// 返回 bool 是否执行写入
PhinxExtend::write2menu(array $menus, $exists = []): bool

如果 $exists 不为空且能在 system_menu 中查到记录,方法会返回 false 并跳过写入;查询异常时同样返回 false。写入时最多处理三层菜单,字段会映射为 pidtitleurlnodeiconparamstargetsort,其中 urlnode 会互相兜底,target 默认 _self,不会写入 status

菜单数据格式:

[
    [
        'name' => '菜单分组名称',
        'subs' => [
            [
                'name' => '菜单项名称',
                'icon' => 'layui-icon layui-icon-set',  // 图标(可选)
                'node' => 'admin/config/index',         // 权限节点
            ],
            // 更多菜单项...
        ],
    ],
    // 更多菜单分组...
]

实际应用示例:

// 在数据库迁移脚本中调用插件的 Service 类获取菜单
use app\admin\Service as AdminService;

public function change()
{
    PhinxExtend::write2menu([
        [
            'name' => '系统管理',
            'sort' => '100',
            'subs' => AdminService::menu(),
        ],
    ], [
        'url|node' => 'admin/config/index',
    ]);
}

执行前置检查

<?php
use think\migration\Migrator;
use think\migration\db\Column;

class InstallAccount extends Migrator
{
    /**
     * 数据库变更
     */
    public function change()
    {
        // 检查表是否已存在
        if (!$this->hasTable('account_user')) {
            $table = $this->table('account_user');
            // ... 创建表结构
            $table->create();
        } else {
            // 表已存在,只添加缺失的字段
            $table = $this->table('account_user');
            
            if (!$table->hasColumn('avatar')) {
                $table->addColumn('avatar', 'string', ['limit' => 255, 'null' => true])
                      ->update();
            }
        }
    }
}

📋 常用命令

# 生成数据库脚本(从现有数据库)
php think xadmin:package

# 备份所有数据(结构和数据)
php think xadmin:package --all

# 指定结构脚本的数据表;数据安装包脚本仍会按默认规则生成
php think xadmin:package --table system_user,system_auth

# 指定数据安装包的数据表;结构脚本仍会按默认规则生成
php think xadmin:package --backup system_config,system_menu

# 强制生成可更新已有表结构的脚本
php think xadmin:package --force

# 执行数据库迁移
php think migrate:run

# 回滚最后一次迁移
php think migrate:rollback

# 查看迁移状态
php think migrate:status
最近更新:
Contributors: 邹景立, Anyon