mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-07-01 07:22:49 +08:00
feat(scheme): 新增数据库表结构同步方案
This commit is contained in:
121
app/admin/scheme/TestGoods.php
Normal file
121
app/admin/scheme/TestGoods.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace app\admin\scheme;
|
||||
|
||||
use app\common\scheme\BaseScheme;
|
||||
use app\common\scheme\attribute\Table;
|
||||
use app\common\scheme\attribute\Field;
|
||||
use app\common\scheme\attribute\Component;
|
||||
use app\common\scheme\attribute\Index;
|
||||
|
||||
#[Table(name: 'ul_test_goods', comment: '')]
|
||||
#[Index(columns: ['uid'], name: 'uid', type: 'UNIQUE')]
|
||||
#[Index(columns: ['cate_id'], name: 'cate_id', type: 'NORMAL')]
|
||||
#[Index(columns: ['detail'], name: 'detail', type: 'FULLTEXT')]
|
||||
class TestGoods extends BaseScheme
|
||||
{
|
||||
#[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: null, comment: '', unsigned: true, autoIncrement: true, primary: true)]
|
||||
public $id;
|
||||
|
||||
#[Field(type: 'bigint', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '分类ID', unsigned: true, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'relation', options: ['table' => 'mall_cate', 'relationBindSelect' => 'title'])]
|
||||
public $cate_id;
|
||||
|
||||
#[Field(type: 'char', length: 20, precision: 20, scale: 0, nullable: false, default: '', comment: '商品名称', unsigned: false, autoIncrement: false, primary: false)]
|
||||
public $title;
|
||||
|
||||
#[Field(type: 'char', length: 255, precision: 255, scale: 0, nullable: false, default: null, comment: '商品logo', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'image', options: [])]
|
||||
public $logo;
|
||||
|
||||
#[Field(type: 'text', length: null, precision: 0, scale: 0, nullable: false, default: null, comment: '商品图片', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'images', options: [])]
|
||||
public $images;
|
||||
|
||||
#[Field(type: 'text', length: null, precision: 0, scale: 0, nullable: false, default: null, comment: '商品描述', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'editor', options: [])]
|
||||
public $describe;
|
||||
|
||||
#[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '总库存', unsigned: true, autoIncrement: false, primary: false)]
|
||||
public $total_stock;
|
||||
|
||||
#[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '排序', unsigned: true, autoIncrement: false, primary: false)]
|
||||
public $sort;
|
||||
|
||||
#[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '状态', unsigned: true, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'radio', options: ['正常', '禁用'])]
|
||||
public $status;
|
||||
|
||||
#[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '合格证', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'file', options: [])]
|
||||
public $cert_file;
|
||||
|
||||
#[Field(type: 'text', length: null, precision: 0, scale: 0, nullable: false, default: null, comment: '检测报告', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'files', options: [])]
|
||||
public $verfiy_file;
|
||||
|
||||
#[Field(type: 'char', length: 255, precision: 255, scale: 0, nullable: false, default: '', comment: '备注说明', unsigned: false, autoIncrement: false, primary: false)]
|
||||
public $remark;
|
||||
|
||||
#[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '', unsigned: true, autoIncrement: false, primary: false)]
|
||||
public $create_time;
|
||||
|
||||
#[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '', unsigned: true, autoIncrement: false, primary: false)]
|
||||
public $update_time;
|
||||
|
||||
#[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '', unsigned: true, autoIncrement: false, primary: false)]
|
||||
public $delete_time;
|
||||
|
||||
#[Field(type: 'datetime', length: null, precision: 0, scale: 0, nullable: false, default: null, comment: '发布日期', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'date', options: ['date'])]
|
||||
public $publish_time;
|
||||
|
||||
#[Field(type: 'date', length: null, precision: 0, scale: 0, nullable: false, default: null, comment: '售卖日期', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'date', options: ['datetime'])]
|
||||
public $sale_time;
|
||||
|
||||
#[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '简介', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'textarea', options: [])]
|
||||
public $intro;
|
||||
|
||||
#[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: null, comment: '秒杀状态', unsigned: true, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'select', options: [0 => '未参加', 1 => '已开始', 3 => '已结束'])]
|
||||
public $time_status;
|
||||
|
||||
#[Field(type: 'int', length: 11, precision: 0, scale: 0, nullable: false, default: '0', comment: '是否推荐', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'switch', options: ['不推荐', '推荐'])]
|
||||
public $is_recommend;
|
||||
|
||||
#[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: '0', comment: '商品类型', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'checkbox', options: ['taobao' => '淘宝', 'jd' => '京东'])]
|
||||
public $shop_type;
|
||||
|
||||
#[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '商品标签', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'table', options: ['table' => 'mall_tag', 'type' => 'checkbox', 'valueField' => 'id', 'fieldName' => 'title'])]
|
||||
public $tag;
|
||||
|
||||
#[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: true, default: null, comment: '商品标签(单选)', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'table', options: ['table' => 'mall_tag', 'type' => 'radio', 'valueField' => 'id', 'fieldName' => 'title'])]
|
||||
public $tag_backup;
|
||||
|
||||
#[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '产地', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'city', options: ['name-province' => '0', 'code' => '0'])]
|
||||
public $from_area;
|
||||
|
||||
#[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: '山东省/临沂市', comment: '仓库', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'city', options: ['level' => 'city'])]
|
||||
public $store_city;
|
||||
|
||||
#[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '商品标签 (输入)', unsigned: false, autoIncrement: false, primary: false)]
|
||||
#[Component(type: 'tag', options: [])]
|
||||
public $tag_input;
|
||||
|
||||
#[Field(type: 'varchar', length: 100, precision: 100, scale: 0, nullable: false, default: null, comment: '唯一id', unsigned: false, autoIncrement: false, primary: false)]
|
||||
public $uid;
|
||||
|
||||
#[Field(type: 'decimal', length: 10, precision: 10, scale: 0, nullable: true, default: null, comment: '价格', unsigned: false, autoIncrement: false, primary: false)]
|
||||
public $price;
|
||||
|
||||
#[Field(type: 'text', length: null, precision: 0, scale: 0, nullable: true, default: null, comment: '详情', unsigned: false, autoIncrement: false, primary: false)]
|
||||
public $detail;
|
||||
}
|
||||
9
app/common/command/scheme/Backup.php
Normal file
9
app/common/command/scheme/Backup.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\command\scheme;
|
||||
|
||||
use base\common\command\scheme\Backup as BaseCommand;
|
||||
|
||||
class Backup extends BaseCommand
|
||||
{
|
||||
}
|
||||
9
app/common/command/scheme/Make.php
Normal file
9
app/common/command/scheme/Make.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\command\scheme;
|
||||
|
||||
use base\common\command\scheme\Make as BaseCommand;
|
||||
|
||||
class Make extends BaseCommand
|
||||
{
|
||||
}
|
||||
9
app/common/command/scheme/Sync.php
Normal file
9
app/common/command/scheme/Sync.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\command\scheme;
|
||||
|
||||
use base\common\command\scheme\Sync as BaseCommand;
|
||||
|
||||
class Sync extends BaseCommand
|
||||
{
|
||||
}
|
||||
9
app/common/scheme/BaseScheme.php
Normal file
9
app/common/scheme/BaseScheme.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\scheme;
|
||||
|
||||
use base\common\scheme\BaseScheme as Base;
|
||||
|
||||
abstract class BaseScheme extends Base
|
||||
{
|
||||
}
|
||||
11
app/common/scheme/attribute/Component.php
Normal file
11
app/common/scheme/attribute/Component.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\scheme\attribute;
|
||||
|
||||
use Attribute;
|
||||
use base\common\scheme\attribute\Component as BaseComponent;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PROPERTY)]
|
||||
class Component extends BaseComponent
|
||||
{
|
||||
}
|
||||
11
app/common/scheme/attribute/Field.php
Normal file
11
app/common/scheme/attribute/Field.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\scheme\attribute;
|
||||
|
||||
use Attribute;
|
||||
use base\common\scheme\attribute\Field as BaseField;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PROPERTY)]
|
||||
class Field extends BaseField
|
||||
{
|
||||
}
|
||||
11
app/common/scheme/attribute/Index.php
Normal file
11
app/common/scheme/attribute/Index.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\scheme\attribute;
|
||||
|
||||
use Attribute;
|
||||
use base\common\scheme\attribute\Index as BaseIndex;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
|
||||
class Index extends BaseIndex
|
||||
{
|
||||
}
|
||||
11
app/common/scheme/attribute/Table.php
Normal file
11
app/common/scheme/attribute/Table.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\scheme\attribute;
|
||||
|
||||
use Attribute;
|
||||
use base\common\scheme\attribute\Table as BaseTable;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Table extends BaseTable
|
||||
{
|
||||
}
|
||||
9
app/common/service/scheme/DbToSchemeService.php
Normal file
9
app/common/service/scheme/DbToSchemeService.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\scheme;
|
||||
|
||||
use base\common\service\scheme\DbToSchemeService as BaseService;
|
||||
|
||||
class DbToSchemeService extends BaseService
|
||||
{
|
||||
}
|
||||
9
app/common/service/scheme/SchemeToDbService.php
Normal file
9
app/common/service/scheme/SchemeToDbService.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace app\common\service\scheme;
|
||||
|
||||
use base\common\service\scheme\SchemeToDbService as BaseService;
|
||||
|
||||
class SchemeToDbService extends BaseService
|
||||
{
|
||||
}
|
||||
@@ -10,6 +10,9 @@ use app\common\command\admin\Update;
|
||||
use app\common\command\admin\UpdateCode;
|
||||
use app\common\command\curd\Migrate;
|
||||
use app\common\command\Timer;
|
||||
use app\common\command\scheme\Make;
|
||||
use app\common\command\scheme\Sync;
|
||||
use app\common\command\scheme\Backup;
|
||||
|
||||
return [
|
||||
// 指令定义
|
||||
@@ -23,6 +26,9 @@ return [
|
||||
Migrate::class,
|
||||
Clear::class,
|
||||
Update::class,
|
||||
UpdateCode::class
|
||||
UpdateCode::class,
|
||||
Make::class,
|
||||
Sync::class,
|
||||
Backup::class,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -6,7 +6,7 @@ use think\facade\Env;
|
||||
|
||||
return [
|
||||
// 默认使用的数据库连接配置
|
||||
'default' => Env::get('database.type', 'sqlite'),
|
||||
'default' => Env::get('database.main', 'sqlite'),
|
||||
|
||||
// 自定义时间查询规则
|
||||
'time_query_rule' => [],
|
||||
@@ -21,7 +21,7 @@ return [
|
||||
|
||||
// 数据库连接配置信息
|
||||
'connections' => [
|
||||
'mysql' => [
|
||||
'main' => [
|
||||
// 数据库类型
|
||||
'type' => Env::get('database.type', 'mysql'),
|
||||
// 服务器地址
|
||||
|
||||
11
config/scheme.php
Normal file
11
config/scheme.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// 忽略的表 (不参与 scheme 同步)
|
||||
'ignore_tables' => [
|
||||
'migrations',
|
||||
'phinxlog',
|
||||
],
|
||||
// 备份中间前缀
|
||||
'backup_prefix' => 'backup',
|
||||
];
|
||||
25
extend/base/common/command/scheme/Backup.php
Normal file
25
extend/base/common/command/scheme/Backup.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\scheme;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
|
||||
class Backup extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('scheme:backups')
|
||||
->setDescription('List all backup tables');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$tables = Db::query("SHOW TABLES LIKE 'ul_backup%'");
|
||||
foreach ($tables as $t) {
|
||||
$output->writeln(array_values($t)[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
73
extend/base/common/command/scheme/Make.php
Normal file
73
extend/base/common/command/scheme/Make.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\scheme;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\Output;
|
||||
use think\facade\Db;
|
||||
use think\facade\Config;
|
||||
use app\common\service\scheme\DbToSchemeService;
|
||||
|
||||
class Make extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('scheme:make')
|
||||
->addArgument('table', Argument::OPTIONAL, "The table name (without prefix)")
|
||||
->setDescription('Generate Scheme class from Database table');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$table = $input->getArgument('table');
|
||||
$service = new DbToSchemeService();
|
||||
|
||||
$tables = [];
|
||||
if ($table) {
|
||||
$tables[] = $table;
|
||||
} else {
|
||||
// 获取所有表
|
||||
$allTables = Db::getTables();
|
||||
// 过滤掉忽略的表
|
||||
$config = Config::get('scheme.ignore_tables', []);
|
||||
$prefix = Config::get('database.connections.mysql.prefix');
|
||||
|
||||
foreach ($allTables as $t) {
|
||||
// 如果有前缀,去除前缀后再判断
|
||||
$shortName = $t;
|
||||
if ($prefix && str_starts_with($t, $prefix)) {
|
||||
$shortName = substr($t, strlen($prefix));
|
||||
}
|
||||
|
||||
if (!in_array($shortName, $config) && !in_array($t, $config)) {
|
||||
$tables[] = $shortName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tables as $t) {
|
||||
$output->writeln("Processing table: $t");
|
||||
try {
|
||||
$code = $service->generate($t);
|
||||
|
||||
// 提取类名以确定文件名
|
||||
if (preg_match('/class\s+(\w+)/', $code, $matches)) {
|
||||
$className = $matches[1];
|
||||
$path = app()->getAppPath() . 'admin/scheme/' . $className . '.php';
|
||||
|
||||
// 确保目录存在
|
||||
if (!is_dir(dirname($path))) {
|
||||
mkdir(dirname($path), 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($path, $code);
|
||||
$output->writeln("<info>Generated: $path</info>");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln("<error>Error processing $t: " . $e->getMessage() . "</error>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
53
extend/base/common/command/scheme/Sync.php
Normal file
53
extend/base/common/command/scheme/Sync.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\command\scheme;
|
||||
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\App;
|
||||
use app\common\service\scheme\SchemeToDbService;
|
||||
|
||||
class Sync extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('scheme:sync')
|
||||
->addOption('skip-data', null, Option::VALUE_NONE, 'Skip data migration')
|
||||
->addOption('force', null, Option::VALUE_NONE, 'Force execution without confirmation')
|
||||
->setDescription('Synchronize Scheme classes to Database');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$skipData = $input->getOption('skip-data');
|
||||
|
||||
$service = new SchemeToDbService();
|
||||
$schemeDir = app()->getAppPath() . 'admin/scheme/';
|
||||
|
||||
if (!is_dir($schemeDir)) {
|
||||
$output->writeln("<error>Scheme directory not found: $schemeDir</error>");
|
||||
return;
|
||||
}
|
||||
|
||||
$files = glob($schemeDir . '*.php');
|
||||
foreach ($files as $file) {
|
||||
require_once $file;
|
||||
$className = 'app\\admin\\scheme\\' . basename($file, '.php');
|
||||
|
||||
if (class_exists($className)) {
|
||||
$output->writeln("Syncing $className...");
|
||||
try {
|
||||
$backup = $service->sync($className, $skipData);
|
||||
$output->writeln("<info>Success!</info>");
|
||||
if ($backup) {
|
||||
$output->writeln("Backup created: $backup");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$output->writeln("<error>Failed: " . $e->getMessage() . "</error>");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
extend/base/common/scheme/BaseScheme.php
Normal file
8
extend/base/common/scheme/BaseScheme.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\scheme;
|
||||
|
||||
abstract class BaseScheme
|
||||
{
|
||||
// 可以在这里添加一些通用的方法,例如获取所有字段定义的反射方法等
|
||||
}
|
||||
21
extend/base/common/scheme/attribute/Component.php
Normal file
21
extend/base/common/scheme/attribute/Component.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\scheme\attribute;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PROPERTY)]
|
||||
class Component
|
||||
{
|
||||
/**
|
||||
* @param string $type 组件类型 (e.g. radio, image, editor)
|
||||
* @param array $options 选项数据 (e.g. ['1'=>'男', '2'=>'女'])
|
||||
* @param array $extra 额外参数
|
||||
*/
|
||||
public function __construct(
|
||||
public string $type,
|
||||
public array $options = [],
|
||||
public array $extra = []
|
||||
) {
|
||||
}
|
||||
}
|
||||
23
extend/base/common/scheme/attribute/Field.php
Normal file
23
extend/base/common/scheme/attribute/Field.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\scheme\attribute;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PROPERTY)]
|
||||
class Field
|
||||
{
|
||||
public function __construct(
|
||||
public string $type = 'varchar',
|
||||
public ?int $length = null,
|
||||
public int $precision = 0,
|
||||
public int $scale = 0,
|
||||
public bool $nullable = true,
|
||||
public mixed $default = null,
|
||||
public string $comment = '',
|
||||
public bool $unsigned = false,
|
||||
public bool $autoIncrement = false,
|
||||
public bool $primary = false
|
||||
) {
|
||||
}
|
||||
}
|
||||
21
extend/base/common/scheme/attribute/Index.php
Normal file
21
extend/base/common/scheme/attribute/Index.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\scheme\attribute;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
|
||||
class Index
|
||||
{
|
||||
/**
|
||||
* @param string|array $columns 索引包含的列,单个字符串或数组
|
||||
* @param string $name 索引名称,留空则自动生成
|
||||
* @param string $type 索引类型:NORMAL, UNIQUE, FULLTEXT
|
||||
*/
|
||||
public function __construct(
|
||||
public string|array $columns,
|
||||
public string $name = '',
|
||||
public string $type = 'NORMAL'
|
||||
) {
|
||||
}
|
||||
}
|
||||
19
extend/base/common/scheme/attribute/Table.php
Normal file
19
extend/base/common/scheme/attribute/Table.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\scheme\attribute;
|
||||
|
||||
use Attribute;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class Table
|
||||
{
|
||||
public function __construct(
|
||||
public string $name = '',
|
||||
public string $comment = '',
|
||||
public string $engine = 'InnoDB',
|
||||
public string $charset = 'utf8mb4',
|
||||
public string $collation = 'utf8mb4_general_ci',
|
||||
public ?string $connection = null
|
||||
) {
|
||||
}
|
||||
}
|
||||
219
extend/base/common/service/scheme/DbToSchemeService.php
Normal file
219
extend/base/common/service/scheme/DbToSchemeService.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\service\scheme;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\Config;
|
||||
use think\helper\Str;
|
||||
|
||||
class DbToSchemeService
|
||||
{
|
||||
protected string $module = 'admin';
|
||||
protected string $connection;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->connection = Config::get('database.default', 'mysql');
|
||||
}
|
||||
|
||||
public function generate(string $tableName, string $className = null): string
|
||||
{
|
||||
$prefix = Config::get('database.connections.' . $this->connection . '.prefix', '');
|
||||
$fullTableName = $tableName;
|
||||
// 如果表名不包含前缀,且配置了前缀,则添加
|
||||
if ($prefix && !str_starts_with($tableName, $prefix)) {
|
||||
$fullTableName = $prefix . $tableName;
|
||||
}
|
||||
|
||||
// 尝试获取表信息
|
||||
try {
|
||||
// 获取列信息,TP通用方法
|
||||
$columns = Db::connect($this->connection)->getFields($fullTableName);
|
||||
} catch (\Exception $e) {
|
||||
// 尝试直接使用tableName
|
||||
$fullTableName = $tableName;
|
||||
$columns = Db::connect($this->connection)->getFields($fullTableName);
|
||||
}
|
||||
|
||||
// 获取表注释
|
||||
$tableComment = $this->getTableComment($fullTableName);
|
||||
|
||||
// 获取索引信息
|
||||
$indices = $this->getTableIndices($fullTableName);
|
||||
|
||||
// 生成类名
|
||||
if (empty($className)) {
|
||||
// 去除前缀
|
||||
$shortName = $tableName;
|
||||
if ($prefix && str_starts_with($tableName, $prefix)) {
|
||||
$shortName = substr($tableName, strlen($prefix));
|
||||
}
|
||||
$className = Str::studly($shortName);
|
||||
}
|
||||
|
||||
// 构建类内容
|
||||
return $this->buildClass($className, $fullTableName, $tableComment, $columns, $indices);
|
||||
}
|
||||
|
||||
protected function getTableComment($tableName): string
|
||||
{
|
||||
// 简单适配 MySQL
|
||||
try {
|
||||
$config = Config::get('database.connections.' . $this->connection);
|
||||
$database = $config['database'];
|
||||
// 暂时只支持MySQL的表注释获取,其他返回空
|
||||
$sql = "SELECT table_comment FROM information_schema.TABLES WHERE table_schema = ? AND table_name = ?";
|
||||
$res = Db::connect($this->connection)->query($sql, [$database, $tableName]);
|
||||
return $res[0]['table_comment'] ?? '';
|
||||
} catch (\Exception $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
protected function getTableIndices($tableName): array
|
||||
{
|
||||
try {
|
||||
// SHOW KEYS FROM table
|
||||
$keys = Db::connect($this->connection)->query("SHOW KEYS FROM `$tableName`");
|
||||
$indices = [];
|
||||
foreach ($keys as $key) {
|
||||
$name = $key['Key_name'];
|
||||
if ($name === 'PRIMARY') continue; // 主键在 Field 中处理
|
||||
|
||||
if (!isset($indices[$name])) {
|
||||
$indices[$name] = [
|
||||
'name' => $name,
|
||||
'columns' => [],
|
||||
'type' => $key['Non_unique'] == 0 ? 'UNIQUE' : ($key['Index_type'] == 'FULLTEXT' ? 'FULLTEXT' : 'NORMAL')
|
||||
];
|
||||
}
|
||||
$indices[$name]['columns'][] = $key['Column_name'];
|
||||
}
|
||||
return array_values($indices);
|
||||
} catch (\Exception $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildClass($className, $tableName, $tableComment, $columns, $indices): string
|
||||
{
|
||||
$fieldsCode = [];
|
||||
|
||||
foreach ($columns as $field) {
|
||||
$fieldName = $field['name'];
|
||||
$type = $field['type']; // varchar(255)
|
||||
$comment = $field['comment'] ?? '';
|
||||
$default = $field['default'] ?? null;
|
||||
$notNull = $field['notnull'] ?? false;
|
||||
$primary = $field['primary'] ?? false;
|
||||
$autoinc = $field['autoinc'] ?? false;
|
||||
|
||||
// 解析类型和长度
|
||||
preg_match('/(\w+)(?:\((\d+)(?:,(\d+))?\))?/', $type, $matches);
|
||||
$dbType = $matches[1] ?? 'varchar';
|
||||
$length = isset($matches[2]) ? (int)$matches[2] : null;
|
||||
$precision = isset($matches[2]) ? (int)$matches[2] : 0; // For decimal
|
||||
$scale = isset($matches[3]) ? (int)$matches[3] : 0;
|
||||
|
||||
// 解析 Ulthon 组件语法
|
||||
$componentAttr = '';
|
||||
$cleanComment = $comment;
|
||||
|
||||
// 匹配 {type} (options)
|
||||
// 改进:允许 options 中含有冒号,且 key:value 之间可能有空格,或者没有括号
|
||||
// 例子:{radio} (0:禁用,1:启用) 或 {image} 或 {relation} (table:mall_cate,relationBindSelect:title)
|
||||
if (preg_match('/\{(.*?)\}\s*(\((.*?)\))?/', $comment, $cmatch)) {
|
||||
$compType = $cmatch[1];
|
||||
$compOptionsStr = $cmatch[3] ?? '';
|
||||
$cleanComment = trim(str_replace($cmatch[0], '', $comment));
|
||||
|
||||
// 解析选项 (1:A, 2:B) 或 (table:mall_cate,relationBindSelect:title)
|
||||
$optionsCode = '[]';
|
||||
if ($compOptionsStr) {
|
||||
$options = [];
|
||||
// 简单的 explode 可能有问题,如果值里面有逗号。但目前假设选项格式比较简单。
|
||||
$parts = explode(',', $compOptionsStr);
|
||||
foreach ($parts as $p) {
|
||||
// 尝试分割 key:value
|
||||
$kv = explode(':', trim($p), 2); // Limit to 2 parts
|
||||
if (count($kv) >= 2) {
|
||||
$k = trim($kv[0]);
|
||||
$v = trim($kv[1]);
|
||||
$options[$k] = $v;
|
||||
} else {
|
||||
$options[] = trim($p);
|
||||
}
|
||||
}
|
||||
$optionsCode = $this->exportArray($options);
|
||||
}
|
||||
|
||||
$componentAttr = "\n #[Component(type: '$compType', options: $optionsCode)]";
|
||||
}
|
||||
|
||||
// 构建 Field 注解
|
||||
$defaultStr = is_null($default) ? 'null' : (is_string($default) ? "'$default'" : $default);
|
||||
$nullable = $notNull ? 'false' : 'true';
|
||||
$unsigned = str_contains($type, 'unsigned') ? 'true' : 'false';
|
||||
$primaryStr = $primary ? 'true' : 'false';
|
||||
$autoincStr = $autoinc ? 'true' : 'false';
|
||||
|
||||
// 如果是整型且未指定长度,默认11 (兼容旧习俗)
|
||||
if (str_contains($dbType, 'int') && !$length) {
|
||||
$length = 11;
|
||||
}
|
||||
|
||||
$lengthStr = is_null($length) ? 'null' : $length;
|
||||
|
||||
$fieldsCode[] = <<<PHP
|
||||
#[Field(type: '$dbType', length: $lengthStr, precision: $precision, scale: $scale, nullable: $nullable, default: $defaultStr, comment: '$cleanComment', unsigned: $unsigned, autoIncrement: $autoincStr, primary: $primaryStr)]$componentAttr
|
||||
public \$$fieldName;
|
||||
PHP;
|
||||
}
|
||||
|
||||
$fieldsBody = implode("\n\n", $fieldsCode);
|
||||
|
||||
// 构建 Index 注解
|
||||
$indexAttrs = '';
|
||||
foreach ($indices as $idx) {
|
||||
$cols = $this->exportArray($idx['columns']);
|
||||
$indexAttrs .= "\n#[Index(columns: $cols, name: '{$idx['name']}', type: '{$idx['type']}')]";
|
||||
}
|
||||
|
||||
return <<<PHP
|
||||
<?php
|
||||
|
||||
namespace app\\{$this->module}\scheme;
|
||||
|
||||
use app\common\scheme\BaseScheme;
|
||||
use app\common\scheme\attribute\Table;
|
||||
use app\common\scheme\attribute\Field;
|
||||
use app\common\scheme\attribute\Component;
|
||||
use app\common\scheme\attribute\Index;
|
||||
|
||||
#[Table(name: '$tableName', comment: '$tableComment')]$indexAttrs
|
||||
class $className extends BaseScheme
|
||||
{
|
||||
$fieldsBody
|
||||
}
|
||||
PHP;
|
||||
}
|
||||
|
||||
protected function exportArray(array $array): string
|
||||
{
|
||||
// 简易数组导出
|
||||
$items = [];
|
||||
// 判断是否是关联数组
|
||||
$isAssoc = array_keys($array) !== range(0, count($array) - 1);
|
||||
|
||||
foreach ($array as $k => $v) {
|
||||
$val = is_string($v) ? "'$v'" : $v;
|
||||
if ($isAssoc) {
|
||||
$key = is_int($k) ? $k : "'$k'";
|
||||
$items[] = "$key => $val";
|
||||
} else {
|
||||
$items[] = $val;
|
||||
}
|
||||
}
|
||||
return '[' . implode(', ', $items) . ']';
|
||||
}
|
||||
}
|
||||
243
extend/base/common/service/scheme/SchemeToDbService.php
Normal file
243
extend/base/common/service/scheme/SchemeToDbService.php
Normal file
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\service\scheme;
|
||||
|
||||
use think\facade\Db;
|
||||
use think\facade\Config;
|
||||
use app\common\scheme\attribute\Table;
|
||||
use app\common\scheme\attribute\Field;
|
||||
use app\common\scheme\attribute\Component;
|
||||
use app\common\scheme\attribute\Index;
|
||||
use ReflectionClass;
|
||||
use ReflectionProperty;
|
||||
|
||||
class SchemeToDbService
|
||||
{
|
||||
protected $connection;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->connection = Config::get('database.default', 'mysql');
|
||||
}
|
||||
|
||||
public function sync(string $className, bool $skipData = false)
|
||||
{
|
||||
if (!class_exists($className)) {
|
||||
throw new \Exception("Class $className not found");
|
||||
}
|
||||
|
||||
$ref = new ReflectionClass($className);
|
||||
|
||||
// 获取 Table 注解
|
||||
$tableAttrs = $ref->getAttributes(Table::class);
|
||||
if (empty($tableAttrs)) {
|
||||
throw new \Exception("Class $className missing #[Table] attribute");
|
||||
}
|
||||
/** @var Table $tableAttr */
|
||||
$tableAttr = $tableAttrs[0]->newInstance();
|
||||
|
||||
$tableName = $tableAttr->name;
|
||||
$prefix = Config::get('database.connections.' . $this->connection . '.prefix', '');
|
||||
|
||||
// 确保表名带前缀
|
||||
$fullTableName = $tableName;
|
||||
if ($prefix && !str_starts_with($tableName, $prefix)) {
|
||||
$fullTableName = $prefix . $tableName;
|
||||
}
|
||||
|
||||
// 检查表是否存在
|
||||
$tableExists = $this->checkTableExists($fullTableName);
|
||||
$backupTableName = null;
|
||||
|
||||
// 1. 备份
|
||||
if ($tableExists) {
|
||||
$backupPrefix = Config::get('scheme.backup_prefix', 'backup');
|
||||
$backupTableName = $prefix . $backupPrefix . '_' . date('YmdHis') . '_' . $fullTableName;
|
||||
|
||||
try {
|
||||
// 确保没有残留的同名备份表(极端情况)
|
||||
Db::connect($this->connection)->execute("DROP TABLE IF EXISTS `$backupTableName`");
|
||||
Db::connect($this->connection)->execute("RENAME TABLE `$fullTableName` TO `$backupTableName`");
|
||||
} catch (\Exception $e) {
|
||||
// 如果备份失败,可能是权限问题或其他,抛出异常
|
||||
throw new \Exception("Backup failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 建表
|
||||
$sql = $this->buildCreateTableSql($fullTableName, $tableAttr, $ref);
|
||||
Db::connect($this->connection)->execute($sql);
|
||||
|
||||
// 3. 恢复数据
|
||||
if ($tableExists && !$skipData && $backupTableName) {
|
||||
try {
|
||||
$this->restoreData($fullTableName, $backupTableName, $ref->getProperties());
|
||||
} catch (\Exception $e) {
|
||||
// 如果数据恢复失败,尝试回滚(这里只是简单的删除新表,重命名回旧表)
|
||||
// 实际生产环境可能需要更复杂的恢复机制
|
||||
// Db::connect($this->connection)->execute("DROP TABLE IF EXISTS `$fullTableName`");
|
||||
// Db::connect($this->connection)->execute("RENAME TABLE `$backupTableName` TO `$fullTableName`");
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return $backupTableName;
|
||||
}
|
||||
|
||||
protected function checkTableExists($tableName)
|
||||
{
|
||||
$tables = Db::connect($this->connection)->query("SHOW TABLES LIKE '$tableName'");
|
||||
return !empty($tables);
|
||||
}
|
||||
|
||||
protected function buildCreateTableSql($tableName, Table $tableAttr, ReflectionClass $ref)
|
||||
{
|
||||
$properties = $ref->getProperties();
|
||||
$lines = [];
|
||||
$primaryKeys = [];
|
||||
|
||||
foreach ($properties as $prop) {
|
||||
$fieldAttrs = $prop->getAttributes(Field::class);
|
||||
if (empty($fieldAttrs)) {
|
||||
continue;
|
||||
}
|
||||
/** @var Field $field */
|
||||
$field = $fieldAttrs[0]->newInstance();
|
||||
$fieldName = $prop->getName();
|
||||
|
||||
$line = "`$fieldName` {$field->type}";
|
||||
|
||||
// Length/Precision
|
||||
if ($field->length !== null) {
|
||||
$line .= "({$field->length})";
|
||||
} elseif ($field->precision > 0) {
|
||||
$line .= "({$field->precision},{$field->scale})";
|
||||
}
|
||||
|
||||
// Unsigned
|
||||
if ($field->unsigned) {
|
||||
$line .= " UNSIGNED";
|
||||
}
|
||||
|
||||
// Nullable
|
||||
if (!$field->nullable) {
|
||||
$line .= " NOT NULL";
|
||||
} else {
|
||||
$line .= " DEFAULT NULL";
|
||||
}
|
||||
|
||||
// AutoIncrement
|
||||
if ($field->autoIncrement) {
|
||||
$line .= " AUTO_INCREMENT";
|
||||
}
|
||||
|
||||
// Default
|
||||
if (!is_null($field->default)) {
|
||||
$def = $field->default;
|
||||
if (is_string($def)) {
|
||||
$line .= " DEFAULT '$def'";
|
||||
} elseif (is_bool($def)) {
|
||||
$line .= " DEFAULT " . ($def ? 1 : 0);
|
||||
} else {
|
||||
$line .= " DEFAULT $def";
|
||||
}
|
||||
}
|
||||
|
||||
// Comment + Component Restore
|
||||
$comment = $field->comment;
|
||||
|
||||
// 检查是否有 Component 注解
|
||||
$compAttrs = $prop->getAttributes(Component::class);
|
||||
if (!empty($compAttrs)) {
|
||||
/** @var Component $comp */
|
||||
$comp = $compAttrs[0]->newInstance();
|
||||
$typeStr = "{{$comp->type}}";
|
||||
$optionsStr = '';
|
||||
if (!empty($comp->options)) {
|
||||
$parts = [];
|
||||
foreach ($comp->options as $k => $v) {
|
||||
// 如果是数字索引,且连续,可能就是简单的值列表?
|
||||
// 但 Ulthon 格式通常是 k:v。如果 k 是数字,也要输出。
|
||||
$parts[] = "$k:$v";
|
||||
}
|
||||
$optionsStr = ' (' . implode(',', $parts) . ')';
|
||||
}
|
||||
// 拼接回注释:原注释 {type} (options)
|
||||
$comment = trim($comment . " $typeStr$optionsStr");
|
||||
}
|
||||
|
||||
if ($comment) {
|
||||
$line .= " COMMENT '$comment'";
|
||||
}
|
||||
|
||||
$lines[] = $line;
|
||||
|
||||
if ($field->primary) {
|
||||
$primaryKeys[] = "`$fieldName`";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($primaryKeys)) {
|
||||
$lines[] = "PRIMARY KEY (" . implode(',', $primaryKeys) . ")";
|
||||
}
|
||||
|
||||
// 处理 Index 注解
|
||||
$indexAttrs = $ref->getAttributes(Index::class);
|
||||
foreach ($indexAttrs as $attr) {
|
||||
/** @var Index $idx */
|
||||
$idx = $attr->newInstance();
|
||||
$cols = is_array($idx->columns) ? $idx->columns : [$idx->columns];
|
||||
$colStr = "`" . implode("`,`", $cols) . "`";
|
||||
$keyName = $idx->name ?: $cols[0]; // 默认使用第一列名
|
||||
|
||||
if ($idx->type === 'UNIQUE') {
|
||||
$lines[] = "UNIQUE KEY `$keyName` ($colStr)";
|
||||
} elseif ($idx->type === 'FULLTEXT') {
|
||||
$lines[] = "FULLTEXT KEY `$keyName` ($colStr)";
|
||||
} else {
|
||||
$lines[] = "KEY `$keyName` ($colStr)";
|
||||
}
|
||||
}
|
||||
|
||||
$body = implode(",\n ", $lines);
|
||||
$comment = $tableAttr->comment ? " COMMENT='{$tableAttr->comment}'" : "";
|
||||
|
||||
return "CREATE TABLE `$tableName` (\n $body\n) ENGINE={$tableAttr->engine} DEFAULT CHARSET={$tableAttr->charset}$comment";
|
||||
}
|
||||
|
||||
protected function restoreData($newTable, $oldTable, array $properties)
|
||||
{
|
||||
$fields = [];
|
||||
foreach ($properties as $prop) {
|
||||
$fieldAttrs = $prop->getAttributes(Field::class);
|
||||
if (!empty($fieldAttrs)) {
|
||||
$fields[] = "`" . $prop->getName() . "`";
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($fields)) return;
|
||||
|
||||
try {
|
||||
$oldFieldsRaw = Db::connect($this->connection)->getFields($oldTable);
|
||||
$oldFields = array_keys($oldFieldsRaw);
|
||||
|
||||
$commonFields = [];
|
||||
foreach ($properties as $prop) {
|
||||
$name = $prop->getName();
|
||||
if (in_array($name, $oldFields)) {
|
||||
$commonFields[] = "`$name`";
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($commonFields)) return;
|
||||
|
||||
$commonStr = implode(',', $commonFields);
|
||||
|
||||
$sql = "INSERT INTO `$newTable` ($commonStr) SELECT $commonStr FROM `$oldTable`";
|
||||
Db::connect($this->connection)->execute($sql);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
throw new \Exception("Data migration failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user