mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-07-09 19:42:47 +08:00
feat(scheme): 增强 Scheme 与数据库同步机制并添加严格校验
This commit is contained in:
18
CODERULE.md
18
CODERULE.md
@@ -85,6 +85,7 @@ Ulthon Admin 采用独特的双层架构设计,其中 **基础核心层 (`exte
|
|||||||
|
|
||||||
1. **设计数据库表结构**
|
1. **设计数据库表结构**
|
||||||
- 严格遵循【第一部分:通用基础规范】中的数据库设计规范。
|
- 严格遵循【第一部分:通用基础规范】中的数据库设计规范。
|
||||||
|
- 可以直接在数据库中创建表,也可以使用 Scheme 层代码化创建。
|
||||||
|
|
||||||
2. **生成基础代码 (CURD)**
|
2. **生成基础代码 (CURD)**
|
||||||
- 使用 `php think curd` 命令生成控制器、模型、视图等。
|
- 使用 `php think curd` 命令生成控制器、模型、视图等。
|
||||||
@@ -100,7 +101,7 @@ Ulthon Admin 采用独特的双层架构设计,其中 **基础核心层 (`exte
|
|||||||
### 4. 常用开发工具
|
### 4. 常用开发工具
|
||||||
|
|
||||||
#### 4.1 代码生成命令 (CURD)
|
#### 4.1 代码生成命令 (CURD)
|
||||||
> 文档: [CURD 命令详解](https://doc.ulthon.com/read/augushong/ulthon_admin/619efc816472e/zh-cn/2.x.html)
|
> 文档: [CURD 命令详解](https://doc.ulthon.com/read/augushong/ulthon_admin/curd-command/zh-cn/2.x.html)
|
||||||
|
|
||||||
**常用命令示例**:
|
**常用命令示例**:
|
||||||
```bash
|
```bash
|
||||||
@@ -152,3 +153,18 @@ php think scheme:make -t test_goods
|
|||||||
# 将 app/admin/scheme/ 下的代码同步到数据库 (自动备份原表)
|
# 将 app/admin/scheme/ 下的代码同步到数据库 (自动备份原表)
|
||||||
php think scheme:sync
|
php think scheme:sync
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 6.4 标准 CURD 开发工作流
|
||||||
|
Scheme 机制是 CURD 生成的前置依赖,开发流程如下:
|
||||||
|
|
||||||
|
1. **设计表结构**:
|
||||||
|
- 方式 A:手动创建/修改数据库表结构。
|
||||||
|
- 方式 B:手动编写/修改 `app/admin/scheme/` 下的 Scheme 类。
|
||||||
|
|
||||||
|
2. **同步结构(确保一致性)**:
|
||||||
|
- 若采用方式 A,执行 `php think scheme:make -t {table}` 将变更同步到 Scheme。
|
||||||
|
- 若采用方式 B,执行 `php think scheme:sync` 将变更同步到数据库。
|
||||||
|
- **注意**:生成 CURD 前,Scheme 与数据库结构必须完全一致,否则会被拒绝。
|
||||||
|
|
||||||
|
3. **生成 CURD**:
|
||||||
|
- 执行 `php think curd -t {table}` 生成业务代码。
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
namespace base\admin\service\curd;
|
namespace base\admin\service\curd;
|
||||||
|
|
||||||
use app\admin\service\curd\exceptions\TableException;
|
use app\admin\service\curd\exceptions\TableException;
|
||||||
|
use app\common\scheme\attribute\Table;
|
||||||
|
use base\common\service\scheme\SchemeToDbService;
|
||||||
|
use ReflectionClass;
|
||||||
use think\exception\FileException;
|
use think\exception\FileException;
|
||||||
use think\facade\Db;
|
use think\facade\Db;
|
||||||
use think\helper\Str;
|
use think\helper\Str;
|
||||||
@@ -229,8 +232,9 @@ class BuildCurdServiceBase
|
|||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->tablePrefix = config('database.connections.mysql.prefix');
|
$connection = config('database.default', 'mysql');
|
||||||
$this->dbName = config('database.connections.mysql.database');
|
$this->tablePrefix = config("database.connections.{$connection}.prefix");
|
||||||
|
$this->dbName = config("database.connections.{$connection}.database");
|
||||||
$this->dir = __DIR__;
|
$this->dir = __DIR__;
|
||||||
$this->rootDir = root_path();
|
$this->rootDir = root_path();
|
||||||
|
|
||||||
@@ -258,36 +262,58 @@ class BuildCurdServiceBase
|
|||||||
public function setTable($table)
|
public function setTable($table)
|
||||||
{
|
{
|
||||||
$this->table = $table;
|
$this->table = $table;
|
||||||
|
|
||||||
|
$schemeClass = 'app\\admin\\scheme\\' . Str::studly($this->table);
|
||||||
|
if (!class_exists($schemeClass)) {
|
||||||
|
throw new TableException("未找到 {$schemeClass},请先执行:php think scheme:make -t {$this->table} 或手动创建 Scheme");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取表列注释
|
$ref = new ReflectionClass($schemeClass);
|
||||||
$colums = Db::query("SHOW FULL COLUMNS FROM {$this->tablePrefix}{$this->table}");
|
} catch (\Throwable $e) {
|
||||||
|
|
||||||
foreach ($colums as $vo) {
|
|
||||||
$colum = [
|
|
||||||
'type' => $vo['Type'],
|
|
||||||
'comment' => !empty($vo['Comment']) ? $vo['Comment'] : $vo['Field'],
|
|
||||||
'required' => $vo['Null'] == 'NO' ? true : false,
|
|
||||||
'default' => $vo['Default'],
|
|
||||||
'field' => $vo['Field'],
|
|
||||||
];
|
|
||||||
|
|
||||||
// 格式化列数据
|
|
||||||
$this->buildColum($colum);
|
|
||||||
|
|
||||||
$this->tableColumns[$vo['Field']] = $colum;
|
|
||||||
|
|
||||||
if ($vo['Field'] == 'delete_time') {
|
|
||||||
$this->delete = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取表名注释
|
|
||||||
$tableSchema = Db::query("SELECT table_name,table_comment FROM information_schema.TABLES WHERE table_schema = 'ulthon_admin' AND table_name = '{$this->tablePrefix}{$this->table}'");
|
|
||||||
$this->tableComment = (isset($tableSchema[0]['table_comment']) && !empty($tableSchema[0]['table_comment'])) ? $tableSchema[0]['table_comment'] : $this->table;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
throw new TableException($e->getMessage());
|
throw new TableException($e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$tableAttrs = $ref->getAttributes(Table::class);
|
||||||
|
if (empty($tableAttrs)) {
|
||||||
|
throw new TableException("{$schemeClass} 未设置 Table(name: ...) ,无法生成 CURD");
|
||||||
|
}
|
||||||
|
/** @var Table $tableAttr */
|
||||||
|
$tableAttr = $tableAttrs[0]->newInstance();
|
||||||
|
|
||||||
|
$expectedTableName = "{$this->tablePrefix}{$this->table}";
|
||||||
|
if ((string)$tableAttr->name !== $expectedTableName) {
|
||||||
|
throw new TableException("Scheme 表名不匹配:{$schemeClass} => {$tableAttr->name},期望 {$expectedTableName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$schemeService = new SchemeToDbService();
|
||||||
|
try {
|
||||||
|
$diffs = $schemeService->diff($schemeClass);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
throw new TableException($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($diffs)) {
|
||||||
|
$message = "CURD 生成已拒绝:数据库结构与 Scheme 不一致({$expectedTableName})\n";
|
||||||
|
$message .= implode("\n", array_slice($diffs, 0, 50));
|
||||||
|
$message .= "\n\n请先执行:\n";
|
||||||
|
$message .= "- 从数据库生成 Scheme:php think scheme:make -t {$this->table}\n";
|
||||||
|
$message .= "- 或从 Scheme 同步数据库:php think scheme:sync\n\n";
|
||||||
|
$message .= "详细文档请参考:https://doc.ulthon.com/read/augushong/ulthon_admin/curd-command/zh-cn/2.x.html\n";
|
||||||
|
throw new TableException($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$columns = $schemeService->getColumnsForCurd($schemeClass);
|
||||||
|
foreach ($columns as $field => $colum) {
|
||||||
|
$this->buildColum($colum);
|
||||||
|
$this->tableColumns[$field] = $colum;
|
||||||
|
if ($field == 'delete_time') {
|
||||||
|
$this->delete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->tableComment = !empty($tableAttr->comment) ? $tableAttr->comment : $this->table;
|
||||||
|
|
||||||
$this->controllerFilename = $this->getTableControllerName($this->table);
|
$this->controllerFilename = $this->getTableControllerName($this->table);
|
||||||
|
|
||||||
// 初始化默认模型名
|
// 初始化默认模型名
|
||||||
@@ -342,62 +368,92 @@ class BuildCurdServiceBase
|
|||||||
if (!isset($this->tableColumns[$foreignKey])) {
|
if (!isset($this->tableColumns[$foreignKey])) {
|
||||||
throw new TableException("主表不存在外键字段:{$foreignKey}");
|
throw new TableException("主表不存在外键字段:{$foreignKey}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$schemeClass = 'app\\admin\\scheme\\' . Str::studly($relationTable);
|
||||||
|
if (!class_exists($schemeClass)) {
|
||||||
|
throw new TableException("未找到 {$schemeClass},请先执行:php think scheme:make -t {$relationTable} 或手动创建 Scheme");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ref = new ReflectionClass($schemeClass);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
throw new TableException($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableAttrs = $ref->getAttributes(Table::class);
|
||||||
|
if (empty($tableAttrs)) {
|
||||||
|
throw new TableException("{$schemeClass} 未设置 Table(name: ...) ,无法生成 CURD");
|
||||||
|
}
|
||||||
|
/** @var Table $tableAttr */
|
||||||
|
$tableAttr = $tableAttrs[0]->newInstance();
|
||||||
|
|
||||||
|
$expectedTableName = "{$this->tablePrefix}{$relationTable}";
|
||||||
|
if ((string)$tableAttr->name !== $expectedTableName) {
|
||||||
|
throw new TableException("关联 Scheme 表名不匹配:{$schemeClass} => {$tableAttr->name},期望 {$expectedTableName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
$schemeService = new SchemeToDbService();
|
||||||
|
try {
|
||||||
|
$diffs = $schemeService->diff($schemeClass);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
throw new TableException($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($diffs)) {
|
||||||
|
$message = "CURD 生成已拒绝:数据库结构与 Scheme 不一致({$expectedTableName})\n";
|
||||||
|
$message .= implode("\n", array_slice($diffs, 0, 50));
|
||||||
|
$message .= "\n\n请先执行:\n";
|
||||||
|
$message .= "- 从数据库生成 Scheme:php think scheme:make -t {$relationTable}\n";
|
||||||
|
$message .= "- 或从 Scheme 同步数据库:php think scheme:sync\n\n";
|
||||||
|
$message .= "详细文档请参考:https://doc.ulthon.com/read/augushong/ulthon_admin/curd-command/zh-cn/2.x.html\n";
|
||||||
|
throw new TableException($message);
|
||||||
|
}
|
||||||
|
|
||||||
if (!empty($modelFilename)) {
|
if (!empty($modelFilename)) {
|
||||||
$modelFilename = str_replace('/', $this->DS, $modelFilename);
|
$modelFilename = str_replace('/', $this->DS, $modelFilename);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
$colums = Db::query("SHOW FULL COLUMNS FROM {$this->tablePrefix}{$relationTable}");
|
|
||||||
$formatColums = [];
|
|
||||||
$delete = false;
|
|
||||||
if (!empty($bindSelectField) && !in_array($bindSelectField, array_column($colums, 'Field'))) {
|
|
||||||
throw new TableException("关联表{$relationTable}不存在该字段: {$bindSelectField}");
|
|
||||||
}
|
|
||||||
foreach ($colums as $vo) {
|
|
||||||
if (empty($primaryKey) && $vo['Key'] == 'PRI') {
|
|
||||||
$primaryKey = $vo['Field'];
|
|
||||||
}
|
|
||||||
if (!empty($onlyShowFileds) && !in_array($vo['Field'], $onlyShowFileds)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$colum = [
|
|
||||||
'type' => $vo['Type'],
|
|
||||||
'comment' => $vo['Comment'],
|
|
||||||
'default' => $vo['Default'],
|
|
||||||
'field' => $vo['Field'],
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->buildColum($colum);
|
$allColumns = $schemeService->getColumnsForCurd($schemeClass);
|
||||||
|
if (!empty($bindSelectField) && !isset($allColumns[$bindSelectField])) {
|
||||||
$formatColums[$vo['Field']] = $colum;
|
throw new TableException("关联表{$relationTable}不存在该字段: {$bindSelectField}");
|
||||||
if ($vo['Field'] == 'delete_time') {
|
|
||||||
$delete = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$modelFilename = empty($modelFilename) ? Str::studly($relationTable) : $modelFilename;
|
|
||||||
$modelArray = explode($this->DS, $modelFilename);
|
|
||||||
$modelName = array_pop($modelArray);
|
|
||||||
|
|
||||||
$relation = [
|
|
||||||
'modelFilename' => $modelFilename,
|
|
||||||
'modelName' => $modelName,
|
|
||||||
'foreignKey' => $foreignKey,
|
|
||||||
'primaryKey' => $primaryKey,
|
|
||||||
'bindSelectField' => $bindSelectField,
|
|
||||||
'delete' => $delete,
|
|
||||||
'tableColumns' => $formatColums,
|
|
||||||
];
|
|
||||||
if (!empty($bindSelectField)) {
|
|
||||||
$relationArray = explode('\\', $modelFilename);
|
|
||||||
$this->tableColumns[$foreignKey]['bindSelectField'] = $bindSelectField;
|
|
||||||
$this->tableColumns[$foreignKey]['bindRelation'] = end($relationArray);
|
|
||||||
}
|
|
||||||
$this->relationArray[$relationTable] = $relation;
|
|
||||||
$this->selectFileds[] = $foreignKey;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
throw new TableException($e->getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (empty($primaryKey)) {
|
||||||
|
$primaryKey = $schemeService->getPrimaryKey($schemeClass);
|
||||||
|
}
|
||||||
|
if (empty($primaryKey)) {
|
||||||
|
throw new TableException("关联表{$relationTable}未找到主键字段");
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatColums = $schemeService->getColumnsForCurd($schemeClass, $onlyShowFileds);
|
||||||
|
$delete = isset($allColumns['delete_time']);
|
||||||
|
|
||||||
|
foreach ($formatColums as $field => $colum) {
|
||||||
|
$this->buildColum($colum);
|
||||||
|
$formatColums[$field] = $colum;
|
||||||
|
}
|
||||||
|
|
||||||
|
$modelFilename = empty($modelFilename) ? Str::studly($relationTable) : $modelFilename;
|
||||||
|
$modelArray = explode($this->DS, $modelFilename);
|
||||||
|
$modelName = array_pop($modelArray);
|
||||||
|
|
||||||
|
$relation = [
|
||||||
|
'modelFilename' => $modelFilename,
|
||||||
|
'modelName' => $modelName,
|
||||||
|
'foreignKey' => $foreignKey,
|
||||||
|
'primaryKey' => $primaryKey,
|
||||||
|
'bindSelectField' => $bindSelectField,
|
||||||
|
'delete' => $delete,
|
||||||
|
'tableColumns' => $formatColums,
|
||||||
|
];
|
||||||
|
if (!empty($bindSelectField)) {
|
||||||
|
$relationArray = explode('\\', $modelFilename);
|
||||||
|
$this->tableColumns[$foreignKey]['bindSelectField'] = $bindSelectField;
|
||||||
|
$this->tableColumns[$foreignKey]['bindRelation'] = end($relationArray);
|
||||||
|
}
|
||||||
|
$this->relationArray[$relationTable] = $relation;
|
||||||
|
$this->selectFileds[] = $foreignKey;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,24 +5,28 @@ namespace base\common\command\scheme;
|
|||||||
use think\console\Command;
|
use think\console\Command;
|
||||||
use think\console\Input;
|
use think\console\Input;
|
||||||
use think\console\input\Argument;
|
use think\console\input\Argument;
|
||||||
|
use think\console\input\Option;
|
||||||
use think\console\Output;
|
use think\console\Output;
|
||||||
use think\facade\Db;
|
use think\facade\Db;
|
||||||
use think\facade\Config;
|
use think\facade\Config;
|
||||||
use app\common\service\scheme\DbToSchemeService;
|
use app\common\service\scheme\DbToSchemeService;
|
||||||
|
use app\common\service\scheme\SchemeToDbService;
|
||||||
|
|
||||||
class Make extends Command
|
class Make extends Command
|
||||||
{
|
{
|
||||||
protected function configure()
|
protected function configure()
|
||||||
{
|
{
|
||||||
$this->setName('scheme:make')
|
$this->setName('scheme:make')
|
||||||
|
->addOption('table', 't', Option::VALUE_REQUIRED, "The table name (without prefix)")
|
||||||
->addArgument('table', Argument::OPTIONAL, "The table name (without prefix)")
|
->addArgument('table', Argument::OPTIONAL, "The table name (without prefix)")
|
||||||
->setDescription('Generate Scheme class from Database table');
|
->setDescription('Generate Scheme class from Database table');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(Input $input, Output $output)
|
protected function execute(Input $input, Output $output)
|
||||||
{
|
{
|
||||||
$table = $input->getArgument('table');
|
$table = $input->getOption('table') ?: $input->getArgument('table');
|
||||||
$service = new DbToSchemeService();
|
$service = new DbToSchemeService();
|
||||||
|
$compare = new SchemeToDbService();
|
||||||
|
|
||||||
$tables = [];
|
$tables = [];
|
||||||
if ($table) {
|
if ($table) {
|
||||||
@@ -63,6 +67,18 @@ class Make extends Command
|
|||||||
$className = $matches[1];
|
$className = $matches[1];
|
||||||
$path = app()->getAppPath() . 'admin/scheme/' . $className . '.php';
|
$path = app()->getAppPath() . 'admin/scheme/' . $className . '.php';
|
||||||
|
|
||||||
|
if (is_file($path)) {
|
||||||
|
require_once $path;
|
||||||
|
$schemeClass = 'app\\admin\\scheme\\' . $className;
|
||||||
|
if (class_exists($schemeClass)) {
|
||||||
|
$diffs = $compare->diff($schemeClass);
|
||||||
|
if (empty($diffs)) {
|
||||||
|
$output->writeln("<info>Skipping (no schema changes): $path</info>");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 确保目录存在
|
// 确保目录存在
|
||||||
if (!is_dir(dirname($path))) {
|
if (!is_dir(dirname($path))) {
|
||||||
mkdir(dirname($path), 0755, true);
|
mkdir(dirname($path), 0755, true);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use think\console\Output;
|
|||||||
use think\facade\Config;
|
use think\facade\Config;
|
||||||
use think\facade\Db;
|
use think\facade\Db;
|
||||||
use app\common\service\scheme\SchemeToDbService;
|
use app\common\service\scheme\SchemeToDbService;
|
||||||
use app\common\service\scheme\DbToSchemeService;
|
|
||||||
use app\common\scheme\attribute\Table;
|
use app\common\scheme\attribute\Table;
|
||||||
use ReflectionClass;
|
use ReflectionClass;
|
||||||
|
|
||||||
@@ -28,7 +27,6 @@ class Sync extends Command
|
|||||||
$skipData = $input->getOption('skip-data');
|
$skipData = $input->getOption('skip-data');
|
||||||
|
|
||||||
$service = new SchemeToDbService();
|
$service = new SchemeToDbService();
|
||||||
$generator = new DbToSchemeService();
|
|
||||||
$schemeDir = app()->getAppPath() . 'admin/scheme/';
|
$schemeDir = app()->getAppPath() . 'admin/scheme/';
|
||||||
$ignoreTables = Config::get('scheme.ignore_tables', []);
|
$ignoreTables = Config::get('scheme.ignore_tables', []);
|
||||||
$connection = Config::get('database.default', 'mysql');
|
$connection = Config::get('database.default', 'mysql');
|
||||||
@@ -63,19 +61,21 @@ class Sync extends Command
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->checkTableExists($connection, $fullTableName)) {
|
try {
|
||||||
try {
|
$diffs = $service->diff($className);
|
||||||
$expectedClass = basename($file, '.php');
|
} catch (\Throwable $e) {
|
||||||
$generated = $generator->generate($fullTableName, $expectedClass);
|
$output->writeln("<error>Check failed for $className: " . $e->getMessage() . "</error>");
|
||||||
$existing = (string)file_get_contents($file);
|
continue;
|
||||||
if ($this->normalizeCode($generated) === $this->normalizeCode($existing)) {
|
}
|
||||||
$output->writeln("Skipping $className (no schema changes)");
|
|
||||||
continue;
|
if (count($diffs) === 1 && str_starts_with($diffs[0], '无法读取数据库表结构')) {
|
||||||
}
|
$output->writeln("<error>Check failed for $className: {$diffs[0]}</error>");
|
||||||
} catch (\Exception $e) {
|
continue;
|
||||||
$output->writeln("<error>Check failed for $className: " . $e->getMessage() . "</error>");
|
}
|
||||||
continue;
|
|
||||||
}
|
if (empty($diffs)) {
|
||||||
|
$output->writeln("Skipping $className (no schema changes)");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$output->writeln("Syncing $className...");
|
$output->writeln("Syncing $className...");
|
||||||
|
|||||||
@@ -36,8 +36,9 @@ class SchemeToDbService
|
|||||||
/** @var Table $tableAttr */
|
/** @var Table $tableAttr */
|
||||||
$tableAttr = $tableAttrs[0]->newInstance();
|
$tableAttr = $tableAttrs[0]->newInstance();
|
||||||
|
|
||||||
|
$connection = $this->resolveConnection($tableAttr);
|
||||||
$tableName = $tableAttr->name;
|
$tableName = $tableAttr->name;
|
||||||
$prefix = Config::get('database.connections.' . $this->connection . '.prefix', '');
|
$prefix = (string)Config::get('database.connections.' . $connection . '.prefix', '');
|
||||||
|
|
||||||
// 确保表名带前缀
|
// 确保表名带前缀
|
||||||
$fullTableName = $tableName;
|
$fullTableName = $tableName;
|
||||||
@@ -46,7 +47,7 @@ class SchemeToDbService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查表是否存在
|
// 检查表是否存在
|
||||||
$tableExists = $this->checkTableExists($fullTableName);
|
$tableExists = $this->checkTableExists($connection, $fullTableName);
|
||||||
$backupTableName = null;
|
$backupTableName = null;
|
||||||
|
|
||||||
// 1. 备份
|
// 1. 备份
|
||||||
@@ -56,8 +57,8 @@ class SchemeToDbService
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 确保没有残留的同名备份表(极端情况)
|
// 确保没有残留的同名备份表(极端情况)
|
||||||
Db::connect($this->connection)->execute("DROP TABLE IF EXISTS `$backupTableName`");
|
Db::connect($connection)->execute("DROP TABLE IF EXISTS `$backupTableName`");
|
||||||
Db::connect($this->connection)->execute("RENAME TABLE `$fullTableName` TO `$backupTableName`");
|
Db::connect($connection)->execute("RENAME TABLE `$fullTableName` TO `$backupTableName`");
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// 如果备份失败,可能是权限问题或其他,抛出异常
|
// 如果备份失败,可能是权限问题或其他,抛出异常
|
||||||
throw new \Exception("Backup failed: " . $e->getMessage());
|
throw new \Exception("Backup failed: " . $e->getMessage());
|
||||||
@@ -66,12 +67,12 @@ class SchemeToDbService
|
|||||||
|
|
||||||
// 2. 建表
|
// 2. 建表
|
||||||
$sql = $this->buildCreateTableSql($fullTableName, $tableAttr, $ref);
|
$sql = $this->buildCreateTableSql($fullTableName, $tableAttr, $ref);
|
||||||
Db::connect($this->connection)->execute($sql);
|
Db::connect($connection)->execute($sql);
|
||||||
|
|
||||||
// 3. 恢复数据
|
// 3. 恢复数据
|
||||||
if ($tableExists && !$skipData && $backupTableName) {
|
if ($tableExists && !$skipData && $backupTableName) {
|
||||||
try {
|
try {
|
||||||
$this->restoreData($fullTableName, $backupTableName, $ref->getProperties());
|
$this->restoreData($connection, $fullTableName, $backupTableName, $ref->getProperties());
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// 如果数据恢复失败,尝试回滚(这里只是简单的删除新表,重命名回旧表)
|
// 如果数据恢复失败,尝试回滚(这里只是简单的删除新表,重命名回旧表)
|
||||||
// 实际生产环境可能需要更复杂的恢复机制
|
// 实际生产环境可能需要更复杂的恢复机制
|
||||||
@@ -84,9 +85,187 @@ class SchemeToDbService
|
|||||||
return $backupTableName;
|
return $backupTableName;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function checkTableExists($tableName)
|
public function diff(string $className): array
|
||||||
{
|
{
|
||||||
$tables = Db::connect($this->connection)->query("SHOW TABLES LIKE '$tableName'");
|
if (!class_exists($className)) {
|
||||||
|
throw new \Exception("Class $className not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
$ref = new ReflectionClass($className);
|
||||||
|
$tableAttr = $this->getTableAttribute($ref, $className);
|
||||||
|
$connection = $this->resolveConnection($tableAttr);
|
||||||
|
$prefix = (string)Config::get('database.connections.' . $connection . '.prefix', '');
|
||||||
|
|
||||||
|
$fullTableName = $tableAttr->name;
|
||||||
|
if ($prefix && !str_starts_with($fullTableName, $prefix)) {
|
||||||
|
$fullTableName = $prefix . $fullTableName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->checkTableExists($connection, $fullTableName)) {
|
||||||
|
return ["表不存在:{$fullTableName}"];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$dbColumnsRows = Db::connect($connection)->query("SHOW FULL COLUMNS FROM `{$fullTableName}`");
|
||||||
|
$dbKeysRows = Db::connect($connection)->query("SHOW KEYS FROM `{$fullTableName}`");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return ["无法读取数据库表结构:{$fullTableName},{$e->getMessage()}"];
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbColumns = [];
|
||||||
|
foreach ($dbColumnsRows as $row) {
|
||||||
|
$dbColumns[$row['Field']] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schemeColumns = $this->buildSchemeColumnSignature($ref);
|
||||||
|
$schemeIndexes = $this->buildSchemeIndexSignature($ref);
|
||||||
|
$dbIndexes = $this->buildDbIndexSignature($dbKeysRows);
|
||||||
|
|
||||||
|
$diffs = [];
|
||||||
|
|
||||||
|
foreach ($schemeColumns as $field => $sig) {
|
||||||
|
if (!isset($dbColumns[$field])) {
|
||||||
|
$diffs[] = "缺少字段:{$field}";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $dbColumns[$field];
|
||||||
|
|
||||||
|
$dbType = $this->normalizeDbType((string)$row['Type']);
|
||||||
|
$schemeType = $this->normalizeDbType($sig['type']);
|
||||||
|
if ($dbType !== $schemeType) {
|
||||||
|
$diffs[] = "字段类型不一致:{$field} DB={$dbType} Scheme={$schemeType}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbNull = (string)$row['Null'];
|
||||||
|
$schemeNull = $sig['null'];
|
||||||
|
if ($dbNull !== $schemeNull) {
|
||||||
|
$diffs[] = "字段可空不一致:{$field} DB={$dbNull} Scheme={$schemeNull}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbDefault = $row['Default'];
|
||||||
|
$schemeDefault = $sig['default'];
|
||||||
|
if (!$this->defaultEquals($dbDefault, $schemeDefault)) {
|
||||||
|
$dbStr = is_null($dbDefault) ? 'NULL' : (string)$dbDefault;
|
||||||
|
$schemeStr = is_null($schemeDefault) ? 'NULL' : (string)$schemeDefault;
|
||||||
|
$diffs[] = "字段默认值不一致:{$field} DB={$dbStr} Scheme={$schemeStr}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbExtra = (string)$row['Extra'];
|
||||||
|
$schemeExtra = $sig['extra'];
|
||||||
|
if ($dbExtra !== $schemeExtra) {
|
||||||
|
$diffs[] = "字段 Extra 不一致:{$field} DB={$dbExtra} Scheme={$schemeExtra}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbPrimary = (string)$row['Key'] === 'PRI';
|
||||||
|
if ($dbPrimary !== $sig['primary']) {
|
||||||
|
$diffs[] = "字段主键不一致:{$field} DB=" . ($dbPrimary ? 'PRI' : '') . " Scheme=" . ($sig['primary'] ? 'PRI' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbComment = (string)$row['Comment'];
|
||||||
|
$schemeComment = $sig['comment'];
|
||||||
|
if ($this->parseComment($dbComment) !== $this->parseComment($schemeComment)) {
|
||||||
|
$diffs[] = "字段注释不一致:{$field} DB={$dbComment} Scheme={$schemeComment}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($dbColumns as $field => $row) {
|
||||||
|
if (!isset($schemeColumns[$field])) {
|
||||||
|
$diffs[] = "多余字段:{$field}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($schemeIndexes as $name => $idx) {
|
||||||
|
if (!isset($dbIndexes[$name])) {
|
||||||
|
$diffs[] = "缺少索引:{$name}";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dbIdx = $dbIndexes[$name];
|
||||||
|
if ($dbIdx['type'] !== $idx['type']) {
|
||||||
|
$diffs[] = "索引类型不一致:{$name} DB={$dbIdx['type']} Scheme={$idx['type']}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($dbIdx['columns'] !== $idx['columns']) {
|
||||||
|
$diffs[] = "索引字段不一致:{$name} DB=" . implode(',', $dbIdx['columns']) . " Scheme=" . implode(',', $idx['columns']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($dbIndexes as $name => $idx) {
|
||||||
|
if (!isset($schemeIndexes[$name])) {
|
||||||
|
$diffs[] = "多余索引:{$name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableCommentDiff = $this->diffTableComment($connection, $fullTableName, $tableAttr->comment);
|
||||||
|
if ($tableCommentDiff !== null) {
|
||||||
|
$diffs[] = $tableCommentDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diffs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getColumnsForCurd(string $className, array $onlyFields = []): array
|
||||||
|
{
|
||||||
|
if (!class_exists($className)) {
|
||||||
|
throw new \Exception("Class $className not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
$ref = new ReflectionClass($className);
|
||||||
|
$columns = [];
|
||||||
|
|
||||||
|
foreach ($ref->getProperties() as $prop) {
|
||||||
|
$fieldAttrs = $prop->getAttributes(Field::class);
|
||||||
|
if (empty($fieldAttrs)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldName = $prop->getName();
|
||||||
|
if (!empty($onlyFields) && !in_array($fieldName, $onlyFields, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Field $field */
|
||||||
|
$field = $fieldAttrs[0]->newInstance();
|
||||||
|
|
||||||
|
$columns[$fieldName] = [
|
||||||
|
'type' => $this->buildMysqlTypeFromSchemeField($field),
|
||||||
|
'comment' => $this->buildColumnComment($field, $prop),
|
||||||
|
'required' => !$field->nullable,
|
||||||
|
'default' => $field->default,
|
||||||
|
'field' => $fieldName,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPrimaryKey(string $className): ?string
|
||||||
|
{
|
||||||
|
if (!class_exists($className)) {
|
||||||
|
throw new \Exception("Class $className not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
$ref = new ReflectionClass($className);
|
||||||
|
foreach ($ref->getProperties() as $prop) {
|
||||||
|
$fieldAttrs = $prop->getAttributes(Field::class);
|
||||||
|
if (empty($fieldAttrs)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Field $field */
|
||||||
|
$field = $fieldAttrs[0]->newInstance();
|
||||||
|
if ($field->primary) {
|
||||||
|
return $prop->getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function checkTableExists(string $connection, string $tableName): bool
|
||||||
|
{
|
||||||
|
$tables = Db::connect($connection)->query("SHOW TABLES LIKE '$tableName'");
|
||||||
return !empty($tables);
|
return !empty($tables);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,27 +323,7 @@ class SchemeToDbService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Comment + Component Restore
|
// Comment + Component Restore
|
||||||
$comment = $field->comment;
|
$comment = $this->buildColumnComment($field, $prop);
|
||||||
|
|
||||||
// 检查是否有 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) {
|
if ($comment) {
|
||||||
$line .= " COMMENT '$comment'";
|
$line .= " COMMENT '$comment'";
|
||||||
@@ -205,7 +364,7 @@ class SchemeToDbService
|
|||||||
return "CREATE TABLE `$tableName` (\n $body\n) ENGINE={$tableAttr->engine} DEFAULT CHARSET={$tableAttr->charset}$comment";
|
return "CREATE TABLE `$tableName` (\n $body\n) ENGINE={$tableAttr->engine} DEFAULT CHARSET={$tableAttr->charset}$comment";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function restoreData($newTable, $oldTable, array $properties)
|
protected function restoreData(string $connection, $newTable, $oldTable, array $properties)
|
||||||
{
|
{
|
||||||
$fields = [];
|
$fields = [];
|
||||||
foreach ($properties as $prop) {
|
foreach ($properties as $prop) {
|
||||||
@@ -218,7 +377,7 @@ class SchemeToDbService
|
|||||||
if (empty($fields)) return;
|
if (empty($fields)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$oldFieldsRaw = Db::connect($this->connection)->getFields($oldTable);
|
$oldFieldsRaw = Db::connect($connection)->getFields($oldTable);
|
||||||
$oldFields = array_keys($oldFieldsRaw);
|
$oldFields = array_keys($oldFieldsRaw);
|
||||||
|
|
||||||
$commonFields = [];
|
$commonFields = [];
|
||||||
@@ -234,10 +393,247 @@ class SchemeToDbService
|
|||||||
$commonStr = implode(',', $commonFields);
|
$commonStr = implode(',', $commonFields);
|
||||||
|
|
||||||
$sql = "INSERT INTO `$newTable` ($commonStr) SELECT $commonStr FROM `$oldTable`";
|
$sql = "INSERT INTO `$newTable` ($commonStr) SELECT $commonStr FROM `$oldTable`";
|
||||||
Db::connect($this->connection)->execute($sql);
|
Db::connect($connection)->execute($sql);
|
||||||
|
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
throw new \Exception("Data migration failed: " . $e->getMessage());
|
throw new \Exception("Data migration failed: " . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function resolveConnection(Table $tableAttr): string
|
||||||
|
{
|
||||||
|
return !empty($tableAttr->connection) ? (string)$tableAttr->connection : (string)$this->connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getTableAttribute(ReflectionClass $ref, string $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();
|
||||||
|
|
||||||
|
return $tableAttr;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildSchemeColumnSignature(ReflectionClass $ref): array
|
||||||
|
{
|
||||||
|
$sig = [];
|
||||||
|
|
||||||
|
foreach ($ref->getProperties() as $prop) {
|
||||||
|
$fieldAttrs = $prop->getAttributes(Field::class);
|
||||||
|
if (empty($fieldAttrs)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldName = $prop->getName();
|
||||||
|
/** @var Field $field */
|
||||||
|
$field = $fieldAttrs[0]->newInstance();
|
||||||
|
|
||||||
|
$sig[$fieldName] = [
|
||||||
|
'type' => $this->buildMysqlTypeFromSchemeField($field),
|
||||||
|
'null' => $field->nullable ? 'YES' : 'NO',
|
||||||
|
'default' => $field->default,
|
||||||
|
'extra' => $field->autoIncrement ? 'auto_increment' : '',
|
||||||
|
'primary' => (bool)$field->primary,
|
||||||
|
'comment' => $this->buildColumnComment($field, $prop),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildSchemeIndexSignature(ReflectionClass $ref): array
|
||||||
|
{
|
||||||
|
$sig = [];
|
||||||
|
|
||||||
|
$indexAttrs = $ref->getAttributes(Index::class);
|
||||||
|
foreach ($indexAttrs as $attr) {
|
||||||
|
/** @var Index $idx */
|
||||||
|
$idx = $attr->newInstance();
|
||||||
|
$cols = is_array($idx->columns) ? $idx->columns : [$idx->columns];
|
||||||
|
$keyName = $idx->name ?: $cols[0];
|
||||||
|
|
||||||
|
$sig[$keyName] = [
|
||||||
|
'type' => $idx->type,
|
||||||
|
'columns' => array_values($cols),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sig;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildDbIndexSignature(array $dbKeysRows): array
|
||||||
|
{
|
||||||
|
$idx = [];
|
||||||
|
|
||||||
|
foreach ($dbKeysRows as $row) {
|
||||||
|
$name = $row['Key_name'];
|
||||||
|
if ($name === 'PRIMARY') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($idx[$name])) {
|
||||||
|
$type = 'NORMAL';
|
||||||
|
if ((int)$row['Non_unique'] === 0) {
|
||||||
|
$type = 'UNIQUE';
|
||||||
|
} elseif (strtoupper((string)$row['Index_type']) === 'FULLTEXT') {
|
||||||
|
$type = 'FULLTEXT';
|
||||||
|
}
|
||||||
|
|
||||||
|
$idx[$name] = [
|
||||||
|
'type' => $type,
|
||||||
|
'columns' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$seq = (int)$row['Seq_in_index'];
|
||||||
|
$idx[$name]['columns'][$seq] = $row['Column_name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($idx as $name => $val) {
|
||||||
|
ksort($idx[$name]['columns']);
|
||||||
|
$idx[$name]['columns'] = array_values($idx[$name]['columns']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildMysqlTypeFromSchemeField(Field $field): string
|
||||||
|
{
|
||||||
|
$type = strtolower($field->type);
|
||||||
|
|
||||||
|
if ($field->length !== null) {
|
||||||
|
$type .= "({$field->length})";
|
||||||
|
} elseif ($field->precision > 0) {
|
||||||
|
$type .= "({$field->precision},{$field->scale})";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($field->unsigned) {
|
||||||
|
$type .= ' unsigned';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildColumnComment(Field $field, ReflectionProperty $prop): string
|
||||||
|
{
|
||||||
|
$comment = (string)$field->comment;
|
||||||
|
$compAttrs = $prop->getAttributes(Component::class);
|
||||||
|
if (empty($compAttrs)) {
|
||||||
|
return trim($comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Component $comp */
|
||||||
|
$comp = $compAttrs[0]->newInstance();
|
||||||
|
$typeStr = "{{$comp->type}}";
|
||||||
|
$optionsStr = '';
|
||||||
|
|
||||||
|
if (!empty($comp->options)) {
|
||||||
|
$parts = [];
|
||||||
|
foreach ($comp->options as $k => $v) {
|
||||||
|
$parts[] = "$k:$v";
|
||||||
|
}
|
||||||
|
$optionsStr = ' (' . implode(',', $parts) . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim(trim($comment) . " $typeStr$optionsStr");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeDbType(string $type): string
|
||||||
|
{
|
||||||
|
$type = strtolower(trim($type));
|
||||||
|
$type = preg_replace('/\\s+/', ' ', $type);
|
||||||
|
// 移除整数类型的显示宽度,例如 int(11) -> int, bigint(20) -> bigint
|
||||||
|
// 但不移除 decimal(10,2) 或 char(32) 这种有实际意义的长度
|
||||||
|
$type = preg_replace('/(tinyint|smallint|mediumint|int|integer|bigint)\s*\(\d+\)/', '$1', $type);
|
||||||
|
$type = preg_replace('/^decimal\\((\\d+)\\)$/', 'decimal($1,0)', $type);
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function parseComment(string $comment): array
|
||||||
|
{
|
||||||
|
$comment = trim($comment);
|
||||||
|
$result = [
|
||||||
|
'content' => '',
|
||||||
|
'type' => '',
|
||||||
|
'options' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Extract options (...)
|
||||||
|
if (preg_match('/^(.*)\s*\((.*)\)$/', $comment, $matches)) {
|
||||||
|
$comment = trim($matches[1]);
|
||||||
|
$optsStr = $matches[2];
|
||||||
|
$pairs = explode(',', $optsStr);
|
||||||
|
foreach ($pairs as $p) {
|
||||||
|
$kv = explode(':', $p, 2);
|
||||||
|
$k = trim($kv[0]);
|
||||||
|
$v = isset($kv[1]) ? trim($kv[1]) : '';
|
||||||
|
if ($k !== '') {
|
||||||
|
$result['options'][$k] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract type {...}
|
||||||
|
if (preg_match('/^(.*)\s*\{(\w+)\}$/', $comment, $matches)) {
|
||||||
|
$comment = trim($matches[1]);
|
||||||
|
$result['type'] = strtolower($matches[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['content'] = $comment;
|
||||||
|
|
||||||
|
// Sort options for comparison
|
||||||
|
ksort($result['options']);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function defaultEquals($dbDefault, $schemeDefault): bool
|
||||||
|
{
|
||||||
|
if (is_null($dbDefault) && is_null($schemeDefault)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_null($dbDefault) xor is_null($schemeDefault)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_bool($schemeDefault)) {
|
||||||
|
$schemeDefault = $schemeDefault ? '1' : '0';
|
||||||
|
} elseif (is_int($schemeDefault) || is_float($schemeDefault)) {
|
||||||
|
$schemeDefault = (string)$schemeDefault;
|
||||||
|
} elseif (is_string($schemeDefault)) {
|
||||||
|
$schemeDefault = (string)$schemeDefault;
|
||||||
|
} else {
|
||||||
|
$schemeDefault = (string)$schemeDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string)$dbDefault === (string)$schemeDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function diffTableComment(string $connection, string $tableName, string $schemeComment): ?string
|
||||||
|
{
|
||||||
|
$database = (string)Config::get('database.connections.' . $connection . '.database', '');
|
||||||
|
if ($database === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$rows = Db::connect($connection)->query(
|
||||||
|
'SELECT table_comment FROM information_schema.TABLES WHERE table_schema = ? AND table_name = ? LIMIT 1',
|
||||||
|
[$database, $tableName]
|
||||||
|
);
|
||||||
|
$dbComment = isset($rows[0]['table_comment']) ? (string)$rows[0]['table_comment'] : '';
|
||||||
|
if ($dbComment !== (string)$schemeComment) {
|
||||||
|
return "表注释不一致:DB={$dbComment} Scheme={$schemeComment}";
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user