feat(scheme): 增强 Scheme 与数据库同步机制并添加严格校验

This commit is contained in:
augushong
2026-01-12 12:37:37 +08:00
parent 2f7ec93f89
commit ee40374732
5 changed files with 612 additions and 128 deletions

View File

@@ -3,6 +3,9 @@
namespace base\admin\service\curd;
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\facade\Db;
use think\helper\Str;
@@ -229,8 +232,9 @@ class BuildCurdServiceBase
*/
public function __construct()
{
$this->tablePrefix = config('database.connections.mysql.prefix');
$this->dbName = config('database.connections.mysql.database');
$connection = config('database.default', 'mysql');
$this->tablePrefix = config("database.connections.{$connection}.prefix");
$this->dbName = config("database.connections.{$connection}.database");
$this->dir = __DIR__;
$this->rootDir = root_path();
@@ -258,36 +262,58 @@ class BuildCurdServiceBase
public function setTable($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 {
// 获取表列注释
$colums = Db::query("SHOW FULL COLUMNS FROM {$this->tablePrefix}{$this->table}");
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) {
$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}{$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 .= "- 从数据库生成 Schemephp 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);
// 初始化默认模型名
@@ -342,62 +368,92 @@ class BuildCurdServiceBase
if (!isset($this->tableColumns[$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 .= "- 从数据库生成 Schemephp 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)) {
$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);
$formatColums[$vo['Field']] = $colum;
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());
$allColumns = $schemeService->getColumnsForCurd($schemeClass);
if (!empty($bindSelectField) && !isset($allColumns[$bindSelectField])) {
throw new TableException("关联表{$relationTable}不存在该字段: {$bindSelectField}");
}
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;
}