mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-07-01 15:32:48 +08:00
244 lines
8.6 KiB
PHP
244 lines
8.6 KiB
PHP
<?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());
|
|
}
|
|
}
|
|
}
|