Files
nl-admin-api/app/Service/DatabaseService.php
李琦 a1abba3835
Some checks failed
Tests / PHP 8.2 (push) Has been cancelled
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
表前缀
2026-01-23 17:13:23 +08:00

1018 lines
34 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Service;
use App\BaseApp\BaseService;
use App\Service\common\UtilsService;
use Exception;
use Illuminate\Support\Facades\DB;
class DatabaseService extends BaseService
{
public function __construct()
{
parent::__construct();
// 检查权限只有role_id=1才能操作
if ($this->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('表不存在');
}
}
}