roleId !== 1) { $this->utils->errorThrow('权限不足,只有超级管理员可以操作数据库管理功能'); } } /** * 获取数据库表列表 * @return array */ public function listTables(): array { $database = config('database.connections.mysql.database'); $tables = DB::select(" SELECT TABLE_NAME as `name`, TABLE_COMMENT as `comment`, TABLE_ROWS as `rows`, DATA_LENGTH as data_length, INDEX_LENGTH as index_length, CREATE_TIME as create_time, UPDATE_TIME as update_time, TABLE_COLLATION as collation, ENGINE as engine FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME ", [$database]); $result = []; foreach ($tables as $table) { $result[] = [ 'name' => $table->name, 'comment' => $table->comment ?: '', 'rows' => (int)$table->rows, 'data_length' => (int)$table->data_length, 'index_length' => (int)$table->index_length, 'create_time' => $table->create_time, 'update_time' => $table->update_time, 'collation' => $table->collation, 'engine' => $table->engine, ]; } return $result; } /** * 获取表详细信息 * @param string $tableName * @return array * @throws Exception */ public function getTableInfo(string $tableName): array { $this->validateTableName($tableName); $database = config('database.connections.mysql.database'); // 获取表基本信息 $tableInfo = DB::selectOne(" SELECT TABLE_NAME as `name`, TABLE_COMMENT as `comment`, TABLE_ROWS as `rows`, DATA_LENGTH as data_length, INDEX_LENGTH as index_length, CREATE_TIME as create_time, UPDATE_TIME as update_time, TABLE_COLLATION as collation, ENGINE as engine FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ", [$database, $tableName]); if (!$tableInfo) { $this->utils->errorThrow('表不存在'); } // 获取字段信息 $columns = $this->getTableColumns($tableName); // 获取索引信息 $indexes = $this->getTableIndexes($tableName); return [ 'name' => $tableInfo->name, 'comment' => $tableInfo->comment ?: '', 'rows' => (int)$tableInfo->rows, 'data_length' => (int)$tableInfo->data_length, 'index_length' => (int)$tableInfo->index_length, 'create_time' => $tableInfo->create_time, 'update_time' => $tableInfo->update_time, 'collation' => $tableInfo->collation, 'engine' => $tableInfo->engine, 'columns' => $columns, 'indexes' => $indexes, ]; } /** * 获取表字段信息 * @param string $tableName * @return array */ public function getTableColumns(string $tableName): array { $this->validateTableName($tableName); $database = config('database.connections.mysql.database'); $columns = DB::select(" SELECT COLUMN_NAME as `name`, DATA_TYPE as `type`, CHARACTER_MAXIMUM_LENGTH as `length`, NUMERIC_PRECISION as `precision`, NUMERIC_SCALE as `scale`, COLUMN_DEFAULT as default_value, COLUMN_COMMENT as `comment`, IS_NULLABLE as nullable, COLUMN_KEY as key_type, EXTRA as extra, COLUMN_TYPE as column_type, ORDINAL_POSITION as `position` FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION ", [$database, $tableName]); $result = []; foreach ($columns as $column) { $result[] = [ 'name' => $column->name, 'type' => $column->type, 'length' => $column->length, 'precision' => $column->precision, 'scale' => $column->scale, 'default_value' => $column->default_value, 'comment' => $column->comment ?: '', 'nullable' => $column->nullable === 'YES', 'key_type' => $column->key_type, 'extra' => $column->extra, 'column_type' => $column->column_type, 'position' => (int)$column->position, ]; } return $result; } /** * 获取表索引信息 * @param string $tableName * @return array */ public function getTableIndexes(string $tableName): array { $this->validateTableName($tableName); $database = config('database.connections.mysql.database'); $indexes = DB::select(" SELECT INDEX_NAME as `name`, COLUMN_NAME as column_name, NON_UNIQUE as non_unique, SEQ_IN_INDEX as seq_in_index, INDEX_TYPE as `type` FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY INDEX_NAME, SEQ_IN_INDEX ", [$database, $tableName]); $result = []; $indexMap = []; foreach ($indexes as $index) { $indexName = $index->name; if (!isset($indexMap[$indexName])) { $indexMap[$indexName] = [ 'name' => $indexName, 'columns' => [], 'unique' => $index->non_unique == 0, 'type' => $index->type, ]; } $indexMap[$indexName]['columns'][] = $index->column_name; } return array_values($indexMap); } /** * 获取表数据(分页) * @param string $tableName * @param int $page * @param int $pageSize * @param array $where * @return array * @throws Exception */ public function getTableData(string $tableName, int $page = 1, int $pageSize = 20, array $where = []): array { $this->validateTableName($tableName); // 移除表名前缀,因为 DB::table() 会自动添加 $tableNameWithoutPrefix = $this->removeTablePrefix($tableName); $query = DB::table($tableNameWithoutPrefix); // 应用查询条件 $this->applyWhereConditions($query, $where); $total = $query->count(); $data = $query->skip(($page - 1) * $pageSize)->take($pageSize)->get()->toArray(); return [ 'page' => $page, 'size' => $pageSize, 'total' => $total, 'page_count' => ceil($total / $pageSize), 'items' => $data, ]; } /** * 应用WHERE条件到查询构建器 * @param \Illuminate\Database\Query\Builder $query * @param array $where * @return void */ private function applyWhereConditions($query, array $where): void { $logic = 'AND'; // 默认逻辑连接符 foreach ($where as $index => $condition) { // 处理逻辑连接符(AND/OR) if (isset($condition['logic'])) { $logic = strtoupper($condition['logic']); } elseif ($index > 0) { // 如果没有指定,默认使用AND $logic = 'AND'; } if (!isset($condition['field']) || !isset($condition['operator'])) { continue; } $field = $condition['field']; $operator = strtolower($condition['operator']); $value = $condition['value'] ?? null; // 根据逻辑连接符选择方法 $whereMethod = $logic === 'OR' ? 'orWhere' : 'where'; switch ($operator) { case '=': case '!=': case '>': case '>=': case '<': case '<=': if ($value !== null) { $query->{$whereMethod}($field, $operator, $value); } break; case 'like': if ($value !== null) { $query->{$whereMethod}($field, 'like', "%{$value}%"); } break; case 'not like': if ($value !== null) { $query->{$whereMethod}($field, 'not like', "%{$value}%"); } break; case 'in': if (is_array($value) && !empty($value)) { $query->{$whereMethod . 'In'}($field, $value); } break; case 'not in': if (is_array($value) && !empty($value)) { $query->{$whereMethod . 'NotIn'}($field, $value); } break; case 'between': if (is_array($value) && count($value) === 2) { $query->{$whereMethod . 'Between'}($field, $value); } break; case 'is null': $query->{$whereMethod . 'Null'}($field); break; case 'is not null': $query->{$whereMethod . 'NotNull'}($field); break; } } } /** * 更新表数据 * @param string $tableName * @param array $data * @param array $where * @return bool * @throws Exception */ public function updateTableData(string $tableName, array $data, array $where): bool { $this->validateTableName($tableName); // 移除表名前缀,因为 DB::table() 会自动添加 $tableNameWithoutPrefix = $this->removeTablePrefix($tableName); $query = DB::table($tableNameWithoutPrefix); // 获取变更前的数据 $beforeQuery = clone $query; $this->applyWhereConditions($beforeQuery, $where); $beforeData = $beforeQuery->get()->toArray(); // 应用WHERE条件 $this->applyWhereConditions($query, $where); $affected = $query->update($data); if ($affected > 0) { // 获取变更后的数据 $afterQuery = DB::table($tableNameWithoutPrefix); $this->applyWhereConditions($afterQuery, $where); $afterData = $afterQuery->get()->toArray(); // 记录变更日志 $this->logChange('data_change', $tableName, [ 'action' => 'update', 'affected_rows' => $affected, 'where' => $where, ], null, ['data' => $beforeData], ['data' => $afterData]); } return $affected > 0; } /** * 删除表数据 * @param string $tableName * @param array $where * @return bool * @throws Exception */ public function deleteTableData(string $tableName, array $where): bool { $this->validateTableName($tableName); // 移除表名前缀,因为 DB::table() 会自动添加 $tableNameWithoutPrefix = $this->removeTablePrefix($tableName); $query = DB::table($tableNameWithoutPrefix); // 获取变更前的数据 $beforeQuery = clone $query; $this->applyWhereConditions($beforeQuery, $where); $beforeData = $beforeQuery->get()->toArray(); // 应用WHERE条件 $this->applyWhereConditions($query, $where); $affected = $query->delete(); if ($affected > 0) { // 记录变更日志 $this->logChange('data_change', $tableName, [ 'action' => 'delete', 'affected_rows' => $affected, 'where' => $where, ], null, ['data' => $beforeData], null); } return $affected > 0; } /** * 批量删除表数据 * @param string $tableName * @param array $where 多个WHERE条件,使用OR连接 * @return int 删除的记录数 * @throws Exception */ public function batchDeleteTableData(string $tableName, array $where): int { $this->validateTableName($tableName); if (empty($where)) { $this->utils->errorThrow('删除条件不能为空'); } // 移除表名前缀,因为 DB::table() 会自动添加 $tableNameWithoutPrefix = $this->removeTablePrefix($tableName); $query = DB::table($tableNameWithoutPrefix); // 获取变更前的数据 $beforeQuery = clone $query; $beforeQuery->where(function ($q) use ($where) { foreach ($where as $index => $condition) { if (isset($condition['field']) && isset($condition['operator']) && isset($condition['value'])) { if ($index === 0) { $q->where($condition['field'], $condition['operator'], $condition['value']); } else { $q->orWhere($condition['field'], $condition['operator'], $condition['value']); } } } }); $beforeData = $beforeQuery->get()->toArray(); // 批量删除:使用OR连接多个条件 $query->where(function ($q) use ($where) { foreach ($where as $index => $condition) { if (isset($condition['field']) && isset($condition['operator']) && isset($condition['value'])) { if ($index === 0) { $q->where($condition['field'], $condition['operator'], $condition['value']); } else { $q->orWhere($condition['field'], $condition['operator'], $condition['value']); } } } }); $affected = $query->delete(); if ($affected > 0) { // 记录变更日志 $this->logChange('data_change', $tableName, [ 'action' => 'batch_delete', 'affected_rows' => $affected, 'where' => $where, ], null, ['data' => $beforeData], null); } return $affected; } /** * 插入表数据 * @param string $tableName * @param array $data * @return int * @throws Exception */ public function insertTableData(string $tableName, array $data): int { $this->validateTableName($tableName); // 移除表名前缀,因为 DB::table() 会自动添加 $tableNameWithoutPrefix = $this->removeTablePrefix($tableName); $id = DB::table($tableNameWithoutPrefix)->insertGetId($data); // 记录变更日志 $this->logChange('data_change', $tableName, [ 'action' => 'insert', 'inserted_id' => $id, ], null, null, ['data' => $data]); return $id; } /** * 更新表注释 * @param string $tableName * @param string $comment * @return bool * @throws Exception */ public function updateTableComment(string $tableName, string $comment): bool { $this->validateTableName($tableName); // 获取变更前的注释 $beforeInfo = DB::selectOne(" SELECT TABLE_COMMENT as comment FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ", [config('database.connections.mysql.database'), $tableName]); $database = config('database.connections.mysql.database'); $sql = "ALTER TABLE `{$tableName}` COMMENT = " . DB::getPdo()->quote($comment); DB::statement($sql); // 记录变更日志 $this->logChange('structure_change', $tableName, [ 'action' => 'update_table_comment', 'before_comment' => $beforeInfo->comment ?? '', 'after_comment' => $comment, ], $sql); return true; } /** * 更新字段注释 * @param string $tableName * @param string $columnName * @param string $comment * @return bool * @throws Exception */ public function updateColumnComment(string $tableName, string $columnName, string $comment): bool { $this->validateTableName($tableName); // 获取字段的完整定义 $column = DB::selectOne(" SELECT COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? ", [config('database.connections.mysql.database'), $tableName, $columnName]); if (!$column) { $this->utils->errorThrow('字段不存在'); } $nullable = $column->IS_NULLABLE === 'YES' ? 'NULL' : 'NOT NULL'; $default = $column->COLUMN_DEFAULT !== null ? "DEFAULT " . DB::getPdo()->quote($column->COLUMN_DEFAULT) : ''; $extra = $column->EXTRA ?: ''; // 获取变更前的注释 $beforeColumn = DB::selectOne(" SELECT COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? ", [config('database.connections.mysql.database'), $tableName, $columnName]); $beforeComment = $beforeColumn->COLUMN_COMMENT ?? ''; $sql = "ALTER TABLE `{$tableName}` MODIFY COLUMN `{$columnName}` {$column->COLUMN_TYPE} {$nullable} {$default} {$extra} COMMENT " . DB::getPdo()->quote($comment); DB::statement($sql); // 记录变更日志 $this->logChange('structure_change', $tableName, [ 'action' => 'update_column_comment', 'column_name' => $columnName, 'before_comment' => $beforeComment, 'after_comment' => $comment, ], $sql); return true; } /** * 添加索引 * @param string $tableName * @param string $indexName * @param array $columns * @param bool $unique * @param string $type 索引类型:BTREE、HASH、FULLTEXT、SPATIAL * @return bool * @throws Exception */ public function addIndex(string $tableName, string $indexName, array $columns, bool $unique = false, string $type = 'BTREE'): bool { $this->validateTableName($tableName); if (empty($columns)) { $this->utils->errorThrow('索引列不能为空'); } // 验证索引类型 $allowedTypes = ['BTREE', 'HASH', 'FULLTEXT', 'SPATIAL']; $type = strtoupper($type); if (!in_array($type, $allowedTypes)) { $this->utils->errorThrow('不支持的索引类型'); } $uniqueStr = $unique ? 'UNIQUE' : ''; $columnsStr = '`' . implode('`, `', $columns) . '`'; $usingType = "USING {$type}"; $sql = "ALTER TABLE `{$tableName}` ADD {$uniqueStr} INDEX `{$indexName}` ({$columnsStr}) {$usingType}"; DB::statement($sql); // 记录变更日志 $this->logChange('structure_change', $tableName, [ 'action' => 'add_index', 'index_name' => $indexName, 'columns' => $columns, 'unique' => $unique, 'type' => $type, ], $sql); return true; } /** * 删除索引 * @param string $tableName * @param string $indexName * @return bool * @throws Exception */ public function dropIndex(string $tableName, string $indexName): bool { $this->validateTableName($tableName); $sql = "ALTER TABLE `{$tableName}` DROP INDEX `{$indexName}`"; DB::statement($sql); // 记录变更日志 $this->logChange('structure_change', $tableName, [ 'action' => 'drop_index', 'index_name' => $indexName, ], $sql); return true; } /** * 修改表结构(添加/修改/删除字段) * @param string $tableName * @param string $action add|modify|drop * @param array $columnInfo * @return bool * @throws Exception */ public function updateTableStructure(string $tableName, string $action, array $columnInfo): bool { $this->validateTableName($tableName); switch ($action) { case 'add': return $this->addColumn($tableName, $columnInfo); case 'modify': return $this->modifyColumn($tableName, $columnInfo); case 'drop': if (!isset($columnInfo['name'])) { $this->utils->errorThrow('字段名不能为空'); } // 获取变更前的字段信息 $beforeColumn = $this->getTableColumns($tableName); $droppedColumn = null; foreach ($beforeColumn as $col) { if ($col['name'] === $columnInfo['name']) { $droppedColumn = $col; break; } } $sql = "ALTER TABLE `{$tableName}` DROP COLUMN `{$columnInfo['name']}`"; DB::statement($sql); // 记录变更日志 $this->logChange('structure_change', $tableName, [ 'action' => 'drop_column', 'column_name' => $columnInfo['name'], 'column_info' => $droppedColumn, ], $sql, $droppedColumn ? ['column' => $droppedColumn] : null); return true; default: $this->utils->errorThrow('不支持的操作类型'); } } /** * 添加字段 * @param string $tableName * @param array $columnInfo * @return bool * @throws Exception */ private function addColumn(string $tableName, array $columnInfo): bool { if (!isset($columnInfo['name']) || !isset($columnInfo['type'])) { $this->utils->errorThrow('字段名和类型不能为空'); } $name = $columnInfo['name']; $type = $columnInfo['type']; $length = isset($columnInfo['length']) ? "({$columnInfo['length']})" : ''; $nullable = isset($columnInfo['nullable']) && $columnInfo['nullable'] ? 'NULL' : 'NOT NULL'; $default = isset($columnInfo['default_value']) && $columnInfo['default_value'] !== null ? "DEFAULT " . DB::getPdo()->quote($columnInfo['default_value']) : ''; $comment = isset($columnInfo['comment']) ? "COMMENT " . DB::getPdo()->quote($columnInfo['comment']) : ''; $after = isset($columnInfo['after']) ? "AFTER `{$columnInfo['after']}`" : ''; $sql = "ALTER TABLE `{$tableName}` ADD COLUMN `{$name}` {$type}{$length} {$nullable} {$default} {$comment} {$after}"; DB::statement($sql); // 记录变更日志 $this->logChange('structure_change', $tableName, [ 'action' => 'add_column', 'column_info' => $columnInfo, ], $sql, null, ['column' => $columnInfo]); return true; } /** * 解析字段类型(从 column_type 如 varchar(255) 中提取类型和长度) * @param string $columnType * @return array ['type' => string, 'length' => string] */ private function parseColumnType(string $columnType): array { // 匹配类型和长度,如 varchar(255) 或 decimal(10,2) if (preg_match('/^(\w+)(?:\(([^)]+)\))?/i', $columnType, $matches)) { return [ 'type' => strtoupper($matches[1]), 'length' => isset($matches[2]) ? $matches[2] : '', ]; } return ['type' => strtoupper($columnType), 'length' => '']; } /** * 构建字段类型字符串 * @param string $type * @param string $length * @return string */ private function buildColumnType(string $type, string $length = ''): string { $type = strtoupper($type); // 需要长度的类型 $typesNeedLength = ['VARCHAR', 'CHAR', 'INT', 'BIGINT', 'TINYINT', 'SMALLINT', 'DECIMAL', 'FLOAT', 'DOUBLE']; if (in_array($type, $typesNeedLength) && !empty($length)) { return "{$type}({$length})"; } return $type; } /** * 修改字段 * @param string $tableName * @param array $columnInfo * @return bool * @throws Exception */ private function modifyColumn(string $tableName, array $columnInfo): bool { if (!isset($columnInfo['name']) || !isset($columnInfo['type'])) { $this->utils->errorThrow('字段名和类型不能为空'); } $name = $columnInfo['name']; $type = $columnInfo['type']; // 构建类型和长度 $length = ''; if (isset($columnInfo['length']) && !empty($columnInfo['length'])) { $length = "({$columnInfo['length']})"; } else { // 如果没有提供长度,尝试从现有字段获取 $existingColumn = DB::selectOne(" SELECT COLUMN_TYPE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ? ", [config('database.connections.mysql.database'), $tableName, $name]); if ($existingColumn) { $parsed = $this->parseColumnType($existingColumn->COLUMN_TYPE); // 如果类型需要长度但新类型也需要,尝试保留长度 $typesNeedLength = ['VARCHAR', 'CHAR', 'INT', 'BIGINT', 'TINYINT', 'SMALLINT', 'DECIMAL', 'FLOAT', 'DOUBLE']; if (in_array(strtoupper($type), $typesNeedLength) && !empty($parsed['length'])) { $length = "({$parsed['length']})"; } } } $nullable = isset($columnInfo['nullable']) && $columnInfo['nullable'] ? 'NULL' : 'NOT NULL'; $default = ''; if (isset($columnInfo['default_value']) && $columnInfo['default_value'] !== null && $columnInfo['default_value'] !== '') { $default = "DEFAULT " . DB::getPdo()->quote($columnInfo['default_value']); } $comment = isset($columnInfo['comment']) && !empty($columnInfo['comment']) ? "COMMENT " . DB::getPdo()->quote($columnInfo['comment']) : ''; // 位置调整 $after = ''; if (isset($columnInfo['after']) && !empty($columnInfo['after'])) { $after = "AFTER `{$columnInfo['after']}`"; } // 获取变更前的字段信息 $beforeColumn = $this->getTableColumns($tableName); $beforeColumnInfo = null; foreach ($beforeColumn as $col) { if ($col['name'] === $name) { $beforeColumnInfo = $col; break; } } $sql = "ALTER TABLE `{$tableName}` MODIFY COLUMN `{$name}` {$type}{$length} {$nullable} {$default} {$comment} {$after}"; DB::statement($sql); // 记录变更日志 $this->logChange('structure_change', $tableName, [ 'action' => 'modify_column', 'column_name' => $name, 'column_info' => $columnInfo, ], $sql, $beforeColumnInfo ? ['column' => $beforeColumnInfo] : null, ['column' => $columnInfo]); return true; } /** * 执行SELECT查询(仅支持SELECT语句) * @param string $sql * @return array * @throws Exception */ public function executeSelectQuery(string $sql): array { // 验证SQL安全性 $trimmedSql = trim($sql); $upperSql = strtoupper($trimmedSql); // 必须以SELECT开头 if (!preg_match('/^\s*SELECT/i', $trimmedSql)) { $this->utils->errorThrow('仅支持SELECT查询语句'); } // 禁止的危险关键字 $dangerousKeywords = [ 'DROP', 'DELETE', 'UPDATE', 'INSERT', 'ALTER', 'CREATE', 'TRUNCATE', 'REPLACE', 'GRANT', 'REVOKE', 'EXEC', 'EXECUTE', 'CALL', 'PROCEDURE', 'INTO OUTFILE', 'INTO DUMPFILE', 'LOAD_FILE', 'LOAD DATA', ]; foreach ($dangerousKeywords as $keyword) { if (strpos($upperSql, $keyword) !== false) { $this->utils->errorThrow("SQL语句包含危险关键字: {$keyword}"); } } try { // 执行查询并限制结果数量 $results = DB::select($trimmedSql); // 限制返回结果数量(最多1000条) $maxResults = 1000; if (count($results) > $maxResults) { $results = array_slice($results, 0, $maxResults); } // 转换为数组格式 $data = array_map(function ($row) { return (array) $row; }, $results); // 记录查询日志 $this->logChange('sql_query', '', [ 'action' => 'select_query', 'result_count' => count($data), ], $trimmedSql); return $data; } catch (Exception $e) { $this->utils->errorThrow('SQL执行失败: ' . $e->getMessage()); } } /** * 移除表名前缀(如果存在) * @param string $tableName * @return string */ private function removeTablePrefix(string $tableName): string { $prefix = config('database.connections.mysql.prefix', ''); if (!empty($prefix) && strpos($tableName, $prefix) === 0) { return substr($tableName, strlen($prefix)); } return $tableName; } /** * 记录数据库变更日志 * @param string $operationType 操作类型:structure_change, data_change, sql_query * @param string $tableName 表名 * @param array $detail 操作详情 * @param string|null $sql SQL语句 * @param array|null $beforeData 变更前数据 * @param array|null $afterData 变更后数据 * @return void */ private function logChange(string $operationType, string $tableName, array $detail, ?string $sql = null, ?array $beforeData = null, ?array $afterData = null): void { try { DB::table('nl_database_change_log')->insert([ 'user_id' => $this->userId ?? 0, 'table_name' => $tableName, 'operation_type' => $operationType, 'operation_detail' => json_encode($detail, JSON_UNESCAPED_UNICODE), 'sql_statement' => $sql, 'before_data' => $beforeData ? json_encode($beforeData, JSON_UNESCAPED_UNICODE) : null, 'after_data' => $afterData ? json_encode($afterData, JSON_UNESCAPED_UNICODE) : null, 'created_at' => time(), ]); } catch (Exception $e) { // 日志记录失败不影响主操作,只记录错误 \Log::error('数据库变更日志记录失败: ' . $e->getMessage()); } } /** * 获取变更记录列表 * @param int $page * @param int $pageSize * @param array $filters 筛选条件:table_name, operation_type, start_time, end_time * @return array */ public function getChangeLog(int $page = 1, int $pageSize = 20, array $filters = []): array { $query = DB::table('database_change_log'); // 应用筛选条件 if (!empty($filters['table_name'])) { $query->where('table_name', $filters['table_name']); } if (!empty($filters['operation_type'])) { $query->where('operation_type', $filters['operation_type']); } if (!empty($filters['start_time'])) { $query->where('created_at', '>=', $filters['start_time']); } if (!empty($filters['end_time'])) { $query->where('created_at', '<=', $filters['end_time']); } $total = $query->count(); $items = $query->orderBy('created_at', 'desc') ->skip(($page - 1) * $pageSize) ->take($pageSize) ->get() ->toArray(); // 解析JSON字段 foreach ($items as &$item) { $item = (array) $item; if (!empty($item['operation_detail'])) { $item['operation_detail'] = json_decode($item['operation_detail'], true); } if (!empty($item['before_data'])) { $item['before_data'] = json_decode($item['before_data'], true); } if (!empty($item['after_data'])) { $item['after_data'] = json_decode($item['after_data'], true); } } return [ 'page' => $page, 'size' => $pageSize, 'total' => $total, 'page_count' => ceil($total / $pageSize), 'items' => $items, ]; } /** * 验证表名(防止SQL注入) * @param string $tableName * @return void * @throws Exception */ private function validateTableName(string $tableName): void { if (empty($tableName)) { $this->utils->errorThrow('表名不能为空'); } // 只允许字母、数字、下划线 if (!preg_match('/^[a-zA-Z0-9_]+$/', $tableName)) { $this->utils->errorThrow('表名格式不正确'); } // 检查表是否存在 $database = config('database.connections.mysql.database'); $exists = DB::selectOne(" SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ", [$database, $tableName]); if ($exists->count == 0) { $this->utils->errorThrow('表不存在'); } } }