From a1abba3835f94b79d5611bd61d4caf4b3ec3e7dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Fri, 23 Jan 2026 17:13:23 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A8=E5=89=8D=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 +- .../Controllers/Api/DatabaseController.php | 308 +++++ .../core/CodeGenerationController.php | 96 ++ app/Service/DatabaseService.php | 1017 +++++++++++++++++ app/Service/core/CodeGenerationService.php | 169 +++ public/cc_admin.sql | 256 +++++ public/nl_admin.sql | 1 + routes/api.php | 1 + 8 files changed, 1850 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/Api/DatabaseController.php create mode 100644 app/Service/DatabaseService.php create mode 100644 public/cc_admin.sql diff --git a/.env.example b/.env.example index d6a69f99..7d97071d 100644 --- a/.env.example +++ b/.env.example @@ -43,8 +43,8 @@ CACHE_PREFIX= MEMCACHED_HOST=127.0.0.1 REDIS_CLIENT=phpredis -REDIS_HOST=redis -REDIS_PASSWORD=GS2H37yZAxNPBtXA +REDIS_HOST=1Panel-redis-TxPD +REDIS_PASSWORD=redis_Pa2Y7J REDIS_PORT=6379 # 邮件发送配置 diff --git a/app/Http/Controllers/Api/DatabaseController.php b/app/Http/Controllers/Api/DatabaseController.php new file mode 100644 index 00000000..01743b3a --- /dev/null +++ b/app/Http/Controllers/Api/DatabaseController.php @@ -0,0 +1,308 @@ +service = DatabaseService::getInstance(); + } + + /** + * 获取表列表 + * @Method GET + * @return JsonResponse + */ + public function listTables(): JsonResponse + { + return jok($this->service->listTables()); + } + + /** + * 获取表详细信息 + * @Method GET + * @return JsonResponse + * @throws Exception + */ + public function getTableInfo(): JsonResponse + { + $tableName = request()->get('table_name'); + if (!$tableName) { + return jerr('表名不能为空'); + } + return jok($this->service->getTableInfo($tableName)); + } + + /** + * 获取表数据(分页) + * @Method POST + * @return JsonResponse + * @throws Exception + */ + public function getTableData(): JsonResponse + { + $tableName = request()->post('table_name'); + $page = request()->post('page', 1); + $pageSize = request()->post('page_size', 20); + $where = request()->post('where', []); + + if (!$tableName) { + return jerr('表名不能为空'); + } + + return jok($this->service->getTableData($tableName, $page, $pageSize, $where)); + } + + /** + * 更新表数据 + * @Method POST + * @return JsonResponse + * @throws Exception + */ + public function updateTableData(): JsonResponse + { + $tableName = request()->post('table_name'); + $data = request()->post('data', []); + $where = request()->post('where', []); + + if (!$tableName) { + return jerr('表名不能为空'); + } + if (empty($data)) { + return jerr('更新数据不能为空'); + } + if (empty($where)) { + return jerr('WHERE条件不能为空'); + } + + $result = $this->service->updateTableData($tableName, $data, $where); + return jok($result, '更新成功'); + } + + /** + * 删除表数据 + * @Method POST + * @return JsonResponse + * @throws Exception + */ + public function deleteTableData(): JsonResponse + { + $tableName = request()->post('table_name'); + $where = request()->post('where', []); + + if (!$tableName) { + return jerr('表名不能为空'); + } + if (empty($where)) { + return jerr('WHERE条件不能为空'); + } + + $result = $this->service->deleteTableData($tableName, $where); + return jok($result, '删除成功'); + } + + /** + * 批量删除表数据 + * @Method POST + * @return JsonResponse + * @throws Exception + */ + public function batchDeleteTableData(): JsonResponse + { + $tableName = request()->post('table_name'); + $where = request()->post('where', []); + + if (!$tableName) { + return jerr('表名不能为空'); + } + if (empty($where)) { + return jerr('WHERE条件不能为空'); + } + + $count = $this->service->batchDeleteTableData($tableName, $where); + return jok(['count' => $count], "成功删除 {$count} 条记录"); + } + + /** + * 插入表数据 + * @Method POST + * @return JsonResponse + * @throws Exception + */ + public function insertTableData(): JsonResponse + { + $tableName = request()->post('table_name'); + $data = request()->post('data', []); + + if (!$tableName) { + return jerr('表名不能为空'); + } + if (empty($data)) { + return jerr('插入数据不能为空'); + } + + $id = $this->service->insertTableData($tableName, $data); + return jok(['id' => $id], '插入成功'); + } + + /** + * 更新表注释 + * @Method POST + * @return JsonResponse + * @throws Exception + */ + public function updateTableComment(): JsonResponse + { + $tableName = request()->post('table_name'); + $comment = request()->post('comment', ''); + + if (!$tableName) { + return jerr('表名不能为空'); + } + + $this->service->updateTableComment($tableName, $comment); + return jok(true, '更新成功'); + } + + /** + * 更新字段注释 + * @Method POST + * @return JsonResponse + * @throws Exception + */ + public function updateColumnComment(): JsonResponse + { + $tableName = request()->post('table_name'); + $columnName = request()->post('column_name'); + $comment = request()->post('comment', ''); + + if (!$tableName) { + return jerr('表名不能为空'); + } + if (!$columnName) { + return jerr('字段名不能为空'); + } + + $this->service->updateColumnComment($tableName, $columnName, $comment); + return jok(true, '更新成功'); + } + + /** + * 添加索引 + * @Method POST + * @return JsonResponse + * @throws Exception + */ + public function addIndex(): JsonResponse + { + $tableName = request()->post('table_name'); + $indexName = request()->post('index_name'); + $columns = request()->post('columns', []); + $unique = request()->post('unique', false); + $type = request()->post('type', 'BTREE'); + + if (!$tableName) { + return jerr('表名不能为空'); + } + if (!$indexName) { + return jerr('索引名不能为空'); + } + if (empty($columns)) { + return jerr('索引列不能为空'); + } + + $this->service->addIndex($tableName, $indexName, $columns, $unique, $type); + return jok(true, '添加成功'); + } + + /** + * 删除索引 + * @Method POST + * @return JsonResponse + * @throws Exception + */ + public function dropIndex(): JsonResponse + { + $tableName = request()->post('table_name'); + $indexName = request()->post('index_name'); + + if (!$tableName) { + return jerr('表名不能为空'); + } + if (!$indexName) { + return jerr('索引名不能为空'); + } + + $this->service->dropIndex($tableName, $indexName); + return jok(true, '删除成功'); + } + + /** + * 修改表结构 + * @Method POST + * @return JsonResponse + * @throws Exception + */ + public function updateTableStructure(): JsonResponse + { + $tableName = request()->post('table_name'); + $action = request()->post('action'); // add|modify|drop + $columnInfo = request()->post('column_info', []); + + if (!$tableName) { + return jerr('表名不能为空'); + } + if (!in_array($action, ['add', 'modify', 'drop'])) { + return jerr('操作类型不正确'); + } + + $this->service->updateTableStructure($tableName, $action, $columnInfo); + return jok(true, '操作成功'); + } + + /** + * 执行SQL查询(仅SELECT) + * @Method POST + * @return JsonResponse + * @throws Exception + */ + public function executeSqlQuery(): JsonResponse + { + $sql = request()->post('sql'); + + if (empty($sql)) { + return jerr('SQL语句不能为空'); + } + + $results = $this->service->executeSelectQuery($sql); + return jok($results, '查询成功'); + } + + /** + * 获取变更记录列表 + * @Method GET + * @return JsonResponse + * @throws Exception + */ + public function getChangeLog(): JsonResponse + { + $page = request()->get('page', 1); + $pageSize = request()->get('page_size', 20); + $filters = [ + 'table_name' => request()->get('table_name'), + 'operation_type' => request()->get('operation_type'), + 'start_time' => request()->get('start_time'), + 'end_time' => request()->get('end_time'), + ]; + + $result = $this->service->getChangeLog((int)$page, (int)$pageSize, array_filter($filters)); + return jok($result, '获取成功'); + } +} diff --git a/app/Http/Controllers/core/CodeGenerationController.php b/app/Http/Controllers/core/CodeGenerationController.php index dbda7b38..410bacb0 100644 --- a/app/Http/Controllers/core/CodeGenerationController.php +++ b/app/Http/Controllers/core/CodeGenerationController.php @@ -42,6 +42,49 @@ class CodeGenerationController extends BaseController ); } + /** + * 批量生成代码 + * @Method POST + * @return JsonResponse + * @throws \Exception + */ + public function batchGeneration() + { + $params = request()->post(); + + if (!is_array($params) || empty($params)) { + return jerr('参数错误,需要数组格式!'); + } + + $results = []; + $errors = []; + + foreach ($params as $index => $item) { + try { + // 验证必填字段 + if (empty($item['class_name']) || empty($item['field']) || !is_array($item['field'])) { + $errors[] = "模块 " . ($index + 1) . " 参数不完整"; + continue; + } + + $result = $this->service->generate($item); + $results[] = $result; + } catch (\Exception $e) { + $errors[] = "模块 " . ($index + 1) . " 生成失败: " . $e->getMessage(); + } + } + + if (empty($results)) { + return jerr('批量生成失败:' . implode('; ', $errors)); + } + + if (!empty($errors)) { + return jok($results, '部分生成成功,共生成 ' . count($results) . ' 个模块。错误:' . implode('; ', $errors)); + } + + return jok($results, '批量生成成功,共生成 ' . count($results) . ' 个模块!'); + } + public function download() { // $id = request()->post('id'); @@ -64,4 +107,57 @@ class CodeGenerationController extends BaseController ); } + /** + * 获取数据库表列表 + * @Method GET + * @return JsonResponse + */ + public function getTables() + { + return jok( + $this->service->getTableList() + ); + } + + /** + * 获取表结构 + * @Method GET + * @return JsonResponse + * @throws \Exception + */ + public function getTableStructure() + { + $tableName = request()->get('table_name'); + if (!$tableName) { + return jerr('表名不能为空!'); + } + return jok( + $this->service->getTableColumns($tableName) + ); + } + + /** + * 从数据库表生成代码生成数据 + * @Method POST + * @return JsonResponse + * @throws \Exception + */ + public function generateFromTable() + { + $tableName = request()->post('table_name'); + $className = request()->post('class_name', ''); + $classComment = request()->post('class_comment', ''); + $icon = request()->post('icon', ''); + $sort = request()->post('sort', 9999); + $pid = request()->post('pid', 0); + + if (!$tableName) { + return jerr('表名不能为空!'); + } + + $codeGenData = $this->service->convertTableToCodeGenData($tableName, $className, $classComment, $icon, $sort, $pid); + + return jok($codeGenData); + } + } diff --git a/app/Service/DatabaseService.php b/app/Service/DatabaseService.php new file mode 100644 index 00000000..266a4ee4 --- /dev/null +++ b/app/Service/DatabaseService.php @@ -0,0 +1,1017 @@ +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('表不存在'); + } + } +} diff --git a/app/Service/core/CodeGenerationService.php b/app/Service/core/CodeGenerationService.php index de81c09c..b0d793ad 100755 --- a/app/Service/core/CodeGenerationService.php +++ b/app/Service/core/CodeGenerationService.php @@ -260,6 +260,7 @@ class CodeGenerationService $this->genViews(); // 生成菜单 $this->genMenu(); + DB::commit(); } catch (Exception $e) { DB::rollBack(); // ds([ @@ -537,4 +538,172 @@ class CodeGenerationService $this->utils->errorThrow($e->getMessage()); } } + + /** + * 获取数据库表列表 + * @return array + */ + public function getTableList(): array + { + $database = config('database.connections.mysql.database'); + $prefix = config('database.prefix', 'nl_'); + + $tables = DB::select(" + SELECT + TABLE_NAME as `name`, + TABLE_COMMENT as `comment`, + TABLE_ROWS as `rows`, + CREATE_TIME as create_time + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? + AND TABLE_NAME LIKE ? + ORDER BY TABLE_NAME + ", [$database, $prefix . '%']); + + $result = []; + foreach ($tables as $table) { + $result[] = [ + 'name' => $table->name, + 'comment' => $table->comment ?: $table->name, + 'rows' => $table->rows, + 'create_time' => $table->create_time, + ]; + } + + return $result; + } + + /** + * 获取表字段信息 + * @param string $tableName + * @return array + * @throws Exception + */ + public function getTableColumns(string $tableName): array + { + $database = config('database.connections.mysql.database'); + + $columns = DB::select(" + SELECT + COLUMN_NAME as `name`, + DATA_TYPE as `type`, + CHARACTER_MAXIMUM_LENGTH as `length`, + COLUMN_DEFAULT as default_value, + COLUMN_COMMENT as `comment`, + IS_NULLABLE as nullable, + COLUMN_KEY as key_type, + EXTRA as extra + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = ? + AND TABLE_NAME = ? + ORDER BY ORDINAL_POSITION + ", [$database, $tableName]); + + if (empty($columns)) { + $this->utils->errorThrow('表不存在或没有字段'); + } + + $result = []; + foreach ($columns as $column) { + // 跳过id字段(代码生成会自动添加) + if ($column->name === 'id') { + continue; + } + + // 跳过系统字段 + if (in_array($column->name, ['created_at', 'updated_at', 'deleted_at', 'status'])) { + continue; + } + + // 转换数据类型 + $type = strtoupper($column->type); + $typeLength = ''; + + if (in_array($type, ['VARCHAR', 'CHAR'])) { + $typeLength = $column->length ?: '255'; + } elseif (in_array($type, ['INT', 'TINYINT', 'BIGINT', 'SMALLINT'])) { + $typeLength = $column->length ?: '11'; + } elseif (in_array($type, ['DECIMAL', 'FLOAT', 'DOUBLE'])) { + $typeLength = $column->length ?: '10,2'; + } + + // 判断表单组件类型 + $formType = 'VbenInput'; + if (strpos($type, 'TEXT') !== false) { + $formType = 'VbenTextarea'; + } elseif (in_array($type, ['TINYINT', 'INT']) && $column->length == 1) { + $formType = 'VbenSwitch'; + } elseif (strpos($column->name, 'time') !== false || strpos($column->name, 'date') !== false) { + $formType = 'VbenDatePicker'; + } + + $result[] = [ + 'name' => $column->name, + 'type' => $type, + 'type_length' => $typeLength, + 'default' => $column->default_value ?? '', + 'comment' => $column->comment ?: $column->name, + 'not_null' => $column->nullable === 'NO' ? 1 : 0, + 'formShow' => 1, + 'tableShow' => 1, + 'formType' => $formType, + 'search' => in_array($type, ['VARCHAR', 'CHAR', 'INT', 'TINYINT', 'BIGINT']) ? 1 : 0, + 'searchValue' => in_array($type, ['VARCHAR', 'CHAR']) ? 'like' : '=', + ]; + } + + return $result; + } + + /** + * 将数据库表结构转换为代码生成数据格式 + * @param string $tableName + * @param string $className + * @param string $classComment + * @param string $icon + * @param int $sort + * @param int $pid + * @return array + * @throws Exception + */ + public function convertTableToCodeGenData( + string $tableName, + string $className = '', + string $classComment = '', + string $icon = '', + int $sort = 9999, + int $pid = 0 + ): array { + // 如果没有提供类名,从表名生成 + if (empty($className)) { + $prefix = config('database.prefix', 'nl_'); + $tableNameWithoutPrefix = str_replace($prefix, '', $tableName); + // 下划线转驼峰 + $className = str_replace('_', '', ucwords($tableNameWithoutPrefix, '_')); + } + + // 如果没有提供中文名称,使用表注释或表名 + if (empty($classComment)) { + $database = config('database.connections.mysql.database'); + $tableInfo = DB::selectOne(" + SELECT TABLE_COMMENT + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? + ", [$database, $tableName]); + + $classComment = $tableInfo->TABLE_COMMENT ?: $tableName; + } + + // 获取字段信息 + $fields = $this->getTableColumns($tableName); + + return [ + 'class_name' => $className, + 'class_comment' => $classComment, + 'icon' => $icon, + 'sort' => $sort, + 'pid' => $pid, + 'field' => $fields, + ]; + } } diff --git a/public/cc_admin.sql b/public/cc_admin.sql new file mode 100644 index 00000000..c3b209f5 --- /dev/null +++ b/public/cc_admin.sql @@ -0,0 +1,256 @@ +/* + Navicat Premium Dump SQL + + Source Server : 开发环境-本地 + Source Server Type : MySQL + Source Server Version : 80407 (8.4.7) + Source Host : localhost:3306 + Source Schema : cc_admin + + Target Server Type : MySQL + Target Server Version : 80407 (8.4.7) + File Encoding : 65001 + + Date: 23/01/2026 17:08:01 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for nl_admin +-- ---------------------------- +DROP TABLE IF EXISTS `nl_admin`; +CREATE TABLE `nl_admin` ( + `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `open_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'OpenID,后期整合萧康服务中心会用到', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '头像', + `nick_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '昵称', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码', + `phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户手机', + `email` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户邮箱', + `code` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '业务员推广码', + `role_id` int NOT NULL DEFAULT 0 COMMENT '角色', + `province_id` int NOT NULL DEFAULT 0 COMMENT '省', + `city_id` int NOT NULL DEFAULT 0 COMMENT '市', + `reg_ip` bigint NOT NULL DEFAULT 0 COMMENT '注册IP', + `last_login_time` int NOT NULL DEFAULT 0 COMMENT '最后登录时间', + `ip` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '0' COMMENT '最后登录IP', + `ip_table` json NOT NULL COMMENT '常用登录IP地址列表', + `operation_password` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '操作密码', + `desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '备注', + `status` tinyint NOT NULL DEFAULT 1 COMMENT '用户状态 0正常 1禁用', + `created_at` int NOT NULL DEFAULT 0 COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT 0 COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '管理员表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of nl_admin +-- ---------------------------- +INSERT INTO `nl_admin` VALUES (1, 'nl_28686ad68682180e26836c721a52', '', '超管', '$2y$12$FXKlnItms3NoCLsJKQyVa.03kuRq8ltzdmc.ksf7rLKKPfGznpjnu', '15100000000', 'workerqi@163.com', '517117', 1, 0, 0, 0, 0, '127.0.0.1', '[\"127.0.0.1\"]', '0', '', 1, 0, 1747026536, 0); + +-- ---------------------------- +-- Table structure for nl_admin_notice +-- ---------------------------- +DROP TABLE IF EXISTS `nl_admin_notice`; +CREATE TABLE `nl_admin_notice` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', + `title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '消息标题', + `detail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '简介', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '消息内容', + `type` int NOT NULL DEFAULT 0 COMMENT '消息类型', + `user_id` int NOT NULL DEFAULT 0 COMMENT '关联用户ID', + `status` int NOT NULL DEFAULT 0 COMMENT '状态 0:未读 1:已读', + `created_at` int NOT NULL DEFAULT 0 COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT 0 COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 84 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '管理员消息列表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of nl_admin_notice +-- ---------------------------- + +-- ---------------------------- +-- Table structure for nl_api_op_log +-- ---------------------------- +DROP TABLE IF EXISTS `nl_api_op_log`; +CREATE TABLE `nl_api_op_log` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', + `user_id` int NOT NULL COMMENT '操作用户ID', + `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '操作路由', + `method` tinyint(1) NOT NULL DEFAULT 0 COMMENT '请求方式 0:未知 1:GET 2:POST', + `controller` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '操作的控制器', + `ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '操作者IP地址', + `param` json NOT NULL COMMENT '请求携带参数', + `result` json NOT NULL COMMENT '返回json', + `type` tinyint(1) NOT NULL DEFAULT 0 COMMENT '操作状态 0:成功 1:失败', + `result_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '返回状态码', + `platform_type` tinyint(1) NOT NULL DEFAULT 0 COMMENT '平台类型 0:总后台 1:门店后台 2:小程序', + `belong_id` int NOT NULL DEFAULT 0 COMMENT '所属平台ID', + `user_type` int NOT NULL DEFAULT 0 COMMENT '用户类型', + `equipment` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '操作系统', + `browser` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '浏览器', + `created_at` int NOT NULL DEFAULT 0 COMMENT '操作时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 16860 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '新的后台API访问日志记录' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of nl_api_op_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for nl_code_generation +-- ---------------------------- +DROP TABLE IF EXISTS `nl_code_generation`; +CREATE TABLE `nl_code_generation` ( + `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `title` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '菜单标题', + `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '文件名称', + `path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '下载链接', + `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '生成参数', + `created_at` int NOT NULL DEFAULT 0 COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT 0 COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '代码生成记录' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of nl_code_generation +-- ---------------------------- + +-- ---------------------------- +-- Table structure for nl_database_change_log +-- ---------------------------- +DROP TABLE IF EXISTS `nl_database_change_log`; +CREATE TABLE `nl_database_change_log` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', + `user_id` int NOT NULL DEFAULT 0 COMMENT '操作用户ID', + `table_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '表名', + `operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '操作类型:structure_change, data_change, sql_query', + `operation_detail` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '操作详情(JSON格式)', + `sql_statement` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '执行的SQL语句', + `before_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '变更前数据(JSON,可选)', + `after_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '变更后数据(JSON,可选)', + `created_at` int NOT NULL DEFAULT 0 COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_table_name`(`table_name` ASC) USING BTREE, + INDEX `idx_operation_type`(`operation_type` ASC) USING BTREE, + INDEX `idx_created_at`(`created_at` ASC) USING BTREE, + INDEX `idx_table_created`(`table_name` ASC, `created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '数据库变更记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of nl_database_change_log +-- ---------------------------- + +-- ---------------------------- +-- Table structure for nl_file +-- ---------------------------- +DROP TABLE IF EXISTS `nl_file`; +CREATE TABLE `nl_file` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', + `user_id` int NOT NULL DEFAULT 0 COMMENT '用户ID', + `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '文件地址', + `type` tinyint(1) NOT NULL DEFAULT 0 COMMENT '文件类型 0:图片 1:视频 2:音频 3:excel 4:压缩包 ', + `source` tinyint(1) NOT NULL DEFAULT 0 COMMENT '来源 0:后台 1:用户端', + `created_at` int NOT NULL COMMENT '上传时间', + `updated_at` int NOT NULL DEFAULT 0 COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 84 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '文件表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of nl_file +-- ---------------------------- + +-- ---------------------------- +-- Table structure for nl_menu +-- ---------------------------- +DROP TABLE IF EXISTS `nl_menu`; +CREATE TABLE `nl_menu` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', + `title` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '菜单标题', + `icon` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '菜单图标', + `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '页面Name,全局唯一', + `path` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '访问路由', + `component` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '组件路径需要去掉 views/ 和 .vue', + `redirect` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '父级菜单重定向的子集菜单', + `keep_alive` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否开启页面缓存 0:开启 1:关闭', + `hide_in_menu` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否将页面展示在菜单栏 0:展示 1:隐藏', + `affix_tab` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否为置顶页 0:是 1:否', + `badge` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '菜单的徽标', + `badge_type` tinyint(1) NOT NULL DEFAULT 0 COMMENT '用于配置页面的徽标类型,0:dot 小红点 1:normal 文本', + `badge_variants` tinyint(1) NOT NULL DEFAULT 0 COMMENT '用于配置页面的徽标颜色 \r\n0:default 1:destructive 2:primary 3:success 4: warning', + `iframe_src` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '内嵌的页面路径', + `pid` int NOT NULL DEFAULT 0 COMMENT '父级菜单id', + `sort` int NOT NULL DEFAULT 0 COMMENT '排序', + `query` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '默认查询参数', + `created_at` int NOT NULL DEFAULT 0 COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT 0 COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of nl_menu +-- ---------------------------- +INSERT INTO `nl_menu` VALUES (1, '概览', 'twemoji:house-with-garden', 'Dashboard', '/dashobard', 'BasicLayout', '', 0, 0, 1, '', 0, 0, '', 0, -1, '', 1734485082, 1734486088, 0); +INSERT INTO `nl_menu` VALUES (2, '分析页', 'lucide:area-chart', 'Analytics', '/analytics', '/dashboard/analytics/index', '', 1, 0, 0, '', 0, 0, '', 1, 0, '', 1734485206, 1734485814, 0); +INSERT INTO `nl_menu` VALUES (3, '工作台', 'carbon:workspace', 'Workspace', '/workspace', '/dashboard/workspace/index', '', 1, 0, 1, '', 0, 0, '', 1, 0, '', 1734486030, 0, 0); +INSERT INTO `nl_menu` VALUES (4, '关于', 'lucide:copyright', 'About', '/about', '/_core/about/index.vue', '', 1, 0, 1, '', 0, 0, '', 0, 999999999, '', 0, 0, 0); +INSERT INTO `nl_menu` VALUES (5, '基础管理', 'logos:openjs-foundation-icon', 'System', '/system', 'BasicLayout', '', 0, 0, 1, '', 0, 0, '', 0, 1000, '', 1734486082, 1735117933, 0); +INSERT INTO `nl_menu` VALUES (6, '管理员管理', 'grommet-icons:user-admin', 'SystemUser', '/system/user', '/system/admin/index', '', 1, 0, 1, '', 0, 0, '', 5, 0, '', 1734486178, 0, 0); +INSERT INTO `nl_menu` VALUES (7, '角色管理', 'carbon:user-role', 'SystemRole', '/system/role', '/system/role/index', '', 1, 0, 1, '', 0, 0, '', 5, 1, '', 1734486291, 1734489429, 0); +INSERT INTO `nl_menu` VALUES (8, '菜单管理', 'line-md:menu', 'SystemMenu', '/system/menu', '/system/menu/index', '', 1, 0, 1, '', 0, 0, '', 5, 2, '', 1734486345, 0, 0); +INSERT INTO `nl_menu` VALUES (9, '代码生成', 'fluent-color:code-20', 'CodeGeneration', '/code-generation', '/code-generation/index', '', 1, 0, 1, '', 0, 0, '', 0, 99999999, '', 0, 0, 0); +INSERT INTO `nl_menu` VALUES (10, '数据库管理', 'material-symbols:database', 'SystemDatabase', '/system/database', '/system/database/index', '', 1, 0, 1, '', 0, 0, '', 5, 3, '', 1734486345, 0, 0); + +-- ---------------------------- +-- Table structure for nl_role +-- ---------------------------- +DROP TABLE IF EXISTS `nl_role`; +CREATE TABLE `nl_role` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色名称', + `value` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色值', + `pid` int NOT NULL DEFAULT 0 COMMENT '上级角色', + `desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色说明', + `created_at` int NOT NULL DEFAULT 0 COMMENT '创建时间', + `updated_at` int NOT NULL DEFAULT 0 COMMENT '更新时间', + `deleted_at` int NOT NULL DEFAULT 0 COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of nl_role +-- ---------------------------- +INSERT INTO `nl_role` VALUES (1, '超级管理员', 'admin', 0, '', 0, 0, 0); + +-- ---------------------------- +-- Table structure for nl_role_menu_relations +-- ---------------------------- +DROP TABLE IF EXISTS `nl_role_menu_relations`; +CREATE TABLE `nl_role_menu_relations` ( + `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `role_id` int NOT NULL DEFAULT 0 COMMENT '角色ID', + `menu_id` int NOT NULL DEFAULT 0 COMMENT '菜单ID', + `created_at` int NOT NULL DEFAULT 0 COMMENT '创建时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色权限绑定表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Records of nl_role_menu_relations +-- ---------------------------- +INSERT INTO `nl_role_menu_relations` VALUES (1, 1, 1, 1747033198); +INSERT INTO `nl_role_menu_relations` VALUES (2, 1, 2, 1747033198); +INSERT INTO `nl_role_menu_relations` VALUES (3, 1, 3, 1747033198); +INSERT INTO `nl_role_menu_relations` VALUES (4, 1, 4, 1747033198); +INSERT INTO `nl_role_menu_relations` VALUES (5, 1, 5, 1747033198); +INSERT INTO `nl_role_menu_relations` VALUES (6, 1, 6, 1747033198); +INSERT INTO `nl_role_menu_relations` VALUES (7, 1, 7, 1747033198); +INSERT INTO `nl_role_menu_relations` VALUES (8, 1, 8, 1747033198); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/public/nl_admin.sql b/public/nl_admin.sql index f2d41b69..640df6ee 100644 --- a/public/nl_admin.sql +++ b/public/nl_admin.sql @@ -181,6 +181,7 @@ INSERT INTO `nl_menu` VALUES (6, '管理员管理', 'grommet-icons:user-admin', INSERT INTO `nl_menu` VALUES (7, '角色管理', 'carbon:user-role', 'SystemRole', '/system/role', '/system/role/index', '', 1, 0, 1, '', 0, 0, '', 5, 1, '', 1734486291, 1734489429, 0); INSERT INTO `nl_menu` VALUES (8, '菜单管理', 'line-md:menu', 'SystemMenu', '/system/menu', '/system/menu/index', '', 1, 0, 1, '', 0, 0, '', 5, 2, '', 1734486345, 0, 0); INSERT INTO `nl_menu` VALUES (9, '代码生成', 'fluent-color:code-20', 'CodeGeneration', '/code-generation', '/code-generation/index', '', 1, 0, 1, '', 0, 0, '', 0, 99999999, '', 0, 0, 0); +INSERT INTO `nl_menu` VALUES (10, '数据库管理', 'carbon:database', 'SystemDatabase', '/system/database', '/system/database/index', '', 1, 0, 1, '', 0, 0, '', 5, 3, '', 1734486345, 0, 0); -- ---------------------------- -- Table structure for nl_role diff --git a/routes/api.php b/routes/api.php index f095c400..7f60813f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -32,6 +32,7 @@ Route::group([], function () { 'role' => \App\Http\Controllers\Api\RoleController::class, // 角色管理 'menu' => \App\Http\Controllers\Api\MenuController::class, // 菜单管理 'upload' => \App\Http\Controllers\Api\UploadController::class, // 上传文件 + 'database' => \App\Http\Controllers\Api\DatabaseController::class, // 数据库管理 // 需要登录的路由生成地址 ]); });