mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-07-08 19:12:48 +08:00
feat: 发布智能体版
This commit is contained in:
326
extend/base/common/service/ErrorHandlerBase.php
Normal file
326
extend/base/common/service/ErrorHandlerBase.php
Normal file
@@ -0,0 +1,326 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\service;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
abstract class ErrorHandlerBase
|
||||
{
|
||||
/**
|
||||
* 错误码注册表
|
||||
*/
|
||||
protected array $errorCodes = [
|
||||
// 通用错误 (ERR_GEN_XXX)
|
||||
'ERR_GEN_UNKNOWN' => '未知错误',
|
||||
'ERR_GEN_INVALID_PARAM' => '参数错误',
|
||||
'ERR_GEN_MISSING_PARAM' => '缺少必需参数',
|
||||
'ERR_GEN_OPERATION_FAILED' => '操作失败',
|
||||
'ERR_GEN_PERMISSION_DENIED' => '权限不足',
|
||||
'ERR_GEN_NOT_FOUND' => '资源不存在',
|
||||
'ERR_GEN_TIMEOUT' => '操作超时',
|
||||
|
||||
// 数据库错误 (ERR_DB_XXX)
|
||||
'ERR_DB_CONNECTION' => '数据库连接失败',
|
||||
'ERR_DB_QUERY' => '数据库查询失败',
|
||||
'ERR_DB_INSERT' => '数据插入失败',
|
||||
'ERR_DB_UPDATE' => '数据更新失败',
|
||||
'ERR_DB_DELETE' => '数据删除失败',
|
||||
'ERR_DB_TRANSACTION' => '数据库事务执行失败',
|
||||
'ERR_DB_SCHEME_MISMATCH' => '数据库结构与期望不符',
|
||||
|
||||
// 文件系统错误 (ERR_FS_XXX)
|
||||
'ERR_FS_NOT_FOUND' => '文件不存在',
|
||||
'ERR_FS_READ_FAILED' => '文件读取失败',
|
||||
'ERR_FS_WRITE_FAILED' => '文件写入失败',
|
||||
'ERR_FS_DELETE_FAILED' => '文件删除失败',
|
||||
'ERR_FS_PERMISSION' => '文件权限不足',
|
||||
|
||||
// 网络错误 (ERR_NET_XXX)
|
||||
'ERR_NET_CONNECTION' => '网络连接失败',
|
||||
'ERR_NET_TIMEOUT' => '网络请求超时',
|
||||
'ERR_NET_RESPONSE' => '网络响应错误',
|
||||
|
||||
// 配置错误 (ERR_CFG_XXX)
|
||||
'ERR_CFG_MISSING' => '配置缺失',
|
||||
'ERR_CFG_INVALID' => '配置无效',
|
||||
];
|
||||
|
||||
/**
|
||||
* 修复建议注册表
|
||||
*/
|
||||
protected array $suggestions = [
|
||||
// 通用错误修复建议
|
||||
'ERR_GEN_UNKNOWN' => '未知错误,请稍后重试或联系管理员',
|
||||
'ERR_GEN_INVALID_PARAM' => '请检查传入的参数是否符合要求',
|
||||
'ERR_GEN_MISSING_PARAM' => '请确保所有必需参数都已提供',
|
||||
'ERR_GEN_OPERATION_FAILED' => '请稍后重试,如果问题持续存在请联系管理员',
|
||||
'ERR_GEN_PERMISSION_DENIED' => '请检查您是否有执行此操作的权限',
|
||||
'ERR_GEN_NOT_FOUND' => '请确认请求的资源ID是否正确',
|
||||
'ERR_GEN_TIMEOUT' => '请检查网络连接或稍后重试',
|
||||
|
||||
// 数据库错误修复建议
|
||||
'ERR_DB_CONNECTION' => '请检查数据库连接配置和网络连接',
|
||||
'ERR_DB_QUERY' => '请检查SQL语句语法和数据库表结构',
|
||||
'ERR_DB_INSERT' => '请检查数据是否符合表结构和约束条件',
|
||||
'ERR_DB_UPDATE' => '请确保要更新的数据存在且符合约束条件',
|
||||
'ERR_DB_DELETE' => '请确保要删除的数据存在',
|
||||
'ERR_DB_TRANSACTION' => '请检查事务中的操作是否都执行成功',
|
||||
'ERR_DB_SCHEME_MISMATCH' => '请运行数据库迁移命令: php think migrate:run',
|
||||
|
||||
// 文件系统错误修复建议
|
||||
'ERR_FS_NOT_FOUND' => '请检查文件路径是否正确',
|
||||
'ERR_FS_READ_FAILED' => '请检查文件是否存在且有读取权限',
|
||||
'ERR_FS_WRITE_FAILED' => '请检查目录是否存在且有写入权限',
|
||||
'ERR_FS_DELETE_FAILED' => '请检查文件是否存在且有删除权限',
|
||||
'ERR_FS_PERMISSION' => '请检查文件或目录的权限设置',
|
||||
|
||||
// 网络错误修复建议
|
||||
'ERR_NET_CONNECTION' => '请检查网络连接和目标服务器状态',
|
||||
'ERR_NET_TIMEOUT' => '请检查网络连接或增加超时时间',
|
||||
'ERR_NET_RESPONSE' => '请检查请求参数和服务器响应',
|
||||
|
||||
// 配置错误修复建议
|
||||
'ERR_CFG_MISSING' => '请在配置文件中添加缺失的配置项',
|
||||
'ERR_CFG_INVALID' => '请检查配置项的值是否符合要求',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取结构化错误信息
|
||||
*
|
||||
* @param string $errorCode 错误码
|
||||
* @param string|null $errorMessage 错误消息(覆盖默认错误消息)
|
||||
* @param array $context 上下文信息
|
||||
* @return array 结构化错误信息
|
||||
*/
|
||||
public function getError(string $errorCode, ?string $errorMessage = null, array $context = []): array
|
||||
{
|
||||
$defaultMessage = $this->errorCodes[$errorCode] ?? $this->errorCodes['ERR_GEN_UNKNOWN'];
|
||||
$actualMessage = $errorMessage ?? $defaultMessage;
|
||||
|
||||
$error = [
|
||||
'success' => false,
|
||||
'error_code' => $errorCode,
|
||||
'error_message' => $actualMessage,
|
||||
];
|
||||
|
||||
// 添加文件和行信息(如果存在)
|
||||
if (isset($context['file'])) {
|
||||
$error['file'] = $context['file'];
|
||||
}
|
||||
if (isset($context['line'])) {
|
||||
$error['line'] = $context['line'];
|
||||
}
|
||||
|
||||
// 添加修复建议
|
||||
$suggestion = $this->suggestions[$errorCode] ?? $this->suggestions['ERR_GEN_UNKNOWN'];
|
||||
$error['suggestion'] = $suggestion;
|
||||
|
||||
// 添加额外的上下文信息
|
||||
if (!empty($context)) {
|
||||
$error['context'] = $context;
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从异常生成结构化错误信息
|
||||
*
|
||||
* @param Throwable $exception 异常对象
|
||||
* @param string|null $customErrorCode 自定义错误码
|
||||
* @return array 结构化错误信息
|
||||
*/
|
||||
public function getErrorForException(Throwable $exception, ?string $customErrorCode = null): array
|
||||
{
|
||||
// 尝试从异常消息推断错误码
|
||||
$errorCode = $customErrorCode ?? $this->inferErrorCodeFromException($exception);
|
||||
|
||||
$error = [
|
||||
'success' => false,
|
||||
'error_code' => $errorCode,
|
||||
'error_message' => $exception->getMessage(),
|
||||
];
|
||||
|
||||
// 添加文件和行信息
|
||||
if ($exception->getFile()) {
|
||||
$error['file'] = $exception->getFile();
|
||||
}
|
||||
if ($exception->getLine()) {
|
||||
$error['line'] = $exception->getLine();
|
||||
}
|
||||
|
||||
// 添加修复建议
|
||||
$suggestion = $this->suggestions[$errorCode] ?? $this->suggestions['ERR_GEN_UNKNOWN'];
|
||||
$error['suggestion'] = $suggestion;
|
||||
|
||||
// 添加异常类型
|
||||
$error['exception_type'] = get_class($exception);
|
||||
|
||||
// 添加堆栈跟踪(在开发模式下)
|
||||
if ($this->isDebugMode()) {
|
||||
$error['trace'] = $this->formatTrace($exception);
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从异常推断错误码
|
||||
*
|
||||
* @param Throwable $exception 异常对象
|
||||
* @return string 错误码
|
||||
*/
|
||||
protected function inferErrorCodeFromException(Throwable $exception): string
|
||||
{
|
||||
$message = $exception->getMessage();
|
||||
$class = get_class($exception);
|
||||
|
||||
// 根据异常类型推断
|
||||
if (strpos($class, 'Db') !== false || strpos($class, 'Query') !== false) {
|
||||
if (strpos($message, 'connection') !== false || strpos($message, 'connect') !== false) {
|
||||
return 'ERR_DB_CONNECTION';
|
||||
}
|
||||
if (strpos($message, 'SQLSTATE') !== false) {
|
||||
return 'ERR_DB_QUERY';
|
||||
}
|
||||
return 'ERR_DB_OPERATION_FAILED';
|
||||
}
|
||||
|
||||
if (strpos($class, 'File') !== false || strpos($class, 'Stream') !== false) {
|
||||
if (strpos($message, 'No such file') !== false || strpos($message, 'not found') !== false) {
|
||||
return 'ERR_FS_NOT_FOUND';
|
||||
}
|
||||
if (strpos($message, 'permission') !== false) {
|
||||
return 'ERR_FS_PERMISSION';
|
||||
}
|
||||
return 'ERR_FS_READ_FAILED';
|
||||
}
|
||||
|
||||
if (strpos($class, 'Network') !== false || strpos($class, 'Curl') !== false) {
|
||||
if (strpos($message, 'timeout') !== false) {
|
||||
return 'ERR_NET_TIMEOUT';
|
||||
}
|
||||
return 'ERR_NET_CONNECTION';
|
||||
}
|
||||
|
||||
// 根据消息内容推断
|
||||
if (strpos($message, 'permission') !== false || strpos($message, 'denied') !== false) {
|
||||
return 'ERR_GEN_PERMISSION_DENIED';
|
||||
}
|
||||
|
||||
if (strpos($message, 'not found') !== false || strpos($message, '不存在') !== false) {
|
||||
return 'ERR_GEN_NOT_FOUND';
|
||||
}
|
||||
|
||||
if (strpos($message, 'timeout') !== false) {
|
||||
return 'ERR_GEN_TIMEOUT';
|
||||
}
|
||||
|
||||
return 'ERR_GEN_UNKNOWN';
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化堆栈跟踪
|
||||
*
|
||||
* @param Throwable $exception 异常对象
|
||||
* @return array 格式化的堆栈跟踪
|
||||
*/
|
||||
protected function formatTrace(Throwable $exception): array
|
||||
{
|
||||
$trace = [];
|
||||
foreach ($exception->getTrace() as $index => $frame) {
|
||||
$traceItem = [
|
||||
'index' => $index,
|
||||
];
|
||||
|
||||
if (isset($frame['file'])) {
|
||||
$traceItem['file'] = $frame['file'];
|
||||
}
|
||||
if (isset($frame['line'])) {
|
||||
$traceItem['line'] = $frame['line'];
|
||||
}
|
||||
if (isset($frame['function'])) {
|
||||
$traceItem['function'] = $frame['function'];
|
||||
}
|
||||
if (isset($frame['class'])) {
|
||||
$traceItem['class'] = $frame['class'];
|
||||
}
|
||||
if (isset($frame['type'])) {
|
||||
$traceItem['type'] = $frame['type'];
|
||||
}
|
||||
|
||||
$trace[] = $traceItem;
|
||||
}
|
||||
return $trace;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为调试模式
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isDebugMode(): bool
|
||||
{
|
||||
try {
|
||||
$app = \think\facade\App::instance();
|
||||
return (bool)($app->config->get('app.app_debug') ?? false);
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出错误(作为JSON)
|
||||
*
|
||||
* @param array $error 错误信息
|
||||
* @return string JSON格式的错误信息
|
||||
*/
|
||||
public function outputError(array $error): string
|
||||
{
|
||||
$options = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
|
||||
return json_encode($error, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册自定义错误码
|
||||
*
|
||||
* @param string $errorCode 错误码
|
||||
* @param string $errorMessage 错误消息
|
||||
* @param string|null $suggestion 修复建议
|
||||
* @return void
|
||||
*/
|
||||
public function registerErrorCode(string $errorCode, string $errorMessage, ?string $suggestion = null): void
|
||||
{
|
||||
$this->errorCodes[$errorCode] = $errorMessage;
|
||||
if ($suggestion !== null) {
|
||||
$this->suggestions[$errorCode] = $suggestion;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量注册错误码
|
||||
*
|
||||
* @param array $errors 错误码数组,格式:['ERR_XXX' => ['message' => '...', 'suggestion' => '...']]
|
||||
* @return void
|
||||
*/
|
||||
public function registerErrorCodes(array $errors): void
|
||||
{
|
||||
foreach ($errors as $errorCode => $config) {
|
||||
$this->errorCodes[$errorCode] = $config['message'] ?? '未知错误';
|
||||
if (isset($config['suggestion'])) {
|
||||
$this->suggestions[$errorCode] = $config['suggestion'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已注册的错误码
|
||||
*
|
||||
* @return array 错误码数组
|
||||
*/
|
||||
public function getRegisteredErrorCodes(): array
|
||||
{
|
||||
return $this->errorCodes;
|
||||
}
|
||||
}
|
||||
@@ -111,4 +111,198 @@ class MenuServiceBase
|
||||
|
||||
return $menuData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建菜单
|
||||
*
|
||||
* @param array $data 菜单数据
|
||||
* @return int 返回新创建的菜单ID
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function create(array $data): int
|
||||
{
|
||||
$menu = Db::name('system_menu');
|
||||
|
||||
// 准备数据
|
||||
$insertData = [
|
||||
'pid' => $data['parent_id'] ?? 0,
|
||||
'title' => $data['title'] ?? '',
|
||||
'icon' => $data['icon'] ?? '',
|
||||
'href' => $data['path'] ?? '',
|
||||
'auth_node' => $data['node'] ?? '',
|
||||
'sort' => $data['sort'] ?? 100,
|
||||
'status' => 1,
|
||||
'target' => '_self',
|
||||
'remark' => $data['remark'] ?? '',
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
'delete_time' => 0,
|
||||
];
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($insertData['title'])) {
|
||||
throw new \Exception('菜单标题不能为空');
|
||||
}
|
||||
|
||||
// 验证父菜单是否存在
|
||||
if ($insertData['pid'] > 0) {
|
||||
$parentMenu = Db::name('system_menu')
|
||||
->where('id', $insertData['pid'])
|
||||
->where('delete_time', 0)
|
||||
->find();
|
||||
if (empty($parentMenu)) {
|
||||
throw new \Exception("父菜单ID {$insertData['pid']} 不存在");
|
||||
}
|
||||
}
|
||||
|
||||
// 插入数据
|
||||
$menuId = $menu->insertGetId($insertData);
|
||||
|
||||
if (!$menuId) {
|
||||
throw new \Exception('菜单创建失败');
|
||||
}
|
||||
|
||||
return (int)$menuId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新菜单
|
||||
*
|
||||
* @param int $menuId 菜单ID
|
||||
* @param array $data 更新数据
|
||||
* @return bool 是否更新成功
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function update(int $menuId, array $data): bool
|
||||
{
|
||||
// 验证菜单是否存在
|
||||
$menu = Db::name('system_menu')
|
||||
->where('id', $menuId)
|
||||
->where('delete_time', 0)
|
||||
->find();
|
||||
|
||||
if (empty($menu)) {
|
||||
throw new \Exception("菜单ID {$menuId} 不存在");
|
||||
}
|
||||
|
||||
// 准备更新数据
|
||||
$updateData = [
|
||||
'update_time' => time(),
|
||||
];
|
||||
|
||||
if (isset($data['title'])) {
|
||||
$updateData['title'] = $data['title'];
|
||||
}
|
||||
if (isset($data['parent_id'])) {
|
||||
$updateData['pid'] = $data['parent_id'];
|
||||
}
|
||||
if (isset($data['icon'])) {
|
||||
$updateData['icon'] = $data['icon'];
|
||||
}
|
||||
if (isset($data['path'])) {
|
||||
$updateData['href'] = $data['path'];
|
||||
}
|
||||
if (isset($data['node'])) {
|
||||
$updateData['auth_node'] = $data['node'];
|
||||
}
|
||||
if (isset($data['sort'])) {
|
||||
$updateData['sort'] = $data['sort'];
|
||||
}
|
||||
if (isset($data['status'])) {
|
||||
$updateData['status'] = $data['status'];
|
||||
}
|
||||
if (isset($data['remark'])) {
|
||||
$updateData['remark'] = $data['remark'];
|
||||
}
|
||||
if (isset($data['target'])) {
|
||||
$updateData['target'] = $data['target'];
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if (empty($menu['title']) && empty($updateData['title'])) {
|
||||
throw new \Exception('菜单标题不能为空');
|
||||
}
|
||||
|
||||
// 验证父菜单是否存在(如果修改了父菜单)
|
||||
if (isset($updateData['pid']) && $updateData['pid'] > 0) {
|
||||
// 不能将自己设置为父菜单
|
||||
if ($updateData['pid'] == $menuId) {
|
||||
throw new \Exception('不能将自己设置为父菜单');
|
||||
}
|
||||
|
||||
$parentMenu = Db::name('system_menu')
|
||||
->where('id', $updateData['pid'])
|
||||
->where('delete_time', 0)
|
||||
->find();
|
||||
if (empty($parentMenu)) {
|
||||
throw new \Exception("父菜单ID {$updateData['pid']} 不存在");
|
||||
}
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
$result = Db::name('system_menu')
|
||||
->where('id', $menuId)
|
||||
->update($updateData);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
*
|
||||
* @param int $menuId 菜单ID
|
||||
* @return bool 是否删除成功
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function delete(int $menuId): bool
|
||||
{
|
||||
// 验证菜单是否存在
|
||||
$menu = Db::name('system_menu')
|
||||
->where('id', $menuId)
|
||||
->where('delete_time', 0)
|
||||
->find();
|
||||
|
||||
if (empty($menu)) {
|
||||
throw new \Exception("菜单ID {$menuId} 不存在");
|
||||
}
|
||||
|
||||
// 检查是否有子菜单
|
||||
$hasChildren = Db::name('system_menu')
|
||||
->where('pid', $menuId)
|
||||
->where('delete_time', 0)
|
||||
->count();
|
||||
|
||||
if ($hasChildren > 0) {
|
||||
throw new \Exception('该菜单下存在子菜单,请先删除子菜单');
|
||||
}
|
||||
|
||||
// 软删除
|
||||
$result = Db::name('system_menu')
|
||||
->where('id', $menuId)
|
||||
->update([
|
||||
'delete_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单详情
|
||||
*
|
||||
* @param int $menuId 菜单ID
|
||||
* @return array|null 菜单数据
|
||||
* @throws \think\db\exception\DataNotFoundException
|
||||
* @throws \think\db\exception\DbException
|
||||
* @throws \think\db\exception\ModelNotFoundException
|
||||
*/
|
||||
public function getMenu(int $menuId): ?array
|
||||
{
|
||||
$menu = Db::name('system_menu')
|
||||
->where('id', $menuId)
|
||||
->where('delete_time', 0)
|
||||
->find();
|
||||
|
||||
return $menu ? $menu : null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ class SchemeToDbService
|
||||
$fullTableName = $prefix . $tableName;
|
||||
}
|
||||
|
||||
$sql = $this->buildCreateTableSql($fullTableName, $tableAttr, $ref);
|
||||
|
||||
// 检查表是否存在
|
||||
$tableExists = $this->checkTableExists($connection, $fullTableName);
|
||||
$backupTableName = null;
|
||||
@@ -66,7 +68,6 @@ class SchemeToDbService
|
||||
}
|
||||
|
||||
// 2. 建表
|
||||
$sql = $this->buildCreateTableSql($fullTableName, $tableAttr, $ref);
|
||||
Db::connect($connection)->execute($sql);
|
||||
|
||||
// 3. 恢复数据
|
||||
@@ -121,88 +122,159 @@ class SchemeToDbService
|
||||
$schemeIndexes = $this->buildSchemeIndexSignature($ref);
|
||||
$dbIndexes = $this->buildDbIndexSignature($dbKeysRows);
|
||||
|
||||
$diffs = [];
|
||||
$missingFields = [];
|
||||
$extraFields = [];
|
||||
$changedFields = [];
|
||||
|
||||
foreach ($schemeColumns as $field => $sig) {
|
||||
if (!isset($dbColumns[$field])) {
|
||||
$diffs[] = "缺少字段:{$field}";
|
||||
$missingFields[] = $field;
|
||||
continue;
|
||||
}
|
||||
|
||||
$row = $dbColumns[$field];
|
||||
$fieldChanges = [];
|
||||
|
||||
$dbType = $this->normalizeDbType((string)$row['Type']);
|
||||
$schemeType = $this->normalizeDbType($sig['type']);
|
||||
if ($dbType !== $schemeType) {
|
||||
$diffs[] = "字段类型不一致:{$field} DB={$dbType} Scheme={$schemeType}";
|
||||
$fieldChanges['type'] = ['db' => $dbType, 'scheme' => $schemeType];
|
||||
}
|
||||
|
||||
$dbNull = (string)$row['Null'];
|
||||
$schemeNull = $sig['null'];
|
||||
if ($dbNull !== $schemeNull) {
|
||||
$diffs[] = "字段可空不一致:{$field} DB={$dbNull} Scheme={$schemeNull}";
|
||||
$fieldChanges['null'] = ['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}";
|
||||
$fieldChanges['default'] = [
|
||||
'db' => $this->stringifyDefault($dbDefault),
|
||||
'scheme' => $this->stringifyDefault($schemeDefault),
|
||||
];
|
||||
}
|
||||
|
||||
$dbExtra = (string)$row['Extra'];
|
||||
$schemeExtra = $sig['extra'];
|
||||
if ($dbExtra !== $schemeExtra) {
|
||||
$diffs[] = "字段 Extra 不一致:{$field} DB={$dbExtra} Scheme={$schemeExtra}";
|
||||
$fieldChanges['extra'] = ['db' => $dbExtra, 'scheme' => $schemeExtra];
|
||||
}
|
||||
|
||||
$dbPrimary = (string)$row['Key'] === 'PRI';
|
||||
if ($dbPrimary !== $sig['primary']) {
|
||||
$diffs[] = "字段主键不一致:{$field} DB=" . ($dbPrimary ? 'PRI' : '') . " Scheme=" . ($sig['primary'] ? 'PRI' : '');
|
||||
$fieldChanges['primary'] = [
|
||||
'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}";
|
||||
$fieldChanges['comment'] = ['db' => $dbComment, 'scheme' => $schemeComment];
|
||||
}
|
||||
|
||||
if (!empty($fieldChanges)) {
|
||||
$changedFields[$field] = $fieldChanges;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dbColumns as $field => $row) {
|
||||
if (!isset($schemeColumns[$field])) {
|
||||
$diffs[] = "多余字段:{$field}";
|
||||
$extraFields[] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
$missingIndexes = [];
|
||||
$extraIndexes = [];
|
||||
$changedIndexes = [];
|
||||
|
||||
foreach ($schemeIndexes as $name => $idx) {
|
||||
if (!isset($dbIndexes[$name])) {
|
||||
$diffs[] = "缺少索引:{$name}";
|
||||
$missingIndexes[] = $name;
|
||||
continue;
|
||||
}
|
||||
|
||||
$dbIdx = $dbIndexes[$name];
|
||||
$idxChanges = [];
|
||||
|
||||
if ($dbIdx['type'] !== $idx['type']) {
|
||||
$diffs[] = "索引类型不一致:{$name} DB={$dbIdx['type']} Scheme={$idx['type']}";
|
||||
$idxChanges['type'] = ['db' => $dbIdx['type'], 'scheme' => $idx['type']];
|
||||
}
|
||||
|
||||
if ($dbIdx['columns'] !== $idx['columns']) {
|
||||
$diffs[] = "索引字段不一致:{$name} DB=" . implode(',', $dbIdx['columns']) . " Scheme=" . implode(',', $idx['columns']);
|
||||
$idxChanges['columns'] = [
|
||||
'db' => implode(',', $dbIdx['columns']),
|
||||
'scheme' => implode(',', $idx['columns']),
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($idxChanges)) {
|
||||
$changedIndexes[$name] = $idxChanges;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dbIndexes as $name => $idx) {
|
||||
if (!isset($schemeIndexes[$name])) {
|
||||
$diffs[] = "多余索引:{$name}";
|
||||
$extraIndexes[] = $name;
|
||||
}
|
||||
}
|
||||
|
||||
$tableCommentDiff = $this->diffTableComment($connection, $fullTableName, $tableAttr->comment);
|
||||
if ($tableCommentDiff !== null) {
|
||||
$diffs[] = $tableCommentDiff;
|
||||
$hasDiff = !empty($missingFields) || !empty($extraFields) || !empty($changedFields) || !empty($missingIndexes) || !empty($extraIndexes) || !empty($changedIndexes) || $tableCommentDiff !== null;
|
||||
if (!$hasDiff) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $diffs;
|
||||
$lines = [];
|
||||
$lines[] = '差异汇总:'
|
||||
. '字段缺失=' . count($missingFields)
|
||||
. ',字段多余=' . count($extraFields)
|
||||
. ',字段修改=' . count($changedFields)
|
||||
. ';索引缺失=' . count($missingIndexes)
|
||||
. ',索引多余=' . count($extraIndexes)
|
||||
. ',索引修改=' . count($changedIndexes)
|
||||
. ';表注释=' . ($tableCommentDiff !== null ? '不一致' : '一致');
|
||||
|
||||
if (!empty($missingFields)) {
|
||||
$lines[] = '字段缺失(' . count($missingFields) . '):' . implode(',', $missingFields);
|
||||
}
|
||||
if (!empty($extraFields)) {
|
||||
$lines[] = '字段多余(' . count($extraFields) . '):' . implode(',', $extraFields);
|
||||
}
|
||||
if (!empty($changedFields)) {
|
||||
$lines[] = '字段修改(' . count($changedFields) . '):' . implode(',', array_keys($changedFields));
|
||||
foreach ($changedFields as $field => $changes) {
|
||||
$lines[] = '字段 ' . $field . ':';
|
||||
foreach ($changes as $k => $v) {
|
||||
$lines[] = ' - ' . $k . ':DB=' . $v['db'] . ' Scheme=' . $v['scheme'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($missingIndexes)) {
|
||||
$lines[] = '索引缺失(' . count($missingIndexes) . '):' . implode(',', $missingIndexes);
|
||||
}
|
||||
if (!empty($extraIndexes)) {
|
||||
$lines[] = '索引多余(' . count($extraIndexes) . '):' . implode(',', $extraIndexes);
|
||||
}
|
||||
if (!empty($changedIndexes)) {
|
||||
$lines[] = '索引修改(' . count($changedIndexes) . '):' . implode(',', array_keys($changedIndexes));
|
||||
foreach ($changedIndexes as $name => $changes) {
|
||||
$lines[] = '索引 ' . $name . ':';
|
||||
foreach ($changes as $k => $v) {
|
||||
$lines[] = ' - ' . $k . ':DB=' . $v['db'] . ' Scheme=' . $v['scheme'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($tableCommentDiff !== null) {
|
||||
$lines[] = $tableCommentDiff;
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
|
||||
public function getColumnsForCurd(string $className, array $onlyFields = []): array
|
||||
@@ -227,6 +299,7 @@ class SchemeToDbService
|
||||
|
||||
/** @var Field $field */
|
||||
$field = $fieldAttrs[0]->newInstance();
|
||||
$this->validateSchemeField($field, $ref->getName(), $fieldName);
|
||||
|
||||
$columns[$fieldName] = [
|
||||
'type' => $this->buildMysqlTypeFromSchemeField($field),
|
||||
@@ -283,6 +356,7 @@ class SchemeToDbService
|
||||
/** @var Field $field */
|
||||
$field = $fieldAttrs[0]->newInstance();
|
||||
$fieldName = $prop->getName();
|
||||
$this->validateSchemeField($field, $ref->getName(), $fieldName);
|
||||
|
||||
$line = "`$fieldName` {$field->type}";
|
||||
|
||||
@@ -364,6 +438,20 @@ class SchemeToDbService
|
||||
return "CREATE TABLE `$tableName` (\n $body\n) ENGINE={$tableAttr->engine} DEFAULT CHARSET={$tableAttr->charset}$comment";
|
||||
}
|
||||
|
||||
protected function validateSchemeField(Field $field, string $className, string $fieldName): void
|
||||
{
|
||||
$type = strtolower(trim($field->type));
|
||||
$length = $field->length;
|
||||
|
||||
if ($length !== null && $length < 1) {
|
||||
throw new \InvalidArgumentException("Scheme 字段定义非法:{$className}::\${$fieldName} length 必须 >= 1,当前={$length}");
|
||||
}
|
||||
|
||||
if ($type === 'char' && $length !== null && $length > 255) {
|
||||
throw new \InvalidArgumentException("Scheme 字段定义非法:{$className}::\${$fieldName} type=char 的 length 超出范围,允许 1-255,当前={$length}");
|
||||
}
|
||||
}
|
||||
|
||||
protected function restoreData(string $connection, $newTable, $oldTable, array $properties)
|
||||
{
|
||||
$fields = [];
|
||||
@@ -431,6 +519,7 @@ class SchemeToDbService
|
||||
$fieldName = $prop->getName();
|
||||
/** @var Field $field */
|
||||
$field = $fieldAttrs[0]->newInstance();
|
||||
$this->validateSchemeField($field, $ref->getName(), $fieldName);
|
||||
|
||||
$sig[$fieldName] = [
|
||||
'type' => $this->buildMysqlTypeFromSchemeField($field),
|
||||
@@ -614,6 +703,23 @@ class SchemeToDbService
|
||||
return (string)$dbDefault === (string)$schemeDefault;
|
||||
}
|
||||
|
||||
protected function stringifyDefault($value): string
|
||||
{
|
||||
if (is_null($value)) {
|
||||
return 'NULL';
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
if (is_int($value) || is_float($value)) {
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
return (string)$value;
|
||||
}
|
||||
|
||||
protected function diffTableComment(string $connection, string $tableName, string $schemeComment): ?string
|
||||
{
|
||||
$type = strtolower((string)Config::get('database.connections.' . $connection . '.type', ''));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace base\common\service;
|
||||
namespace base\common\service\tools;
|
||||
|
||||
use think\console\Input;
|
||||
use think\console\Output;
|
||||
@@ -9,7 +9,7 @@ use think\facade\Env;
|
||||
use think\facade\Config;
|
||||
use think\Exception;
|
||||
|
||||
class ToolsDbServiceBase
|
||||
class DbServiceBase
|
||||
{
|
||||
protected $connection = 'main';
|
||||
|
||||
Reference in New Issue
Block a user