From bf4ff4d9ce94082e71814dfa5f7fda1eccfb3bd5 Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Sun, 30 Nov 2025 11:07:29 +0800 Subject: [PATCH 01/14] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A41.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/springboot_demo | 1 + 1 file changed, 1 insertion(+) create mode 160000 src/springboot_demo diff --git a/src/springboot_demo b/src/springboot_demo new file mode 160000 index 00000000..b2860b02 --- /dev/null +++ b/src/springboot_demo @@ -0,0 +1 @@ +Subproject commit b2860b02fcc059351bf8caac6be1c67251fa8286 -- 2.34.1 From fcb95bb89427bdcddfdd47fc5b30cdbee31f65a7 Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Sun, 30 Nov 2025 11:13:34 +0800 Subject: [PATCH 02/14] ? --- src/springboot_demo | 1 - 1 file changed, 1 deletion(-) delete mode 160000 src/springboot_demo diff --git a/src/springboot_demo b/src/springboot_demo deleted file mode 160000 index b2860b02..00000000 --- a/src/springboot_demo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b2860b02fcc059351bf8caac6be1c67251fa8286 -- 2.34.1 From 48902e636b3309ae5ed87301d5d3f8aff4ce9303 Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Sun, 30 Nov 2025 11:16:28 +0800 Subject: [PATCH 03/14] ? --- src | 1 + 1 file changed, 1 insertion(+) create mode 160000 src diff --git a/src b/src new file mode 160000 index 00000000..b2860b02 --- /dev/null +++ b/src @@ -0,0 +1 @@ +Subproject commit b2860b02fcc059351bf8caac6be1c67251fa8286 -- 2.34.1 From 31ff369df128fa9e415a94942763102a1eb6827f Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Sun, 30 Nov 2025 11:40:47 +0800 Subject: [PATCH 04/14] feat: add complete springboot project with source code and configuration --- src | 1 - src/springboot_demo/.gitattributes | 2 + src/springboot_demo/.gitignore | 33 + .../.mvn/wrapper/maven-wrapper.properties | 3 + src/springboot_demo/198.18.0.22443 | 0 src/springboot_demo/API_INTERFACE.md | 575 ++++ src/springboot_demo/DATABASE_SETUP.md | 202 ++ src/springboot_demo/ERROR_FIXES.md | 46 + src/springboot_demo/PROJECT_STRUCTURE.md | 80 + src/springboot_demo/README_CONNECTION.md | 129 + src/springboot_demo/TESTING_GUIDE.md | 323 ++ src/springboot_demo/api-test.http | 636 ++++ src/springboot_demo/docker-compose.yml | 123 + src/springboot_demo/frontend/.gitignore | 24 + src/springboot_demo/frontend/App.tsx | 732 +++++ src/springboot_demo/frontend/README.md | 20 + .../frontend/components/AccountPage.tsx | 222 ++ .../frontend/components/ChatMessage.tsx | 40 + .../frontend/components/ChatModal.tsx | 384 +++ .../frontend/components/ComparisonModal.tsx | 232 ++ .../frontend/components/DataAdminPage.tsx | 123 + .../frontend/components/DataAdminSidebar.tsx | 93 + .../frontend/components/Dropdown.tsx | 63 + .../frontend/components/FriendsPage.tsx | 728 +++++ .../frontend/components/HistoryPage.tsx | 455 +++ .../frontend/components/HistorySidebar.tsx | 130 + .../frontend/components/LoginPage.tsx | 211 ++ .../frontend/components/Modal.tsx | 64 + .../frontend/components/NotificationsPage.tsx | 156 + .../frontend/components/PlaceholderPage.tsx | 18 + .../frontend/components/QueryPage.tsx | 260 ++ .../frontend/components/QueryResult.tsx | 264 ++ .../frontend/components/RightSidebar.tsx | 83 + .../frontend/components/Sidebar.tsx | 94 + .../frontend/components/SysAdminPage.tsx | 43 + .../frontend/components/SysAdminSidebar.tsx | 90 + .../frontend/components/TopHeader.tsx | 267 ++ .../components/admin/AdminAccountPage.tsx | 92 + .../frontend/components/admin/AdminModal.tsx | 28 + .../components/admin/DashboardPage.tsx | 243 ++ .../components/admin/LLMConfigPage.tsx | 357 +++ .../admin/NotificationManagementPage.tsx | 129 + .../components/admin/SystemLogPage.tsx | 130 + .../components/admin/UserManagementPage.tsx | 263 ++ .../data-admin/ConnectionLogPage.tsx | 169 ++ .../data-admin/DataAdminDashboardPage.tsx | 126 + .../data-admin/DataAdminNotificationPage.tsx | 107 + .../data-admin/DataSourceManagementPage.tsx | 172 ++ .../data-admin/UserPermissionPage.tsx | 365 +++ src/springboot_demo/frontend/constants.ts | 270 ++ src/springboot_demo/frontend/env.d.ts | 14 + src/springboot_demo/frontend/index.html | 66 + src/springboot_demo/frontend/index.tsx | 16 + src/springboot_demo/frontend/metadata.json | 5 + .../frontend/package-lock.json | 2630 +++++++++++++++++ src/springboot_demo/frontend/package.json | 25 + src/springboot_demo/frontend/services/api.ts | 246 ++ src/springboot_demo/frontend/tsconfig.json | 30 + src/springboot_demo/frontend/types.ts | 193 ++ src/springboot_demo/frontend/vite.config.ts | 35 + src/springboot_demo/last.md | 507 ++++ .../mongodb_schema_from_last.js | 482 +++ src/springboot_demo/mvnw | 295 ++ src/springboot_demo/mvnw.cmd | 189 ++ .../mysql_schema_from_last.sql | 341 +++ src/springboot_demo/package-lock.json | 6 + src/springboot_demo/pom.xml | 127 + .../SpringbootDemoApplication.java | 13 + .../springboot_demo/common/Result.java | 37 + .../springboot_demo/config/CorsConfig.java | 26 + .../config/JwtInterceptor.java | 42 + .../config/SecurityConfig.java | 33 + .../springboot_demo/config/WebMvcConfig.java | 27 + .../controller/AuthController.java | 31 + .../controller/ColumnMetadataController.java | 76 + .../controller/DbConnectionController.java | 86 + .../controller/DbTypeController.java | 71 + .../controller/DialogController.java | 34 + .../controller/ErrorLogController.java | 86 + .../controller/ErrorTypeController.java | 74 + .../controller/LlmConfigController.java | 91 + .../controller/LlmStatusController.java | 71 + .../controller/NotificationController.java | 125 + .../NotificationTargetController.java | 74 + .../controller/OperationLogController.java | 82 + .../controller/PriorityController.java | 74 + .../controller/QueryController.java | 29 + .../controller/QueryLogController.java | 78 + .../controller/RoleController.java | 28 + .../controller/SystemHealthController.java | 77 + .../controller/TableMetadataController.java | 75 + .../controller/TestController.java | 107 + .../controller/UserController.java | 87 + .../UserDbPermissionController.java | 95 + .../example/springboot_demo/dto/LoginDTO.java | 11 + .../springboot_demo/dto/QueryRequestDTO.java | 13 + .../entity/mongodb/DialogRecord.java | 29 + .../entity/mysql/ColumnMetadata.java | 30 + .../entity/mysql/DbConnection.java | 35 + .../springboot_demo/entity/mysql/DbType.java | 22 + .../entity/mysql/ErrorLog.java | 29 + .../entity/mysql/ErrorType.java | 25 + .../entity/mysql/LlmConfig.java | 37 + .../entity/mysql/LlmStatus.java | 22 + .../entity/mysql/Notification.java | 39 + .../entity/mysql/NotificationTarget.java | 25 + .../entity/mysql/OperationLog.java | 36 + .../entity/mysql/Priority.java | 25 + .../entity/mysql/QueryLog.java | 31 + .../springboot_demo/entity/mysql/Role.java | 22 + .../entity/mysql/SystemHealth.java | 32 + .../entity/mysql/TableMetadata.java | 27 + .../springboot_demo/entity/mysql/User.java | 38 + .../entity/mysql/UserDbPermission.java | 30 + .../mapper/ColumnMetadataMapper.java | 12 + .../mapper/DbConnectionMapper.java | 11 + .../springboot_demo/mapper/DbTypeMapper.java | 12 + .../mapper/ErrorLogMapper.java | 15 + .../mapper/ErrorTypeMapper.java | 15 + .../mapper/LlmConfigMapper.java | 11 + .../mapper/LlmStatusMapper.java | 12 + .../mapper/NotificationMapper.java | 15 + .../mapper/NotificationTargetMapper.java | 15 + .../mapper/OperationLogMapper.java | 12 + .../mapper/PriorityMapper.java | 15 + .../mapper/QueryLogMapper.java | 12 + .../springboot_demo/mapper/RoleMapper.java | 11 + .../mapper/SystemHealthMapper.java | 15 + .../mapper/TableMetadataMapper.java | 11 + .../mapper/UserDbPermissionMapper.java | 12 + .../springboot_demo/mapper/UserMapper.java | 11 + .../repository/DialogRecordRepository.java | 15 + .../springboot_demo/service/AuthService.java | 10 + .../service/ColumnMetadataService.java | 16 + .../service/DbConnectionService.java | 20 + .../service/DbTypeService.java | 14 + .../service/DialogService.java | 12 + .../service/ErrorLogService.java | 24 + .../service/ErrorTypeService.java | 17 + .../service/LlmConfigService.java | 15 + .../springboot_demo/service/LlmService.java | 20 + .../service/LlmStatusService.java | 14 + .../service/NotificationService.java | 29 + .../service/NotificationTargetService.java | 17 + .../service/OperationLogService.java | 26 + .../service/PriorityService.java | 24 + .../service/QueryLogService.java | 21 + .../springboot_demo/service/QueryService.java | 10 + .../springboot_demo/service/RoleService.java | 7 + .../service/SystemHealthService.java | 24 + .../service/TableMetadataService.java | 14 + .../service/UserDbPermissionService.java | 26 + .../springboot_demo/service/UserService.java | 24 + .../service/impl/AuthServiceImpl.java | 70 + .../impl/ColumnMetadataServiceImpl.java | 27 + .../service/impl/DbConnectionServiceImpl.java | 36 + .../service/impl/DbTypeServiceImpl.java | 22 + .../service/impl/DialogServiceImpl.java | 28 + .../service/impl/ErrorLogServiceImpl.java | 36 + .../service/impl/ErrorTypeServiceImpl.java | 25 + .../service/impl/LlmConfigServiceImpl.java | 24 + .../service/impl/LlmServiceImpl.java | 380 +++ .../service/impl/LlmStatusServiceImpl.java | 22 + .../service/impl/NotificationServiceImpl.java | 48 + .../impl/NotificationTargetServiceImpl.java | 25 + .../service/impl/OperationLogServiceImpl.java | 41 + .../service/impl/PriorityServiceImpl.java | 34 + .../service/impl/QueryLogServiceImpl.java | 33 + .../service/impl/QueryServiceImpl.java | 247 ++ .../service/impl/RoleServiceImpl.java | 12 + .../service/impl/SystemHealthServiceImpl.java | 36 + .../impl/TableMetadataServiceImpl.java | 25 + .../impl/UserDbPermissionServiceImpl.java | 39 + .../service/impl/UserServiceImpl.java | 69 + .../springboot_demo/utils/JwtUtil.java | 57 + .../springboot_demo/vo/ChartDataVO.java | 13 + .../example/springboot_demo/vo/DatasetVO.java | 13 + .../example/springboot_demo/vo/LoginVO.java | 16 + .../springboot_demo/vo/QueryResponseVO.java | 18 + .../springboot_demo/vo/TableDataVO.java | 12 + .../src/main/resources/application.yml | 75 + .../SpringbootDemoApplicationTests.java | 13 + 182 files changed, 19005 insertions(+), 1 deletion(-) delete mode 160000 src create mode 100644 src/springboot_demo/.gitattributes create mode 100644 src/springboot_demo/.gitignore create mode 100644 src/springboot_demo/.mvn/wrapper/maven-wrapper.properties create mode 100644 src/springboot_demo/198.18.0.22443 create mode 100644 src/springboot_demo/API_INTERFACE.md create mode 100644 src/springboot_demo/DATABASE_SETUP.md create mode 100644 src/springboot_demo/ERROR_FIXES.md create mode 100644 src/springboot_demo/PROJECT_STRUCTURE.md create mode 100644 src/springboot_demo/README_CONNECTION.md create mode 100644 src/springboot_demo/TESTING_GUIDE.md create mode 100644 src/springboot_demo/api-test.http create mode 100644 src/springboot_demo/docker-compose.yml create mode 100644 src/springboot_demo/frontend/.gitignore create mode 100644 src/springboot_demo/frontend/App.tsx create mode 100644 src/springboot_demo/frontend/README.md create mode 100644 src/springboot_demo/frontend/components/AccountPage.tsx create mode 100644 src/springboot_demo/frontend/components/ChatMessage.tsx create mode 100644 src/springboot_demo/frontend/components/ChatModal.tsx create mode 100644 src/springboot_demo/frontend/components/ComparisonModal.tsx create mode 100644 src/springboot_demo/frontend/components/DataAdminPage.tsx create mode 100644 src/springboot_demo/frontend/components/DataAdminSidebar.tsx create mode 100644 src/springboot_demo/frontend/components/Dropdown.tsx create mode 100644 src/springboot_demo/frontend/components/FriendsPage.tsx create mode 100644 src/springboot_demo/frontend/components/HistoryPage.tsx create mode 100644 src/springboot_demo/frontend/components/HistorySidebar.tsx create mode 100644 src/springboot_demo/frontend/components/LoginPage.tsx create mode 100644 src/springboot_demo/frontend/components/Modal.tsx create mode 100644 src/springboot_demo/frontend/components/NotificationsPage.tsx create mode 100644 src/springboot_demo/frontend/components/PlaceholderPage.tsx create mode 100644 src/springboot_demo/frontend/components/QueryPage.tsx create mode 100644 src/springboot_demo/frontend/components/QueryResult.tsx create mode 100644 src/springboot_demo/frontend/components/RightSidebar.tsx create mode 100644 src/springboot_demo/frontend/components/Sidebar.tsx create mode 100644 src/springboot_demo/frontend/components/SysAdminPage.tsx create mode 100644 src/springboot_demo/frontend/components/SysAdminSidebar.tsx create mode 100644 src/springboot_demo/frontend/components/TopHeader.tsx create mode 100644 src/springboot_demo/frontend/components/admin/AdminAccountPage.tsx create mode 100644 src/springboot_demo/frontend/components/admin/AdminModal.tsx create mode 100644 src/springboot_demo/frontend/components/admin/DashboardPage.tsx create mode 100644 src/springboot_demo/frontend/components/admin/LLMConfigPage.tsx create mode 100644 src/springboot_demo/frontend/components/admin/NotificationManagementPage.tsx create mode 100644 src/springboot_demo/frontend/components/admin/SystemLogPage.tsx create mode 100644 src/springboot_demo/frontend/components/admin/UserManagementPage.tsx create mode 100644 src/springboot_demo/frontend/components/data-admin/ConnectionLogPage.tsx create mode 100644 src/springboot_demo/frontend/components/data-admin/DataAdminDashboardPage.tsx create mode 100644 src/springboot_demo/frontend/components/data-admin/DataAdminNotificationPage.tsx create mode 100644 src/springboot_demo/frontend/components/data-admin/DataSourceManagementPage.tsx create mode 100644 src/springboot_demo/frontend/components/data-admin/UserPermissionPage.tsx create mode 100644 src/springboot_demo/frontend/constants.ts create mode 100644 src/springboot_demo/frontend/env.d.ts create mode 100644 src/springboot_demo/frontend/index.html create mode 100644 src/springboot_demo/frontend/index.tsx create mode 100644 src/springboot_demo/frontend/metadata.json create mode 100644 src/springboot_demo/frontend/package-lock.json create mode 100644 src/springboot_demo/frontend/package.json create mode 100644 src/springboot_demo/frontend/services/api.ts create mode 100644 src/springboot_demo/frontend/tsconfig.json create mode 100644 src/springboot_demo/frontend/types.ts create mode 100644 src/springboot_demo/frontend/vite.config.ts create mode 100644 src/springboot_demo/last.md create mode 100644 src/springboot_demo/mongodb_schema_from_last.js create mode 100644 src/springboot_demo/mvnw create mode 100644 src/springboot_demo/mvnw.cmd create mode 100644 src/springboot_demo/mysql_schema_from_last.sql create mode 100644 src/springboot_demo/package-lock.json create mode 100644 src/springboot_demo/pom.xml create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/common/Result.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/config/CorsConfig.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/config/SecurityConfig.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/AuthController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbTypeController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DialogController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/OperationLogController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/PriorityController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryLogController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/RoleController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TestController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/dto/LoginDTO.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Role.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/User.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserMapper.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/AuthService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbConnectionService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbTypeService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/DialogService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorLogService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmConfigService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmStatusService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/OperationLogService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/PriorityService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryLogService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/RoleService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/SystemHealthService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/TableMetadataService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserService.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/utils/JwtUtil.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/vo/DatasetVO.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/vo/LoginVO.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java create mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/vo/TableDataVO.java create mode 100644 src/springboot_demo/src/main/resources/application.yml create mode 100644 src/springboot_demo/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java diff --git a/src b/src deleted file mode 160000 index b2860b02..00000000 --- a/src +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b2860b02fcc059351bf8caac6be1c67251fa8286 diff --git a/src/springboot_demo/.gitattributes b/src/springboot_demo/.gitattributes new file mode 100644 index 00000000..3b41682a --- /dev/null +++ b/src/springboot_demo/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/src/springboot_demo/.gitignore b/src/springboot_demo/.gitignore new file mode 100644 index 00000000..667aaef0 --- /dev/null +++ b/src/springboot_demo/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/src/springboot_demo/.mvn/wrapper/maven-wrapper.properties b/src/springboot_demo/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..c0bcafe9 --- /dev/null +++ b/src/springboot_demo/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/src/springboot_demo/198.18.0.22443 b/src/springboot_demo/198.18.0.22443 new file mode 100644 index 00000000..e69de29b diff --git a/src/springboot_demo/API_INTERFACE.md b/src/springboot_demo/API_INTERFACE.md new file mode 100644 index 00000000..d82ce444 --- /dev/null +++ b/src/springboot_demo/API_INTERFACE.md @@ -0,0 +1,575 @@ +# 前后端接口文档 + +## 基础配置 + +### API 基础地址 +- **默认地址**: `http://localhost:8080` +- **前端配置**: 通过环境变量 `VITE_API_BASE_URL` 配置 +- **配置文件**: `frontend/services/api.ts` + +### 认证方式 +- **认证类型**: JWT (JSON Web Token) +- **Token 位置**: HTTP Header `Authorization: Bearer {token}` +- **Token 存储**: 前端 localStorage +- **用户ID**: HTTP Header `userId` (部分接口需要) + +### 统一响应格式 + +所有接口返回统一的 `Result` 格式: + +```typescript +interface Result { + code: number; // 200: 成功, 500: 失败 + message: string; // 响应消息 + data: T; // 响应数据 +} +``` + +**前端自动处理**: `frontend/services/api.ts` 中的 `request()` 函数会自动解析 `Result` 格式,直接返回 `data` 部分。 + +--- + +## 一、认证接口 + +### 1.1 用户登录 + +**接口**: `POST /auth/login` + +**认证要求**: ❌ 无需认证(公开接口) + +**请求体**: +```typescript +{ + username: string; // 用户名 + password: string; // 密码 +} +``` + +**响应**: +```typescript +{ + token: string; // JWT token + userId: number; // 用户ID + username: string; // 用户名 + email: string; // 邮箱 + roleId: number; // 角色ID (1:系统管理员, 2:数据管理员, 3:普通用户) + roleName: string; // 角色名称 + avatarUrl: string; // 头像URL +} +``` + +**前端调用**: +```typescript +import { authApi } from './services/api'; + +const response = await authApi.login({ + username: 'admin', + password: '123456' +}); +// 登录成功后,token会自动保存到localStorage +``` + +**后端实现**: `AuthController.java` + +--- + +## 二、查询接口 + +### 2.1 执行自然语言查询 + +**接口**: `POST /query/execute` + +**认证要求**: ✅ 需要JWT认证 + +**请求头**: +- `Authorization: Bearer {token}` +- `userId: {userId}` (从localStorage获取) + +**请求体**: +```typescript +{ + userPrompt: string; // 用户自然语言查询 + model: string; // 大模型名称 (如: "gemini-2.5-pro") + database: string; // 数据库名称 (如: "销售数据库") + conversationId?: string; // 对话ID (可选,用于多轮对话) +} +``` + +**响应**: +```typescript +{ + id: string; // 查询ID + userPrompt: string; // 用户查询 + sqlQuery: string; // 生成的SQL语句 + conversationId: string; // 对话ID + queryTime: string; // 查询时间 (ISO格式) + executionTime: string; // 执行耗时 (如: "2.5秒") + database: string; // 数据库名称 + model: string; // 使用的模型 + tableData: { // 表格数据 + headers: string[]; // 表头 + rows: string[][]; // 数据行 + }; + chartData?: { // 图表数据 (可选) + type: string; // 图表类型: "bar" | "line" | "pie" + labels: string[]; // 标签 + datasets: Array<{ + label: string; + data: number[]; + backgroundColor?: string | string[]; + }>; + }; +} +``` + +**前端调用**: +```typescript +import { queryApi } from './services/api'; + +const result = await queryApi.execute({ + userPrompt: '展示2023年各季度的订单量', + model: 'gemini-2.5-pro', + database: '销售数据库', + conversationId: 'conv_12345678' // 可选 +}); +``` + +**后端实现**: `QueryController.java` + +--- + +## 三、对话接口 + +### 3.1 获取用户对话列表 + +**接口**: `GET /dialog/list` + +**认证要求**: ✅ 需要JWT认证 + +**请求头**: +- `Authorization: Bearer {token}` +- `userId: {userId}` + +**响应**: +```typescript +DialogRecord[] { + dialogId: string; // 对话ID + userId: number; // 用户ID + topic: string; // 对话主题 + totalRounds: number; // 对话轮次 + startTime: string; // 开始时间 + lastTime: string; // 最后更新时间 +}[] +``` + +**前端调用**: +```typescript +import { dialogApi } from './services/api'; + +const dialogs = await dialogApi.getList(); +``` + +**后端实现**: `DialogController.java` + +### 3.2 获取对话详情 + +**接口**: `GET /dialog/{dialogId}` + +**认证要求**: ✅ 需要JWT认证 + +**路径参数**: +- `dialogId`: 对话ID + +**响应**: 同 3.1 的单个 `DialogRecord` 对象 + +**前端调用**: +```typescript +const dialog = await dialogApi.getById('conv_12345678'); +``` + +--- + +## 四、用户管理接口 + +### 4.1 获取用户列表 + +**接口**: `GET /user/list` + +**认证要求**: ✅ 需要JWT认证 + +**响应**: +```typescript +User[] { + id: number; + username: string; + email: string; + phonenumber: string; + roleId: number; + avatarUrl: string; + status: number; // 0: 禁用, 1: 正常 +}[] +``` + +**前端调用**: +```typescript +import { userApi } from './services/api'; + +const users = await userApi.getList(); +``` + +**后端实现**: `UserController.java` + +### 4.2 根据ID获取用户 + +**接口**: `GET /user/{id}` + +**认证要求**: ✅ 需要JWT认证 + +**路径参数**: `id` - 用户ID + +**响应**: 单个 `User` 对象 + +### 4.3 根据用户名获取用户 + +**接口**: `GET /user/username/{username}` + +**认证要求**: ✅ 需要JWT认证 + +**路径参数**: `username` - 用户名 + +**响应**: 单个 `User` 对象 + +### 4.4 分页查询用户 + +**接口**: `GET /user/page?current=1&size=10` + +**认证要求**: ✅ 需要JWT认证 + +**查询参数**: +- `current`: 当前页码 (默认: 1) +- `size`: 每页大小 (默认: 10) + +**响应**: 分页对象 + +### 4.5 创建用户 + +**接口**: `POST /user` + +**认证要求**: ✅ 需要JWT认证 + +**请求体**: `User` 对象(密码会自动BCrypt加密) + +### 4.6 更新用户 + +**接口**: `PUT /user` + +**认证要求**: ✅ 需要JWT认证 + +**请求体**: `User` 对象 + +### 4.7 删除用户 + +**接口**: `DELETE /user/{id}` + +**认证要求**: ✅ 需要JWT认证 + +**路径参数**: `id` - 用户ID + +--- + +## 五、数据库连接管理接口 + +### 5.1 获取数据库连接列表 + +**接口**: `GET /db-connection/list` + +**认证要求**: ✅ 需要JWT认证 + +**响应**: +```typescript +DbConnection[] { + id: number; + name: string; + dbTypeId: number; + url: string; + username: string; + password?: string; + status: string; // "connected" | "disconnected" | "error" + createUserId: number; +}[] +``` + +**前端调用**: +```typescript +import { dbConnectionApi } from './services/api'; + +const connections = await dbConnectionApi.getList(); +``` + +**后端实现**: `DbConnectionController.java` + +### 5.2 根据ID获取数据库连接 + +**接口**: `GET /db-connection/{id}` + +**认证要求**: ✅ 需要JWT认证 + +### 5.3 根据创建者获取数据库连接列表 + +**接口**: `GET /db-connection/list/{createUserId}` + +**认证要求**: ✅ 需要JWT认证 + +### 5.4 创建数据库连接 + +**接口**: `POST /db-connection` + +**认证要求**: ✅ 需要JWT认证 + +**请求体**: `DbConnection` 对象 + +### 5.5 更新数据库连接 + +**接口**: `PUT /db-connection` + +**认证要求**: ✅ 需要JWT认证 + +### 5.6 删除数据库连接 + +**接口**: `DELETE /db-connection/{id}` + +**认证要求**: ✅ 需要JWT认证 + +### 5.7 测试数据库连接 + +**接口**: `GET /db-connection/test/{id}` + +**认证要求**: ✅ 需要JWT认证 + +**响应**: `boolean` - 连接是否成功 + +**前端调用**: +```typescript +const isConnected = await dbConnectionApi.test(1); +``` + +--- + +## 六、大模型配置接口 + +### 6.1 获取大模型配置列表 + +**接口**: `GET /llm-config/list` + +**认证要求**: ✅ 需要JWT认证 + +**响应**: +```typescript +LlmConfig[] { + id: number; + name: string; // 模型名称 + version: string; // 版本号 + apiKey?: string; // API密钥 (可能不返回) + apiUrl: string; // API地址 + statusId: number; // 状态ID + isDisabled: number; // 0: 启用, 1: 禁用 + timeout: number; // 超时时间(毫秒) +}[] +``` + +**前端调用**: +```typescript +import { llmConfigApi } from './services/api'; + +const configs = await llmConfigApi.getList(); +``` + +**后端实现**: `LlmConfigController.java` + +### 6.2 获取可用的大模型配置 + +**接口**: `GET /llm-config/list/available` + +**认证要求**: ✅ 需要JWT认证 + +**响应**: 仅返回 `isDisabled = 0` 的配置列表 + +**前端调用**: +```typescript +const availableConfigs = await llmConfigApi.getAvailable(); +``` + +### 6.3 根据ID获取大模型配置 + +**接口**: `GET /llm-config/{id}` + +**认证要求**: ✅ 需要JWT认证 + +### 6.4 创建大模型配置 + +**接口**: `POST /llm-config` + +**认证要求**: ✅ 需要JWT认证 + +### 6.5 更新大模型配置 + +**接口**: `PUT /llm-config` + +**认证要求**: ✅ 需要JWT认证 + +### 6.6 删除大模型配置 + +**接口**: `DELETE /llm-config/{id}` + +**认证要求**: ✅ 需要JWT认证 + +### 6.7 启用/禁用大模型配置 + +**接口**: `PUT /llm-config/{id}/toggle` + +**认证要求**: ✅ 需要JWT认证 + +**功能**: 切换 `isDisabled` 状态 (0 ↔ 1) + +--- + +## 七、其他接口 + +### 7.1 测试接口(无需认证) + +**接口**: `GET /test/hello` + +**响应**: `"Hello, Spring Boot!"` + +**接口**: `GET /test/all` + +**响应**: 数据库连接测试结果 +```json +{ + "mysql": { "status": "success", ... }, + "mongodb": { "status": "success", ... }, + "redis": { "status": "success", ... } +} +``` + +--- + +## 八、认证拦截器配置 + +### 免认证接口 + +以下接口无需JWT认证(在 `WebMvcConfig.java` 中配置): + +- `/auth/**` - 所有认证相关接口 +- `/role` - 角色接口(仅POST) +- `/user` - 用户注册接口 +- `/error` - 错误处理接口 + +### 认证流程 + +1. **登录**: 调用 `/auth/login` 获取token +2. **保存**: 前端自动保存token到localStorage +3. **携带**: 后续请求自动在Header中添加 `Authorization: Bearer {token}` +4. **验证**: 后端 `JwtInterceptor` 验证token有效性 +5. **提取**: 从token中提取userId并设置到request attribute + +--- + +## 九、前端API服务使用示例 + +### 完整示例 + +```typescript +import { authApi, queryApi, dialogApi } from './services/api'; + +// 1. 登录 +try { + const loginResponse = await authApi.login({ + username: 'admin', + password: '123456' + }); + console.log('登录成功:', loginResponse); +} catch (error) { + console.error('登录失败:', error.message); +} + +// 2. 执行查询 +try { + const queryResult = await queryApi.execute({ + userPrompt: '展示2023年各季度的订单量', + model: 'gemini-2.5-pro', + database: '销售数据库' + }); + console.log('查询结果:', queryResult); +} catch (error) { + console.error('查询失败:', error.message); +} + +// 3. 获取对话列表 +try { + const dialogs = await dialogApi.getList(); + console.log('对话列表:', dialogs); +} catch (error) { + console.error('获取对话列表失败:', error.message); +} + +// 4. 登出 +authApi.logout(); +``` + +### 错误处理 + +所有API调用都会自动处理错误: + +```typescript +try { + const result = await queryApi.execute({...}); +} catch (error) { + // error.message 包含后端返回的错误信息 + console.error('请求失败:', error.message); +} +``` + +--- + +## 十、接口状态码说明 + +### HTTP状态码 +- `200`: 请求成功 +- `401`: 未认证(token无效或过期) +- `500`: 服务器内部错误 + +### Result.code +- `200`: 业务成功 +- `500`: 业务失败 + +--- + +## 十一、注意事项 + +1. **Token过期**: 如果token过期,需要重新登录 +2. **CORS配置**: 后端已配置CORS,允许前端跨域访问 +3. **userId Header**: 部分接口需要 `userId` header,前端会自动添加 +4. **密码加密**: 创建/更新用户时,密码会自动BCrypt加密 +5. **时间格式**: 所有时间字段使用ISO 8601格式 (如: `2024-01-01T10:00:00`) + +--- + +## 十二、接口文件位置 + +### 前端 +- **API服务**: `frontend/services/api.ts` +- **类型定义**: `frontend/services/api.ts` (接口中定义) + +### 后端 +- **控制器**: `src/main/java/com/example/springboot_demo/controller/` +- **DTO**: `src/main/java/com/example/springboot_demo/dto/` +- **VO**: `src/main/java/com/example/springboot_demo/vo/` +- **统一响应**: `src/main/java/com/example/springboot_demo/common/Result.java` + +--- + +## 更新日志 + +- **2024-01-XX**: 初始版本,包含认证、查询、对话、用户管理等核心接口 + diff --git a/src/springboot_demo/DATABASE_SETUP.md b/src/springboot_demo/DATABASE_SETUP.md new file mode 100644 index 00000000..5d06f510 --- /dev/null +++ b/src/springboot_demo/DATABASE_SETUP.md @@ -0,0 +1,202 @@ +# 数据库配置说明 + +## 是否必须使用Docker? + +**答案:不是必须的!** 你可以使用以下任一方式: + +1. ✅ **使用Docker Compose**(推荐,最简单) +2. ✅ **使用本地已安装的数据库** +3. ✅ **使用远程数据库服务器** + +## 方式一:使用Docker Compose(推荐) + +### 优点 +- 一键启动所有数据库服务 +- 自动初始化数据库和表结构 +- 环境隔离,不影响本地其他项目 +- 配置已预设好,无需额外配置 + +### 启动步骤 +```bash +# 在项目根目录执行 +docker-compose up -d + +# 验证服务启动 +docker-compose ps +``` + +## 方式二:使用本地已安装的数据库 + +### 前提条件 +- 本地已安装 MySQL 8.4+ +- 本地已安装 MongoDB 8.2+ +- 本地已安装 Redis(可选) + +### 配置步骤 + +#### 1. 创建MySQL数据库 +```sql +CREATE DATABASE natural_language_query_system + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +-- 创建用户(可选) +CREATE USER 'nlq_user'@'localhost' IDENTIFIED BY 'nlq_pass123'; +GRANT ALL PRIVILEGES ON natural_language_query_system.* TO 'nlq_user'@'localhost'; +FLUSH PRIVILEGES; +``` + +#### 2. 初始化MySQL表结构 +```bash +# 执行SQL脚本 +mysql -u root -p natural_language_query_system < mysql_schema_from_last.sql +``` + +#### 3. 配置MongoDB +```javascript +// 连接到MongoDB +mongosh + +// 创建数据库和用户 +use natural_language_query_system; +db.createUser({ + user: "admin", + pwd: "admin123456", + roles: [{ role: "readWrite", db: "natural_language_query_system" }] +}); +``` + +#### 4. 初始化MongoDB集合 +```bash +# 执行初始化脚本 +mongosh -u admin -p admin123456 --authenticationDatabase admin < mongodb_schema_from_last.js +``` + +#### 5. 修改 application.yml(如果需要) + +如果本地数据库的用户名、密码或端口与配置不同,修改 `src/main/resources/application.yml`: + +```yaml +spring: + datasource: + url: jdbc:mysql://localhost:3306/natural_language_query_system?... + username: your_username + password: your_password + + data: + mongodb: + host: 127.0.0.1 + port: 27017 + username: your_mongo_username + password: your_mongo_password +``` + +## 方式三:使用远程数据库服务器 + +### 修改 application.yml + +```yaml +spring: + datasource: + url: jdbc:mysql://your-mysql-server:3306/natural_language_query_system?... + username: remote_user + password: remote_password + + data: + mongodb: + host: your-mongo-server + port: 27017 + username: remote_mongo_user + password: remote_mongo_password +``` + +## 启动后端(不依赖Docker) + +### 如果数据库已就绪 + +```bash +cd springboot_demo +mvn spring-boot:run +``` + +### 如果数据库未启动 + +Spring Boot会在启动时尝试连接数据库。如果连接失败,**默认情况下应用会启动失败**。 + +### 可选:配置延迟初始化(不推荐用于生产环境) + +如果你想在数据库不可用时也能启动应用(仅用于开发测试),可以修改 `application.yml`: + +```yaml +spring: + datasource: + # 添加连接失败时的处理 + hikari: + connection-timeout: 30000 + # 允许启动时数据库不可用(仅开发环境) + initialization-fail-timeout: -1 + + # MongoDB延迟初始化 + data: + mongodb: + # 如果MongoDB不可用,应用仍可启动(但相关功能会失败) +``` + +**注意**:这种方式虽然可以让应用启动,但数据库相关的功能(登录、查询等)将无法正常工作。 + +## 数据库连接检查 + +启动后端后,可以通过以下接口检查数据库连接: + +```bash +# 检查所有数据库 +GET http://localhost:8080/test/all + +# 单独检查 +GET http://localhost:8080/test/mysql +GET http://localhost:8080/test/mongodb +GET http://localhost:8080/test/redis +``` + +## 常见问题 + +### 1. MySQL连接失败 + +**错误信息**:`Communications link failure` + +**解决方案**: +- 检查MySQL服务是否启动:`mysql -u root -p` +- 检查端口3306是否被占用 +- 检查用户名密码是否正确 +- 检查数据库是否存在 + +### 2. MongoDB连接失败 + +**错误信息**:`Exception authenticating MongoCredential` + +**解决方案**: +- 检查MongoDB服务是否启动:`mongosh` +- 检查端口27017是否被占用 +- 检查认证信息是否正确 +- 检查authentication-database配置 + +### 3. Redis连接失败(可选) + +Redis是可选的,如果未启动Redis,应用仍可正常启动,只是缓存功能不可用。 + +## 推荐方案 + +- **开发环境**:使用Docker Compose(最简单) +- **生产环境**:使用独立的数据库服务器 +- **本地测试**:可以使用本地已安装的数据库 + +## 快速启动检查清单 + +- [ ] MySQL服务运行中(端口3306) +- [ ] MongoDB服务运行中(端口27017) +- [ ] Redis服务运行中(端口6379,可选) +- [ ] 数据库已创建并初始化表结构 +- [ ] application.yml配置正确 +- [ ] 后端可以成功启动 + + diff --git a/src/springboot_demo/ERROR_FIXES.md b/src/springboot_demo/ERROR_FIXES.md new file mode 100644 index 00000000..5b6c2a0e --- /dev/null +++ b/src/springboot_demo/ERROR_FIXES.md @@ -0,0 +1,46 @@ +# 错误修复清单 + +## 已修复的错误 + +### 1. ✅ 前端API服务命名冲突 +**文件**: `frontend/services/api.ts` +**问题**: `request` 参数名与 `request()` 函数名冲突 +**修复**: 将参数名从 `request` 改为 `queryRequest` + +### 2. ✅ 后端API响应解析缺少错误检查 +**文件**: `src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java` +**问题**: 所有大模型API调用缺少响应结构验证,可能导致空指针异常 +**修复**: +- Gemini API: 添加 candidates、content、parts 结构检查 +- OpenAI API: 添加 choices、message 结构检查 +- GLM API: 添加 choices、message 结构检查 +- Qwen API: 添加 choices、message 结构检查 +- Kimi API: 添加 choices、message 结构检查 +- 所有API: 添加内容为空检查 + +### 3. ✅ LoginPage语法错误 +**文件**: `frontend/components/LoginPage.tsx` +**问题**: 多余的 `};` 导致语法错误 +**修复**: 删除多余的闭合括号 + +## 代码质量改进 + +### 错误处理增强 +- 所有API调用现在都有完整的响应结构验证 +- 提供更清晰的错误消息,便于调试 +- 防止空指针异常 + +### 类型安全 +- 修复了TypeScript类型错误 +- 确保所有函数参数类型正确 + +## 建议的后续改进 + +1. **统一错误处理**: 考虑创建统一的异常处理类 +2. **日志记录**: 添加详细的日志记录,便于排查问题 +3. **重试机制**: 为API调用添加重试逻辑 +4. **超时处理**: 优化超时时间配置 +5. **响应缓存**: 考虑添加响应缓存机制 + + + diff --git a/src/springboot_demo/PROJECT_STRUCTURE.md b/src/springboot_demo/PROJECT_STRUCTURE.md new file mode 100644 index 00000000..7b962b49 --- /dev/null +++ b/src/springboot_demo/PROJECT_STRUCTURE.md @@ -0,0 +1,80 @@ +# 项目结构说明 + +## 项目架构 + +``` +springboot_demo/ +├── frontend/ # 前端项目(React + TypeScript) +│ ├── components/ # React组件 +│ │ ├── admin/ # 系统管理员页面组件 +│ │ ├── data-admin/ # 数据管理员页面组件 +│ │ └── *.tsx # 通用组件 +│ ├── services/ # API服务层 +│ │ └── api.ts # 后端API封装 +│ ├── constants.ts # 常量定义(Mock数据) +│ ├── types.ts # TypeScript类型定义 +│ ├── App.tsx # 应用主组件 +│ └── index.tsx # 入口文件 +│ +├── src/main/java/ # 后端Java代码 +│ └── com/example/springboot_demo/ +│ ├── common/ # 通用类(Result等) +│ ├── config/ # 配置类(CORS、JWT、Security等) +│ ├── controller/ # REST控制器 +│ ├── service/ # 业务服务层 +│ │ └── impl/ # 服务实现 +│ ├── entity/ # 实体类 +│ │ ├── mysql/ # MySQL实体 +│ │ └── mongodb/ # MongoDB实体 +│ ├── mapper/ # MyBatis Mapper接口 +│ ├── repository/ # MongoDB Repository +│ ├── dto/ # 数据传输对象 +│ ├── vo/ # 视图对象 +│ └── utils/ # 工具类 +│ +├── src/main/resources/ # 资源文件 +│ ├── application.yml # 应用配置 +│ └── mapper/ # MyBatis XML映射文件 +│ +├── docker-compose.yml # Docker Compose配置 +├── pom.xml # Maven配置 +└── README_CONNECTION.md # 前后端连接说明 +``` + +## 已删除的冗余文件 + +1. ✅ `frontend/services/geminiService.ts` - 前端不再直接调用大模型API +2. ✅ `frontend/login.html` - 已被React组件LoginPage替代 +3. ✅ `frontend/DataAdminPage.tsx` - 与components目录下重复 +4. ✅ `frontend/DataAdminSidebar.tsx` - 与components目录下重复 + +## 保留的测试/工具文件 + +- `api-test.http` - HTTP测试文件,用于API测试 +- `src/.../controller/TestController.java` - 数据库连接测试接口 +- `last.md` - 数据库Schema文档(参考用) + +## 核心功能模块 + +### 前端 +- **认证**: LoginPage组件,调用后端 `/auth/login` +- **查询**: QueryPage组件,调用后端 `/query/execute` +- **对话管理**: HistorySidebar组件,调用后端 `/dialog/*` +- **API服务**: `services/api.ts` 统一封装所有后端接口 + +### 后端 +- **认证服务**: AuthService + JWT +- **查询服务**: QueryService + LlmService(大模型调用) +- **对话服务**: DialogService(MongoDB) +- **用户管理**: UserService +- **数据源管理**: DbConnectionService +- **大模型配置**: LlmConfigService + +## 数据存储 + +- **MySQL**: 用户、角色、数据源、配置等结构化数据 +- **MongoDB**: 对话记录、查询详情等非结构化数据 +- **Redis**: 缓存(可选) + + + diff --git a/src/springboot_demo/README_CONNECTION.md b/src/springboot_demo/README_CONNECTION.md new file mode 100644 index 00000000..cc241007 --- /dev/null +++ b/src/springboot_demo/README_CONNECTION.md @@ -0,0 +1,129 @@ +# 前后端连接说明 + +## 已完成的工作 + +### 1. 前端API服务层 +- 创建了 `frontend/services/api.ts`,封装了所有后端API调用 +- 支持认证token自动管理 +- 包含登录、查询、对话、用户管理等接口 + +### 2. 前端登录功能 +- 修改了 `LoginPage.tsx`,现在调用后端 `/auth/login` 接口 +- 登录成功后自动保存JWT token和用户信息到localStorage +- 支持错误提示 + +### 3. 前端查询功能 +- 修改了 `QueryPage.tsx`,现在调用后端 `/query/execute` 接口 +- 移除了直接调用大模型API的逻辑 +- 支持对话ID管理和多轮对话 + +### 4. 后端大模型服务 +- 创建了 `LlmService` 接口和 `LlmServiceImpl` 实现 +- 支持多种大模型:Gemini、OpenAI、GLM、Qwen、Kimi +- 统一处理模型响应格式 + +### 5. 后端查询服务 +- 更新了 `QueryServiceImpl`,集成大模型调用 +- 实现了对话记录管理(MongoDB) +- 解析并返回表格和图表数据 + +## 配置说明 + +### 前端配置 + +1. **API基础URL** + - 默认:`http://localhost:8080` + - 可通过环境变量 `VITE_API_BASE_URL` 配置 + - 在 `frontend/services/api.ts` 中设置 + +2. **环境变量** + - 前端不再需要大模型API密钥(已迁移到后端) + - 只需配置后端API地址 + +### 后端配置 + +1. **大模型API密钥** + - 通过系统环境变量配置: + - `GEMINI_API_KEY` + - `OPENAI_API_KEY` + - `GLM_API_KEY` + - `QWEN_API_KEY` + - `KIMI_API_KEY` + +2. **数据库配置** + - MySQL: 在 `application.yml` 中配置 + - MongoDB: 在 `application.yml` 中配置 + +## 使用步骤 + +### 1. 启动后端 +```bash +cd springboot_demo +mvn spring-boot:run +``` + +### 2. 配置环境变量(后端) +在系统环境变量或启动脚本中设置大模型API密钥: +```bash +export GEMINI_API_KEY=your_key_here +export OPENAI_API_KEY=your_key_here +# ... 其他密钥 +``` + +### 3. 启动前端 +```bash +cd springboot_demo/frontend +npm install +npm run dev +``` + +### 4. 访问系统 +- 前端:http://localhost:3000 +- 后端:http://localhost:8080 + +## API接口 + +### 认证接口 +- `POST /auth/login` - 用户登录 + +### 查询接口 +- `POST /query/execute` - 执行自然语言查询 + +### 对话接口 +- `GET /dialog/list` - 获取用户对话列表 +- `GET /dialog/{dialogId}` - 获取对话详情 + +### 用户接口 +- `GET /user/list` - 获取用户列表 +- `GET /user/{id}` - 获取用户详情 + +## 注意事项 + +1. **CORS配置** + - 后端已配置CORS,允许前端跨域访问 + - 配置在 `CorsConfig.java` 中 + +2. **JWT认证** + - 登录后token保存在localStorage + - API请求自动携带token + - 后端通过 `JwtInterceptor` 验证token + +3. **大模型调用** + - 所有大模型调用现在在后端进行 + - API密钥安全存储在服务器端 + - 前端不再直接访问大模型API + +4. **错误处理** + - 前端会显示后端返回的错误信息 + - 网络错误会自动提示 + +## 后续优化建议 + +1. 实现真正的SQL执行功能(连接数据库执行SQL) +2. 添加SQL安全检查和验证 +3. 实现查询结果缓存 +4. 添加更多错误处理和日志记录 +5. 优化大模型调用性能 + + + diff --git a/src/springboot_demo/TESTING_GUIDE.md b/src/springboot_demo/TESTING_GUIDE.md new file mode 100644 index 00000000..f22d1354 --- /dev/null +++ b/src/springboot_demo/TESTING_GUIDE.md @@ -0,0 +1,323 @@ +# 测试验证指南 + +## 前置准备 + +### 1. 环境要求 +- Java 21+ +- Maven 3.6+ +- Node.js 18+ +- Docker & Docker Compose(用于数据库) + +### 2. 启动数据库服务 + +```bash +# 在项目根目录执行 +docker-compose up -d + +# 验证服务启动 +docker-compose ps +``` + +应该看到以下服务运行: +- MySQL (端口 3306) +- MongoDB (端口 27017) +- Redis (端口 6379) +- Mongo Express (端口 8081) - 可选 +- Adminer (端口 8082) - 可选 + +### 3. 配置环境变量 + +#### 后端环境变量(系统环境变量或启动脚本) +```bash +export GEMINI_API_KEY=your_gemini_api_key +export OPENAI_API_KEY=your_openai_api_key +export GLM_API_KEY=your_glm_api_key +export QWEN_API_KEY=your_qwen_api_key +export KIMI_API_KEY=your_kimi_api_key +``` + +#### 前端环境变量(可选) +创建 `frontend/.env` 文件: +```env +VITE_API_BASE_URL=http://localhost:8080 +``` + +## 启动服务 + +### 1. 启动后端 + +```bash +cd springboot_demo +mvn clean install +mvn spring-boot:run +``` + +验证后端启动: +- 访问 http://localhost:8080/test/hello 应返回 "Hello, Spring Boot!" +- 访问 http://localhost:8080/test/all 应返回数据库连接测试结果 + +### 2. 启动前端 + +```bash +cd springboot_demo/frontend +npm install +npm run dev +``` + +前端应运行在 http://localhost:3000 + +## 功能测试 + +### 1. 数据库连接测试 + +**测试接口**: `GET http://localhost:8080/test/all` + +**预期结果**: +```json +{ + "mysql": { + "status": "success", + "version": "8.4.x", + "database": "natural_language_query_system", + "tableCount": 17 + }, + "mongodb": { + "status": "success", + "database": "natural_language_query_system", + "collectionCount": 7 + }, + "redis": { + "status": "success", + "message": "Redis 连接正常" + } +} +``` + +### 2. 用户登录测试 + +**步骤**: +1. 访问 http://localhost:3000 +2. 输入用户名和密码(需要先在数据库中创建用户) +3. 选择角色(系统管理员/数据管理员/普通用户) +4. 点击"安全登录" + +**验证点**: +- ✅ 登录成功后跳转到主页面 +- ✅ localStorage中保存了token和用户信息 +- ✅ 根据角色显示对应的侧边栏 + +**API测试**: +```http +POST http://localhost:8080/auth/login +Content-Type: application/json + +{ + "username": "admin", + "password": "123456" +} +``` + +**预期响应**: +```json +{ + "code": 200, + "data": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "userId": 1, + "username": "admin", + "email": "admin@example.com", + "roleId": 1, + "roleName": "系统管理员", + "avatarUrl": "/default-avatar.png" + } +} +``` + +### 3. 自然语言查询测试 + +**步骤**: +1. 登录系统 +2. 进入"数据查询"页面 +3. 选择模型(如:gemini-2.5-pro) +4. 选择数据库(如:销售数据库) +5. 输入查询:"展示2023年各季度的订单量" +6. 点击"发送" + +**验证点**: +- ✅ 显示加载状态 +- ✅ 返回SQL查询语句 +- ✅ 显示表格数据 +- ✅ 显示图表数据(如果有) +- ✅ 对话记录保存到MongoDB + +**API测试**: +```http +POST http://localhost:8080/query/execute +Content-Type: application/json +Authorization: Bearer {your_token} +userId: 1 + +{ + "userPrompt": "展示2023年各季度的订单量", + "model": "gemini-2.5-pro", + "database": "销售数据库" +} +``` + +**预期响应**: +```json +{ + "code": 200, + "data": { + "id": "query_xxxxx", + "userPrompt": "展示2023年各季度的订单量", + "sqlQuery": "SELECT ...", + "conversationId": "conv_xxxxx", + "queryTime": "2024-01-01T10:00:00", + "executionTime": "2.5秒", + "database": "销售数据库", + "model": "gemini-2.5-pro", + "tableData": { + "headers": ["季度", "订单量"], + "rows": [["2023-Q1", "1200"], ...] + }, + "chartData": { + "type": "bar", + "labels": ["2023-Q1", "2023-Q2", ...], + "datasets": [...] + } + } +} +``` + +### 4. 对话历史测试 + +**步骤**: +1. 执行多次查询 +2. 点击左侧历史记录图标 +3. 查看对话列表 + +**验证点**: +- ✅ 显示所有对话记录 +- ✅ 可以切换对话 +- ✅ 可以创建新对话 +- ✅ 可以删除对话 + +**API测试**: +```http +GET http://localhost:8080/dialog/list +Authorization: Bearer {your_token} +userId: 1 +``` + +### 5. 用户管理测试(系统管理员) + +**步骤**: +1. 以系统管理员身份登录 +2. 进入"用户管理"页面 +3. 测试增删改查功能 + +**API测试**: +```http +# 获取用户列表 +GET http://localhost:8080/user/list +Authorization: Bearer {your_token} + +# 创建用户 +POST http://localhost:8080/user +Content-Type: application/json +Authorization: Bearer {your_token} + +{ + "username": "testuser", + "password": "123456", + "email": "test@example.com", + "phonenumber": "13800138000", + "roleId": 3, + "status": 1 +} +``` + +### 6. 数据源管理测试(数据管理员) + +**步骤**: +1. 以数据管理员身份登录 +2. 进入"数据源管理"页面 +3. 添加数据库连接 +4. 测试连接 + +**API测试**: +```http +# 获取数据源列表 +GET http://localhost:8080/db-connection/list +Authorization: Bearer {your_token} + +# 测试连接 +GET http://localhost:8080/db-connection/test/1 +Authorization: Bearer {your_token} +``` + +## 常见问题排查 + +### 1. 后端启动失败 + +**检查项**: +- ✅ Java版本是否为21+ +- ✅ MySQL、MongoDB、Redis是否启动 +- ✅ 数据库连接配置是否正确(application.yml) +- ✅ 端口8080是否被占用 + +### 2. 前端无法连接后端 + +**检查项**: +- ✅ 后端是否正常运行 +- ✅ CORS配置是否正确 +- ✅ API_BASE_URL配置是否正确 +- ✅ 浏览器控制台是否有错误 + +### 3. 大模型调用失败 + +**检查项**: +- ✅ 环境变量是否配置 +- ✅ API密钥是否有效 +- ✅ 网络是否可访问模型API +- ✅ 模型名称是否正确 + +### 4. 登录失败 + +**检查项**: +- ✅ 数据库中是否有用户数据 +- ✅ 密码是否正确(BCrypt加密) +- ✅ JWT配置是否正确 + +## 测试检查清单 + +- [ ] 数据库服务启动正常 +- [ ] 后端服务启动正常(8080端口) +- [ ] 前端服务启动正常(3000端口) +- [ ] 用户登录功能正常 +- [ ] JWT token生成和验证正常 +- [ ] 自然语言查询功能正常 +- [ ] 大模型API调用正常 +- [ ] 对话历史保存和读取正常 +- [ ] 用户管理功能正常(系统管理员) +- [ ] 数据源管理功能正常(数据管理员) +- [ ] 权限控制正常 +- [ ] 错误处理正常 + +## 性能测试建议 + +1. **并发查询测试**: 使用工具(如JMeter)模拟多用户同时查询 +2. **大模型响应时间**: 监控不同模型的响应时间 +3. **数据库连接池**: 检查连接池使用情况 +4. **缓存效果**: 测试Redis缓存是否生效 + +## 安全测试建议 + +1. **JWT验证**: 测试无效token是否被拒绝 +2. **SQL注入**: 测试SQL注入防护 +3. **XSS攻击**: 测试前端XSS防护 +4. **CORS配置**: 测试跨域请求是否被正确控制 + + + diff --git a/src/springboot_demo/api-test.http b/src/springboot_demo/api-test.http new file mode 100644 index 00000000..80801dfb --- /dev/null +++ b/src/springboot_demo/api-test.http @@ -0,0 +1,636 @@ +### 测试 Hello +GET http://localhost:8080/test/hello + +### 测试所有数据库 +GET http://localhost:8080/test/all + +### ==================== 认证接口 ==================== + +### 登录 +POST http://localhost:8080/auth/login +Content-Type: application/json + +{ + "username": "admin", + "password": "123456" +} + +### ==================== 查询执行接口 ==================== + +### 执行查询(新对话) +POST http://localhost:8080/query/execute +Content-Type: application/json +userId: 1 + +{ + "userPrompt": "查询所有用户信息", + "model": "gemini-2.5-pro", + "database": "销售数据库" +} + +### 执行查询(继续对话) +POST http://localhost:8080/query/execute +Content-Type: application/json +userId: 1 + +{ + "userPrompt": "按订单量排序", + "model": "gemini-2.5-pro", + "database": "销售数据库", + "conversationId": "conv_12345678" +} + +### ==================== 对话历史接口 ==================== + +### 获取用户对话列表 +GET http://localhost:8080/dialog/list +userId: 1 + +### 获取对话详情 +GET http://localhost:8080/dialog/conv_12345678 + +### ==================== 用户管理接口 ==================== + +### 查询所有用户 +GET http://localhost:8080/user/list + +### 添加用户 +POST http://localhost:8080/user +Content-Type: application/json + +{ + "username": "admin", + "password": "123456", + "email": "admin@example.com", + "phonenumber": "13800138000", + "roleId": 1, + "status": 1 +} + +### 分页查询 +GET http://localhost:8080/user/page?current=1&size=10 + +### 根据ID查询 +GET http://localhost:8080/user/3 + +### 更新用户 +PUT http://localhost:8080/user +Content-Type: application/json + +{ + "id": 3, + "username": "admin", + "email": "newemail@example.com", + "status": 1 +} + +### 删除用户 +DELETE http://localhost:8080/user/3 + +### 根据用户名查询 +GET http://localhost:8080/user/username/admin + +### ==================== 数据库连接管理接口 ==================== + +### 查询所有数据库连接 +GET http://localhost:8080/db-connection/list + +### 根据创建者查询数据库连接 +GET http://localhost:8080/db-connection/list/4 + +### 根据ID查询数据库连接 +GET http://localhost:8080/db-connection/1 + +### 添加数据库连接 +POST http://localhost:8080/db-connection +Content-Type: application/json + +{ + "name": "测试MySQL连接", + "dbTypeId": 1, + "url": "127.0.0.1:3306/test_db", + "username": "root", + "password": "password", + "status": "disconnected", + "createUserId": 4 +} + +### 更新数据库连接 +PUT http://localhost:8080/db-connection +Content-Type: application/json + +{ + "id": 1, + "name": "更新后的连接名称", + "status": "connected" +} + +### 测试数据库连接 +GET http://localhost:8080/db-connection/test/1 + +### 删除数据库连接 +DELETE http://localhost:8080/db-connection/1 + +### ==================== 大模型配置接口 ==================== + +### 查询所有大模型配置 +GET http://localhost:8080/llm-config/list + +### 查询可用的大模型配置 +GET http://localhost:8080/llm-config/list/available + +### 根据ID查询大模型配置 +GET http://localhost:8080/llm-config/1 + +### 添加大模型配置 +POST http://localhost:8080/llm-config +Content-Type: application/json + +{ + "name": "智谱AI", + "version": "4.0", + "apiKey": "your-api-key-here", + "apiUrl": "https://api.zhipuai.com/v4/chat/completions", + "statusId": 1, + "isDisabled": 0, + "timeout": 5000, + "createUserId": 4 +} + +### 更新大模型配置 +PUT http://localhost:8080/llm-config +Content-Type: application/json + +{ + "id": 1, + "name": "智谱AI", + "version": "4.5", + "timeout": 8000 +} + +### 禁用/启用大模型配置 +PUT http://localhost:8080/llm-config/1/toggle + +### 删除大模型配置 +DELETE http://localhost:8080/llm-config/1 + +### ==================== 数据表元数据接口 ==================== + +### 查询所有表元数据 +GET http://localhost:8080/table-metadata/list + +### 根据数据库连接ID查询表元数据 +GET http://localhost:8080/table-metadata/list/1 + +### 根据ID查询表元数据 +GET http://localhost:8080/table-metadata/1 + +### 添加表元数据 +POST http://localhost:8080/table-metadata +Content-Type: application/json + +{ + "dbConnectionId": 1, + "tableName": "orders", + "description": "订单表,存储所有订单信息" +} + +### 更新表元数据 +PUT http://localhost:8080/table-metadata +Content-Type: application/json + +{ + "id": 1, + "tableName": "orders", + "description": "订单表(已更新)" +} + +### 删除表元数据 +DELETE http://localhost:8080/table-metadata/1 + +### ==================== 字段元数据接口 ==================== + +### 查询所有字段元数据 +GET http://localhost:8080/column-metadata/list + +### 根据表ID查询字段元数据 +GET http://localhost:8080/column-metadata/list/1 + +### 根据ID查询字段元数据 +GET http://localhost:8080/column-metadata/1 + +### 添加字段元数据 +POST http://localhost:8080/column-metadata +Content-Type: application/json + +{ + "tableId": 1, + "columnName": "order_id", + "dataType": "bigint(20)", + "description": "订单唯一标识", + "isPrimary": 1 +} + +### 更新字段元数据 +PUT http://localhost:8080/column-metadata +Content-Type: application/json + +{ + "id": 1, + "columnName": "order_id", + "description": "订单ID(主键)" +} + +### 删除字段元数据 +DELETE http://localhost:8080/column-metadata/1 + +### ==================== 查询日志接口 ==================== + +### 查询所有查询日志 +GET http://localhost:8080/query-log/list + +### 根据用户ID查询查询日志 +GET http://localhost:8080/query-log/list/user/4 + +### 根据对话ID查询查询日志 +GET http://localhost:8080/query-log/list/dialog/conv_12345678 + +### 根据ID查询查询日志 +GET http://localhost:8080/query-log/1 + +### 添加查询日志 +POST http://localhost:8080/query-log +Content-Type: application/json + +{ + "dialogId": "conv_12345678", + "dataSourceId": 1, + "userId": 4, + "executeResult": 1 +} + +### 删除查询日志 +DELETE http://localhost:8080/query-log/1 + +### ==================== 数据库类型字典接口 ==================== + +### 查询所有数据库类型 +GET http://localhost:8080/db-type/list + +### 根据ID查询数据库类型 +GET http://localhost:8080/db-type/1 + +### 根据类型编码查询 +GET http://localhost:8080/db-type/code/mysql + +### 添加数据库类型 +POST http://localhost:8080/db-type +Content-Type: application/json + +{ + "typeName": "MySQL", + "typeCode": "mysql", + "description": "关系型数据库" +} + +### 更新数据库类型 +PUT http://localhost:8080/db-type +Content-Type: application/json + +{ + "id": 1, + "typeName": "MySQL", + "description": "关系型数据库(已更新)" +} + +### 删除数据库类型 +DELETE http://localhost:8080/db-type/1 + +### ==================== 大模型状态字典接口 ==================== + +### 查询所有大模型状态 +GET http://localhost:8080/llm-status/list + +### 根据ID查询大模型状态 +GET http://localhost:8080/llm-status/1 + +### 根据状态编码查询 +GET http://localhost:8080/llm-status/code/available + +### 添加大模型状态 +POST http://localhost:8080/llm-status +Content-Type: application/json + +{ + "statusName": "可用", + "statusCode": "available", + "description": "API成功率≥95%" +} + +### 更新大模型状态 +PUT http://localhost:8080/llm-status +Content-Type: application/json + +{ + "id": 1, + "statusName": "可用", + "description": "API成功率≥98%" +} + +### 删除大模型状态 +DELETE http://localhost:8080/llm-status/1 + +### ==================== 用户数据权限接口 ==================== + +### 查询所有权限 +GET http://localhost:8080/user-db-permission/list + +### 查询已分配权限的用户 +GET http://localhost:8080/user-db-permission/list/assigned + +### 查询未分配权限的用户 +GET http://localhost:8080/user-db-permission/list/unassigned + +### 根据ID查询权限 +GET http://localhost:8080/user-db-permission/1 + +### 根据用户ID查询权限 +GET http://localhost:8080/user-db-permission/user/4 + +### 添加用户权限 +POST http://localhost:8080/user-db-permission +Content-Type: application/json + +{ + "userId": 4, + "permissionDetails": "[{\"db_connection_id\":1,\"table_ids\":[1,2,3]}]", + "lastGrantUserId": 4, + "isAssigned": 1 +} + +### 更新用户权限 +PUT http://localhost:8080/user-db-permission +Content-Type: application/json + +{ + "id": 1, + "userId": 4, + "permissionDetails": "[{\"db_connection_id\":1,\"table_ids\":[1,2,3,4,5]}]", + "lastGrantUserId": 4 +} + +### 删除用户权限 +DELETE http://localhost:8080/user-db-permission/1 + +### ==================== 系统操作日志接口 ==================== + +### 查询所有操作日志 +GET http://localhost:8080/operation-log/list + +### 根据用户ID查询操作日志 +GET http://localhost:8080/operation-log/list/user/4 + +### 根据模块查询操作日志 +GET http://localhost:8080/operation-log/list/module/数据源管理 + +### 查询失败的操作日志 +GET http://localhost:8080/operation-log/list/failed + +### 根据ID查询操作日志 +GET http://localhost:8080/operation-log/1 + +### 添加操作日志 +POST http://localhost:8080/operation-log +Content-Type: application/json + +{ + "userId": 4, + "username": "admin", + "operation": "创建数据库连接", + "module": "数据源管理", + "relatedLlm": null, + "ipAddress": "127.0.0.1", + "result": 1, + "errorMsg": null +} + +### 删除操作日志 +DELETE http://localhost:8080/operation-log/1 + +### ==================== 错误日志接口 ==================== + +### 查询所有错误日志 +GET http://localhost:8080/error-log/list + +### 根据错误类型查询 +GET http://localhost:8080/error-log/list/type/1 + +### 根据统计周期查询 +GET http://localhost:8080/error-log/list/period/today + +### 根据ID查询错误日志 +GET http://localhost:8080/error-log/1 + +### 添加错误日志 +POST http://localhost:8080/error-log +Content-Type: application/json + +{ + "errorTypeId": 1, + "errorCount": 5, + "errorRate": 2.5, + "period": "today" +} + +### 更新错误日志 +PUT http://localhost:8080/error-log +Content-Type: application/json + +{ + "id": 1, + "errorCount": 10, + "errorRate": 5.0 +} + +### 删除错误日志 +DELETE http://localhost:8080/error-log/1 + +### ==================== 错误类型字典接口 ==================== + +### 查询所有错误类型 +GET http://localhost:8080/error-type/list + +### 根据ID查询错误类型 +GET http://localhost:8080/error-type/1 + +### 根据错误编码查询 +GET http://localhost:8080/error-type/code/llm_timeout + +### 添加错误类型 +POST http://localhost:8080/error-type +Content-Type: application/json + +{ + "errorName": "模型调用超时", + "errorCode": "llm_timeout", + "description": "大模型API响应时间超过设定阈值" +} + +### 更新错误类型 +PUT http://localhost:8080/error-type +Content-Type: application/json + +{ + "id": 1, + "errorName": "模型调用超时", + "description": "大模型API响应时间超过5秒" +} + +### 删除错误类型 +DELETE http://localhost:8080/error-type/1 + +### ==================== 通知目标字典接口 ==================== + +### 查询所有通知目标 +GET http://localhost:8080/notification-target/list + +### 根据ID查询通知目标 +GET http://localhost:8080/notification-target/1 + +### 根据目标编码查询 +GET http://localhost:8080/notification-target/code/all + +### 添加通知目标 +POST http://localhost:8080/notification-target +Content-Type: application/json + +{ + "targetName": "所有用户", + "targetCode": "all", + "description": "发送给系统所有用户" +} + +### 更新通知目标 +PUT http://localhost:8080/notification-target +Content-Type: application/json + +{ + "id": 1, + "targetName": "所有用户", + "description": "发送给系统所有注册用户" +} + +### 删除通知目标 +DELETE http://localhost:8080/notification-target/1 + +### ==================== 优先级字典接口 ==================== + +### 查询所有优先级(按排序) +GET http://localhost:8080/priority/list + +### 根据ID查询优先级 +GET http://localhost:8080/priority/1 + +### 根据优先级编码查询 +GET http://localhost:8080/priority/code/urgent + +### 添加优先级 +POST http://localhost:8080/priority +Content-Type: application/json + +{ + "priorityName": "紧急", + "priorityCode": "urgent", + "sort": 1 +} + +### 更新优先级 +PUT http://localhost:8080/priority +Content-Type: application/json + +{ + "id": 1, + "priorityName": "紧急", + "sort": 0 +} + +### 删除优先级 +DELETE http://localhost:8080/priority/1 + +### ==================== 通知管理接口 ==================== + +### 查询所有通知 +GET http://localhost:8080/notification/list + +### 查询已发布的通知 +GET http://localhost:8080/notification/list/published + +### 查询草稿 +GET http://localhost:8080/notification/list/drafts + +### 根据目标ID查询通知 +GET http://localhost:8080/notification/list/target/1 + +### 根据ID查询通知 +GET http://localhost:8080/notification/1 + +### 添加通知(草稿) +POST http://localhost:8080/notification +Content-Type: application/json + +{ + "title": "系统维护通知", + "content": "系统将于今晚22:00-24:00进行维护,请提前保存数据", + "targetId": 1, + "priorityId": 1, + "publisherId": 4, + "isTop": 0 +} + +### 更新通知 +PUT http://localhost:8080/notification +Content-Type: application/json + +{ + "id": 1, + "title": "系统维护通知(已更新)", + "content": "系统将于今晚22:00-次日00:30进行维护,请提前保存数据" +} + +### 发布通知 +PUT http://localhost:8080/notification/1/publish + +### 置顶/取消置顶 +PUT http://localhost:8080/notification/1/toggle-top + +### 删除通知 +DELETE http://localhost:8080/notification/1 + +### ==================== 系统健康监控接口 ==================== + +### 查询所有健康记录 +GET http://localhost:8080/system-health/list + +### 查询最新的健康记录 +GET http://localhost:8080/system-health/latest + +### 查询最近10条健康记录 +GET http://localhost:8080/system-health/recent/10 + +### 根据ID查询健康记录 +GET http://localhost:8080/system-health/1 + +### 添加健康记录 +POST http://localhost:8080/system-health +Content-Type: application/json + +{ + "dbDelay": 10, + "cacheDelay": 2, + "llmDelay": 150, + "storageUsage": 75.5 +} + +### 删除健康记录 +DELETE http://localhost:8080/system-health/1 + diff --git a/src/springboot_demo/docker-compose.yml b/src/springboot_demo/docker-compose.yml new file mode 100644 index 00000000..d4a846fb --- /dev/null +++ b/src/springboot_demo/docker-compose.yml @@ -0,0 +1,123 @@ +services: + # MySQL 8.4 服务 + mysql: + image: mysql:8.4 + container_name: nlq_mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: root123456 + MYSQL_DATABASE: natural_language_query_system + MYSQL_USER: nlq_user + MYSQL_PASSWORD: nlq_pass123 + TZ: Asia/Shanghai + ports: + - "3306:3306" + volumes: + # 挂载初始化脚本(MySQL会自动执行/docker-entrypoint-initdb.d/目录下的.sql文件) + - ./mysql_schema_from_last.sql:/docker-entrypoint-initdb.d/init.sql + # 数据持久化 + - mysql_data:/var/lib/mysql + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --mysql-native-password=ON + networks: + - nlq_network + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123456"] + interval: 10s + timeout: 5s + retries: 5 + + # MongoDB 8.2 服务 + mongodb: + image: mongo:8.2 + container_name: nlq_mongodb + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: admin + MONGO_INITDB_ROOT_PASSWORD: admin123456 + MONGO_INITDB_DATABASE: natural_language_query_system + TZ: Asia/Shanghai + ports: + - "27017:27017" + volumes: + # 挂载初始化脚本 + - ./mongodb_schema_from_last.js:/docker-entrypoint-initdb.d/init.js + # 数据持久化 + - mongodb_data:/data/db + - mongodb_config:/data/configdb + networks: + - nlq_network + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + + # 可选:Mongo Express (MongoDB 的 Web 管理界面) + mongo-express: + image: mongo-express:latest + container_name: nlq_mongo_express + restart: always + ports: + - "8081:8081" + environment: + ME_CONFIG_MONGODB_ADMINUSERNAME: admin + ME_CONFIG_MONGODB_ADMINPASSWORD: admin123456 + ME_CONFIG_MONGODB_URL: mongodb://admin:admin123456@mongodb:27017/ + ME_CONFIG_BASICAUTH_USERNAME: admin + ME_CONFIG_BASICAUTH_PASSWORD: admin + depends_on: + - mongodb + networks: + - nlq_network + + # 可选:Adminer (MySQL 的 Web 管理界面) + adminer: + image: adminer:latest + container_name: nlq_adminer + restart: always + ports: + - "8082:8080" + environment: + ADMINER_DEFAULT_SERVER: mysql + depends_on: + - mysql + networks: + - nlq_network + + # Redis 缓存服务 + redis: + image: redis:7-alpine + container_name: nlq_redis + restart: always + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + networks: + - nlq_network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + +# 数据卷(数据持久化) +volumes: + mysql_data: + driver: local + mongodb_data: + driver: local + mongodb_config: + driver: local + redis_data: + driver: local + +# 网络配置 +networks: + nlq_network: + driver: bridge + diff --git a/src/springboot_demo/frontend/.gitignore b/src/springboot_demo/frontend/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/src/springboot_demo/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/springboot_demo/frontend/App.tsx b/src/springboot_demo/frontend/App.tsx new file mode 100644 index 00000000..2310b52f --- /dev/null +++ b/src/springboot_demo/frontend/App.tsx @@ -0,0 +1,732 @@ +import React, { useState, useEffect } from 'react'; +// 页面组件导入 +import { LoginPage } from './components/LoginPage'; +import { Sidebar } from './components/Sidebar'; +import { QueryPage } from './components/QueryPage'; +import { HistoryPage } from './components/HistoryPage'; +import { NotificationsPage } from './components/NotificationsPage'; +import { AccountPage } from './components/AccountPage'; +import { FriendsPage } from './components/FriendsPage'; +import { SysAdminSidebar } from './components/SysAdminSidebar'; +import { DataAdminSidebar } from './components/DataAdminSidebar'; +import { SysAdminPage } from './components/SysAdminPage'; +import { DataAdminPage } from './components/DataAdminPage'; +import { TopHeader } from './components/TopHeader'; +import { ComparisonModal } from './components/ComparisonModal'; +import { Modal } from './components/Modal'; + +// 模拟数据导入 +import { + MOCK_INITIAL_CONVERSATION, + MOCK_SAVED_QUERIES, + MOCK_USER_PROFILE, + MOCK_NOTIFICATIONS, + MOCK_QUERY_SHARES, + MOCK_FRIENDS_LIST +} from './constants'; + +// 类型定义导入 +import { + Conversation, + MessageRole, + Page, + QueryResultData, + UserRole, + SysAdminPageType, + DataAdminPageType, + QueryShare, + Notification +} from './types'; + +/** + * 各角色侧边栏配置项 + * 按角色划分功能菜单,与对应Sidebar组件菜单结构保持一致 + */ +// 普通用户侧边栏项目 - 基础数据查询与个人中心功能 +const normalUserSidebarItems = [ + { href: 'query', label: '数据查询' }, + { href: 'history', label: '收藏夹' }, + { href: 'notifications', label: '通知中心' }, + { href: 'friends', label: '好友管理' }, + { href: 'account', label: '账户管理' }, +] as const; + +// 数据管理员侧边栏项目 - 包含数据管理与基础功能 +const dataAdminSidebarItems = [ + { href: 'query', label: '数据查询' }, + { href: 'history', label: '收藏夹' }, + { href: 'datasource', label: '数据源管理' }, + { href: 'dashboard', label: '数据源概览' }, + { href: 'user-permission', label: '用户权限管理' }, + { href: 'notification-management', label: '通知管理(数据员)' }, + { href: 'connection-log', label: '数据源连接日志' }, + { href: 'notifications', label: '通知中心' }, + { href: 'account', label: '我的账户' }, + { href: 'friends', label: '好友管理' }, +] as const; + +// 系统管理员侧边栏项目 - 系统配置与管理功能 +const sysAdminSidebarItems = [ + { href: 'dashboard', label: '系统概览' }, + { href: 'user-management', label: '用户管理' }, + { href: 'account', label: '我的账户' }, + { href: 'system-log', label: '系统日志' }, + { href: 'llm-config', label: '大模型配置' }, + { href: 'notification-management', label: '通知管理' }, +] as const; + +/** + * 应用根组件 - 全局状态管理与角色路由控制 + * 负责用户身份验证、页面切换、数据状态维护 + */ +const App: React.FC = () => { + // ===== 全局核心状态 ===== + // 当前用户角色(null表示未登录) + const [userRole, setUserRole] = useState(null); + // 所有对话列表 + const [conversations, setConversations] = useState([MOCK_INITIAL_CONVERSATION]); + // 当前激活的对话ID + const [currentConversationId, setCurrentConversationId] = useState(MOCK_INITIAL_CONVERSATION.id); + // 历史对话面板显示状态(仅query页面生效) + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + // 普通用户当前激活页面 + const [activePage, setActivePage] = useState('query'); + // 已保存的查询结果列表 + const [savedQueries, setSavedQueries] = useState(MOCK_SAVED_QUERIES); + // 新对话初始提示语(用于重新运行查询场景) + const [initialPrompt, setInitialPrompt] = useState(undefined); + // 控制弹窗显示/隐藏 + const [isCompareModalOpen, setIsCompareModalOpen] = useState(false); + // 存储要对比的查询 + const [compareQueries, setCompareQueries] = useState<{ oldQuery: QueryResultData; newQuery: QueryResultData } | null>(null); + // 待对比的查询ID组合(旧查询ID, 新查询ID) + const [comparisonQueryIds, setComparisonQueryIds] = useState<[string, string] | null>(null); + // 对话不存在提示弹窗显示状态 + const [notFoundModalOpen, setNotFoundModalOpen] = useState(false); + // 查询分享记录列表 + const [queryShares, setQueryShares] = useState(MOCK_QUERY_SHARES); + // 通知消息列表 + const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS); + + // ===== 管理员专属状态 ===== + // 系统管理员当前激活页面 + const [activeSysAdminPage, setActiveSysAdminPage] = useState('dashboard'); + // 数据管理员当前激活页面 + const [activeDataAdminPage, setActiveDataAdminPage] = useState('dashboard'); + + // 当前激活的对话详情(通过currentConversationId从列表中匹配) + const currentConversation = conversations.find(c => c.id === currentConversationId); + + // ===== 辅助函数 ===== + /** + * 获取侧边栏当前页面名称 + * 根据用户角色和激活页面,匹配对应的菜单名称 + * @returns 当前页面的显示名称(空字符串表示无需显示) + */ + const getSidebarPageName = () => { + if (userRole === 'normal-user') { + // 普通用户query页面不显示侧边栏名称 + if (activePage === 'query') return ''; + return normalUserSidebarItems.find(item => item.href === activePage)?.label || ''; + } + + if (userRole === 'data-admin') { + // 数据管理员query页面不显示侧边栏名称 + if (activeDataAdminPage === 'query') return ''; + return dataAdminSidebarItems.find(item => item.href === activeDataAdminPage)?.label || ''; + } + + if (userRole === 'sys-admin') { + return sysAdminSidebarItems.find(item => item.href === activeSysAdminPage)?.label || ''; + } + + return ''; + }; + + /** + * 获取顶部导航栏标题 + * 规则:query页面显示对话标题,其他页面显示侧边栏菜单名称 + * @returns 顶部导航栏显示标题 + */ + const getHeaderTitle = () => { + // 判定是否为query页面(跨所有角色) + const isQueryPage = ( + (userRole === 'normal-user' && activePage === 'query') || + (userRole === 'data-admin' && activeDataAdminPage === 'query') || + (userRole === 'sys-admin' && activeSysAdminPage === 'query') + ); + + // query页面优先显示对话标题,无则显示默认文本 + if (isQueryPage) { + return currentConversation?.title || '新对话'; + } + + // 非query页面显示侧边栏菜单名称 + return getSidebarPageName(); + }; + + // 顶部导航栏最终显示标题 + const headerTitle = getHeaderTitle(); + + // ===== 事件处理函数 ===== + /** + * 登录处理函数 + * 优先使用传入的角色,无则从sessionStorage读取保存的角色 + * @param role - 登录选择的用户角色 + */ + const handleLogin = (role: UserRole) => { + const savedRole = sessionStorage.getItem('userRole') as UserRole; + const roleToSet = role || savedRole; + if (roleToSet) { + setUserRole(roleToSet); + } + }; + + /** + * 组件挂载时初始化登录状态 + * 从sessionStorage读取保存的角色,自动登录 + */ + useEffect(() => { + handleLogin(userRole); + }, []); + + /** + * 退出登录处理函数 + * 清除sessionStorage中的角色信息,重置所有状态到初始值 + */ + const handleLogout = () => { + sessionStorage.removeItem('userRole'); + setUserRole(null); + setActivePage('query'); + setActiveSysAdminPage('dashboard'); + setActiveDataAdminPage('dashboard'); + }; + + /** + * 添加对话消息 + * 向当前激活的对话中追加新消息,首次用户消息自动生成对话标题 + * @param role - 消息发送者角色(user/ai) + * @param content - 消息内容(文本或查询结果数据) + */ + const handleAddMessage = (role: MessageRole, content: string | QueryResultData) => { + // 优先使用传入的目标对话ID,无则使用当前激活对话ID + const convId = currentConversationId; + if (!convId) return; + + setConversations(prev => prev.map(conv => { + // 只更新当前激活的对话 + if (conv.id === currentConversationId) { + const newMessages = [...conv.messages, { role, content }]; + let newTitle = conv.title; + + // 首次用户消息(对话初始状态),截取前20字作为对话标题 + if (conv.messages.length === 1 && role === 'user' && typeof content === 'string') { + newTitle = content.substring(0, 20); + } + + return { ...conv, title: newTitle, messages: newMessages }; + } + return conv; + })); + }; + + /** + * 创建新对话 + * 优先复用现有空对话,无则创建全新对话,自动切换到query页面 + */ + const handleNewConversation = () => { + // 判断对话是否为空(仅包含AI初始问候语) + const isEmptyConv = (conv: Conversation) => { + return conv.messages.length === 1 && + conv.messages[0].role === 'ai' && + typeof conv.messages[0].content === 'string' && + conv.messages[0].content.includes('您好!我是数据查询助手'); + }; + + // 查找现有空对话 + const existingEmptyConv = conversations.find(isEmptyConv); + + if (existingEmptyConv) { + // 复用空对话 + setCurrentConversationId(existingEmptyConv.id); + } else { + // 创建新对话 + const newConv: Conversation = { + id: 'conv-' + Date.now(), // 时间戳生成唯一ID + title: '新对话', + messages: [{ + role: 'ai', + content: '您好!我是数据查询助手,您可以通过自然语言描述您的查询需求(例如:"展示2023年各季度的订单量"),我会为您生成相应的结果。' + }], + createTime: new Date().toISOString(), + }; + // 添加到对话列表头部 + setConversations(prev => [newConv, ...prev]); + setCurrentConversationId(newConv.id); + } + + // 关闭历史对话面板 + setIsHistoryOpen(false); + + // 切换到query页面(区分角色) + if (userRole === 'data-admin') { + setActiveDataAdminPage('query'); + } else { + setActivePage('query'); + } + }; + + /** + * 切换对话 + * 从历史对话列表中选择已有对话,自动切换到query页面 + * @param id - 目标对话ID + */ + const handleSwitchConversation = (id: string) => { + setCurrentConversationId(id); + setIsHistoryOpen(true); + setActivePage('query'); + }; + + /** + * 删除对话 + * 从对话列表中移除目标对话,若删除的是当前对话则自动切换到首个对话(无则创建新对话) + * @param deleteId - 待删除对话ID + */ + const handleDeleteConversation = (deleteId: string) => { + const updatedConversations = conversations.filter(conv => conv.id !== deleteId); + + // 若删除的是当前激活的对话 + if (currentConversationId === deleteId) { + if (updatedConversations.length > 0) { + // 切换到剩余对话的首个 + setCurrentConversationId(updatedConversations[0].id); + } else { + // 无剩余对话时创建新对话 + const newConv: Conversation = { + id: 'conv-' + Date.now(), + title: '新对话', + messages: [{ + role: 'ai', + content: '您好!我是数据查询助手,您可以通过自然语言描述您的查询需求...' + }], + createTime: new Date().toISOString(), + }; + updatedConversations.unshift(newConv); + setCurrentConversationId(newConv.id); + } + } + + setConversations(updatedConversations); + }; + + /** + * 保存查询结果 + * 将查询结果添加到收藏夹,避免重复保存 + * @param query - 待保存的查询结果数据 + */ + const handleSaveQuery = (query: QueryResultData) => { + setSavedQueries(prev => { + if (prev.some(q => q.id === query.id)) return prev; + return [query, ...prev]; + }); + }; + + /** + * 删除收藏的查询结果 + * 从收藏夹中移除目标查询 + * @param id - 待删除查询的ID + */ + const handleDeleteSavedQuery = (id: string) => { + setSavedQueries(prev => prev.filter(q => q.id !== id)); + }; + + /** + * 在对话中查看收藏的查询 + * 匹配对应的对话,存在则切换到该对话,不存在则显示提示弹窗 + * @param conversationId - 收藏查询关联的对话ID + */ + const handleViewInChat = (conversationId: string) => { + const convExists = conversations.some(c => c.id === conversationId); + if (convExists) { + setCurrentConversationId(conversationId); + // 切换到query页面(区分角色) + if (userRole === 'data-admin') { + setActiveDataAdminPage('query'); + } else { + setActivePage('query'); + } + } else { + setNotFoundModalOpen(true); + } + }; + + /** + * 重新运行查询 + * 创建新对话并带入原始查询提示语 + * @param prompt - 原始查询提示语 + */ + const handleRerunQuery = (prompt: string) => { + handleNewConversation(); + setInitialPrompt(prompt); + }; + + + /** + * 对比两个查询结果 + * 按查询时间排序(旧在前,新在后),打开对比弹窗 + * @param queryId1 - 第一个查询ID + * @param queryId2 - 第二个查询ID + */ + const handleCompareQueries = (queryId1: string, queryId2: string) => { + const query1 = savedQueries.find(q => q.id === queryId1); + const query2 = savedQueries.find(q => q.id === queryId2); + if (!query1 || !query2) return; + + // 按查询时间排序,确保旧查询在前 + const date1 = new Date(query1.queryTime); + const date2 = new Date(query2.queryTime); + const oldQuery = date1 < date2 ? query1 : query2; + const newQuery = date1 < date2 ? query2 : query1; + + //设置弹窗数据并打开弹窗 + setCompareQueries({ oldQuery, newQuery }); + setIsCompareModalOpen(true); +}; + + /** + * 分享查询结果给好友 + * 创建分享记录并生成对应通知 + * @param queryId - 待分享的查询ID + * @param friendId - 接收分享的好友ID + */ + const handleShareQuery = (queryId: string, friendId: string) => { + const queryToShare = savedQueries.find(q => q.id === queryId); + const friend = MOCK_FRIENDS_LIST.find(f => f.id === friendId); + + if (queryToShare && friend) { + // 创建分享记录 + const newShare: QueryShare = { + id: `share-${Date.now()}`, + sender: { ...MOCK_USER_PROFILE, isOnline: true }, // 发送者为当前用户 + recipientId: friend.id, // 接收者为选中的好友 + querySnapshot: queryToShare, // 分享的查询快照 + timestamp: new Date().toISOString(), + status: 'unread', // 初始状态为未读 + }; + setQueryShares(prev => [newShare, ...prev]); + + // 生成分享通知(模拟) + const newNotification: Notification = { + id: `notif-${Date.now()}`, + type: 'share', + title: `${MOCK_USER_PROFILE.name} 分享了一个查询给你`, + content: `"${queryToShare.userPrompt}"`, + timestamp: new Date().toISOString(), + isRead: false, + isPinned: false, + fromUser: { name: MOCK_USER_PROFILE.name, avatarUrl: MOCK_USER_PROFILE.avatarUrl }, + relatedShareId: newShare.id, + }; + setNotifications(prev => [newNotification, ...prev]); + } + }; + + /** + * 标记分享记录为已读 + * 更新目标分享记录的状态 + * @param shareId - 待标记的分享ID + */ + const handleMarkShareAsRead = (shareId: string) => { + setQueryShares(prev => prev.map(s => s.id === shareId ? { ...s, status: 'read' } : s)); + }; + + /** + * 删除分享记录 + * 从分享列表中移除目标记录 + * @param shareId - 待删除的分享ID + */ + const handleDeleteShare = (shareId: string) => { + setQueryShares(prev => prev.filter(s => s.id !== shareId)); + }; + + /** + * 点击通知图标处理 + * 切换到通知中心页面(区分角色) + */ + const handleNotificationClick = () => { + if (userRole === 'normal-user') { + setActivePage('notifications'); + } else if (userRole === 'data-admin') { + setActiveDataAdminPage('notifications'); + } + }; + + /** + * 点击头像处理 + * 切换到个人中心页面(区分角色) + */ + const handleAvatarClick = () => { + if (userRole === 'normal-user') { + setActivePage('account'); + } else if (userRole === 'data-admin') { + setActiveDataAdminPage('account'); + } else if (userRole === 'sys-admin') { + setActiveSysAdminPage('account'); + } + }; + + // ===== 页面渲染函数 ===== + /** + * 普通用户页面渲染 + * 根据当前激活页面,渲染对应的功能页面 + * @param page - 普通用户当前激活页面 + * @returns 对应的功能页面组件 + */ + const renderNormalUserPage = (page: Page) => { + switch (page) { + case 'query': + return ( + setIsHistoryOpen(!isHistoryOpen)} + isHistoryOpen={isHistoryOpen} + onAddMessage={handleAddMessage} + onSaveQuery={handleSaveQuery} + onShareQuery={handleShareQuery} + savedQueries={savedQueries} + initialPrompt={initialPrompt} + onClearInitialPrompt={() => setInitialPrompt(undefined)} + conversations={conversations} + currentConversationId={currentConversationId || ''} + onSwitchConversation={handleSwitchConversation} + onNewConversation={handleNewConversation} + onDeleteConversation={handleDeleteConversation} + /> + ); + case 'history': + return ( + + ); + case 'notifications': + return ; + case 'account': + return ; + case 'friends': + return ( + + ); + default: + return
Page not found
; + } + }; + + // ===== 角色路由渲染 ===== + // 未登录状态 - 渲染登录页面 + if (!userRole) { + return ; + } + + // 系统管理员 - 渲染系统管理页面 + if (userRole === 'sys-admin') { + return ( +
+ +
+ !n.isRead).length} + notifications={notifications} + onNotificationClick={handleNotificationClick} + onAvatarClick={handleAvatarClick} + currentConversationName={headerTitle} + /> + +
+
+ ); + } + + // 数据管理员 - 渲染数据管理页面 + if (userRole === 'data-admin') { + return ( + <> +
+ +
+ !n.isRead).length} + notifications={notifications} + onNotificationClick={handleNotificationClick} + showHistoryToggle={activeDataAdminPage === 'query'} // 仅query页面显示历史面板切换按钮 + onToggleHistory={() => setIsHistoryOpen(!isHistoryOpen)} + onAvatarClick={handleAvatarClick} + onNewConversation={handleNewConversation} + currentConversationName={headerTitle} + /> + setIsHistoryOpen(!isHistoryOpen)} + isHistoryOpen={isHistoryOpen} + onAddMessage={handleAddMessage} + onSaveQuery={handleSaveQuery} + onShareQuery={handleShareQuery} + savedQueries={savedQueries} + initialPrompt={initialPrompt} + onClearInitialPrompt={() => setInitialPrompt(undefined)} + conversations={conversations} + currentConversationId={currentConversationId || ''} + onSwitchConversation={handleSwitchConversation} + onNewConversation={handleNewConversation} + onDelete={handleDeleteSavedQuery} + onViewInChat={handleViewInChat} + onRerun={handleRerunQuery} + onCompare={handleCompareQueries} + shares={queryShares} + onMarkShareAsRead={handleMarkShareAsRead} + onDeleteShare={handleDeleteShare} + /> +
+
+ + {/* 对话不存在提示弹窗 */} + setNotFoundModalOpen(false)} + title="操作提示" + > +
+
+ +
+

对话记录不存在或已被删除。

+

这可能是由于它已被手动删除或系统自动清理。

+
+ +
+
+
+ {/* 2. 对比弹窗(全局浮层,独立于其他弹窗!) */} + {compareQueries && ( + { + setIsCompareModalOpen(false); + setCompareQueries(null); + }} + oldQuery={compareQueries.oldQuery} + newQuery={compareQueries.newQuery} + /> + )} + + ); + } + + // 普通用户 - 渲染基础功能页面 + return ( + <> +
+ +
+ !n.isRead).length} + notifications={notifications} + onNotificationClick={handleNotificationClick} + showHistoryToggle={activePage === 'query'} // 仅query页面显示历史面板切换按钮 + onToggleHistory={() => setIsHistoryOpen(!isHistoryOpen)} + onAvatarClick={handleAvatarClick} + onNewConversation={handleNewConversation} + currentConversationName={headerTitle} + /> + {renderNormalUserPage(activePage)} +
+
+ + {/* 对话不存在提示弹窗 */} + setNotFoundModalOpen(false)} + title="操作提示" + > +
+
+ +
+

对话记录不存在或已被删除。

+

这可能是由于它已被手动删除或系统自动清理。

+
+ +
+
+
+ {/* 2. 对比弹窗(全局浮层,独立于其他弹窗!) */} + {compareQueries && ( + { + setIsCompareModalOpen(false); + setCompareQueries(null); + }} + oldQuery={compareQueries.oldQuery} + newQuery={compareQueries.newQuery} + /> + )} + + ); +}; + +export default App; \ No newline at end of file diff --git a/src/springboot_demo/frontend/README.md b/src/springboot_demo/frontend/README.md new file mode 100644 index 00000000..94bfbe13 --- /dev/null +++ b/src/springboot_demo/frontend/README.md @@ -0,0 +1,20 @@ +
+GHBanner +
+ +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1l5dsPVkxm5nl4yN-ckKSgRGs0nlklvz9 + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/src/springboot_demo/frontend/components/AccountPage.tsx b/src/springboot_demo/frontend/components/AccountPage.tsx new file mode 100644 index 00000000..f640d37a --- /dev/null +++ b/src/springboot_demo/frontend/components/AccountPage.tsx @@ -0,0 +1,222 @@ +import React, { useState, useRef } from 'react'; +import { MOCK_USER_PROFILE } from '../constants'; +import { UserProfile } from '../types'; +import { Modal } from './Modal'; + +const InfoField: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => ( +
+
{label}
+
{value}
+
+); + +const maskPhoneNumber = (phone: string) => { + if (phone.length === 11) { + return `${phone.substring(0, 3)}****${phone.substring(7)}`; + } + return phone; +}; + +export const AccountPage: React.FC = () => { + const [user, setUser] = useState(MOCK_USER_PROFILE); + const [isEditModalOpen, setEditModalOpen] = useState(false); + const [isPasswordModalOpen, setPasswordModalOpen] = useState(false); + const [isPhoneModalOpen, setPhoneModalOpen] = useState(false); + const [editForm, setEditForm] = useState({ name: '', email: '', phoneNumber: '' }); + const [avatarPreview, setAvatarPreview] = useState(user.avatarUrl); + const fileInputRef = useRef(null); + + const handleOpenEditModal = () => { + setEditForm({ + name: user.name, + email: user.email, + phoneNumber: user.phoneNumber, + }); + setEditModalOpen(true); + }; + + const handleSaveProfile = () => { + setUser(prev => ({ + ...prev, + name: editForm.name, + email: editForm.email, + phoneNumber: editForm.phoneNumber, + avatarUrl: avatarPreview + })); + setEditModalOpen(false); + }; + + const handleAvatarChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setAvatarPreview(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + return ( +
+
+
+
+ {/* Personal Info Card */} +
+
+ {/* Left side: Avatar */} +
+ User Avatar + + +
+ + {/* Right side: Info */} +
+
+

个人基本信息

+
+
+ + + + + + + {user.accountStatus === 'normal' ? '正常' : '禁用'} + + } + /> +
+
+ +
+
+
+
+ + {/* Security Settings Card */} +
+

安全设置

+
+
+
+

修改密码

+

上次修改时间:2025-05-10

+
+ +
+
+
+

绑定手机

+

已绑定:{maskPhoneNumber(user.phoneNumber)}

+
+ +
+
+
+
+ + {/* Edit Profile Modal */} + setEditModalOpen(false)} title="编辑个人信息"> +
+
+ + setEditForm(prev => ({...prev, name: e.target.value}))} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary/30 focus:border-primary" + /> +
+
+ + setEditForm(prev => ({...prev, email: e.target.value}))} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary/30 focus:border-primary" + /> +
+
+ + setEditForm(prev => ({...prev, phoneNumber: e.target.value}))} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary/30 focus:border-primary" + /> +
+
+
+ + +
+
+ + {/* Password Change Modal */} + setPasswordModalOpen(false)} title="修改密码"> +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ + {/* Phone Change Modal */} + setPhoneModalOpen(false)} title="更换绑定手机"> +
+
+ + +
+
+ +
+ + +
+
+
+
+ +
+
+ +
+ ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/ChatMessage.tsx b/src/springboot_demo/frontend/components/ChatMessage.tsx new file mode 100644 index 00000000..e3109870 --- /dev/null +++ b/src/springboot_demo/frontend/components/ChatMessage.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Message, QueryResultData } from '../types'; +import { QueryResult } from './QueryResult'; + +interface ChatMessageProps { + message: Message; + onSaveQuery: (query: QueryResultData) => void; + onShareQuery: (queryId: string, friendId: string) => void; + savedQueries: QueryResultData[]; +} + +export const ChatMessage: React.FC = ({ message, onSaveQuery, onShareQuery, savedQueries }) => { + const { role, content } = message; + + const isUser = role === 'user'; + const wrapperClass = `flex items-start gap-3 ${isUser ? 'flex-row-reverse' : ''}`; + const avatarClass = `w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 text-lg ${isUser ? 'bg-primary text-white' : 'bg-primary/10 text-primary'}`; + const iconClass = `fa ${isUser ? 'fa-user' : 'fa-robot'}`; + const bubbleClass = `p-4 shadow-sm max-w-[85%] md:max-w-[80%] text-sm rounded-lg ${isUser ? 'bg-primary text-white rounded-tr-none' : 'bg-white border border-gray-200 rounded-tl-none'}`; + + return ( +
+
+ +
+
+ {typeof content === 'string' ? ( +

{content}

+ ) : ( + + )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/ChatModal.tsx b/src/springboot_demo/frontend/components/ChatModal.tsx new file mode 100644 index 00000000..5731f9c0 --- /dev/null +++ b/src/springboot_demo/frontend/components/ChatModal.tsx @@ -0,0 +1,384 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Modal } from './Modal'; +import { Friend, QueryResultData } from '../types'; + +// 聊天消息接口定义:描述单条聊天消息的结构规范 +interface Message { + id: string; // 消息唯一标识(时间戳+随机字符串生成) + content: string; // 消息文本内容 + isSent: boolean; // 发送者标识(true:当前用户发送;false:好友发送) + timestamp: Date; // 消息发送时间 + isRead: boolean; // 消息已读状态(true:已读;false:未读) +} + +// 聊天模态框属性接口:定义组件接收的外部参数 +interface ChatModalProps { + isOpen: boolean; // 控制聊天弹窗显示/隐藏 + onClose: () => void; // 弹窗关闭回调函数 + friend: Friend; // 聊天对象(好友)的基础信息 + savedQueries: QueryResultData[]; // 可分享的查询记录列表(来自收藏夹) + currentUnreadCount: number; // 当前好友的未读消息数 + updateUnreadCount: (friendId: string, count: number) => void; // 更新未读消息数的回调 + messages: Message[]; // 聊天记录列表(从父组件传入,双向绑定) + updateMessages: (newMessages: Message[]) => void; // 更新聊天记录的回调(通知父组件同步) +} + +export const ChatModal: React.FC = ({ + isOpen, onClose, friend, savedQueries, + currentUnreadCount, updateUnreadCount, + messages, + updateMessages +}) => { + // 消息输入框内容状态:绑定输入框的值 + const [inputText, setInputText] = useState(''); + // 消息展示容器的DOM引用:核心用于滚动控制(定位到消息底部) + const messagesContainerRef = useRef(null); + // 输入框的DOM引用:用于手动控制输入框聚焦 + const inputRef = useRef(null); + // 分享查询弹窗的显示状态:false=关闭,true=打开 + const [isShareModalOpen, setShareModalOpen] = useState(false); + // 选中待分享的查询记录ID:null=未选中,存储选中查询的唯一标识 + const [selectedQueryId, setSelectedQueryId] = useState(null); + + // 模拟好友回复的随机文本池:避免回复内容重复,提升交互自然度 + const randomReplies = [ + '收到,我看看~', + '好的,我研究一下', + '这个问题我得仔细瞧瞧', + '明白,我马上处理', + '哦,这个我知道怎么弄', + '稍等,我查一下资料', + ]; + + // ===== 终极修复:每次打开弹窗都滚到底(不管关闭多少次)===== + useEffect(() => { + // 只在弹窗打开时执行(isOpen为true) + if (isOpen) { + // 延迟200ms:给Modal动画+DOM重新渲染留足时间(关闭再打开时DOM要重新生成) + const scrollTimer = setTimeout(() => { + // 用requestAnimationFrame确保DOM完全更新后再滚动(解决scrollHeight计算不准确的问题) + requestAnimationFrame(() => { + if (messagesContainerRef.current) { + const container = messagesContainerRef.current; + // 强制滚到底部(和HTML的window.scrollTo逻辑完全一致) + container.scrollTop = container.scrollHeight; + // 双重保险:如果第一次没滚到(距离底部超过10px),再补一次滚动 + if (Math.abs(container.scrollHeight - (container.scrollTop + container.clientHeight)) > 10) { + container.scrollTop = container.scrollHeight; + } + } + }); + }, 200); + + // 弹窗关闭时清除定时器:避免定时器残留导致无效滚动或内存泄漏 + return () => clearTimeout(scrollTimer); + } + }, [isOpen]); // 只依赖isOpen状态:确保每次打开弹窗都触发滚动,不受其他状态影响 + + // ===== 其他初始化逻辑(不变)===== + useEffect(() => { + if (isOpen) { + // 1. 打开弹窗时,清空当前好友的未读消息数(通知父组件更新) + updateUnreadCount(friend.id, 0); + // 2. 标记好友发送的未读消息为"已读"(遍历消息列表,修改未读状态后同步给父组件) + const updatedMessages = messages.map(msg => + !msg.isSent && !msg.isRead ? { ...msg, isRead: true } : msg + ); + updateMessages(updatedMessages); + + // 延迟200ms聚焦输入框:和滚动延迟同步,确保DOM渲染完成后再聚焦 + setTimeout(() => inputRef.current?.focus(), 200); + } else { + // 关闭弹窗时,清空输入框内容(避免下次打开残留上次输入) + setInputText(''); + } + }, [isOpen, friend.id, updateUnreadCount, messages, updateMessages]); + + // ===== 发送消息(不变,保留滚动)===== + const handleSendMessage = () => { + // 1. 获取输入框内容(去首尾空格,避免发送空消息) + const currentValue = inputRef.current?.value.trim() || ''; + if (!currentValue) return; // 内容为空则不执行发送逻辑 + + // 2. 生成当前用户发送的新消息(唯一ID由时间戳+随机字符串组成,确保不重复) + const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const newMessage: Message = { + id: uniqueId, + content: currentValue, + isSent: true, + timestamp: new Date(), + isRead: false, + }; + + // 3. 更新消息列表,拼接新消息后通知父组件同步 + const updatedMessages = [...messages, newMessage]; + updateMessages(updatedMessages); + + // 4. 清空输入框(同时更新状态和DOM,适配中文输入法等特殊场景) + setInputText(''); + if (inputRef.current) inputRef.current.value = ''; + + // 发送后自动滚到底部:确保用户能看到自己发送的最新消息 + requestAnimationFrame(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } + }); + + // 模拟好友1秒后回复消息(延迟1000ms,模拟实时聊天交互) + setTimeout(() => { + // 1. 先标记用户发送的消息为"已读"(模拟好友已查看) + const messagesWithRead = updatedMessages.map(msg => + msg.isSent && !msg.isRead ? { ...msg, isRead: true } : msg + ); + // 2. 生成好友回复消息(随机选择回复文本,生成唯一ID) + const replyId = `${Date.now()}-reply-${Math.random().toString(36).slice(2, 8)}`; + const randomReply = randomReplies[Math.floor(Math.random() * randomReplies.length)]; + const replyMessage: Message = { + id: replyId, + content: randomReply, + isSent: false, + timestamp: new Date(), + isRead: false, + }; + // 3. 一次更新:同时包含"已读标记"和"好友回复",同步给父组件 + const finalMessages = [...messagesWithRead, replyMessage]; + updateMessages(finalMessages); + + // 回复后自动滚到底部:确保用户能看到好友的最新回复 + requestAnimationFrame(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } + }); + }, 1000); + }; + + // ===== 事件:打开"分享查询"弹窗 ===== + const handleOpenShareModal = () => { + setSelectedQueryId(null); // 重置选中状态(避免残留上次选中的查询ID) + setShareModalOpen(true); // 显示分享查询弹窗 + }; + + // ===== 事件:确认分享查询给好友 ===== + const handleConfirmShare = () => { + // 1. 校验:未选择查询记录时,不执行分享逻辑 + if (!selectedQueryId) return; + // 根据选中的ID找到对应的查询记录 + const selectedQuery = savedQueries.find(q => q.id === selectedQueryId); + if (!selectedQuery) return; + + // 2. 生成"分享查询"的消息(作为一条聊天消息发送) + const shareId = `${Date.now()}-share-${Math.random().toString(36).slice(2, 8)}`; + const shareMessage: Message = { + id: shareId, + content: `分享了查询:${selectedQuery.userPrompt}`, + isSent: true, + timestamp: new Date(), + isRead: false, + }; + + // 3. 更新消息列表,添加分享消息后通知父组件同步 + const updatedMessages = [...messages, shareMessage]; + updateMessages(updatedMessages); + + // 4. 关闭分享弹窗,清空输入框 + setInputText(''); + setShareModalOpen(false); + + // 分享后自动滚到底部:确保分享消息显示在最新位置 + requestAnimationFrame(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } + }); + + // 模拟好友1.5秒后回复分享内容(延迟1500ms,适配分享场景的交互节奏) + setTimeout(() => { + // 1. 先标记分享消息为"已读"(模拟好友已查看分享) + const messagesWithRead = updatedMessages.map(msg => + msg.isSent && !msg.isRead ? { ...msg, isRead: true } : msg + ); + // 2. 生成好友回复消息(替换关键词,让回复更贴合分享场景) + const replyShareId = `${Date.now()}-share-reply-${Math.random().toString(36).slice(2, 8)}`; + const randomShareReply = randomReplies[Math.floor(Math.random() * randomReplies.length)].replace('看看', '研究'); + const replyMessage: Message = { + id: replyShareId, + content: randomShareReply || '我看到了,这个查询很有用!', + isSent: false, + timestamp: new Date(), + isRead: false, + }; + // 3. 一次更新:同时包含"已读标记"和"好友回复",同步给父组件 + const finalMessages = [...messagesWithRead, replyMessage]; + updateMessages(finalMessages); + + // 回复后自动滚到底部:确保好友的回复显示在最新位置 + requestAnimationFrame(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } + }); + }, 1500); + }; + + // ===== 事件:回车键发送消息 ===== + const handleKeyDown = (e: React.KeyboardEvent) => { + // 监听回车键,触发发送消息逻辑 + if (e.key === 'Enter') { + e.preventDefault(); // 阻止输入框默认的换行行为(单行输入框无需换行) + handleSendMessage(); // 调用发送消息函数 + } + }; + + // ===== 渲染(不变)===== + return ( + <> + {/* 1. 聊天主弹窗:展示聊天记录、输入框、分享按钮 */} + +
+ {/* 消息展示区域:可滚动,自动定位到底部 */} +
+ {/* 渲染所有聊天消息:遍历messages数组,每条消息对应一个DOM节点 */} + {messages.map((message) => ( +
+ {/* 头像:自己用默认头像,好友用其专属头像(无则用默认) */} + {message.isSent + + {/* 消息内容+时间戳容器:对齐方式与消息一致 */} +
+ {/* 消息气泡:区分自己和好友的样式(颜色、圆角、边框) */} +
+

{message.content}

+
+ + {/* 时间戳+已读状态:小字体弱化显示,区分自己和好友的文本颜色 */} +
+ {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + {message.isRead ? '已读' : '未读'} +
+
+
+ ))} +
+ + {/* 消息输入区域:包含分享按钮、输入框、发送按钮 */} +
+
+ {/* 分享查询按钮:点击打开分享弹窗 */} + + {/* 消息输入框:绑定输入状态,支持回车发送 */} + setInputText(e.target.value.trimStart())} + onKeyDown={handleKeyDown} + className="flex-1 px-4 py-3 border border-gray-300 rounded-full focus:ring-2 focus:ring-primary/30 outline-none" + readOnly={false} + /> + {/* 发送按钮:空消息时禁用,点击发送消息 */} + +
+
+
+
+ + {/* 2. 分享查询弹窗:选择要分享的查询记录 */} + setShareModalOpen(false)} title="分享查询结果"> +
+

从您的历史记录中选择一个查询结果分享给 {friend.name}。

+ {/* 可分享查询列表:滚动展示,单选模式 */} +
+ {savedQueries.length > 0 ? ( + savedQueries.map(q => ( + + )) + ) : ( +

没有收藏的查询记录

+ )} +
+
+ {/* 分享弹窗底部按钮:取消/确认分享 */} +
+ + +
+
+ + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/ComparisonModal.tsx b/src/springboot_demo/frontend/components/ComparisonModal.tsx new file mode 100644 index 00000000..0bc5e5d3 --- /dev/null +++ b/src/springboot_demo/frontend/components/ComparisonModal.tsx @@ -0,0 +1,232 @@ +import React, { useState, useMemo } from 'react'; +import { Chart as ChartJS, registerables } from 'chart.js'; +import { Chart } from 'react-chartjs-2'; +import { QueryResultData } from '../types'; +import { Modal } from './Modal'; // 引入你的Modal组件(确保路径正确) + +// 注册ChartJS所需的所有组件 +ChartJS.register(...registerables); + +// 表格行差异结果类型定义:包含新增、删除、修改和共同的行数据 +type DiffResult = { + added: string[][]; // 新增的行 + deleted: string[][]; // 删除的行 + modified: { oldRow: string[], newRow: string[] }[]; // 修改的行(包含旧行和新行) + common: string[][]; // 未变化的共同行 +}; + +// 查找两个表格数据集之间行差异的辅助函数 +// 参数:旧行数据、新行数据;返回:差异结果对象 +const findRowChanges = (oldRows: string[][], newRows: string[][]): DiffResult => { + // 以每行第一个元素为键,构建旧行和新行的映射表(便于快速查找) + const oldMap = new Map(oldRows.map(row => [row[0], row])); + const newMap = new Map(newRows.map(row => [row[0], row])); + + // 初始化差异结果对象 + const result: DiffResult = { added: [], deleted: [], modified: [], common: [] }; + + // 遍历旧行映射,检查每行在新行中的状态(删除/修改/共同) + oldMap.forEach((oldRow, key) => { + if (!newMap.has(key)) { + result.deleted.push(oldRow); // 新行中无此键:标记为删除 + } else { + const newRow = newMap.get(key)!; + // 行数据字符串化后不等:标记为修改 + if (JSON.stringify(oldRow) !== JSON.stringify(newRow)) { + result.modified.push({ oldRow, newRow }); + } else { + result.common.push(oldRow); // 行数据相同:标记为共同 + } + } + }); + + // 遍历新行映射,检查新增的行(旧行中无此键) + newMap.forEach((newRow, key) => { + if (!oldMap.has(key)) { + result.added.push(newRow); + } + }); + + return result; +}; + +// 表格对比组件:展示两个查询结果的表格差异(新增/删除/修改行) +// 参数:旧查询结果、新查询结果、差异结果 +const TableComparison: React.FC<{ oldQuery: QueryResultData; newQuery: QueryResultData, changes: DiffResult }> = ({ oldQuery, newQuery, changes }) => { + // 渲染单元格内容:若新旧单元格内容不同,高亮显示新内容并标注旧内容 + const renderCell = (oldCell: string, newCell: string) => { + if (oldCell !== newCell) { + return ( + + {newCell} {oldCell} + + ); + } + return newCell; + }; + + return ( +
+ {/* 旧数据表格 */} +
+

旧数据 ({new Date(oldQuery.queryTime).toLocaleString()})

+
+ + {oldQuery.tableData.headers.map(h => )} + + {/* 显示删除的行(红色背景+删除线) */} + {changes.deleted.map((row, i) => )} + {/* 显示修改的旧行 */} + {changes.modified.map(({ oldRow }, i) => ( + + {oldRow.map((cell, j) => )} + + ))} + {/* 显示未变化的共同行 */} + {changes.common.map((row, i) => {row.map((cell, j) => )})} + +
{h}
{row.join(' | ')}
{cell}
{cell}
+
+
+ + {/* 新数据表格 */} +
+

新数据 ({new Date(newQuery.queryTime).toLocaleString()})

+
+ + {newQuery.tableData.headers.map(h => )} + + {/* 显示新增的行(绿色背景) */} + {changes.added.map((row, i) => {row.map((cell, j) => )})} + {/* 显示修改的新行(高亮变化的单元格) */} + {changes.modified.map(({ oldRow, newRow }, i) => ( + + {newRow.map((cell, j) => )} + + ))} + {/* 显示未变化的共同行 */} + {changes.common.map((row, i) => {row.map((cell, j) => )})} + +
{h}
{cell}
{renderCell(oldRow[j], cell)}
{cell}
+
+
+
+ ); +}; + +// 图表对比组件:在同一图表中展示新旧查询结果的图表数据 +// 参数:旧查询结果、新查询结果 +const ChartComparison: React.FC<{ oldQuery: QueryResultData; newQuery: QueryResultData }> = ({ oldQuery, newQuery }) => { + // 构建图表数据:合并新旧标签,分别配置新旧数据集样式 + const chartData = { + labels: [...new Set([...oldQuery.chartData.labels, ...newQuery.chartData.labels])], // 合并去重标签 + datasets: [ + { + ...oldQuery.chartData.datasets[0], + label: `${oldQuery.chartData.datasets[0].label} (旧)`, + backgroundColor: 'rgba(22, 93, 255, 0.3)', + borderColor: 'rgba(22, 93, 255, 0.5)', + borderDash: [5, 5], // 旧数据用虚线 + }, + { + ...newQuery.chartData.datasets[0], + label: `${newQuery.chartData.datasets[0].label} (新)`, + backgroundColor: 'rgba(54, 162, 235, 0.6)', + borderColor: 'rgba(54, 162, 235, 1)', // 新数据用实线 + } + ], + }; + + return ( +
+
+ +
+
+ ); +}; + +// 弹窗式对比组件属性接口:新增弹窗控制参数,保留原有对比参数 +interface ComparisonModalProps { + oldQuery: QueryResultData; + newQuery: QueryResultData; + isOpen: boolean; // 控制弹窗显示/隐藏 + onClose: () => void; // 弹窗关闭回调 +} + +// 弹窗式对比组件:用Modal包裹原有对比内容,适配弹窗形态 +export const ComparisonModal: React.FC = ({ oldQuery, newQuery, isOpen, onClose }) => { + // 视图切换状态:表格视图/图表视图 + const [activeView, setActiveView] = useState<'table' | 'chart'>('table'); + // 计算表格行差异(使用useMemo避免重复计算) + const changes = useMemo(() => findRowChanges(oldQuery.tableData.rows, newQuery.tableData.rows), [oldQuery, newQuery]); + + // 对比摘要统计项:新增/删除/变更行数量 + const summaryItems = [ + { icon: 'fa-plus-circle', color: 'text-green-500', value: changes.added.length, label: '新增行' }, + { icon: 'fa-minus-circle', color: 'text-red-500', value: changes.deleted.length, label: '删除行' }, + { icon: 'fa-pencil-square-o', color: 'text-yellow-500', value: changes.modified.length, label: '变更行' }, + ]; + + return ( + + {/* 弹窗内容区域:限制高度+滚动 */} +
+ {/* 对比说明 */} +
+

对比查询 "{oldQuery.userPrompt}" 的两个不同版本。

+
+ + {/* 对比摘要卡片 */} +
+

对比摘要

+
+ {summaryItems.map(item => ( +
+ +
+

{item.value}

+

{item.label}

+
+
+ ))} +
+
+ + {/* 对比视图切换区域 */} +
+
+
+ + +
+
+ {/* 根据当前视图状态渲染对应的对比组件 */} + {activeView === 'table' + ? + : + } +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/DataAdminPage.tsx b/src/springboot_demo/frontend/components/DataAdminPage.tsx new file mode 100644 index 00000000..95706be8 --- /dev/null +++ b/src/springboot_demo/frontend/components/DataAdminPage.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { DataAdminPageType, Page, Conversation, QueryResultData, MessageRole, QueryShare } from '../types'; +import { DataSourceManagementPage } from './data-admin/DataSourceManagementPage'; +import { UserPermissionPage } from './data-admin/UserPermissionPage'; +import { ConnectionLogPage } from './data-admin/ConnectionLogPage'; +import { QueryPage } from './QueryPage'; +import { HistoryPage } from './HistoryPage'; +import { AccountPage } from './AccountPage'; +import { FriendsPage } from './FriendsPage'; +import { NotificationsPage } from './NotificationsPage'; +import { DataAdminNotificationPage } from './data-admin/DataAdminNotificationPage'; +import { ComparisonModal } from './ComparisonModal'; +import { DataAdminDashboardPage } from './data-admin/DataAdminDashboardPage'; + +interface DataAdminPageProps { + activePage: DataAdminPageType; + setActivePage: (page: DataAdminPageType) => void; + // Props for QueryPage and others + currentConversation: Conversation | undefined; + onToggleHistory: () => void; + isHistoryOpen: boolean; + onAddMessage: (role: MessageRole, content: string | QueryResultData) => void; + onSaveQuery: (query: QueryResultData) => void; + onShareQuery: (queryId: string, friendId: string) => void; + savedQueries: QueryResultData[]; + initialPrompt?: string; + onClearInitialPrompt: () => void; + // Props for HistoryPage + conversations: Conversation[]; + currentConversationId: string; + onSwitchConversation: (id: string) => void; + onNewConversation: () => void; + onDelete: (id: string) => void; + onViewInChat: (conversationId: string) => void; + onRerun: (prompt: string) => void; + onCompare: (queryId1: string, queryId2: string) => void; + // Props for FriendsPage + shares: QueryShare[]; + onMarkShareAsRead: (shareId: string) => void; + onDeleteShare: (shareId: string) => void; + onDeleteConversation: (id: string) => void; +} + +export const DataAdminPage: React.FC = (props) => { + const { activePage } = props; + + // Duplicated from App.tsx to render shared pages + const renderNormalUserPage = (page: Page) => { + switch (page) { + case 'query': + return ( + + ); + case 'history': + return ( + + ); + case 'notifications': + return ; + case 'account': + return ; + case 'friends': + return ( + + ); + default: + // This case handles 'comparison', which is rendered by the parent switch + return
Page not found
; + } + }; + + switch (activePage) { + // Admin specific pages + case 'dashboard': + return ; + case 'datasource': + return ; + case 'user-permission': + return ; + case 'notification-management': + return ; + case 'connection-log': + return ; + case 'query': + case 'history': + case 'notifications': + case 'account': + case 'friends': + return renderNormalUserPage(activePage); + default: + const exhaustiveCheck: never = activePage; + return
Page not found
; + } +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/DataAdminSidebar.tsx b/src/springboot_demo/frontend/components/DataAdminSidebar.tsx new file mode 100644 index 00000000..ea2a521d --- /dev/null +++ b/src/springboot_demo/frontend/components/DataAdminSidebar.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { DataAdminPageType } from '../types'; + +interface SidebarItemProps { + href: DataAdminPageType; + icon: string; + label: string; + isActive: boolean; + onClick: (page: DataAdminPageType) => void; +} + +const SidebarItem: React.FC = ({ href, icon, label, isActive, onClick }) => { + const activeClass = 'bg-primary/10 text-primary border-l-4 border-primary'; + const inactiveClass = 'text-gray-600 hover:bg-gray-50'; + + return ( +
  • + { + e.preventDefault(); + onClick(href); + }} + > + + {label} + +
  • + ); +}; + +interface SidebarProps { + activePage: DataAdminPageType; + setActivePage: (page: DataAdminPageType) => void; + onLogout: () => void; +} + +export const DataAdminSidebar: React.FC = ({ activePage, setActivePage, onLogout }) => { + const queryItems = [ + { href: 'query', icon: 'fa-search', label: '数据查询' }, + { href: 'history', icon: 'fa-star', label: '收藏夹' }, + ] as const; + + const managementItems = [ + { href: 'dashboard', icon: 'fa-tachometer', label: '仪表盘' }, + { href: 'datasource', icon: 'fa-plug', label: '数据源管理' }, + { href: 'user-permission', icon: 'fa-key', label: '用户权限管理' }, + { href: 'notification-management', icon: 'fa-bullhorn', label: '通知管理' }, + { href: 'connection-log', icon: 'fa-link', label: '连接日志' }, + ] as const; + + const personalItems = [ + { href: 'notifications', icon: 'fa-bell', label: '通知中心' }, + { href: 'account', icon: 'fa-user', label: '账户管理' }, + { href: 'friends', icon: 'fa-users', label: '好友管理' }, + ] as const; + + return ( + + ); +}; diff --git a/src/springboot_demo/frontend/components/Dropdown.tsx b/src/springboot_demo/frontend/components/Dropdown.tsx new file mode 100644 index 00000000..4611cee1 --- /dev/null +++ b/src/springboot_demo/frontend/components/Dropdown.tsx @@ -0,0 +1,63 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { ModelOption } from '../types'; + +interface DropdownProps { + options: ModelOption[]; + selected: string; + setSelected: (option: string) => void; + icon: string; +} + +export const Dropdown: React.FC = ({ options, selected, setSelected, icon }) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleSelect = (option: ModelOption) => { + if (option.disabled) return; + setSelected(option.name); + setIsOpen(false); + }; + + return ( +
    + + {isOpen && ( +
    + {options.map(option => ( + + ))} +
    + )} +
    + ); +}; diff --git a/src/springboot_demo/frontend/components/FriendsPage.tsx b/src/springboot_demo/frontend/components/FriendsPage.tsx new file mode 100644 index 00000000..c77a78a0 --- /dev/null +++ b/src/springboot_demo/frontend/components/FriendsPage.tsx @@ -0,0 +1,728 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { MOCK_FRIENDS_LIST, MOCK_FRIEND_REQUESTS } from '../constants'; +import { Friend, FriendRequest, QueryResultData, QueryShare } from '../types'; +import { Modal } from './Modal'; +import { ChatModal } from './ChatModal'; +import { QueryResult } from './QueryResult'; + +// 补充Message接口定义(若types文件已包含可删除) +interface Message { + id: string; + content: string; + isSent: boolean; + timestamp: Date; + isRead: boolean; +} + +// 标签类型定义 +type ActiveTab = 'friends' | 'requests' | 'shares'; + +// 1. 好友卡片组件(新增备注显示+备注/主页按钮) +const FriendCard: React.FC<{ + friend: Friend; + onChat: (friend: Friend) => void; + onDelete: (friend: Friend) => void; + onRemark: (friend: Friend) => void; // 新增:触发备注弹窗 + onViewProfile: (friend: Friend) => void; // 新增:触发查看主页 + unreadCount: number; +}> = ({ friend, onChat, onDelete, onRemark, onViewProfile, unreadCount }) => ( +
    +
    +
    + {friend.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} + /> + {/* 在线状态标记 */} + {friend.isOnline && ( + + )} +
    +
    +
    + {/* 有备注显示备注,无备注显示原名(作为主要标识) */} +

    + {friend.remark || friend.name} +

    + {/* 未读消息提示 */} + {unreadCount > 0 && ( + + {unreadCount} + + )} +
    + {/* 有备注时才显示原名(作为补充),无备注则不显示 */} + {friend.remark && ( +

    「{friend.name}」

    + )} +
    +
    +
    + {/* 聊天按钮(原有) */} + + {/* 新增:备注按钮 */} + + {/* 新增:访问主页按钮 */} + + {/* 删除好友按钮(原有) */} + +
    +
    +); + +// 2. 好友请求卡片组件(原有功能不变) +const RequestCard: React.FC<{ + request: FriendRequest; + onAccept: (request: FriendRequest) => void; + onReject: (requestId: string) => void; +}> = ({ request, onAccept, onReject }) => ( +
    +
    + {request.fromUser.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} + /> +
    +

    {request.fromUser.name}

    +

    {request.timestamp}

    +
    +
    +
    + + +
    +
    +); + +// 3. 分享记录卡片组件(原有功能不变) +const ShareRecordCard: React.FC<{ + share: QueryShare; + onView: () => void; + onRerun: () => void; + onDelete: () => void; + onSave: () => void; +}> = ({ share, onView, onRerun, onDelete, onSave }) => ( +
    +
    + {share.sender.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} + /> +
    +

    {share.sender.name}分享了一个查询:

    +

    "{share.querySnapshot.userPrompt}"

    +

    {new Date(share.timestamp).toLocaleString()}

    +
    +
    +
    +
    + + + + +
    +
    +
    +); + +// 4. 标签按钮组件(原有功能不变) +const TabButton: React.FC<{ + tab: ActiveTab; + label: string; + count?: number; + activeTab: ActiveTab; + onTabChange: (tab: ActiveTab) => void; +}> = ({ tab, label, count, activeTab, onTabChange }) => ( + +); + +// 页面Props定义(原有功能不变) +interface FriendsPageProps { + savedQueries: QueryResultData[]; + shares: QueryShare[]; + onMarkShareAsRead: (shareId: string) => void; + onDeleteShare: (shareId: string) => void; + onRerunQuery: (prompt: string) => void; + onSaveQuery: (query: QueryResultData) => void; +} + +export const FriendsPage: React.FC = ({ + savedQueries, shares, onMarkShareAsRead, + onDeleteShare, onRerunQuery, onSaveQuery +}) => { + // 页面状态 + const [friends, setFriends] = useState( + // 初始化好友数据:给原有Mock数据添加默认空备注 + MOCK_FRIENDS_LIST.map(friend => ({ ...friend, remark: '' })) + ); + const [requests, setRequests] = useState(MOCK_FRIEND_REQUESTS); + const [isAddFriendModalOpen, setAddFriendModalOpen] = useState(false); + const [chattingWith, setChattingWith] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [friendToDelete, setFriendToDelete] = useState(null); + const [activeTab, setActiveTab] = useState('friends'); + const [viewingShare, setViewingShare] = useState(null); + const [activeModal, setActiveModal] = useState(null); + + // 新增:备注相关状态 + const [isRemarkModalOpen, setIsRemarkModalOpen] = useState(false); + const [currentRemarkFriend, setCurrentRemarkFriend] = useState(null); + const [remarkInput, setRemarkInput] = useState(''); + + // 新增:好友主页相关状态 + const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); + const [currentProfileFriend, setCurrentProfileFriend] = useState(null); + + // 未读消息状态 + const [unreadMessages, setUnreadMessages] = useState>({}); + + // 聊天记录状态 + const [friendMessages, setFriendMessages] = useState>(() => ({ + 'friend-1': [ + { + id: `${Date.now()}-init-1`, + content: '你好!最近在忙什么?', + isSent: false, + timestamp: new Date(Date.now() - 3600000), + isRead: true, + }, + { + id: `${Date.now()}-init-2`, + content: '我在处理一个数据查询,挺复杂的~', + isSent: true, + timestamp: new Date(Date.now() - 3500000), + isRead: false, + } + ], + 'friend-2': [ + { + id: `${Date.now()}-init-3`, + content: '在吗?上次分享的查询结果很有用!', + isSent: false, + timestamp: new Date(Date.now() - 86400000), + isRead: true, + } + ] + })); + + // 初始化未读消息 + useEffect(() => { + setUnreadMessages({ + 'friend-1': 2, + 'friend-2': 1 + }); + }, []); + + // 新增:打开备注弹窗时初始化输入值 + useEffect(() => { + if (currentRemarkFriend) { + setRemarkInput(currentRemarkFriend.remark || ''); + } + }, [currentRemarkFriend]); + + // 更新未读消息数量 + const updateUnreadCount = (friendId: string, count: number) => { + setUnreadMessages(prev => ({ + ...prev, + [friendId]: count + })); + }; + + // 更新聊天记录 + const updateFriendMessages = (friendId: string, newMessages: Message[]) => { + setFriendMessages(prev => ({ + ...prev, + [friendId]: newMessages + })); + }; + + // 打开聊天 + const handleOpenChat = (friend: Friend) => { + setChattingWith(friend); + if (!friendMessages[friend.id]) { + setFriendMessages(prev => ({ + ...prev, + [friend.id]: [ + { + id: `${Date.now()}-init-empty`, + content: `你好!开始和${friend.name}聊天吧~`, + isSent: false, + timestamp: new Date(), + isRead: true, + } + ] + })); + } + }; + + // 模拟新消息(测试用) + const mockNewMessage = (friendId: string) => { + setUnreadMessages(prev => ({ + ...prev, + [friendId]: (prev[friendId] || 0) + 1 + })); + + const friend = friends.find(f => f.id === friendId); + if (friend) { + const randomTexts = [ + "你好!在忙吗?", + "这个查询结果我觉得很有用", + "我们下次什么时候再讨论一下?", + "你之前分享的内容帮了我大忙", + "有空可以看看我刚发的查询" + ]; + const randomText = randomTexts[Math.floor(Math.random() * randomTexts.length)]; + + const newMessage: Message = { + id: `mock-${Date.now()}`, + content: randomText, + isSent: false, + timestamp: new Date(), + isRead: false + }; + + setFriendMessages(prev => ({ + ...prev, + [friendId]: prev[friendId] + ? [...prev[friendId], newMessage] + : [newMessage] + })); + } + }; + + // 筛选好友 + const filteredFriends = useMemo(() => + friends.filter(friend => + friend.name.toLowerCase().includes(searchTerm.toLowerCase()) || + (friend.remark && friend.remark.toLowerCase().includes(searchTerm.toLowerCase())) // 支持按备注搜索 + ), + [friends, searchTerm] + ); + + // 删除好友逻辑 + const leteRequest = (friend: Friend) => setFriendToDelete(friend); + const handleConfirmDelete = () => { + if (friendToDelete) { + setFriends(prev => prev.filter(f => f.id !== friendToDelete.id)); + setUnreadMessages(prev => { + const newUnread = { ...prev }; + delete newUnread[friendToDelete.id]; + return newUnread; + }); + setFriendMessages(prev => { + const newMessages = { ...prev }; + delete newMessages[friendToDelete.id]; + return newMessages; + }); + setFriendToDelete(null); + } + }; + + // 好友请求处理 + const handleAcceptRequest = (request: FriendRequest) => { + const newFriend: Friend = { + id: `friend-${Date.now()}`, + name: request.fromUser.name, + avatarUrl: request.fromUser.avatarUrl, + isOnline: false, + remark: '', + email:'' + }; + setFriends(prev => [newFriend, ...prev]); + setRequests(prev => prev.filter(req => req.id !== request.id)); + }; + const handleRejectRequest = (requestId: string) => { + setRequests(prev => prev.filter(req => req.id !== requestId)); + }; + + // 分享记录处理 + const handleViewShare = (share: QueryShare) => { + setViewingShare(share); + onMarkShareAsRead(share.id); + }; + const handleRerunShare = (share: QueryShare) => { + onRerunQuery(share.querySnapshot.userPrompt); + }; + const handleSaveShare = (share: QueryShare) => { + onSaveQuery(share.querySnapshot); + setActiveModal('saveSuccess'); + }; + const [shareToDelete, setShareToDelete] = useState(null); + // 新增:备注功能相关函数 + const handleOpenRemarkModal = (friend: Friend) => { + setCurrentRemarkFriend(friend); + setIsRemarkModalOpen(true); + }; + const handleSaveRemark = () => { + if (currentRemarkFriend) { + setFriends(prev => prev.map(friend => + friend.id === currentRemarkFriend.id + ? { ...friend, remark: remarkInput.trim() } + : friend + )); + setIsRemarkModalOpen(false); + setCurrentRemarkFriend(null); + } + }; + + // 新增:好友主页功能相关函数 + const handleOpenProfileModal = (friend: Friend) => { + console.log('打开主页的好友数据:', friend); + setCurrentProfileFriend(friend); + setIsProfileModalOpen(true); + }; + // 新增:处理分享删除请求(触发确认弹窗) +const handleDeleteShareRequest = (share: QueryShare) => { + setShareToDelete(share); +}; + +// 新增:确认删除分享 +const handleConfirmDeleteShare = () => { + if (shareToDelete) { + onDeleteShare(shareToDelete.id); + setShareToDelete(null); + } +}; + return ( +
    + {/* 页面标题栏 */} +
    +

    + +

    +
    + + +
    +
    + + {/* 标签栏 */} +
    +
    + + + s.status === 'unread').length} + activeTab={activeTab} + onTabChange={setActiveTab} + /> +
    +
    + + {/* 内容区域 */} +
    + {/* 1. 好友列表标签 */} + {activeTab === 'friends' && ( +
    + {/* 搜索框(支持搜索名称/备注) */} +
    +
    + + setSearchTerm(e.target.value)} + className="w-full pl-9 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/30" + /> +
    +
    + + {/* 好友列表 */} + {filteredFriends.length > 0 ? ( +
    + {filteredFriends.map(friend => ( + + ))} +
    + ) : ( +
    + +

    未找到匹配的好友

    +
    + )} +
    + )} + + {/* 2. 好友请求标签(原有不变) */} + {activeTab === 'requests' && ( +
    + {requests.length > 0 ? ( + requests.map(req => ( + + )) + ) : ( +
    + +

    没有新的好友请求

    +
    + )} +
    + )} + + {/* 3. 分享记录标签(原有不变) */} + {activeTab === 'shares' && ( +
    + {shares.length > 0 ? ( + shares.map(share => ( + handleViewShare(share)} + onRerun={() => handleRerunShare(share)} + onDelete={() => handleDeleteShareRequest(share)} + onSave={() => handleSaveShare(share)} + /> + )) + ) : ( +
    + +

    没有收到任何分享

    +
    + )} +
    + )} +
    + + {/* 原有模态框:添加好友 */} + setAddFriendModalOpen(false)} title="添加好友"> +
    +
    + + +
    +
    +
    + + +
    +
    + + {/* 原有模态框:删除好友确认 */} + setFriendToDelete(null)} title="删除好友"> +

    您确定要删除好友 "{friendToDelete?.name}" 吗?此操作无法撤销。

    +
    + + +
    +
    + + {/* 原有模态框:聊天窗口 */} + {chattingWith && ( + setChattingWith(null)} + savedQueries={savedQueries} + currentUnreadCount={unreadMessages[chattingWith.id] || 0} + updateUnreadCount={updateUnreadCount} + messages={friendMessages[chattingWith.id] || []} + updateMessages={(newMessages) => updateFriendMessages(chattingWith.id, newMessages)} + /> + )} + + {/* 原有模态框:查看分享结果 */} + setViewingShare(null)} title={`查看分享: "${viewingShare?.querySnapshot.userPrompt}"`}> + {viewingShare && ( +
    + +
    + )} +
    + + {/* 原有模态框:保存成功提示 */} + setActiveModal(null)} hideTitle> +
    +
    + +
    +

    保存成功

    +

    该查询结果已保存至您的历史记录中。

    + +
    +
    + + {/* 新增:备注好友模态框 */} + setIsRemarkModalOpen(false)} title="备注好友"> +
    +
    + + setRemarkInput(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg" + maxLength={20} // 限制备注长度 + /> +

    备注将显示在好友名称下方,最多20个字符

    +
    +
    +
    + + +
    +
    + + {/* 新增:好友主页模态框 */} + setIsProfileModalOpen(false)} title="好友主页" hideTitle={false}> + {currentProfileFriend && ( +
    + {/* 好友头像+基本信息 */} +
    +
    + {currentProfileFriend.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} + /> +
    +

    {currentProfileFriend.name}

    + {currentProfileFriend.remark && ( +

    备注:{currentProfileFriend.remark}

    + )} +

    + + {currentProfileFriend?.email || '未设置邮箱'} +

    + + {currentProfileFriend.isOnline ? '在线' : '离线'} + + +
    + + {/* 操作按钮 */} +
    + + +
    +
    + )} +
    + {/* 新增:删除分享确认模态框 */} + setShareToDelete(null)} title="删除分享"> +

    + 您确定要删除 "{shareToDelete?.sender.name}" 分享的查询吗?此操作无法撤销。 +

    +
    + + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/HistoryPage.tsx b/src/springboot_demo/frontend/components/HistoryPage.tsx new file mode 100644 index 00000000..5ba30779 --- /dev/null +++ b/src/springboot_demo/frontend/components/HistoryPage.tsx @@ -0,0 +1,455 @@ +import React, { useState, useMemo, useCallback } from 'react'; +import { Conversation, QueryResultData } from '../types'; +import { Modal } from './Modal'; +import { QueryResult } from './QueryResult'; +import { MODEL_OPTIONS, DATABASE_OPTIONS } from '../constants'; // 引入真实选项数据 + +// 日期筛选类型定义:全部、今天、近7天、近30天 +type FilterType = 'all' | 'today' | '7days' | '30days'; +// 确认弹窗操作类型定义:单条删除、重新执行、批量删除 +type ConfirmAction = 'delete' | 'rerun' | 'bulkDelete'; + +// 历史页面属性接口定义 +interface HistoryPageProps { + savedQueries: QueryResultData[]; + conversations: Conversation[]; + onDelete: (id: string) => void; + onViewInChat: (conversationId: string) => void; + onRerun: (prompt: string) => void; + onCompare: (queryId1: string, queryId2: string) => void; +} + +// 校验日期是否在指定天数范围内(含起始日期) +const isDateInRage = (date: Date, days: number): boolean => { + const today = new Date(); + const pastDate = new Date(); + if (days > 0) { + pastDate.setDate(today.getDate() - (days - 1)); + } + today.setHours(23, 59, 59, 999); + pastDate.setHours(0, 0, 0, 0); + return date >= pastDate && date <= today; +}; + +// 历史查询页面组件 +export const HistoryPage: React.FC = ({ savedQueries, conversations, onDelete, onViewInChat, onRerun, onCompare }) => { + // 搜索关键词状态 + const [searchTerm, setSearchTerm] = useState(''); + // 当前激活的筛选条件状态 + const [activeFilters, setActiveFilters] = useState({ + date: 'all' as FilterType, + model: 'all', + database: 'all' + }); + // 展开的查询分组标识状态(按用户查询提示语分组) + const [expandedGroup, setExpandedGroup] = useState(null); + // 选中的查询记录ID集合(用于批量操作或对比) + const [selectedIds, setSelectedIds] = useState>(new Set()); + // 正在查看详情的查询记录状态 + const [viewingQuery, setViewingQuery] = useState(null); + + // 确认弹窗状态管理:控制弹窗显示、操作类型及目标数据 + const [confirmModalState, setConfirmModalState] = useState<{ + isOpen: boolean; + action: ConfirmAction | null; + targetId: string | null; + targetPrompt: string | null; + targetIds: string[] | null; + }>({ + isOpen: false, + action: null, + targetId: null, + targetPrompt: null, + targetIds: null, + }); + + // 根据对话ID获取对应的对话标题 + const getConversationTitle = (id: string) => { + return conversations.find(c => c.id === id)?.title || '未知对话'; + }; + + // 切换查询分组的展开/收起状态,切换时清空已选中的查询记录 + const handleToggleGroup = (prompt: string) => { + setExpandedGroup(prev => (prev === prompt ? null : prompt)); + setSelectedIds(new Set()); + }; + + // 切换单个查询记录的选中状态(最多支持100条选中) + const handleSelectSnapshot = (id: string) => { + setSelectedIds(prev => { + const newSet = new Set(prev); + if (newSet.has(id)) { + newSet.delete(id); + } else if (newSet.size < 100) { + newSet.add(id); + } + return newSet; + }); + }; + + // 执行两条选中查询记录的对比操作 + const handleCompare = () => { + if (selectedIds.size !== 2) return; + const [id1, id2] = Array.from(selectedIds); + onCompare(id1, id2); + setSelectedIds(new Set()); + }; + + // 打开单条查询记录的删除确认弹窗 + const openDeleteConfirm = useCallback((id: string) => { + setConfirmModalState({ + isOpen: true, + action: 'delete', + targetId: id, + targetPrompt: null, + targetIds: null, + }); + }, []); + + // 打开查询的重新执行确认弹窗 + const openRerunConfirm = useCallback((prompt: string) => { + setConfirmModalState({ + isOpen: true, + action: 'rerun', + targetId: null, + targetPrompt: prompt, + targetIds: null, + }); + }, []); + + // 打开批量删除确认弹窗 + const openBulkDeleteConfirm = useCallback(() => { + setConfirmModalState({ + isOpen: true, + action: 'bulkDelete', + targetId: null, + targetPrompt: null, + targetIds: Array.from(selectedIds), + }); + }, [selectedIds]); + + // 确认弹窗的操作执行逻辑(根据不同操作类型分发处理) + const handleConfirmAction = () => { + if (confirmModalState.action === 'delete' && confirmModalState.targetId) { + onDelete(confirmModalState.targetId); + } else if (confirmModalState.action === 'rerun' && confirmModalState.targetPrompt) { + onRerun(confirmModalState.targetPrompt); + } else if (confirmModalState.action === 'bulkDelete' && confirmModalState.targetIds) { + confirmModalState.targetIds.forEach(id => onDelete(id)); + setSelectedIds(new Set()); + } + setConfirmModalState({ isOpen: false, action: null, targetId: null, targetPrompt: null, targetIds: null }); + }; + + // 取消确认弹窗操作 + const handleCancelConfirm = () => { + setConfirmModalState({ isOpen: false, action: null, targetId: null, targetPrompt: null, targetIds: null }); + }; + + // 切换筛选条件 + const handleFilterChange = (type: 'date' | 'model' | 'database', value: string) => { + setActiveFilters(prev => ({ + ...prev, + [type]: value + })); + }; + + // 重置所有筛选条件 + const handleResetFilters = () => { + setActiveFilters({ + date: 'all', + model: 'all', + database: 'all' + }); + setSearchTerm(''); + }; + + // 按用户查询提示语分组查询记录,每组内按执行时间倒序排序 + const queryGroups = useMemo(() => { + const groups: Record = {}; + + savedQueries.forEach(query => { + if (!groups[query.userPrompt]) { + groups[query.userPrompt] = []; + } + groups[query.userPrompt].push(query); + }); + + Object.values(groups).forEach(snapshots => { + snapshots.sort((a, b) => new Date(b.queryTime).getTime() - new Date(a.queryTime).getTime()); + }); + return groups; + }, [savedQueries]); + + // 根据搜索关键词和所有筛选条件,过滤查询分组 + const filteredGroups = useMemo(() => { + return Object.entries(queryGroups) + .filter(([prompt, snapshots]) => { + const matchesSearch = prompt.toLowerCase().includes(searchTerm.toLowerCase()); + if (!matchesSearch) return false; + + // 过滤出符合所有筛选条件的记录 + const filteredSnapshots = snapshots.filter(query => { + // 日期筛选 + if (activeFilters.date !== 'all') { + const queryDate = new Date(query.queryTime); + const dateMatch = + activeFilters.date === 'today' ? isDateInRage(queryDate, 1) : + activeFilters.date === '7days' ? isDateInRage(queryDate, 7) : + activeFilters.date === '30days' ? isDateInRage(queryDate, 30) : + false; + if (!dateMatch) return false; + } + + // 大模型筛选(使用真实model字段) + if (activeFilters.model !== 'all' && query.model !== activeFilters.model) { + return false; + } + + // 数据库筛选(使用真实database字段) + if (activeFilters.database !== 'all' && query.database !== activeFilters.database) { + return false; + } + + return true; + }); + + return filteredSnapshots.length > 0; + }); + }, [queryGroups, searchTerm, activeFilters]); + + // 渲染下拉筛选组件(复用逻辑) + const renderFilterDropdown = ( + label: string, + type: 'date' | 'model' | 'database', + options: { value: string; label: string }[] + ) => ( +
    + +
    + ); + + + // 大模型筛选选项(从MODEL_OPTIONS提取,去重并添加"全部") + const modelOptions = useMemo(() => { + const uniqueModels = Array.from(new Set(MODEL_OPTIONS.map(option => option.name))); + return [ + { value: 'all', label: '全部大模型' }, + ...uniqueModels.map(model => ({ value: model, label: model })) + ]; + }, []); + + // 数据库筛选选项(从DATABASE_OPTIONS提取,去重并添加"全部") + const databaseOptions = useMemo(() => { + const uniqueDatabases = Array.from(new Set(DATABASE_OPTIONS.map(option => option.name))); + return [ + { value: 'all', label: '全部数据库' }, + ...uniqueDatabases.map(db => ({ value: db, label: db })) + ]; + }, []); + + + // 日期筛选选项 + const dateOptions = [ + { value: 'all', label: '全部日期' }, + { value: 'today', label: '今天' }, + { value: '7days', label: '近7天' }, + { value: '30days', label: '近30天' } + ]; + + // 根据确认弹窗的操作类型,生成对应的弹窗内容(标题、提示信息、按钮文本及样式) + const confirmModalContent = useMemo(() => { + if (confirmModalState.action === 'delete') { + return { + title: '确认删除查询记录?', + message: '此操作将永久删除该条查询快照。请确认是否继续?', + buttonText: '确认删除', + buttonClass: 'bg-red-600 hover:bg-red-700', + }; + } + if (confirmModalState.action === 'rerun') { + return { + title: '确认重新执行查询?', + message: `您确定要重新执行查询:"${confirmModalState.targetPrompt}" 吗? 这将消耗新的计算资源。`, + buttonText: '重新执行', + buttonClass: 'bg-primary hover:bg-primary/90', + }; + } + if (confirmModalState.action === 'bulkDelete' && confirmModalState.targetIds) { + return { + title: '确认批量删除?', + message: `您确定要永久删除选中的 ${confirmModalState.targetIds.length} 条查询记录吗?此操作不可恢复。`, + buttonText: '批量删除', + buttonClass: 'bg-red-600 hover:bg-red-700', + }; + } + return { title: '', message: '', buttonText: '', buttonClass: '' }; + }, [confirmModalState.action, confirmModalState.targetPrompt, confirmModalState.targetIds]); + + return ( +
    + +
    +
    + {/* 搜索框 - 左侧缩小 */} +
    + + setSearchTerm(e.target.value)} + className="w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/30" + /> +
    + + {/* 筛选区域 - 右侧横向分布 */} +
    + {renderFilterDropdown('大模型', 'model', modelOptions)} + {renderFilterDropdown('数据库', 'database', databaseOptions)} + {renderFilterDropdown('日期', 'date', dateOptions)} + + {/* 重置按钮 */} + + + {/* 批量删除按钮*/} + +
    +
    +
    + +
    + {filteredGroups.length > 0 ? ( + filteredGroups.map(([prompt, snapshots]) => ( +
    +
    handleToggleGroup(prompt)}> +
    +

    {prompt}

    +

    {snapshots.length} 次执行

    +
    +
    + {snapshots.length > 1 && expandedGroup === prompt && ( + + )} + +
    +
    + {expandedGroup === prompt && ( +
    + {snapshots.map(query => ( +
    +
    +
    + handleSelectSnapshot(query.id)} + className="mr-4 h-4 w-4 text-primary focus:ring-primary/50 border-gray-300 rounded" + /> +

    执行于: {new Date(query.queryTime).toLocaleString()}

    +
    + {/* 展示真实的查询详情信息 */} +
    + 耗时: {query.executionTime} + 大模型: {query.model} + 数据库: {query.database} + 所属对话: "{query.conversationId}" +
    +
    +
    + + + + +
    +
    + ))} +
    + )} +
    + )) + ) : ( +
    + +

    未找到匹配的查询记录

    +
    + )} +
    + + {/* 查询详情弹窗 */} + setViewingQuery(null)} + > + {viewingQuery && ( +
    + +
    + )} +
    + + {/* 通用确认弹窗(支持单删/批量删/重新执行) */} + +

    {confirmModalContent.message}

    + +
    + + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/HistorySidebar.tsx b/src/springboot_demo/frontend/components/HistorySidebar.tsx new file mode 100644 index 00000000..93d8f634 --- /dev/null +++ b/src/springboot_demo/frontend/components/HistorySidebar.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import { Conversation } from '../types'; + +interface HistorySidebarProps { + isOpen: boolean; + onClose: () => void; + conversations: Conversation[]; + currentConversationId: string; + onSwitchConversation: (id: string) => void; + onNewConversation: () => void; + onDeleteConversation: (id: string) => void; +} + +export const HistorySidebar: React.FC = ({ + isOpen, + onClose, + conversations, + currentConversationId, + onSwitchConversation, + onNewConversation, + onDeleteConversation +}) => { + // 新增:删除确认弹窗状态 + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleteTargetId, setDeleteTargetId] = useState(null); + + const sidebarClasses = ` + bg-white border-l border-gray-200 h-full flex flex-col + transition-transform duration-300 ease-in-out + fixed top-0 right-0 z-40 transform + ${isOpen ? 'translate-x-0' : 'translate-x-full'} + w-80 shadow-lg + `; + + const innerContentClasses = `w-80 h-full flex flex-col overflow-hidden`; + + return ( + <> + {isOpen &&
    } + + + + {/* 新增:删除确认弹窗 */} + {showDeleteConfirm && ( +
    +
    +

    确认删除

    +

    + 确定要删除这条对话吗?删除后无法恢复。 +

    +
    + + +
    +
    +
    + )} + + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/LoginPage.tsx b/src/springboot_demo/frontend/components/LoginPage.tsx new file mode 100644 index 00000000..9238d268 --- /dev/null +++ b/src/springboot_demo/frontend/components/LoginPage.tsx @@ -0,0 +1,211 @@ +import React, { useState } from 'react'; +import { UserRole } from '../types'; +import { authApi, LoginRequest } from '../services/api'; + +interface LoginPageProps { + onLogin: (role: UserRole) => void; +} + +const roles: { id: UserRole; name: string; icon: string }[] = [ + { id: 'sys-admin', name: '系统管理员', icon: 'fa-shield' }, + { id: 'data-admin', name: '数据管理员', icon: 'fa-database' }, + { id: 'normal-user', name: '普通用户', icon: 'fa-user' }, +]; + + +export const LoginPage: React.FC = ({ onLogin }) => { + const [selectedRole, setSelectedRole] = useState('sys-admin'); + const [isForgotModalOpen, setForgotModalOpen] = useState(false); + const [resetEmailSent, setResetEmailSent] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + + const handleLogin = async (e: React.MouseEvent) => { + e.preventDefault(); + setError(null); + + if (!username.trim() || !password.trim()) { + setError('请输入用户名和密码'); + return; + } + + setIsLoading(true); + try { + const loginRequest: LoginRequest = { + username: username.trim(), + password: password, + }; + + const response = await authApi.login(loginRequest); + + // 根据角色ID映射到前端角色 + // 假设: 1=系统管理员, 2=数据管理员, 3=普通用户 + let role: UserRole = 'normal-user'; + if (response.roleId === 1) { + role = 'sys-admin'; + } else if (response.roleId === 2) { + role = 'data-admin'; + } else if (response.roleId === 3) { + role = 'normal-user'; + } + + onLogin(role); + } catch (err) { + setError(err instanceof Error ? err.message : '登录失败,请检查用户名和密码'); + } finally { + setIsLoading(false); + } + }; + + const handleRequestReset = (e: React.FormEvent) => { + e.preventDefault(); + setResetEmailSent(true); + setTimeout(() => { + handleCloseForgotModal(); + }, 3000); + }; + + const handleCloseForgotModal = () => { + setForgotModalOpen(false); + setTimeout(() => setResetEmailSent(false), 300); + }; + + const illustrationSvg = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbD0iI0VBRUlGRiIgZD0iTTQzMyAxNDFhMTQgMTQgMCAwIDAtMTQgMTQuMDQ0djE5OC45MTJBMTQgMTQgMCAwIDAgNDMzIDM2OGgxNFYxNDFoLTE0eiIvPjxwYXRoIGZpbGw9IiNDRUQ4RkYiIGQ9Ik0zMzUgMTg0YTQgNCAwIDAgMC00IDR2MTM2YTQgNCAwIDAgMCA4IDBWMTg4YTQgNCAwIDAgMC00LTR6TTI1NSA5N2E0IDQgMCAwIDAtNCA0djI1MGE0IDQgMCAwIDAgOCAwdl0yNTBhNCA0IDAgMCAwLTQtNHpNMzc1IDIyN2E0IDQgMCAwIDAtNCA0djg5YTQgNCAwIDAgMCA4IDB2LTg5YTQgNCAwIDAgMC00LTR6Ii8+PHBhdGggZmlsbD0iI0VBRUlGRiIgZD0iTTgxIDIwMWExNCAxNCAwIDAgMC0xNCAxNC4wNDR2MTM3LjkyQTE0IDE0IDAgMCAwIDgxIDM2OGgxNFYyMDFIODF6Ii8+PHBhdGggZmlsbD0iI0NFRDhGRiIgZD0iTTE5NSA2MWExNCAxNCAwIDAgMC0xNCAxNC4wNDR2MjgwLjkyQTE0IDE0IDAgMCAwIDE5NSAzNjhIMTlWMjYwaDE2NHYtNTZIMTlWOTFoMTc2VjYxaC0xNHptLTE2NCAxODVWMjEzaDE2NFYxNTVIMzF2NDh6bTE2NC05NVY5OWgtMTZWNzdhNCA0IDAgMCAwLTQtNFY2MWgtNDB2MTJhNCA0IDAgMCAwLTQgN3YxNkgzMXY0MGgxNjR6Ii8+PHBhdGggZmlsbD0iI0VBRUlGRiIgZD0iTTI5NiAyNTlhMTQgMTQgMCAwIDAtMTQgMTQuMDQ0djk0LjkyQTE0IDE0IDAgMCAwIDI5NiAzODJoMTRWMjU5aC0xNHpNMzU2IDMwOWExNCAxNCAwIDAgMC0xNCAxNC4wNDR2NTUuOTJBMTQgMTQgMCAwIDAgMzU2IDM5MWgxNFYzMDloLTM1eiIvPjxwYXRoIGZpbGw9IiNDRUQ4RkYiIGQ9Ik0xMzUgMTI0YTQgNCAwIDAgMC00IDR2MjE3YTQgNCAwIDAgMCA4IDBWMTE4YTQgNCAwIDAgMC00LTR6Ii8+PHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTQyMyAzODRINjlhMTQgMTQgMCAwIDAtMTQgMTR2NTRoMzg1VjM5OGExNCAxNCAwIDAgMC0xNC0xNHoiLz48cGF0aCBmaWxsPSIjQ0VEOEZGIiBkPSJNMzY4IDQyNEgxNDR2MTZoMjI0di0xNnpNMzYxIDQwNUg4NmExNCAxNCAwIDAgMCAwIDI4aDI3NWExNCAxNCAwIDAgMCAwLTI4eiIvPjxwYXRoIGZpbGw9IiNGNEY5RkYiIGQ9Ik01NSAzOTIuMTY0VjM5OGExNCAxNCAwIDAgMCAxNCAxNGg1NDFBMTQgMTQgMCAwIDEgNDM3IDQyNkg1OFY0MDZoMzAzdjEyaDI0di0xMmgyOHYxMmgyNHYtMTJoLTh2LTEySDU4djEyLjE2NEExMy45IDEzLjkgMCAwIDEgNTUgMzkyLjE2NHptMCAzMS44MzZWMzk4YTE0IDE0IDAgMCAxIDE0LTE0aDM1N2EzMyAzMyAwIDAgMSAzMyAzM2gtNDIydi0zMmgzMDN2LTguMTY0QTEzLjkgMTMuOSAwIDAgMSA0MjQgNDI0eiIvPjxwYXRoIGZpbGw9IiM0Q0I2RkYiIGQ9Ik0yMTEgMjg4aC0zM2wtMzUtOTBoLTI4bDQ3IDEyM2g0OGwxNi00NGg0NmwtOCA0NGg0Mmw0Ny0xMjNoLTM4bC0yMyA2M2gtNDVsMTYtNDVaIi8+PHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTk1IDQ1MmgyNHYyNEg5NXptMjA4IDBoMjR2MjRIMzAzem0xMjAgMGgyNHYyNGgtMjR6Ii8+PC9zdmc+"; + + return ( +
    +
    +
    +
    +
    + +

    自然语言查询系统

    +
    +

    欢迎回来

    +

    请选择您的角色并登录。

    +
    + +
    +
    + +
    + {roles.map((role) => ( + + ))} +
    +
    +
    +
    + + setUsername(e.target.value)} + className="w-full px-4 py-3 pl-10 bg-gray-50 text-dark border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary placeholder-gray-400" + /> +
    +
    +
    +
    + + setPassword(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleLogin(e as any)} + className="w-full px-4 py-3 pl-10 bg-gray-50 text-dark border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary placeholder-gray-400" + /> +
    +
    + {error && ( +
    + {error} +
    + )} +
    + + +
    +
    +
    + © 2024 Your Company. All Rights Reserved. +
    +
    + +
    +
    +

    智能数据,一语洞穿

    +

    + 无需复杂的SQL,只需用您最熟悉的自然语言提问,即可获得精准的数据洞察、直观的可视化图表,并轻松与团队分享。 +

    +
      +
    • 自然语言查询
    • +
    • 智能图表生成
    • +
    • 结果轻松分享
    • +
    +
    +
    + Data Analysis Illustration +
    +
    +
    + + {/* 忘记密码模态框 */} + {isForgotModalOpen && ( +
    +
    e.stopPropagation()}> + {!resetEmailSent ? ( + <> +

    重置密码

    +
    +

    请输入您注册时使用的邮箱地址,我们将向您发送一封密码重置邮件。

    +
    + + +
    +
    + + +
    +
    + + ) : ( +
    +
    + +
    +

    已发送

    +

    如果该邮箱地址已注册,一封包含密码重置链接的邮件已经发送到您的邮箱。

    +
    + )} +
    +
    + )} +
    + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/Modal.tsx b/src/springboot_demo/frontend/components/Modal.tsx new file mode 100644 index 00000000..16850394 --- /dev/null +++ b/src/springboot_demo/frontend/components/Modal.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useState } from 'react'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title?: string; + hideTitle?: boolean; + children: React.ReactNode; + contentClassName?: string; // 外部自定义样式(会覆盖默认) +} + +export const Modal: React.FC = ({ + isOpen, + onClose, + title, + hideTitle = false, + children, + contentClassName = "" +}) => { + const [isRendered, setIsRendered] = useState(isOpen); + + useEffect(() => { + if (isOpen) { + setIsRendered(true); + } else { + const timer = setTimeout(() => setIsRendered(false), 300); + return () => clearTimeout(timer); + } + }, [isOpen]); + + if (!isRendered) { + return null; + } + + // 容器样式(保持居中逻辑不变) + const modalContainerClass = `fixed inset-0 bg-black/50 flex flex-col items-center justify-center z-50 transition-opacity duration-300 ${ + isOpen ? 'opacity-100' : 'opacity-0' + }`; + + // 核心修改:添加默认宽度 max-w-md(和你原来的一样),同时让 contentClassName 能覆盖它 + // 注意类的顺序:默认样式在前,自定义样式在后(后定义的会覆盖前定义的) + const modalContentClass = ` + bg-white rounded-xl p-6 w-full mx-4 my-auto transition-all duration-300 + max-w-md // 默认宽度 + max-h-[90vh] // 弹窗最大高度(限制上限) + height: 100% // 让内容容器高度充满可用空间 + ${isOpen ? 'scale-100 opacity-100' : 'scale-95 opacity-0'} + ${contentClassName} + `; + + return ( +
    +
    e.stopPropagation()}> + {!hideTitle && ( +
    +

    {title}

    + +
    + )} + {children} +
    +
    + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/NotificationsPage.tsx b/src/springboot_demo/frontend/components/NotificationsPage.tsx new file mode 100644 index 00000000..fee2ee8f --- /dev/null +++ b/src/springboot_demo/frontend/components/NotificationsPage.tsx @@ -0,0 +1,156 @@ +import React, { useState } from 'react'; +import { MOCK_NOTIFICATIONS } from '../constants'; +import { Notification } from '../types'; +import { Modal } from './Modal'; // 引入Modal组件 + +const NotificationIcon: React.FC<{ type: 'share' | 'system' }> = ({ type }) => { + switch (type) { + case 'share': + return ; + case 'system': + return ; + default: + return null; + } +}; + +const NotificationItem: React.FC<{ notification: Notification; onToggleRead: (id: string) => void; onDelete: (id: string) => void; }> = ({ notification, onToggleRead, onDelete }) => { + const { id, type, title, content, timestamp, isRead, isPinned } = notification; + + return ( +
  • +
    + + {isPinned && } +
    +
    +

    {title}

    +

    {content}

    + {new Date(timestamp).toLocaleString()} +
    +
    + + {!isPinned && ( + + )} +
    +
  • + ); +}; + + +export const NotificationsPage: React.FC = () => { + const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS); + // 新增:删除确认弹窗状态 + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleteTargetId, setDeleteTargetId] = useState(null); + + const pinnedNotifications = notifications.filter(n => n.isPinned); + const regularNotifications = notifications.filter(n => !n.isPinned); + + const handleToggleRead = (id: string) => { + setNotifications( + notifications.map(n => n.id === id ? { ...n, isRead: !n.isRead } : n) + ); + }; + + const handleMarkAllRead = () => { + setNotifications(notifications.map(n => ({ ...n, isRead: true }))); + }; + + // 修改:点击删除按钮时显示弹窗 + const handleDelete = (id: string) => { + setDeleteTargetId(id); + setShowDeleteConfirm(true); + }; + + // 新增:确认删除后执行的逻辑 + const handleConfirmDelete = () => { + if (deleteTargetId) { + setNotifications(notifications.filter(n => n.id !== deleteTargetId)); + } + setShowDeleteConfirm(false); + setDeleteTargetId(null); + }; + + const handleClearAll = () => { + setNotifications(notifications.filter(n => n.isPinned)); + }; + + return ( +
    +
    +
    + + +
    +
    + + {pinnedNotifications.length > 0 && ( +
    +

    置顶通知

    +
    +
      + {pinnedNotifications.map(n => ( + + ))} +
    +
    +
    + )} + +
    +

    + {pinnedNotifications.length > 0 ? '普通通知' : ''} +

    +
    +
      + {regularNotifications.length > 0 ? regularNotifications.map(n => ( + + )) : ( +
      + +

      没有新的通知

      +
      + )} +
    +
    +
    + + {/* 新增:删除确认弹窗 */} + { + setShowDeleteConfirm(false); + setDeleteTargetId(null); + }} + title="确认删除" + > +
    +

    确定要删除这条通知吗?删除后无法恢复。

    +
    + + +
    +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/PlaceholderPage.tsx b/src/springboot_demo/frontend/components/PlaceholderPage.tsx new file mode 100644 index 00000000..11c299f7 --- /dev/null +++ b/src/springboot_demo/frontend/components/PlaceholderPage.tsx @@ -0,0 +1,18 @@ + +import React from 'react'; + +interface PlaceholderPageProps { + title: string; +} + +export const PlaceholderPage: React.FC = ({ title }) => { + return ( +
    +
    + +

    {title}

    +

    此页面正在建设中。

    +
    +
    + ); +}; diff --git a/src/springboot_demo/frontend/components/QueryPage.tsx b/src/springboot_demo/frontend/components/QueryPage.tsx new file mode 100644 index 00000000..e0ff69f9 --- /dev/null +++ b/src/springboot_demo/frontend/components/QueryPage.tsx @@ -0,0 +1,260 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Conversation, MessageRole, QueryResultData } from '../types'; +import { ChatMessage } from './ChatMessage'; +import { Dropdown } from './Dropdown'; +import { MODEL_OPTIONS, DATABASE_OPTIONS } from '../constants'; +import { HistorySidebar } from './HistorySidebar'; +import { RightSidebar } from './RightSidebar'; +import { queryApi, QueryResponse } from '../services/api'; + +interface QueryPageProps { + currentConversation: Conversation | undefined; + onToggleHistory: () => void; + isHistoryOpen: boolean; + onAddMessage: (role: MessageRole, content: string | QueryResultData) => void; + onSaveQuery: (query: QueryResultData) => void; + onShareQuery: (queryId: string, friendId: string) => void; + savedQueries: QueryResultData[]; + initialPrompt?: string; + onClearInitialPrompt: () => void; + conversations: Conversation[]; + currentConversationId: string; // 当前激活的对话ID(关键) + onSwitchConversation: (id: string) => void; + onNewConversation: () => void; + onDeleteConversation: (id: string) => void; +} + +export const QueryPage: React.FC = ({ + currentConversation, + onToggleHistory, + isHistoryOpen, + onAddMessage, + onSaveQuery, + onShareQuery, + savedQueries, + initialPrompt, + onClearInitialPrompt, + conversations, + currentConversationId, // 接收当前对话ID + onSwitchConversation, + onNewConversation, + onDeleteConversation, +}) => { + const [prompt, setPrompt] = useState(''); + const [selectedModel, setSelectedModel] = useState(MODEL_OPTIONS[0].name); + const [selectedDatabase, setSelectedDatabase] = useState(DATABASE_OPTIONS[0].name); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [abortController, setAbortController] = useState(null); + // 新增:记录当前正在处理的请求对应的对话ID + const [pendingConversationId, setPendingConversationId] = useState(null); + const chatContainerRef = useRef(null); + + // 自动滚动到底部 + useEffect(() => { + if (chatContainerRef.current) { + chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; + } + }, [currentConversation?.messages]); + + // 处理初始提示 + useEffect(() => { + if (initialPrompt) { + setPrompt(initialPrompt); + const syntheticEvent = { preventDefault: () => {} } as React.FormEvent; + handleSubmit(syntheticEvent, initialPrompt); + onClearInitialPrompt(); + } + }, [initialPrompt, onClearInitialPrompt]); + + // 关键修改1:当对话ID变化时,自动中断当前请求 + useEffect(() => { + // 如果正在加载中,且当前对话ID与请求时的ID不一致,中断请求 + if (isLoading && pendingConversationId && pendingConversationId !== currentConversationId) { + handleStop(); + } + }, [currentConversationId, isLoading, pendingConversationId]); + + const handleSubmit = async (e: React.FormEvent, customPrompt?: string) => { + e.preventDefault(); + const finalPrompt = customPrompt || prompt; + if (!finalPrompt.trim() || isLoading) return; + + // 1. 记录当前请求对应的对话ID(关键) + const requestConversationId = currentConversationId; + setPendingConversationId(requestConversationId); + + // 2. 创建中断控制器 + const controller = new AbortController(); + setAbortController(controller); + + onAddMessage('user', finalPrompt); + setPrompt(''); + setIsLoading(true); + setError(null); + + try { + if (!currentConversation) throw new Error("No active conversation."); + + // 3. 调用后端API + const response: QueryResponse = await queryApi.execute({ + userPrompt: finalPrompt, + model: selectedModel, + database: selectedDatabase, + conversationId: currentConversation.id !== 'conv-1' ? currentConversation.id : undefined, + }); + + // 4. 将后端响应转换为前端格式 + const result: QueryResultData = { + id: response.id, + userPrompt: response.userPrompt, + sqlQuery: response.sqlQuery, + conversationId: response.conversationId, + queryTime: response.queryTime, + executionTime: response.executionTime, + database: response.database, + model: response.model, + tableData: response.tableData, + chartData: response.chartData ? { + type: (response.chartData.type || 'bar') as 'bar' | 'line' | 'pie', + labels: response.chartData.labels || [], + datasets: response.chartData.datasets || [], + } : undefined, + }; + + // 5. 关键校验:只有当前对话仍为请求时的对话,才添加AI回复 + if (currentConversationId === requestConversationId) { + onAddMessage('ai', result); + } else { + // 对话已切换,丢弃回复(可选:添加日志便于调试) + console.log(`AI回复已丢弃(目标对话已切换):原对话ID=${requestConversationId},新对话ID=${currentConversationId}`); + } + } catch (err) { + // 6. 错误处理也需校验对话ID + if (err instanceof Error && err.name === 'AbortError') { + // 仅在原对话中显示"已停止"提示 + if (currentConversationId === requestConversationId) { + onAddMessage('ai', '查询已被手动停止'); + } + } else { + const errorMessage = err instanceof Error ? err.message : '查询失败,请稍后重试'; + // 仅在原对话中显示错误 + if (currentConversationId === requestConversationId) { + setError(errorMessage); + onAddMessage('ai', errorMessage); + } + } + } finally { + // 7. 仅在原对话中重置状态 + if (currentConversationId === requestConversationId) { + setIsLoading(false); + setAbortController(null); + setPendingConversationId(null); + } + } + }; + + // 停止请求 + const handleStop = () => { + if (abortController) { + abortController.abort(); // 触发中断 + setIsLoading(false); + setAbortController(null); + setPendingConversationId(null); + } + }; + + const handleRecommendationClick = (recommendation: string) => { + const fakeEvent = { preventDefault: () => {} } as React.FormEvent; + handleSubmit(fakeEvent, recommendation); + }; + + return ( +
    +
    +
    + {/* Chat Area */} +
    + {currentConversation?.messages.map((msg, index) => ( + + ))} + {isLoading && ( +
    +
    + +
    +
    +
    +
    + 正在生成结果... +
    +
    +
    + )} +
    + + {/* Input Area */} +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + setModal(null)} title="确认删除通知"> +

    您确定要删除通知 "{currentItem?.title}" 吗?

    +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/admin/SystemLogPage.tsx b/src/springboot_demo/frontend/components/admin/SystemLogPage.tsx new file mode 100644 index 00000000..b2fbc6f6 --- /dev/null +++ b/src/springboot_demo/frontend/components/admin/SystemLogPage.tsx @@ -0,0 +1,130 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { AdminModal } from './AdminModal'; +import { SystemLog } from '../../types'; +import { MOCK_SYSTEM_LOGS } from '../../constants'; + +interface SystemLogPageProps { + initialStatusFilter: string; + clearInitialFilter: () => void; +} + +export const SystemLogPage: React.FC = ({ initialStatusFilter, clearInitialFilter }) => { + const [logs, setLogs] = useState(MOCK_SYSTEM_LOGS); + const [filters, setFilters] = useState({ startDate: '', endDate: '', user: '', action: '', status: initialStatusFilter }); + const [isExportModalOpen, setExportModalOpen] = useState(false); + const [viewingLog, setViewingLog] = useState(null); + + useEffect(() => { + // Clear the initial filter from the parent so it's not reapplied on re-renders + if (initialStatusFilter) { + clearInitialFilter(); + } + }, [initialStatusFilter, clearInitialFilter]); + + const filteredLogs = useMemo(() => { + return logs.filter(log => + (!filters.startDate || log.time >= filters.startDate) && + (!filters.endDate || log.time <= `${filters.endDate} 23:59:59`) && + (!filters.user || log.user.toLowerCase().includes(filters.user.toLowerCase())) && + (!filters.action || log.action.toLowerCase().includes(filters.action.toLowerCase())) && + (!filters.status || log.status === filters.status) + ); + }, [logs, filters]); + + const handleFilterChange = (e: React.ChangeEvent) => { + setFilters(prev => ({ ...prev, [e.target.name]: e.target.value })); + }; + + const resetFilters = () => { + setFilters({ startDate: '', endDate: '', user: '', action: '', status: '' }); + }; + + const handleExport = () => { + alert('日志已开始导出...'); + setExportModalOpen(false); + } + + return ( +
    +
    + +
    +
    +
    +
    + +
    -
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    + + + + {filteredLogs.map(log => ( + + + + + + + + + + + ))} + +
    ID操作时间操作用户操作内容涉及模型IP地址状态操作
    {log.id}{log.time}{log.user}{log.action}{log.model}{log.ip}{log.status === 'success' ? '成功' : '失败'} + {log.status === 'failure' && log.details && ( + + )} +
    +
    +

    显示 {filteredLogs.length} 条,共 {logs.length} 条

    +
    + + setExportModalOpen(false)} title="导出系统日志"> +
    { e.preventDefault(); handleExport(); }} className="space-y-4"> +
    + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + setViewingLog(null)} title={`日志详情 (ID: ${viewingLog?.id})`}> +
    +

    错误信息

    +
    +                        {viewingLog?.details}
    +                    
    +
    +
    + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/admin/UserManagementPage.tsx b/src/springboot_demo/frontend/components/admin/UserManagementPage.tsx new file mode 100644 index 00000000..fc232973 --- /dev/null +++ b/src/springboot_demo/frontend/components/admin/UserManagementPage.tsx @@ -0,0 +1,263 @@ +import React, { useState, useMemo, useCallback } from 'react'; +import { AdminModal } from './AdminModal'; +import { AdminUser, UserRole } from '../../types'; + +const MOCK_USERS: AdminUser[] = [ + { id: 1, username: 'admin', role: 'sys-admin', email: 'admin@example.com', regTime: '2024-01-15', status: 'active' }, + { id: 2, username: 'zhangsan', role: 'normal-user', email: 'zhangsan@example.com', regTime: '2024-03-22', status: 'active' }, + { id: 3, username: 'lisi', role: 'data-admin', email: 'lisi@example.com', regTime: '2024-05-10', status: 'disabled' }, + { id: 4, username: 'wangwu', role: 'normal-user', email: 'wangwu@example.com', regTime: '2024-06-01', status: 'active' }, + { id: 5, username: 'zhaoliu', role: 'data-admin', email: 'zhaoliu@example.com', regTime: '2024-07-11', status: 'active' }, +]; + +export const UserManagementPage: React.FC = () => { + const [users, setUsers] = useState(MOCK_USERS); + const [filters, setFilters] = useState({ search: '', role: '', status: '' }); + const [selectedUserIds, setSelectedUserIds] = useState>(new Set()); + const [modal, setModal] = useState<'add' | 'edit' | 'confirmDelete' | 'confirmBatch' | 'confirmResetPassword' | 'confirmToggleStatus' | null>(null); + const [userToProcess, setUserToProcess] = useState(null); + const [batchAction, setBatchAction] = useState<'enable' | 'disable' | 'delete' | ''>(''); + + const filteredUsers = useMemo(() => { + return users.filter(user => + (user.username.toLowerCase().includes(filters.search.toLowerCase()) || user.email.toLowerCase().includes(filters.search.toLowerCase())) && + (filters.role ? user.role === filters.role : true) && + (filters.status ? user.status === filters.status : true) + ); + }, [users, filters]); + + const handleFilterChange = (e: React.ChangeEvent) => { + setFilters(prev => ({ ...prev, [e.target.name]: e.target.value })); + }; + + const handleSelectAll = (e: React.ChangeEvent) => { + if (e.target.checked) { + setSelectedUserIds(new Set(filteredUsers.map(u => u.id))); + } else { + setSelectedUserIds(new Set()); + } + }; + + const handleSelectUser = (id: number, checked: boolean) => { + const newSet = new Set(selectedUserIds); + if (checked) { + newSet.add(id); + } else { + newSet.delete(id); + } + setSelectedUserIds(newSet); + }; + + const requestDeleteUser = useCallback((user: AdminUser) => { + setUserToProcess(user); + setModal('confirmDelete'); + }, []); + + const confirmDeleteUser = () => { + if (userToProcess) { + setUsers(prev => prev.filter(u => u.id !== userToProcess.id)); + } + setModal(null); + }; + + const requestToggleUserStatus = useCallback((user: AdminUser) => { + setUserToProcess(user); + setModal('confirmToggleStatus'); + }, []); + + const confirmToggleUserStatus = () => { + if(userToProcess) { + setUsers(prev => prev.map(u => u.id === userToProcess.id ? { ...u, status: u.status === 'active' ? 'disabled' : 'active' } : u)); + } + setModal(null); + }; + + const handleApplyBatchAction = () => { + if (!batchAction || selectedUserIds.size === 0) return; + setModal('confirmBatch'); + }; + + const confirmBatchAction = () => { + if (batchAction === 'delete') { + setUsers(prev => prev.filter(u => !selectedUserIds.has(u.id))); + } else { + setUsers(prev => prev.map(u => selectedUserIds.has(u.id) ? { ...u, status: batchAction === 'enable' ? 'active' : 'disabled' } : u)); + } + setSelectedUserIds(new Set()); + setModal(null); + }; + + const requestEditUser = useCallback((user: AdminUser) => { + setUserToProcess(user); + setModal('edit'); + }, []); + + const requestAddUser = () => { + setUserToProcess(null); + setModal('add'); + }; + + const handleSaveUser = useCallback((e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + if (modal === 'add') { + const password = formData.get('password') as string; + if (password.length < 6) { + alert('密码至少需要6位!'); + return; + } + const newUser: AdminUser = { + id: Date.now(), + username: formData.get('username') as string, + email: formData.get('email') as string, + role: formData.get('role') as UserRole, + regTime: new Date().toISOString().split('T')[0], + status: 'active', + }; + setUsers(prev => [newUser, ...prev]); + + } else if (modal === 'edit' && userToProcess) { + const updatedUser = { + ...userToProcess, + username: formData.get('username') as string, + email: formData.get('email') as string, + role: formData.get('role') as UserRole, + }; + setUsers(prev => prev.map(u => u.id === updatedUser.id ? updatedUser : u)); + } + + setModal(null); + }, [modal, userToProcess]); + + const requestResetPassword = useCallback((user: AdminUser) => { + setUserToProcess(user); + setModal('confirmResetPassword'); + }, []); + + const confirmResetPassword = () => { + if (userToProcess) { + alert(`已向用户 ${userToProcess.username} (${userToProcess.email}) 发送密码重置邮件。`); + } + setModal(null); + }; + + const getRoleName = (role: UserRole) => ({ 'sys-admin': '系统管理员', 'data-admin': '数据管理员', 'normal-user': '普通用户' }[role]); + const getRoleClass = (role: UserRole) => ({ 'sys-admin': 'bg-primary/10 text-primary', 'data-admin': 'bg-success/10 text-success', 'normal-user': 'bg-secondary/10 text-secondary' }[role]); + + return ( +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + + +
    +
    + +
    +
    + + + + + + + + + + + + + + {filteredUsers.map(user => ( + + + + + + + + + + ))} + +
    0 && selectedUserIds.size === filteredUsers.length} className="w-4 h-4 text-primary focus:ring-primary/30" />用户名角色邮箱注册时间状态操作
    handleSelectUser(user.id, e.target.checked)} className="w-4 h-4 text-primary focus:ring-primary/30" />{user.username}{getRoleName(user.role)}{user.email}{user.regTime}{user.status === 'active' ? '正常' : '禁用'} +
    + + + + +
    +
    +
    +

    显示 {filteredUsers.length} 条,共 {users.length} 条

    +
    + + setModal(null)} title={modal === 'add' ? '添加新用户' : '编辑用户信息'}> +
    +
    +
    + {modal === 'add' && <> +
    + } +
    + + +
    +
    + + +
    +
    +
    + + setModal(null)} title="确认删除用户"> +

    您确定要删除用户 "{userToProcess?.username}" 吗?此操作不可撤销。

    +
    + + +
    +
    + + setModal(null)} title="确认重置密码"> +

    您确定要为用户 "{userToProcess?.username}" 重置密码吗?系统将向其邮箱发送一封密码重置邮件。

    +
    + + +
    +
    + + setModal(null)} title={`确认${userToProcess?.status === 'active' ? '禁用' : '启用'}用户`}> +

    您确定要{userToProcess?.status === 'active' ? '禁用' : '启用'}用户 "{userToProcess?.username}" 吗?

    +
    + + +
    +
    + + setModal(null)} title="确认批量操作"> +

    您确定要对选中的 {selectedUserIds.size} 个用户执行 "{ {enable: '启用', disable: '禁用', delete: '删除'}[batchAction] }" 操作吗?

    +
    + + +
    +
    +
    + ); +}; diff --git a/src/springboot_demo/frontend/components/data-admin/ConnectionLogPage.tsx b/src/springboot_demo/frontend/components/data-admin/ConnectionLogPage.tsx new file mode 100644 index 00000000..ae3f16ca --- /dev/null +++ b/src/springboot_demo/frontend/components/data-admin/ConnectionLogPage.tsx @@ -0,0 +1,169 @@ +import React, { useState, useMemo } from 'react'; +import { AdminModal } from '../admin/AdminModal'; +import { ConnectionLog } from '../../types'; +import { MOCK_CONNECTION_LOGS } from '../../constants'; + +export const ConnectionLogPage: React.FC = () => { + const [logs, setLogs] = useState(MOCK_CONNECTION_LOGS); + const [isExportModalOpen, setExportModalOpen] = useState(false); + const [viewingLog, setViewingLog] = useState(null); + // 新增搜索相关状态 + const [searchTerm, setSearchTerm] = useState(''); + const [searchType, setSearchType] = useState<'all' | 'time' | 'datasource' | 'status'>('all'); + + const getStatusClass = (status: '成功' | '失败') => { + return status === '成功' ? 'text-success' : 'text-danger'; + }; + + const handleExport = () => { + alert('日志已开始导出...'); + setExportModalOpen(false); + }; + + // 搜索筛选逻辑 + const filteredLogs = useMemo(() => { + if (!searchTerm.trim()) return logs; + + const term = searchTerm.toLowerCase(); + return logs.filter(log => { + switch (searchType) { + case 'time': + return new Date(log.time).toLocaleString().toLowerCase().includes(term); + case 'datasource': + return log.datasource.toLowerCase().includes(term); + case 'status': + return log.status.toLowerCase().includes(term); + default: // 'all' + return ( + new Date(log.time).toLocaleString().toLowerCase().includes(term) || + log.datasource.toLowerCase().includes(term) || + log.status.toLowerCase().includes(term) + ); + } + }); + }, [logs, searchTerm, searchType]); + + return ( +
    + {/* 修改顶部区域,添加搜索功能 */} +
    +
    + setSearchTerm(e.target.value)} + className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50" + /> + +
    + {/* 导出按钮靠右放置 */} + +
    + +
    +
    + + + + + + + + + + + {/* 使用筛选后的日志列表 */} + {filteredLogs.map(log => ( + + + + + + + ))} + {/* 无搜索结果时显示 */} + {filteredLogs.length === 0 && ( + + + + )} + +
    时间数据源状态操作
    {new Date(log.time).toLocaleString()}{log.datasource}{log.status} + {log.status === '失败' && log.details && ( + + )} +
    + 没有找到匹配的日志记录 +
    +
    +
    + + setExportModalOpen(false)} title="导出连接日志"> +
    { e.preventDefault(); handleExport(); }} className="space-y-4"> +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + + +
    +
    +
    + + setViewingLog(null)} title={`连接失败详情 (${viewingLog?.datasource})`}> +
    +

    错误信息

    +
    +                        {viewingLog?.details}
    +                    
    +
    +
    + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/data-admin/DataAdminDashboardPage.tsx b/src/springboot_demo/frontend/components/data-admin/DataAdminDashboardPage.tsx new file mode 100644 index 00000000..3d168b60 --- /dev/null +++ b/src/springboot_demo/frontend/components/data-admin/DataAdminDashboardPage.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { DataAdminPageType } from '../../types'; +import { MOCK_DATASOURCES, MOCK_CONNECTION_LOGS, MOCK_PERMISSION_LOGS, MOCK_QUERY_LOAD } from '../../constants'; +import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'; +import { Pie, Bar } from 'react-chartjs-2'; + +ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement); + +// Reusable StatCard component specific for this dashboard +const StatCard: React.FC<{ title: string; value: string; icon: string; color: string; onClick: () => void; }> = ({ title, value, icon, color, onClick }) => ( +
    +
    +
    +

    {title}

    +

    {value}

    +
    +
    + +
    +
    +
    +); + +// Helper to format timestamp into a relative string +const formatTimeAgo = (timestamp: string): string => { + const now = new Date(); + const then = new Date(timestamp); + const diffInSeconds = Math.round((now.getTime() - then.getTime()) / 1000); + + if (diffInSeconds < 60) return `${diffInSeconds}秒前`; + const diffInMinutes = Math.round(diffInSeconds / 60); + if (diffInMinutes < 60) return `${diffInMinutes}分钟前`; + const diffInHours = Math.round(diffInMinutes / 60); + if (diffInHours < 24) return `${diffInHours}小时前`; + const diffInDays = Math.round(diffInHours / 24); + return `${diffInDays}天前`; +}; + +export const DataAdminDashboardPage: React.FC<{ setActivePage: (page: DataAdminPageType) => void; }> = ({ setActivePage }) => { + // Data processing for cards and charts + const connectedCount = MOCK_DATASOURCES.filter(ds => ds.status === 'connected').length; + const errorCount = MOCK_CONNECTION_LOGS.filter(log => log.status === '失败').length; + const pendingRequests = 3; // Mock value + + const healthStatusCounts = MOCK_DATASOURCES.reduce((acc, ds) => { + const statusMap = { connected: '已连接', disconnected: '未连接', error: '错误', testing: '测试中', disabled: '已禁用' }; + const status = statusMap[ds.status]; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, {} as Record); + + const healthStatusData = { + labels: Object.keys(healthStatusCounts), + datasets: [{ + data: Object.values(healthStatusCounts), + backgroundColor: ['#00B42A', '#86909C', '#F53F3F', '#36BFFA', '#C9CDD4'], + borderWidth: 0, + }], + }; + + const queryLoadData = { + labels: MOCK_QUERY_LOAD.labels, + datasets: [{ + label: '查询量', + data: MOCK_QUERY_LOAD.data, + backgroundColor: '#165DFF', + borderRadius: 4, + }], + }; + + const recentFailures = MOCK_CONNECTION_LOGS.filter(log => log.status === '失败').slice(0, 5); + + return ( +
    + +
    + setActivePage('datasource')} /> + setActivePage('datasource')} /> + setActivePage('connection-log')} /> + setActivePage('user-permission')} /> +
    + +
    +
    +

    数据源健康状态

    +
    +
    +
    +

    数据源查询量 Top 5

    +
    +
    +
    + +
    +
    +

    近期连接失败日志

    +
    + {recentFailures.length > 0 ? recentFailures.map(log => ( +
    +
    +

    {log.datasource}: {log.note}

    +

    {log.time}

    +
    + +
    + )) :

    无失败记录

    } +
    +
    +
    +

    近期权限变更动态

    +
    + {MOCK_PERMISSION_LOGS.slice(0, 4).map(log => ( +
    + +
    +

    +

    {formatTimeAgo(log.timestamp)}

    +
    +
    + ))} +
    +
    +
    +
    + ); +}; diff --git a/src/springboot_demo/frontend/components/data-admin/DataAdminNotificationPage.tsx b/src/springboot_demo/frontend/components/data-admin/DataAdminNotificationPage.tsx new file mode 100644 index 00000000..f32c01e9 --- /dev/null +++ b/src/springboot_demo/frontend/components/data-admin/DataAdminNotificationPage.tsx @@ -0,0 +1,107 @@ +import React, { useState, useMemo } from 'react'; +import { AdminModal } from '../admin/AdminModal'; +import { AdminNotification } from '../../types'; +import { MOCK_DATA_ADMIN_NOTIFICATIONS } from '../../constants'; + +export const DataAdminNotificationPage: React.FC = () => { + const [notifications, setNotifications] = useState(MOCK_DATA_ADMIN_NOTIFICATIONS); + const [modal, setModal] = useState<'add' | 'edit' | 'delete' | null>(null); + const [currentItem, setCurrentItem] = useState(null); + + const sortedNotifications = useMemo(() => { + return [...notifications].sort((a, b) => (b.pinned ? 1 : -1) - (a.pinned ? 1 : -1) || new Date(b.publishTime).getTime() - new Date(a.publishTime).getTime()); + }, [notifications]); + + const openModal = (type: 'add' | 'edit' | 'delete', item?: AdminNotification) => { + setCurrentItem(item || null); + setModal(type); + }; + + const handleTogglePin = (item: AdminNotification) => { + setNotifications(prev => prev.map(n => n.id === item.id ? { ...n, pinned: !n.pinned } : n)); + }; + + const handleSave = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + if (modal === 'add') { + const newNotif: AdminNotification = { + id: Date.now(), + title: formData.get('title') as string, + content: formData.get('content') as string, + dataSourceTopic: formData.get('dataSourceTopic') as string, + role: 'all', // Data admins typically notify all or data-related roles + priority: 'normal', + pinned: formData.get('pinned') === 'on', + publisher: '数据管理员', + publishTime: new Date().toISOString().split('T')[0], + status: 'published', + }; + setNotifications(prev => [newNotif, ...prev]); + } else if (modal === 'edit' && currentItem) { + const updated = { + ...currentItem, + title: formData.get('title') as string, + content: formData.get('content') as string, + dataSourceTopic: formData.get('dataSourceTopic') as string, + pinned: formData.get('pinned') === 'on', + }; + setNotifications(prev => prev.map(n => n.id === currentItem.id ? updated : n)); + } + setModal(null); + }; + + const confirmDelete = () => { + if (currentItem) { + setNotifications(prev => prev.filter(n => n.id !== currentItem.id)); + } + setModal(null); + }; + + return ( +
    +
    + +
    + +
    +
    + + + + {sortedNotifications.map(n => ( + + + + + + + + ))} + +
    标题数据源主题发布者发布时间操作
    {n.title} {n.pinned && }{n.dataSourceTopic}{n.publisher}{n.publishTime} + + + +
    +
    +
    + + setModal(null)} title={modal === 'add' ? '发布新通知' : '编辑通知'}> +
    +
    +
    +
    +
    +
    +
    +
    + + setModal(null)} title="确认删除通知"> +

    您确定要删除通知 "{currentItem?.title}" 吗?

    +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/data-admin/DataSourceManagementPage.tsx b/src/springboot_demo/frontend/components/data-admin/DataSourceManagementPage.tsx new file mode 100644 index 00000000..a3a9928d --- /dev/null +++ b/src/springboot_demo/frontend/components/data-admin/DataSourceManagementPage.tsx @@ -0,0 +1,172 @@ +import React, { useState, useMemo } from 'react'; +import { AdminModal } from '../admin/AdminModal'; +import { DataSource } from '../../types'; +import { MOCK_DATASOURCES } from '../../constants'; + +export const DataSourceManagementPage: React.FC = () => { + const [dataSources, setDataSources] = useState(MOCK_DATASOURCES); + const [filters, setFilters] = useState({ search: '', type: '', status: '' }); + const [modal, setModal] = useState<'add' | 'edit' | 'delete' | 'confirmDisable' | null>(null); + const [currentItem, setCurrentItem] = useState(null); + + const filteredDataSources = useMemo(() => { + return dataSources.filter(ds => + ds.name.toLowerCase().includes(filters.search.toLowerCase()) && + (filters.type ? ds.type === filters.type : true) && + (filters.status ? ds.status === filters.status : true) + ); + }, [dataSources, filters]); + + const handleFilterChange = (e: React.ChangeEvent) => { + setFilters(prev => ({ ...prev, [e.target.name]: e.target.value })); + }; + + const resetFilters = () => setFilters({ search: '', type: '', status: '' }); + + const openModal = (type: 'add' | 'edit' | 'delete', item?: DataSource) => { + setCurrentItem(item || null); + setModal(type); + }; + + const handleSave = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const data = Object.fromEntries(formData.entries()); + + if (modal === 'add') { + const newDataSource: DataSource = { + id: `ds-${Date.now()}`, + name: data.name as string, + type: data.type as any, + address: `${data.host}:${data.port}`, + status: 'disconnected', + }; + setDataSources(prev => [newDataSource, ...prev]); + } else if (modal === 'edit' && currentItem) { + setDataSources(prev => prev.map(ds => ds.id === currentItem.id ? { + ...ds, + name: data.name as string, + type: data.type as any, + address: `${data.host}:${data.port}`, + } : ds)); + } + setModal(null); + }; + + const confirmDelete = () => { + if (currentItem) { + setDataSources(prev => prev.filter(ds => ds.id !== currentItem.id)); + } + setModal(null); + }; + + const handleTestConnection = (id: string) => { + setDataSources(prev => prev.map(ds => ds.id === id ? { ...ds, status: 'testing' } : ds)); + setTimeout(() => { + setDataSources(prev => prev.map(ds => { + if (ds.id === id) { + const statuses: DataSource['status'][] = ['connected', 'error']; + return { ...ds, status: statuses[Math.floor(Math.random() * 2)] }; + } + return ds; + })); + }, 2000); + }; + + const requestToggleDisable = (dataSource: DataSource) => { + setCurrentItem(dataSource); + setModal('confirmDisable'); + }; + + const confirmToggleDisable = () => { + if (currentItem) { + setDataSources(prev => prev.map(ds => { + if (ds.id === currentItem.id) { + return { ...ds, status: ds.status === 'disabled' ? 'disconnected' : 'disabled' }; + } + return ds; + })); + } + setModal(null); + }; + + const getStatusChip = (status: DataSource['status']) => { + const styles = { + connected: 'bg-success/10 text-success', + disconnected: 'bg-gray-100 text-gray-600', + error: 'bg-danger/10 text-danger', + testing: 'bg-blue-100 text-blue-600 animate-pulse', + disabled: 'bg-gray-200 text-gray-700' + }; + const text = { + connected: '已连接', + disconnected: '未连接', + error: '连接错误', + testing: '测试中...', + disabled: '已禁用' + }; + return {text[status]} + }; + + return ( +
    + + +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + + + + {filteredDataSources.map(ds => ( + + + + + ))} + +
    数据源名称数据库类型连接地址状态操作
    {ds.name}{ds.type}{ds.address}{getStatusChip(ds.status)} + + + + +
    +
    +
    + + setModal(null)} title={modal === 'add' ? '添加数据源' : '编辑数据源'}> +
    +
    +
    +
    +
    +
    +
    +
    + + setModal(null)} title="确认删除数据源"> +

    您确定要删除数据源 "{currentItem?.name}" 吗?此操作不可撤销。

    +
    +
    + + setModal(null)} title={`确认${currentItem?.status === 'disabled' ? '启用' : '禁用'}数据源`}> +

    您确定要{currentItem?.status === 'disabled' ? '启用' : '禁用'}数据源 "{currentItem?.name}" 吗?

    +
    + + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/data-admin/UserPermissionPage.tsx b/src/springboot_demo/frontend/components/data-admin/UserPermissionPage.tsx new file mode 100644 index 00000000..4bbab325 --- /dev/null +++ b/src/springboot_demo/frontend/components/data-admin/UserPermissionPage.tsx @@ -0,0 +1,365 @@ +import React, { useState, useMemo } from 'react'; +import { AdminModal } from '../admin/AdminModal'; +import { UserPermissionAssignment, UnassignedUser, DataSourcePermission } from '../../types'; + +const MOCK_UNASSIGNED_USERS: UnassignedUser[] = [ + { id: 'user-3', username: '赵文琪', email: 'zhaoliu@example.com', regTime: '2024-08-01' }, + { id: 'user-4', username: '孙七', email: 'sunqi@example.com', regTime: '2024-08-05' }, +]; + +const MOCK_ASSIGNED_PERMISSIONS: UserPermissionAssignment[] = [ + { + id: 'perm-1', + userId: 'user-1', + username: '李瑜清', + permissions: [ + { dataSourceId: 'ds-1', dataSourceName: '销售数据库', tables: ['orders', 'customers'] }, + { dataSourceId: 'ds-2', dataSourceName: '产品数据库', tables: ['products'] }, + ] + }, + { + id: 'perm-2', + userId: 'user-2', + username: '马芳琼', + permissions: [ + { dataSourceId: 'ds-1', dataSourceName: '销售数据库', tables: ['orders'] } + ] + }, +]; + +const MOCK_DATASOURCES = [ + { id: 'ds-1', name: '销售数据库', tables: ['orders', 'customers', 'sales_report', 'inventory'] }, + { id: 'ds-2', name: '产品数据库', tables: ['products', 'categories', 'reviews'] }, + { id: 'ds-3', name: '用户数据库', tables: ['users', 'profiles', 'login_history'] }, +]; + +// 定义支持的搜索类别 +type SearchCategory = 'all' | 'username' | 'email' | 'datasource' | 'table'; + +export const UserPermissionPage: React.FC = () => { + const [unassignedUsers, setUnassignedUsers] = useState(MOCK_UNASSIGNED_USERS); + const [assignedPermissions, setAssignedPermissions] = useState(MOCK_ASSIGNED_PERMISSIONS); + const [selectedUserIds, setSelectedUserIds] = useState>(new Set()); + const [modal, setModal] = useState<'assign' | 'manage' | null>(null); + const [currentItem, setCurrentItem] = useState(null); + const [usersToAssign, setUsersToAssign] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(''); + const [searchCategory, setSearchCategory] = useState('all'); + + // 2. 优化:按「搜索类别+关键词」过滤待分配用户 + const filteredUnassignedUsers = useMemo(() => { + const keyword = searchKeyword.toLowerCase().trim(); + if (!keyword) return unassignedUsers; + + return unassignedUsers.filter(user => { + switch (searchCategory) { + case 'username': + return user.username.toLowerCase().includes(keyword); + case 'email': + return user.email.toLowerCase().includes(keyword); + case 'all': + return user.username.toLowerCase().includes(keyword) || user.email.toLowerCase().includes(keyword); + default: + return false; + } + }); + }, [unassignedUsers, searchKeyword, searchCategory]); + + // 3. 优化:按「搜索类别+关键词」过滤已分配用户 + const filteredAssignedPermissions = useMemo(() => { + const keyword = searchKeyword.toLowerCase().trim(); + if (!keyword) return assignedPermissions; + + return assignedPermissions.filter(assignment => { + switch (searchCategory) { + case 'username': + return assignment.username.toLowerCase().includes(keyword); + case 'email': // 已分配用户无邮箱字段,不匹配 + return false; + case 'datasource': + return assignment.permissions.some(perm => perm.dataSourceName.toLowerCase().includes(keyword)); + case 'table': + return assignment.permissions.some(perm => perm.tables.some(table => table.toLowerCase().includes(keyword))); + case 'all': // 全部:匹配用户名/数据源/表名 + return assignment.username.toLowerCase().includes(keyword) || + assignment.permissions.some(perm => perm.dataSourceName.toLowerCase().includes(keyword)) || + assignment.permissions.some(perm => perm.tables.some(table => table.toLowerCase().includes(keyword))); + default: + return false; + } + }); + }, [assignedPermissions, searchKeyword, searchCategory]); + + const handleSelectAll = (e: React.ChangeEvent) => { + if (e.target.checked) { + setSelectedUserIds(new Set(filteredUnassignedUsers.map(u => u.id))); + } else { + setSelectedUserIds(new Set()); + } + }; + + const handleSelectUser = (id: string, checked: boolean) => { + const newSet = new Set(selectedUserIds); + if (checked) newSet.add(id); + else newSet.delete(id); + setSelectedUserIds(newSet); + }; + + const openAssignModal = (users: UnassignedUser[]) => { + if (users.length === 0) return; + setUsersToAssign(users); + setCurrentItem(null); + setModal('assign'); + }; + + const openManageModal = (permission: UserPermissionAssignment) => { + setCurrentItem(permission); + setModal('manage'); + }; + + const handleSavePermissions = (userIds: string[], permissions: DataSourcePermission[]) => { + if (modal === 'assign') { + const newAssignments: UserPermissionAssignment[] = userIds.map(id => { + const user = unassignedUsers.find(u => u.id === id); + return { + id: `perm-${Date.now()}-${id}`, + userId: id, + username: user?.username || '未知用户', + permissions: permissions.filter(p => p.tables.length > 0) + }; + }); + + setAssignedPermissions(prev => [...prev, ...newAssignments]); + setUnassignedUsers(prev => prev.filter(u => !userIds.includes(u.id))); + setSelectedUserIds(new Set()); + } else if (modal === 'manage' && currentItem) { + setAssignedPermissions(prev => prev.map(p => p.id === currentItem.id ? { + ...p, + permissions: permissions.filter(p => p.tables.length > 0) + } : p)); + } + + setModal(null); + }; + + const PermissionModal: React.FC<{ + users: { id: string, username: string }[]; + existingPermissions?: DataSourcePermission[]; + onSave: (userIds: string[], permissions: DataSourcePermission[]) => void; + onClose: () => void; + }> = ({ users, existingPermissions = [], onSave, onClose }) => { + const [perms, setPerms] = useState( + MOCK_DATASOURCES.map(ds => { + const existing = existingPermissions.find(p => p.dataSourceId === ds.id); + return { + dataSourceId: ds.id, + dataSourceName: ds.name, + tables: existing ? [...existing.tables] : [] + }; + }) + ); + + const handleTableToggle = (dsId: string, table: string, checked: boolean) => { + setPerms(prev => prev.map(p => { + if (p.dataSourceId === dsId) { + const newTables = new Set(p.tables); + if (checked) newTables.add(table); + else newTables.delete(table); + return { ...p, tables: Array.from(newTables) }; + } + return p; + })); + }; + + const handleSelectAllTables = (dsId: string, checked: boolean) => { + const ds = MOCK_DATASOURCES.find(d => d.id === dsId); + if(!ds) return; + setPerms(prev => prev.map(p => p.dataSourceId === dsId ? { ...p, tables: checked ? ds.tables : [] } : p)); + }; + + const title = users.length > 1 ? `为 ${users.length} 位用户分配权限` : `为 ${users[0].username} 分配权限`; + const isEditing = existingPermissions.length > 0; + + return ( + +
    + {MOCK_DATASOURCES.map(ds => { + const currentPerm = perms.find(p => p.dataSourceId === ds.id); + const allTablesForDs = ds.tables; + const allSelected = currentPerm ? currentPerm.tables.length === allTablesForDs.length : false; + + return ( +
    +

    {ds.name}

    +
    + +
    + {allTablesForDs.map(table => ( + + ))} +
    +
    +
    + ) + })} +
    +
    + + +
    +
    + ); + }; + + return ( +
    +
    + + setSearchKeyword(e.target.value)} + className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50" + /> +
    + +
    +
    +

    待分配权限用户 ({filteredUnassignedUsers.length})

    + +
    +
    + + + + + + + + + + + + {filteredUnassignedUsers.map(user => ( + + + + + + + + ))} + {filteredUnassignedUsers.length === 0 && ( + + + + )} + +
    + 0 && selectedUserIds.size === filteredUnassignedUsers.length} + /> + 用户名邮箱注册时间操作
    + handleSelectUser(user.id, e.target.checked)} + /> + {user.username}{user.email}{user.regTime} + +
    未找到匹配的待分配用户
    +
    +
    + +
    +

    已分配权限用户 ({filteredAssignedPermissions.length})

    +
    + + + + + + + + + + {filteredAssignedPermissions.map(p => ( + + + + + + ))} + {filteredAssignedPermissions.length === 0 && ( + + + + )} + +
    用户名数据源权限操作
    {p.username} + {p.permissions.map(perm => ( +
    + {perm.dataSourceName}: + {perm.tables.join(', ')} +
    + ))} +
    + +
    未找到匹配的已分配权限用户
    +
    +
    + + {(modal === 'assign' && usersToAssign.length > 0) && ( + setModal(null)} + /> + )} + + {(modal === 'manage' && currentItem) && ( + setModal(null)} + /> + )} +
    + ); +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/constants.ts b/src/springboot_demo/frontend/constants.ts new file mode 100644 index 00000000..560457e5 --- /dev/null +++ b/src/springboot_demo/frontend/constants.ts @@ -0,0 +1,270 @@ +import { Conversation, QueryResultData, Notification, UserProfile, Friend, FriendRequest, ModelOption, AdminNotification, SystemLog, DataSource, ConnectionLog, PermissionLog, QueryShare } from './types'; + +// Used in QueryPage.tsx +export const MODEL_OPTIONS: ModelOption[] = [ + { name: 'gemini-2.5-pro', disabled: false, description: '最强大的模型,用于复杂查询' }, + { name: 'gemini-2.5-flash', disabled: false, description: '速度最快的模型,用于快速响应' }, + { name: 'GPT-4', disabled: false, description: 'OpenAI 的先进模型' }, + { name: 'GLM-4.6', disabled: false, description: '智谱AI GLM-4 (即将支持)' }, + { name: 'qwen3-max', disabled: false, description: '阿里通义千问 (即将支持)' }, + { name: 'kimi-k2-0905-preview', disabled: false, description: 'kimi-k2-0905-preview K2 (即将支持)' }, +]; + +export const DATABASE_OPTIONS: ModelOption[] = [ + { name: '销售数据库', disabled: false, description: '包含订单、客户和销售数据' }, + { name: '用户数据库', disabled: false, description: '包含用户信息和活动日志' }, + { name: '产品数据库', disabled: false, description: '包含产品目录和库存信息' }, +]; + + +// Mocks for normal user view +export const MOCK_INITIAL_CONVERSATION: Conversation = { + id: 'conv-1', + title: '', + messages: [{ + role: 'ai', + content: '您好!我是数据查询助手,您可以通过自然语言描述您的查询需求(例如:"展示2023年各季度的订单量"),我会为您生成相应的结果。' + }], + createTime: new Date().toISOString(), +}; + +const MOCK_QUERY_RESULT_1: QueryResultData = { + id: 'query-1', + userPrompt: '展示2023年各季度的订单量', + sqlQuery: "SELECT strftime('%Y-Q', order_date) as quarter, COUNT(order_id) as order_count FROM orders WHERE strftime('%Y', order_date) = '2023' GROUP BY quarter ORDER BY quarter;", + conversationId: 'conv-1', + queryTime: new Date('2023-11-20T10:30:00Z').toISOString(), + executionTime: '0.8秒', + tableData: { + headers: ['季度', '订单量', '同比增长'], + rows: [ + ['2023-Q1', '1,200', '+15%'], + ['2023-Q2', '1,550', '+18%'], + ['2023-Q3', '1,400', '+12%'], + ['2023-Q4', '1,850', '+25%'] + ] + }, + chartData: { + type: 'bar', + labels: ['2023-Q1', '2023-Q2', '2023-Q3', '2023-Q4'], + datasets: [{ + label: '订单量', + data: [1200, 1550, 1400, 1850], + backgroundColor: 'rgba(22, 93, 255, 0.6)', + }] + }, + database:"销售数据库", + model:"gemini-2.5-pro", +}; + +const MOCK_QUERY_RESULT_2: QueryResultData = { + id: 'query-2', + userPrompt: '展示2023年各季度的订单量', // Same prompt + sqlQuery: "SELECT strftime('%Y-Q', order_date) as quarter, COUNT(order_id) as order_count FROM orders WHERE strftime('%Y', order_date) = '2023' GROUP BY quarter ORDER BY quarter;", + conversationId: 'conv-2', // Different conversation + queryTime: new Date('2023-11-21T11:00:00Z').toISOString(), // Later time + executionTime: '0.9秒', + tableData: { + headers: ['季度', '订单量', '同比增长'], + rows: [ + // ['2023-Q1', '1,200', '+15%'] is now deleted + ['2023-Q2', '1,600', '+20%'], // Changed value + ['2023-Q3', '1,400', '+12%'], // Same value + ['2023-Q4', '1,850', '+25%'], // Same value + ['2023-Q5 (预测)', '2,100', '+30%'] // Added row + ] + }, + chartData: { + type: 'bar', + labels: ['2023-Q2', '2023-Q3', '2023-Q4', '2023-Q5 (预测)'], // Changed labels + datasets: [{ + label: '订单量', + data: [1600, 1400, 1850, 2100], // Changed data + backgroundColor: 'rgba(54, 162, 235, 0.6)', + }] + }, + database:"销售数据库", + model:"qwen3-max", +}; + +const MOCK_QUERY_RESULT_3: QueryResultData = { + id: 'query-3', + userPrompt: '统计每月新增用户数', // A different prompt + sqlQuery: "SELECT strftime('%Y-%m', registration_date) as month, COUNT(user_id) as new_users FROM users GROUP BY month;", + conversationId: 'conv-3', + queryTime: new Date('2023-11-22T09:00:00Z').toISOString(), + executionTime: '1.1秒', + tableData: { + headers: ['月份', '新增用户数'], + rows: [ + ['2023-09', '5,200'], + ['2023-10', '6,100'], + ] + }, + chartData: { + type: 'line', + labels: ['2023-09', '2023-10'], + datasets: [{ + label: '新增用户数', + data: [5200, 6100], + backgroundColor: 'rgba(75, 192, 192, 0.6)', + }] + }, + database:"产品数据库", + model:"qwen3-max", +} + +const MOCK_QUERY_RESULT_4: QueryResultData = { + id: 'query-4', + userPrompt: '各产品线销售额占比', + sqlQuery: "SELECT product_line, SUM(sales) as total_sales FROM sales_by_product_line GROUP BY product_line;", + conversationId: 'conv-4', + queryTime: new Date('2023-11-23T14:00:00Z').toISOString(), + executionTime: '0.7秒', + tableData: { + headers: ['产品线', '销售额', '占比'], + rows: [ + ['电子产品', '550,000', '55%'], + ['家居用品', '250,000', '25%'], + ['服装配饰', '200,000', '20%'] + ] + }, + chartData: { + type: 'pie', + labels: ['电子产品', '家居用品', '服装配饰'], + datasets: [{ + label: '销售额', + data: [550000, 250000, 200000], + backgroundColor: [ + 'rgba(22, 93, 255, 0.7)', + 'rgba(54, 162, 235, 0.7)', + 'rgba(255, 206, 86, 0.7)', + ], + }] + }, + database:"用户数据库", + model:"qwen3-max", + +}; + + +export const MOCK_SAVED_QUERIES: QueryResultData[] = [MOCK_QUERY_RESULT_1, MOCK_QUERY_RESULT_2, MOCK_QUERY_RESULT_3, MOCK_QUERY_RESULT_4]; + + +export const MOCK_NOTIFICATIONS: Notification[] = [ + { id: '3', type: 'system', title: '系统将在今晚2点进行维护。', content: '系统将在今晚2点进行维护。', timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), isRead: false, isPinned: true }, + { id: '1', type: 'system', title: '新用户 王小明 已注册。', content: '新用户 王小明 已注册。', timestamp: new Date(Date.now() - 5 * 60 * 1000).toISOString(), isRead: false, isPinned: false }, + { id: '2', type: 'system', title: '模型 Gemini 连接失败。', content: '模型 Gemini 连接失败。', timestamp: new Date(Date.now() - 60 * 60 * 1000).toISOString(), isRead: false, isPinned: false }, + { id: 'share-1', type: 'share', title: '李琪雯 分享了一个查询给你', content: '"展示2023年各季度的订单量"', timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), isRead: false, isPinned: false, fromUser: { name: '李琪雯', avatarUrl: 'https://i.pravatar.cc/150?u=li-si' }, relatedShareId: 'share-1' }, +]; + +export const MOCK_USER_PROFILE: UserProfile = { + id: 'user-001', + userId: 'zhangsan', + name: '李瑜清', + email: 'zhangsan@example.com', + phoneNumber: '13812345678', + avatarUrl: 'https://i.pravatar.cc/150?u=zhang-san', + registrationDate: '2024-03-22', + accountStatus: 'normal', + preferences: { + defaultModel: 'gemini-2.5-pro', + defaultDatabase: '销售数据库', + }, +}; + +export const MOCK_FRIENDS_LIST: Friend[] = [{ id: 'friend-1', name: '李琪雯', avatarUrl: 'https://i.pravatar.cc/150?u=li-si', isOnline: true,email:'Maem12129@gmail.com'} + , + { id: 'friend-2', name: '马芳琼', avatarUrl: 'https://i.pravatar.cc/150?u=wang-wu', isOnline: false,email:'DonQuixote@gmail.com' }, +]; + +export const MOCK_FRIEND_REQUESTS: FriendRequest[] = [ + { id: 'req-1', fromUser: { name: '赵文琪', avatarUrl: 'https://i.pravatar.cc/150?u=zhao-liu' }, timestamp: '2小时前' } +]; + +export const MOCK_QUERY_SHARES: QueryShare[] = [ + { + id: 'share-1', + sender: { id: 'friend-1', name: '李琪雯', avatarUrl: 'https://i.pravatar.cc/150?u=li-si', isOnline: true ,email:'Maem12129@gmail.com'}, + recipientId: 'user-001', + querySnapshot: MOCK_QUERY_RESULT_1, + timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), + status: 'unread', + }, + { + id: 'share-2', + sender: { id: 'friend-2', name: '马芳琼', avatarUrl: 'https://i.pravatar.cc/150?u=wang-wu', isOnline: false ,email:'DonQuixote@gmail.com'}, + recipientId: 'user-001', + querySnapshot: MOCK_QUERY_RESULT_3, + timestamp: new Date(Date.now() - 28 * 60 * 60 * 1000).toISOString(), + status: 'read', + } +]; + +// Used in RightSidebar.tsx +export const COMMON_RECOMMENDATIONS = [ + '近7天用户增长趋势', + '上个季度各产品线销售额对比', + '查询华东地区销量最高的产品', + '统计每月新增用户数' +]; + +export const MOCK_SUCCESS_SUGGESTIONS = [ + '按地区细分订单量', + '与去年同期数据进行对比', + '分析各季度订单的平均金额', +]; + +export const MOCK_FAILURE_SUGGESTIONS = [ + '换一种更简单的问法', + '检查是否选择了正确的数据源', + '尝试询问“你能做什么?”', +]; + +// Mocks for Admin Panel +export const MOCK_ADMIN_NOTIFICATIONS: AdminNotification[] = [ + { id: 1, title: '系统将于今晚23:00进行升级维护', content: '...', role: 'all', priority: 'urgent', pinned: true, publisher: '系统管理员', publishTime: '2025-10-28 18:00', status: 'published' }, + { id: 2, title: '【草稿】新功能发布预告', content: '...', role: 'normal-user', priority: 'normal', pinned: false, publisher: '系统管理员', publishTime: '2025-10-27 09:00', status: 'draft' }, +]; + +export const MOCK_DATA_ADMIN_NOTIFICATIONS: AdminNotification[] = [ + { id: 1, title: '销售数据库表结构变更', content: '`orders` 表新增 `discount_rate` 字段。', role: 'data-admin', priority: 'important', pinned: false, publisher: '李琪雯', publishTime: '2025-10-28 10:00', status: 'published', dataSourceTopic: '销售数据库' }, + { id: 2, title: '用户数据库计划下线旧表', content: '`users_old` 表将于11月30日下线,请及时迁移。', role: 'all', priority: 'normal', pinned: false, publisher: '李琪雯', publishTime: '2025-10-26 15:00', status: 'published', dataSourceTopic: '用户数据库' }, +]; + +export const MOCK_SYSTEM_LOGS: SystemLog[] = [ + { id: '#LOG001', time: '2025-10-29 14:32:18', user: '李瑜清', action: '执行自然语言查询', model: 'gemini-2.5-pro', ip: '192.168.1.102', status: 'success' }, + { id: '#LOG002', time: '2025-10-29 14:28:45', user: '李琪雯', action: '添加MySQL数据源', model: '-', ip: '192.168.1.105', status: 'success' }, + { id: '#LOG003', time: '2025-10-29 14:25:10', user: '李瑜清', action: '执行自然语言查询', model: 'GPT-4', ip: '192.168.1.102', status: 'failure', details: 'Error: API call to OpenAI failed with status 429 - Too Many Requests. Please check your plan and billing details.' }, + { id: '#LOG004', time: '2025-10-29 13:50:21', user: '未知用户', action: '尝试登录系统', model: '-', ip: '203.0.113.45', status: 'failure', details: 'Authentication failed: Invalid credentials provided for user "unknown".' }, + { id: '#LOG005', time: '2025-10-29 12:15:05', user: 'admin', action: '更新用户角色', model: '-', ip: '127.0.0.1', status: 'success' }, +]; + +// Mocks for Data Admin Panel +export const MOCK_DATASOURCES: DataSource[] = [ + { id: 'ds-1', name: '销售数据库', type: 'MySQL', address: '192.168.1.101:3306', status: 'connected' }, + { id: 'ds-2', name: '用户数据库', type: 'PostgreSQL', address: '192.168.1.102:5432', status: 'connected' }, + { id: 'ds-3', name: '产品数据库', type: 'MySQL', address: '192.168.1.103:3306', status: 'error' }, + { id: 'ds-4', name: '日志数据库', type: 'SQL Server', address: '192.168.1.104:1433', status: 'disconnected' }, + { id: 'ds-5', name: '库存数据库', type: 'Oracle', address: '192.168.1.105:1521', status: 'disabled' }, +]; + +export const MOCK_CONNECTION_LOGS: ConnectionLog[] = [ + { id: 'log-1', time: '2023-06-15 16:42:30', datasource: '销售数据库', status: '成功' }, + { id: 'log-2', time: '2023-06-15 14:20:15', datasource: '产品数据库', status: '失败', details: "Error: Connection timed out after 15000ms. Could not connect to 192.168.1.103:3306. Please check network connectivity and firewall rules." }, + { id: 'log-3', time: '2023-06-14 11:05:42', datasource: '用户数据库', status: '成功' }, + { id: 'log-4', time: '2023-06-13 09:30:18', datasource: '日志数据库', status: '失败', details: "SQLSTATE[28000]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Login failed for user 'log_reader'." }, + { id: 'log-5', time: new Date(Date.now() - 2 * 60 * 1000).toISOString(), datasource: '产品数据库', status: '失败', details: "Error: Access denied for user 'prod_user'@'localhost' (using password: YES)" } +]; + +export const MOCK_PERMISSION_LOGS: PermissionLog[] = [ + { id: 'plog-1', timestamp: new Date(Date.now() - 5 * 60 * 1000).toISOString(), text: '管理员 李琪雯 授予 李瑜清 "销售数据库" 的访问权限。' }, + { id: 'plog-2', timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), text: '管理员 李琪雯 撤销了 马芳琼 对 "产品数据库" 的所有权限。' }, + { id: 'plog-3', timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), text: '管理员 李琪雯 修改了 李瑜清 对 "销售数据库" 的 orders 表权限。' }, + { id: 'plog-4', timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), text: '系统自动为新用户 赵文琪 分配了默认权限。' }, +]; + +export const MOCK_QUERY_LOAD = { + labels: ['销售数据库', '用户数据库', '产品数据库', '日志数据库', '库存数据库'], + data: [1250, 890, 650, 320, 150] +}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/env.d.ts b/src/springboot_demo/frontend/env.d.ts new file mode 100644 index 00000000..391b89c9 --- /dev/null +++ b/src/springboot_demo/frontend/env.d.ts @@ -0,0 +1,14 @@ +/// + +interface ImportMetaEnv { + // 与 .env.local 中的变量一一对应 + readonly VITE_GEMINI_API_KEY: string; + readonly VITE_OPENAI_API_KEY: string; + readonly VITE_GLM_API_KEY: string; + readonly VITE_QWEN_API_KEY: string; + readonly VITE_KIMI_API_KEY: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} \ No newline at end of file diff --git a/src/springboot_demo/frontend/index.html b/src/springboot_demo/frontend/index.html new file mode 100644 index 00000000..749fd6d4 --- /dev/null +++ b/src/springboot_demo/frontend/index.html @@ -0,0 +1,66 @@ + + + + + + 自然语言数据库查询系统 + + + + + + + + +
    + + + \ No newline at end of file diff --git a/src/springboot_demo/frontend/index.tsx b/src/springboot_demo/frontend/index.tsx new file mode 100644 index 00000000..aaa0c6e4 --- /dev/null +++ b/src/springboot_demo/frontend/index.tsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); diff --git a/src/springboot_demo/frontend/metadata.json b/src/springboot_demo/frontend/metadata.json new file mode 100644 index 00000000..c73209e9 --- /dev/null +++ b/src/springboot_demo/frontend/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Natural Language Database Query System", + "description": "A web application that allows users to query databases using natural language. It provides a chat-based interface, displays results in tables and charts, and manages conversation history.", + "requestFramePermissions": [] +} \ No newline at end of file diff --git a/src/springboot_demo/frontend/package-lock.json b/src/springboot_demo/frontend/package-lock.json new file mode 100644 index 00000000..1ec8261f --- /dev/null +++ b/src/springboot_demo/frontend/package-lock.json @@ -0,0 +1,2630 @@ +{ + "name": "natural-language-database-query-system", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "natural-language-database-query-system", + "version": "0.0.0", + "dependencies": { + "@google/genai": "^1.28.0", + "chart.js": "^4.5.1", + "openai": "^6.8.1", + "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.28.0", + "resolved": "https://registry.npmmirror.com/@google/genai/-/genai-1.28.0.tgz", + "integrity": "sha512-0pfZ1EWQsM9kINsL+mFKJvpzM6NRHS9t360S1MzKq4JtIwTj/RbsPpC/K5wpKiPy9PC+J+bsz/9gvaL51++KrA==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.20.1" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.43", + "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", + "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.0", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.0.tgz", + "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", + "integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.43", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.23", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz", + "integrity": "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.27.0", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001753", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", + "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.244", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", + "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmmirror.com/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmmirror.com/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/google-logging-utils/-/google-logging-utils-1.1.2.tgz", + "integrity": "sha512-YsFPGVgDFf4IzSwbwIR0iaFJQFmR5Jp7V1WuYSjuRgAm9yWqsMhKE9YPlL+wvFLnc/wMiFV4SQUD9Y/JMpxIxQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/openai": { + "version": "6.8.1", + "resolved": "https://registry.npmmirror.com/openai/-/openai-6.8.1.tgz", + "integrity": "sha512-ACifslrVgf+maMz9vqwMP4+v9qvx5Yzssydizks8n+YUJ6YwUoxj51sKRQ8HYMfR6wgKLSIlaI108ZwCk+8yig==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmmirror.com/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.52.5", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/src/springboot_demo/frontend/package.json b/src/springboot_demo/frontend/package.json new file mode 100644 index 00000000..54b7bc61 --- /dev/null +++ b/src/springboot_demo/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "natural-language-database-query-system", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@google/genai": "^1.28.0", + "chart.js": "^4.5.1", + "openai": "^6.8.1", + "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/src/springboot_demo/frontend/services/api.ts b/src/springboot_demo/frontend/services/api.ts new file mode 100644 index 00000000..73aeccc7 --- /dev/null +++ b/src/springboot_demo/frontend/services/api.ts @@ -0,0 +1,246 @@ +// API 基础配置 +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'; + +// 从localStorage获取token +const getToken = (): string | null => { + return localStorage.getItem('token'); +}; + +// 从localStorage获取userId +const getUserId = (): string | null => { + return localStorage.getItem('userId'); +}; + +// 通用请求函数 +async function request( + endpoint: string, + options: RequestInit = {} +): Promise { + const token = getToken(); + const userId = getUserId(); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + // 添加认证token + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + // 添加userId header(如果后端需要) + if (userId) { + headers['userId'] = userId; + } + + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + ...options, + headers, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: '请求失败' })); + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + // 处理统一的Result格式 + if (data.code !== undefined) { + if (data.code === 200 || data.code === 0) { + return data.data as T; + } else { + throw new Error(data.message || '请求失败'); + } + } + + return data as T; +} + +// ==================== 认证接口 ==================== +export interface LoginRequest { + username: string; + password: string; +} + +export interface LoginResponse { + token: string; + userId: number; + username: string; + email: string; + roleId: number; + roleName: string; + avatarUrl: string; +} + +export const authApi = { + login: async (credentials: LoginRequest): Promise => { + const response = await request('/auth/login', { + method: 'POST', + body: JSON.stringify(credentials), + }); + + // 保存token和用户信息 + if (response.token) { + localStorage.setItem('token', response.token); + localStorage.setItem('userId', String(response.userId)); + localStorage.setItem('username', response.username); + localStorage.setItem('roleId', String(response.roleId)); + localStorage.setItem('roleName', response.roleName || ''); + } + + return response; + }, + + logout: () => { + localStorage.removeItem('token'); + localStorage.removeItem('userId'); + localStorage.removeItem('username'); + localStorage.removeItem('roleId'); + localStorage.removeItem('roleName'); + }, + + isAuthenticated: (): boolean => { + return !!getToken(); + }, +}; + +// ==================== 查询接口 ==================== +export interface QueryRequest { + userPrompt: string; + model: string; + database: string; + conversationId?: string; +} + +export interface QueryResponse { + id: string; + userPrompt: string; + sqlQuery: string; + conversationId: string; + queryTime: string; + executionTime: string; + database: string; + model: string; + tableData: { + headers: string[]; + rows: string[][]; + }; + chartData?: { + type: string; + labels: string[]; + datasets: Array<{ + label: string; + data: number[]; + backgroundColor?: string | string[]; + }>; + }; +} + +export const queryApi = { + execute: async (queryRequest: QueryRequest): Promise => { + return await request('/query/execute', { + method: 'POST', + body: JSON.stringify(queryRequest), + }); + }, +}; + +// ==================== 对话接口 ==================== +export interface DialogRecord { + dialogId: string; + userId: number; + topic: string; + totalRounds: number; + startTime: string; + lastTime: string; +} + +export const dialogApi = { + getList: async (): Promise => { + return await request('/dialog/list'); + }, + + getById: async (dialogId: string): Promise => { + return await request(`/dialog/${dialogId}`); + }, +}; + +// ==================== 用户接口 ==================== +export interface User { + id: number; + username: string; + email: string; + phonenumber: string; + roleId: number; + avatarUrl: string; + status: number; +} + +export const userApi = { + getById: async (id: number): Promise => { + return await request(`/user/${id}`); + }, + + getList: async (): Promise => { + return await request('/user/list'); + }, + + getByUsername: async (username: string): Promise => { + return await request(`/user/username/${username}`); + }, +}; + +// ==================== 数据库连接接口 ==================== +export interface DbConnection { + id: number; + name: string; + dbTypeId: number; + url: string; + username: string; + password?: string; + status: string; + createUserId: number; +} + +export const dbConnectionApi = { + getList: async (): Promise => { + return await request('/db-connection/list'); + }, + + getById: async (id: number): Promise => { + return await request(`/db-connection/${id}`); + }, + + test: async (id: number): Promise => { + return await request(`/db-connection/test/${id}`); + }, +}; + +// ==================== 大模型配置接口 ==================== +export interface LlmConfig { + id: number; + name: string; + version: string; + apiKey?: string; + apiUrl: string; + statusId: number; + isDisabled: number; + timeout: number; +} + +export const llmConfigApi = { + getList: async (): Promise => { + return await request('/llm-config/list'); + }, + + getAvailable: async (): Promise => { + return await request('/llm-config/list/available'); + }, + + getById: async (id: number): Promise => { + return await request(`/llm-config/${id}`); + }, +}; + diff --git a/src/springboot_demo/frontend/tsconfig.json b/src/springboot_demo/frontend/tsconfig.json new file mode 100644 index 00000000..531df722 --- /dev/null +++ b/src/springboot_demo/frontend/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "types": [ + "node" + ], + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": ["*.d.ts", "**/*.ts", "**/*.tsx"] +} \ No newline at end of file diff --git a/src/springboot_demo/frontend/types.ts b/src/springboot_demo/frontend/types.ts new file mode 100644 index 00000000..00b7e4ca --- /dev/null +++ b/src/springboot_demo/frontend/types.ts @@ -0,0 +1,193 @@ +// Basic types +export type UserRole = 'sys-admin' | 'data-admin' | 'normal-user'; +export type MessageRole = 'user' | 'ai'; + +// Page navigation types +export type Page = 'query' | 'history' | 'notifications' | 'account' | 'friends' | 'comparison'; +export type SysAdminPageType = 'dashboard' | 'user-management' | 'notification-management' | 'system-log' | 'llm-config' | 'account'; +export type DataAdminPageType = 'dashboard' | 'query' | 'history' | 'datasource' | 'user-permission' | 'notification-management' | 'connection-log' | 'notifications' | 'account' | 'friends' | 'comparison'; + +// Data structure types +export interface ModelOption { + name: string; + disabled: boolean; + description: string; +} + +export interface ChartData { + type: 'bar' | 'line' | 'pie'; + labels: string[]; + datasets: { + label: string; + data: number[]; + backgroundColor: string | string[]; + }[]; +} + +export interface TableData { + headers: string[]; + rows: string[][]; +} + +export interface QueryResultData { + id: string; + userPrompt: string; + sqlQuery: string; + conversationId: string; + queryTime: string; + executionTime: string; + tableData: TableData; + chartData: ChartData; + database: string; + model:string; +} + +export interface Message { + role: MessageRole; + content: string | QueryResultData; +} + +export interface Conversation { + id: string; + title: string; + messages: Message[]; + createTime: string; +} + +export interface Notification { + id: string; + type: 'system' | 'share'; + title: string; + content: string; + timestamp: string; + isRead: boolean; + isPinned: boolean; + fromUser?: { name: string, avatarUrl: string }; + relatedShareId?: string; +} + +export interface UserProfile { + id: string; + userId: string; + name: string; + email: string; + phoneNumber: string; + avatarUrl: string; + registrationDate: string; + accountStatus: 'normal' | 'disabled'; + preferences: { + defaultModel: string; + defaultDatabase: string; + }; +} + +export interface Friend { + id: string; + name: string; + avatarUrl: string; + isOnline: boolean; + email: string; + remark?: string; +} + +export interface FriendRequest { + id: string; + fromUser: { name: string; avatarUrl: string }; + timestamp: string; +} + +export interface QueryShare { + id: string; + sender: Friend; + recipientId: string; // The ID of the user receiving the share + querySnapshot: QueryResultData; + timestamp: string; + status: 'unread' | 'read'; +} + + +// Admin Panel Types +export interface AdminNotification { + id: number; + title: string; + content: string; + role: 'all' | UserRole; + priority: 'urgent' | 'important' | 'normal'; + pinned: boolean; + publisher: string; + publishTime: string; + status: 'published' | 'draft'; + dataSourceTopic?: string; +} + +export interface AdminUser { + id: number; + username: string; + role: UserRole; + email: string; + regTime: string; + status: 'active' | 'disabled'; +} + +export interface SystemLog { + id: string; + time: string; + user: string; + action: string; + model: string; + ip: string; + status: 'success' | 'failure'; + details?: string; +} + +export interface LLMConfig { + id: string; + name: string; + version: string; + apiKey: string; + endpoint: string; + status: 'available' | 'unstable' | 'unavailable' | 'testing' | 'disabled'; +} + +// Data Admin Types +export interface DataSource { + id: string; + name: string; + type: 'MySQL' | 'PostgreSQL' | 'Oracle' | 'SQL Server'; + address: string; + status: 'connected' | 'disconnected' | 'error' | 'testing' | 'disabled'; +} + +export interface DataSourcePermission { + dataSourceId: string; + dataSourceName: string; + tables: string[]; +} + +export interface UserPermissionAssignment { + id: string; + userId: string; + username: string; + permissions: DataSourcePermission[]; +} + +export interface UnassignedUser { + id: string; + username: string; + email: string; + regTime: string; +} + +export interface ConnectionLog { + id: string; + time: string; + datasource: string; + status: '成功' | '失败'; + details?: string; +} + +export interface PermissionLog { + id: string; + timestamp: string; + text: string; +} \ No newline at end of file diff --git a/src/springboot_demo/frontend/vite.config.ts b/src/springboot_demo/frontend/vite.config.ts new file mode 100644 index 00000000..427839e2 --- /dev/null +++ b/src/springboot_demo/frontend/vite.config.ts @@ -0,0 +1,35 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); // 加载环境变量 + return { + server: { + port: 3000, + host: '0.0.0.0', + proxy: { + // 代理通义千问请求(关键配置) + '/api/qwen': { + target: 'https://dashscope.aliyuncs.com/compatible-mode/v1', + changeOrigin: true, // 开启跨域代理 + rewrite: (path) => path.replace(/^\/api\/qwen/, ''), // 重写路径 + headers: { + // 注入API密钥(仅开发环境安全,生产需用后端代理) + Authorization: `Bearer ${env.VITE_QWEN_API_KEY}`, + }, + }, + }, + }, + plugins: [react()], + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + } + } + }; +}); \ No newline at end of file diff --git a/src/springboot_demo/last.md b/src/springboot_demo/last.md new file mode 100644 index 00000000..b71fb456 --- /dev/null +++ b/src/springboot_demo/last.md @@ -0,0 +1,507 @@ +### 一、完整表/集合清单(按MySQL + MongoDB分类) +#### (一)MySQL 8.4 数据表(结构化数据) +##### 1. 基础信息表(用户、角色、字典) +1.1 users(用户表) +1.2 roles(角色表) +1.3 db_types(数据库类型表) +1.4 notification_targets(通知目标表) +1.5 priorities(通知优先级表) +1.6 error_types(错误类型表) +1.7 llm_status(大模型状态表) + +##### 2. 数据资源表(数据库、表、字段元数据) +2.1 db_connections(数据库连接表) +2.2 table_metadata(表元数据表) +2.3 column_metadata(字段元数据表) + +##### 3. 权限与关系表(用户权限、好友关系) +3.1 user_db_permissions(用户数据权限表) +3.2 friend_relations(好友关系表) +3.3 friend_requests(好友请求表) +3.4 query_shares(查询分享记录表) + +##### 4. 系统日志与监控表 +4.1 system_health(系统健康表) +4.2 operation_logs(系统操作日志表) +4.3 llm_configs(大模型配置表) +4.4 notifications(通知表) +4.5 token_consume(token消耗表) +4.6 error_logs(错误分析表) +4.7 performance_metrics(性能趋势表) +4.8 db_connection_logs(数据库连接日志表) +4.9 query_logs(查询日志表) +4.10 user_searches(用户搜索表) + +#### (二)MongoDB 8.2 集合(非结构化数据) +1. query_collections(收藏查询表-组) +2. collection_records(收藏记录表-具体记录) +3. dialog_records(多轮对话列表) +4. dialog_details(多轮对话具体内容表) +5. sql_cache(SQL缓存集合) +6. ai_interaction_logs(AI交互日志集合) +7. friend_chats(好友聊天记录表) + +--- + +### 二、表/集合详细字段(严格遵循文档) +#### (一)MySQL 数据表字段 +##### 1. 基础信息表 +###### 1.1 users(用户表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|--------------------------|--------------------------------------------|-----------------------------------------------------------| +| id | 用户ID | bigint(20) | 是(自增) | - | 唯一标识用户 | PRIMARY KEY (id) | +| username | 用户名 | varchar(50) | - | - | 登录账号,唯一 | UNIQUE (username), NOT NULL | +| password | 密码 | varchar(100)| - | - | BCrypt加密存储 | NOT NULL | +| email | 邮箱 | varchar(100)| - | - | 用于好友添加、通知 | UNIQUE (email), NOT NULL | +| phonenumber | 手机号 | varchar(50) | - | - | 用户绑定的手机号 | UNIQUE (phonenumber), NOT NULL | +| role_id | 角色ID | tinyint(2) | - | 是(关联roles.id) | 关联角色类型 | NOT NULL, FOREIGN KEY (role_id) REFERENCES roles(id) | +| avatar_url | 头像URL | varchar(255)| - | - | 头像路径,默认"/default-avatar.png" | DEFAULT '/default-avatar.png' | +| status | 账号状态 | tinyint(1) | - | - | 0-禁用,1-正常 | DEFAULT 1, NOT NULL | +| online_status | 在线状态 | tinyint(1) | - | - | 0-离线,1-在线 | DEFAULT 0, NOT NULL | +| create_time | 创建时间 | datetime | - | - | 账号创建时间(系统管理员创建) | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| update_time | 更新时间 | datetime | - | - | 信息更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | + +###### 1.2 roles(角色表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 角色ID | tinyint(2) | 是(自增) | - | 唯一标识角色 | PRIMARY KEY (id) | +| role_name | 角色名称 | varchar(30) | - | - | 如"系统管理员"、"数据管理员"、"普通用户" | UNIQUE (role_name), NOT NULL | +| role_code | 角色编码 | varchar(20) | - | - | 如"sys_admin"、"data_admin"、"normal_user" | UNIQUE (role_code), NOT NULL | +| description | 角色描述 | varchar(500)| - | - | 角色权限范围说明 | - | + +###### 1.3 db_types(数据库类型表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 类型ID | tinyint(2) | 是(自增) | - | 唯一标识数据库类型 | PRIMARY KEY (id) | +| type_name | 类型名称 | varchar(50) | - | - | 如"MySQL"、"MongoDB"、"SQL Server" | UNIQUE (type_name), NOT NULL | +| type_code | 类型编码 | varchar(20) | - | - | 如"mysql"、"mongodb"、"mssql" | UNIQUE (type_code), NOT NULL | +| description | 类型描述 | varchar(500)| - | - | 数据库特性说明 | - | + +###### 1.4 notification_targets(通知目标表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 目标ID | tinyint(2) | 是(自增) | - | 唯一标识通知目标 | PRIMARY KEY (id) | +| target_name | 目标名称 | varchar(30) | - | - | 如"所有用户"、"普通用户" | UNIQUE (target_name), NOT NULL | +| target_code | 目标编码 | varchar(20) | - | - | 如"all"、"normal_user" | UNIQUE (target_code), NOT NULL | +| description | 目标描述 | varchar(200)| - | - | 接收范围说明 | - | + +###### 1.5 priorities(通知优先级表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 优先级ID | tinyint(2) | 是(自增) | - | 唯一标识优先级 | PRIMARY KEY (id) | +| priority_name | 优先级名称 | varchar(20) | - | - | 如"紧急"、"普通"、"低" | UNIQUE (priority_name), NOT NULL | +| priority_code | 优先级编码 | varchar(20) | - | - | 如"urgent"、"normal"、"low" | UNIQUE (priority_code), NOT NULL | +| sort | 排序权重 | int(11) | - | - | 数值越小越优先展示(1-紧急,2-普通) | NOT NULL | + +###### 1.6 error_types(错误类型表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 错误ID | tinyint(2) | 是(自增) | - | 唯一标识错误类型 | PRIMARY KEY (id) | +| error_name | 错误名称 | varchar(50) | - | - | 如"模型调用超时"、"数据库连接错误" | UNIQUE (error_name), NOT NULL | +| error_code | 错误编码 | varchar(50) | - | - | 如"llm_timeout"、"db_connection_error" | UNIQUE (error_code), NOT NULL | +| description | 错误描述 | varchar(500)| - | - | 错误原因说明 | - | + +###### 1.7 llm_status(大模型状态表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 状态ID | tinyint(2) | 是(自增) | - | 唯一标识大模型状态 | PRIMARY KEY (id) | +| status_name | 状态名称 | varchar(20) | - | - | 如"可用"、"不可用"、"不稳定" | UNIQUE (status_name), NOT NULL | +| status_code | 状态编码 | varchar(20) | - | - | 如"available"、"unavailable"、"unstable" | UNIQUE (status_code), NOT NULL | +| description | 状态描述 | varchar(200)| - | - | 状态含义说明(如"可用:API成功率≥95%") | - | + +##### 2. 数据资源表 +###### 2.1 db_connections(数据库连接表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|--------------------------|--------------------------------------------|-----------------------------------------------------------| +| id | 连接ID | bigint(20) | 是(自增) | - | 唯一标识数据库连接 | PRIMARY KEY (id) | +| name | 连接名称 | varchar(100)| - | - | 用户自定义名称(如"订单主库") | UNIQUE (name), NOT NULL | +| db_type_id | 数据库类型ID | tinyint(2) | - | 是(关联db_types.id) | 关联数据库类型 | NOT NULL, FOREIGN KEY (db_type_id) REFERENCES db_types(id) | +| url | 连接地址 | varchar(255)| - | - | 如"192.168.1.101:3306/orders_db" | NOT NULL | +| username | 数据库账号 | varchar(50) | - | - | 访问数据库的账号(加密存储) | NOT NULL | +| password | 数据库密码 | varchar(100)| - | - | 访问数据库的密码(加密存储) | NOT NULL | +| status | 连接状态 | varchar(20) | - | - | "connected"-已连接、"error"-连接错误、"disabled"-已禁用 | DEFAULT 'disconnected', NOT NULL | +| create_user_id | 创建者ID | bigint(20) | - | 是(关联users.id) | 仅数据管理员可创建 | NOT NULL, FOREIGN KEY (create_user_id) REFERENCES users(id) | +| create_time | 创建时间 | datetime | - | - | 连接创建时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| update_time | 更新时间 | datetime | - | - | 连接信息更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | + +###### 2.2 table_metadata(表元数据表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|------------------|-------------|------------|------------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 表ID | bigint(20) | 是(自增) | - | 唯一标识表元数据 | PRIMARY KEY (id) | +| db_connection_id | 数据库连接ID | bigint(20) | - | 是(关联db_connections.id) | 所属数据库连接 | NOT NULL, FOREIGN KEY (db_connection_id) REFERENCES db_connections(id) ON DELETE CASCADE | +| table_name | 表名 | varchar(100)| - | - | 数据库中实际表名(如"orders_2023") | NOT NULL | +| description | 表描述 | varchar(500)| - | - | 用于AI理解表含义(如"存储2023年订单数据") | - | +| create_time | 创建时间 | datetime | - | - | 元数据录入时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| update_time | 更新时间 | datetime | - | - | 元数据更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | + +###### 2.3 column_metadata(字段元数据表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|------------------|-------------|------------|----------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 字段ID | bigint(20) | 是(自增) | - | 唯一标识字段元数据 | PRIMARY KEY (id) | +| table_id | 表ID | bigint(20) | - | 是(关联table_metadata.id) | 所属表 | NOT NULL, FOREIGN KEY (table_id) REFERENCES table_metadata(id) ON DELETE CASCADE | +| column_name | 字段名 | varchar(100)| - | - | 表中实际字段名(如"order_amount") | NOT NULL | +| data_type | 数据类型 | varchar(50) | - | - | 字段数据类型(如"int"、"varchar(50)") | NOT NULL | +| description | 字段描述 | varchar(500)| - | - | 用于AI理解字段含义(如"订单金额,单位:元") | - | +| is_primary | 是否主键 | tinyint(1) | - | - | 0-否,1-是 | DEFAULT 0, NOT NULL | +| create_time | 创建时间 | datetime | - | - | 元数据录入时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +##### 3. 权限与关系表 +###### 3.1 user_db_permissions(用户数据权限表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 权限ID | bigint(20) | 是(自增) | - | 唯一标识权限记录 | PRIMARY KEY (id) | +| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 被授权用户(普通用户) | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | +| permission_details | 权限详情 | json | - | - | 可访问的表列表,格式:[{"db_connection_id":1, "table_ids":[1,2]},...] | NOT NULL | +| last_grant_user_id | 最后授权管理员ID | bigint(20) | - | 是(关联users.id) | 最后修改权限的数据管理员/系统管理员 | NOT NULL, FOREIGN KEY (last_grant_user_id) REFERENCES users(id) | +| is_assigned | 是否已分配 | tinyint(1) | - | - | 0-未分配,1-已分配(默认0) | DEFAULT 0, NOT NULL | +| last_grant_time | 最后授权时间 | datetime | - | - | 最后一次权限修改时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| update_time | 更新时间 | datetime | - | - | 记录更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | + +###### 3.2 friend_relations(好友关系表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 关系ID | bigint(20) | 是(自增) | - | 唯一标识好友关系 | PRIMARY KEY (id) | +| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 主动添加方 | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | +| friend_id | 好友ID | bigint(20) | - | 是(关联users.id) | 被动添加方 | NOT NULL, FOREIGN KEY (friend_id) REFERENCES users(id) ON DELETE CASCADE | +| friend_username | 好友用户名 | varchar(50) | - | - | 冗余存储,便于列表展示 | NOT NULL | +| online_status | 好友在线状态 | tinyint(1) | - | - | 0-离线,1-在线 | DEFAULT 0, NOT NULL | +| remark_name | 备注名 | varchar(50) | - | - | 用户对好友的自定义备注 | - | +| create_time | 添加时间 | datetime | - | - | 好友关系建立时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +###### 3.3 friend_requests(好友请求表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 请求ID | bigint(20) | 是(自增) | - | 唯一标识好友请求 | PRIMARY KEY (id) | +| applicant_id | 申请人ID | bigint(20) | - | 是(关联users.id) | 发起请求的用户 | NOT NULL, FOREIGN KEY (applicant_id) REFERENCES users(id) ON DELETE CASCADE | +| recipient_id | 接收人ID | bigint(20) | - | 是(关联users.id) | 收到请求的用户 | NOT NULL, FOREIGN KEY (recipient_id) REFERENCES users(id) ON DELETE CASCADE | +| apply_msg | 申请留言 | varchar(200)| - | - | 申请人留言(如"我是张三,加个好友") | - | +| status | 请求状态 | tinyint(1) | - | - | 0-待处理,1-已同意,2-已拒绝 | DEFAULT 0, NOT NULL | +| create_time | 申请时间 | datetime | - | - | 请求发起时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| handle_time | 处理时间 | datetime | - | - | 接收人处理时间(未处理为NULL) | - | + +###### 3.4 query_shares(查询分享记录表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 分享ID | bigint(20) | 是(自增) | - | 唯一标识分享记录 | PRIMARY KEY (id) | +| share_user_id | 分享人ID | bigint(20) | - | 是(关联users.id) | 发起分享的用户 | NOT NULL, FOREIGN KEY (share_user_id) REFERENCES users(id) ON DELETE CASCADE | +| receive_user_id | 接收人ID | bigint(20) | - | 是(关联users.id) | 接收分享的好友 | NOT NULL, FOREIGN KEY (receive_user_id) REFERENCES users(id) ON DELETE CASCADE | +| dialog_id | 会话ID | varchar(50) | - | - | 关联MongoDB的dialog_details集合,定位具体对话文档 | NOT NULL | +| target_rounds | 会话轮次数组 | json | - | - | 筛选指定轮次序号(roundNum)的元素 | NOT NULL | +| query_title | 查询标题 | varchar(200)| - | - | 冗余存储,便于接收人查看 | NOT NULL | +| share_time | 分享时间 | datetime | - | - | 分享发起时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| receive_status | 接收状态 | tinyint(1) | - | - | 0-未处理,1-已保存,2-已删除 | DEFAULT 0, NOT NULL | + +##### 4. 系统日志与监控表 +###### 4.1 system_health(系统健康表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 记录ID | bigint(20) | 是(自增) | - | 唯一标识健康记录 | PRIMARY KEY (id) | +| db_delay | 数据库延迟 | int(11) | - | - | 数据库服务响应延迟(ms) | NOT NULL | +| cache_delay | 缓存延迟 | int(11) | - | - | 缓存服务响应延迟(ms) | NOT NULL | +| llm_delay | 大模型延迟 | int(11) | - | - | 大模型API响应延迟(ms) | NOT NULL | +| storage_usage | 存储使用率 | decimal(5,2)| - | - | 存储服务使用率(%,如95.50) | NOT NULL | +| collect_time | 采集时间 | datetime | - | - | 数据采集时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +###### 4.2 operation_logs(系统操作日志表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 日志ID | bigint(20) | 是(自增) | - | 唯一标识操作日志 | PRIMARY KEY (id) | +| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 操作人ID(匿名操作为0) | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) | +| username | 用户名 | varchar(50) | - | - | 操作人用户名(冗余,便于查询) | NOT NULL | +| operation | 操作名称 | varchar(100)| - | - | 如"创建数据库连接"、"分配用户权限" | NOT NULL | +| module | 操作模块 | varchar(50) | - | - | 如"数据源管理"、"用户管理" | NOT NULL | +| related_llm | 涉及模型 | varchar(50) | - | - | 关联大模型名称(无则为NULL) | - | +| ip_address | IP地址 | varchar(50) | - | - | 操作人IP地址 | NOT NULL | +| operate_time | 操作时间 | datetime | - | - | 操作执行时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| result | 操作结果 | tinyint(1) | - | - | 0-失败,1-成功 | NOT NULL | +| error_msg | 错误信息 | text | - | - | 失败原因(成功为NULL) | - | + +###### 4.3 llm_configs(大模型配置表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 配置ID | bigint(20) | 是(自增) | - | 唯一标识大模型配置 | PRIMARY KEY (id) | +| name | 模型名称 | varchar(50) | - | - | 如"智谱AI"、"通义千问" | UNIQUE (name), NOT NULL | +| version | 模型版本 | varchar(20) | - | - | 如"4.0"、"3.0" | NOT NULL | +| api_key | API密钥 | varchar(200)| - | - | 大模型API Key(AES加密) | NOT NULL | +| api_url | API地址 | varchar(255)| - | - | 调用地址(如"https://api.zhipuai.com/v4/chat/completions") | NOT NULL | +| status_id | 状态ID | tinyint(2) | - | 是(关联llm_status.id) | 模型状态(可用/不可用/不稳定) | NOT NULL, FOREIGN KEY (status_id) REFERENCES llm_status(id) | +| is_disabled | 是否禁用 | tinyint(1) | - | - | 0-启用,1-禁用(强制下线) | DEFAULT 0, NOT NULL | +| timeout | 超时时间 | int(11) | - | - | API调用超时阈值(ms,如5000) | NOT NULL | +| create_user_id | 创建人ID | bigint(20) | - | 是(关联users.id) | 仅系统管理员可创建 | NOT NULL, FOREIGN KEY (create_user_id) REFERENCES users(id) | +| create_time | 创建时间 | datetime | - | - | 配置创建时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| update_time | 更新时间 | datetime | - | - | 配置更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | + +###### 4.4 notifications(通知表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 通知ID | bigint(20) | 是(自增) | - | 唯一标识通知 | PRIMARY KEY (id) | +| title | 通知标题 | varchar(100)| - | - | 通知标题(如"系统维护通知") | NOT NULL | +| content | 通知内容 | text | - | - | 通知详细内容(支持简单HTML) | NOT NULL | +| target_id | 目标ID | tinyint(2) | - | 是(关联notification_targets.id) | 接收目标(所有用户/指定角色) | NOT NULL, FOREIGN KEY (target_id) REFERENCES notification_targets(id) | +| priority_id | 优先级ID | tinyint(2) | - | 是(关联priorities.id) | 通知优先级(紧急/普通/低) | NOT NULL, FOREIGN KEY (priority_id) REFERENCES priorities(id) | +| publisher_id | 发布者ID | bigint(20) | - | 是(关联users.id) | 发布通知的管理员 | NOT NULL, FOREIGN KEY (publisher_id) REFERENCES users(id) | +| is_top | 是否置顶 | tinyint(1) | - | - | 0-否,1-是(置顶优先展示) | DEFAULT 0, NOT NULL | +| publish_time | 发布时间 | datetime | - | - | 发布时间(草稿为NULL) | - | +| create_time | 创建时间 | datetime | - | - | 草稿创建时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| Latest_updateTime | 上次更新时间 | datetime | - | - | 最近一次编辑并保存的时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +###### 4.5 token_consume(token消耗表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|------|--------------------------------------------|---------------------------------------------------------------| +| id | 记录ID | bigint(20) | 是(自增) | - | 唯一标识消耗记录 | PRIMARY KEY (id) | +| llm_name | 模型名称 | varchar(50) | - | - | 消耗token的大模型 | NOT NULL | +| total_tokens | 总消耗 | int(11) | - | - | 当日总消耗(输入+输出) | NOT NULL | +| prompt_tokens | 输入消耗 | int(11) | - | - | 当日输入token消耗 | NOT NULL | +| completion_tokens | 输出消耗 | int(11) | - | - | 当日输出token消耗 | NOT NULL | +| consume_date | 消耗日期 | date | - | - | 消耗日期(如"2025-11-05") | NOT NULL | +| growth_rate | 增长率 | decimal(5,2)| - | - | 较前一日增长率(%,如+12.50) | - | + +###### 4.6 error_logs(错误分析表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 记录ID | bigint(20) | 是(自增) | - | 唯一标识错误记录 | PRIMARY KEY (id) | +| error_type_id | 错误类型ID | tinyint(2) | - | 是(关联error_types.id) | 错误类型(模型超时/数据库错误等) | NOT NULL, FOREIGN KEY (error_type_id) REFERENCES error_types(id) | +| error_count | 错误次数 | int(11) | - | - | 统计周期内错误次数 | NOT NULL | +| error_rate | 错误率 | decimal(5,2)| - | - | 错误次数占总请求比例(%) | - | +| period | 统计周期 | varchar(20) | - | - | "today"-今日、"7days"-近7日 | NOT NULL | +| stat_time | 统计时间 | datetime | - | - | 统计生成时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +###### 4.7 performance_metrics(性能趋势表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|------|--------------------------------------------|---------------------------------------------------------------| +| id | 指标ID | bigint(20) | 是(自增) | - | 唯一标识性能指标 | PRIMARY KEY (id) | +| metric_type | 指标类型 | varchar(20) | - | - | "query_count"-查询量、"response_time"-响应时间 | NOT NULL | +| metric_value | 指标值 | decimal(10,2)| - | - | 指标数值(查询量为整数,响应时间单位ms) | NOT NULL | +| metric_time | 指标时间 | datetime | - | - | 指标采集时间(精确到小时) | NOT NULL | +| trend | 趋势标识 | tinyint(1) | - | - | 0-下降,1-上升(较上一周期) | - | + +###### 4.8 db_connection_logs(数据库连接日志表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 日志ID | bigint(20) | 是(自增) | - | 唯一标识连接日志 | PRIMARY KEY (id) | +| db_connection_id | 数据库连接ID | bigint(20) | - | 是(关联db_connections.id) | 连接的数据源 | NOT NULL, FOREIGN KEY (db_connection_id) REFERENCES db_connections(id) | +| db_name | 数据库名称 | varchar(100)| - | - | 冗余存储,便于查看 | NOT NULL | +| connect_time | 连接时间 | datetime | - | - | 尝试连接时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| status | 连接状态 | varchar(20) | - | - | "success"-成功、"timeout"-超时、"auth_failed"-认证失败 | NOT NULL | +| remark | 备注信息 | text | - | - | 错误代码 | - | +| handler_id | 处理人ID | bigint(20) | - | 是(关联users.id) | 触发连接的用户(NULL为系统检测) | FOREIGN KEY (handler_id) REFERENCES users(id) | + +###### 4.9 query_logs(查询日志表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 日志ID | bigint(20) | 是(自增) | - | 唯一标识查询日志 | PRIMARY KEY (id) | +| dialog_id | 对话ID | varchar(50) | - | - | 关联多轮对话ID | NOT NULL | +| data_source_id | 数据源ID | bigint(20) | - | 是(关联db_connections.id) | 查询的数据源 | NOT NULL, FOREIGN KEY (data_source_id) REFERENCES db_connections(id) | +| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 执行查询的用户 | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) | +| query_date | 查询日期 | date | - | - | 查询执行日期 | NOT NULL | +| query_time | 查询时间 | datetime | - | - | 查询执行时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| execute_result | 执行结果 | tinyint(1) | - | - | 0-失败,1-成功 | NOT NULL | + +###### 4.10 user_searches(用户搜索表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 搜索ID | bigint(20) | 是(自增) | - | 唯一标识搜索记录 | PRIMARY KEY (id) | +| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 执行搜索的用户 | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | +| sql_content | SQL语句 | text | - | - | 用户执行的SQL语句 | NOT NULL | +| query_title | 查询标题 | varchar(200)| - | - | 大模型生成的标题 | NOT NULL | +| search_count | 搜索次数 | int(11) | - | - | 该搜索被执行的次数 | DEFAULT 1, NOT NULL | +| last_search_time | 最后搜索时间 | datetime | - | - | 最后一次执行该搜索的时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +#### (二)MongoDB 8.2 集合字段 +###### 1. query_collections(收藏查询表-组) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------|--------------------------------------------|---------------------------| +| _id | 组ID | ObjectId | 是(自动生成) | - | 唯一标识收藏组 | PRIMARY KEY (_id) | +| userId | 用户ID | Long | - | users.id | 所属用户 | NOT NULL | +| groupName | 组名称 | String | - | - | 收藏组名称(由大模型生成,如"销售报表查询") | NOT NULL, 同用户内唯一 | +| createTime | 创建时间 | Date | - | - | 组创建时间 | DEFAULT new Date() | + +###### 2. collection_records(收藏记录表-具体记录) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| +| _id | 记录ID | ObjectId | 是(自动生成) | - | 唯一标识收藏记录 | PRIMARY KEY (_id) | +| queryId | 组ID | ObjectId | - | query_collections._id | 所属收藏组 | NOT NULL | +| userId | 用户ID | Long | - | users.id | 所属用户 | NOT NULL | +| sqlContent | SQL语句 | String | - | - | 收藏的SQL语句 | NOT NULL | +| queryResult | 查询结果 | Document | - | - | 结果数据(如{ "columns": [], "data": [] }) | - | +| dbConnectionId | 数据库来源 | Long | - | db_connections.id | 收藏记录来源的数据库连接 | NOT NULL | +| llmConfigId | 大模型来源 | Long | - | llm_configs.id | 收藏记录来源的大模型配置 | NOT NULL | +| createTime | 创建时间 | Date | - | - | 收藏时间 | DEFAULT new Date() | + +###### 3. dialog_records(多轮对话列表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------|--------------------------------------------|---------------------------| +| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识对话列表 | PRIMARY KEY (_id) | +| dialogId | 对话ID | String | - | - | 自定义唯一标识(如"USER123_20251105") | NOT NULL, UNIQUE | +| userId | 用户ID | Long | - | users.id | 所属用户 | NOT NULL | +| topic | 对话主题 | String | - | - | 大模型生成的主题(如"2023年订单分析") | NOT NULL | +| totalRounds | 总轮数 | Int | - | - | 对话总轮次 | DEFAULT 0 | +| startTime | 开始时间 | Date | - | - | 对话开始时间 | DEFAULT new Date() | +| lastTime | 最后对话时间 | Date | - | - | 最后一轮对话时间 | DEFAULT new Date() | + +###### 4. dialog_details(多轮对话具体内容表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| +| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识对话内容 | PRIMARY KEY (_id) | +| dialogId | 对话ID | String | - | dialog_records.dialogId | 关联的对话列表 | NOT NULL | +| rounds | 对话轮次 | Array | - | - | 每轮对话详情:{ "roundNum": Int, "userInput": String, "aiResponse": String, "generatedSql": String, "roundTime": Date } | NOT NULL | + +###### 5. sql_cache(SQL缓存集合) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| +| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识缓存记录 | PRIMARY KEY (_id) | +| nlHash | 自然语言哈希 | String | - | - | 自然语言查询的MD5哈希(快速匹配) | NOT NULL | +| userId | 用户ID | Long | - | users.id | 关联用户(NULL为全局缓存) | - | +| connectionId | 数据源ID | Long | - | db_connections.id | 关联数据源 | NOT NULL | +| tableIds | 表ID列表 | Array | - | table_metadata.id | 涉及的表ID | NOT NULL | +| dbType | 数据库类型 | String | - | db_types.type_code | 如"mysql"、"mongodb" | NOT NULL | +| generatedSql | 生成的SQL | String | - | - | 缓存的SQL语句 | NOT NULL | +| hitCount | 命中次数 | Int | - | - | 缓存被复用次数 | DEFAULT 0 | +| expireTime | 过期时间 | Date | - | - | 缓存过期时间(默认7天) | NOT NULL | + +###### 6. ai_interaction_logs(AI交互日志集合) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| +| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识交互记录 | PRIMARY KEY (_id) | +| userId | 用户ID | Long | - | users.id | 发起请求的用户 | NOT NULL | +| requestType | 请求类型 | String | - | - | "nl2sql"-自然语言转SQL、"sql_optimize"-SQL优化 | NOT NULL | +| llmName | 模型名称 | String | - | llm_configs.name | 调用的大模型 | NOT NULL | +| requestParams | 请求参数 | Document | - | - | 请求参数:{ "naturalLanguage": String, "metadata": Object, "temperature": Double } | NOT NULL | +| responseResult | 响应结果 | Document | - | - | 响应结果:{ "sql": String, "confidence": Double, "suggestion": String } | - | +| tokenUsage | Token消耗 | Document | - | - | { "promptTokens": Int, "completionTokens": Int, "totalTokens": Int } | NOT NULL | +| responseTime | 响应时间 | Int | - | - | 响应耗时(ms) | NOT NULL | +| status | 交互状态 | String | - | - | "success"-成功、"fail"-失败 | NOT NULL | +| errorMsg | 错误信息 | String | - | - | 失败原因(成功为NULL) | - | +| createTime | 创建时间 | Date | - | - | 请求发起时间 | DEFAULT new Date() | + +###### 7. friend_chats(好友聊天记录表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------|--------------------------------------------|---------------------------| +| _id | 记录ID | ObjectId | 是(自动生成) | - | 唯一标识聊天记录 | PRIMARY KEY (_id) | +| user_id | 发送人ID | Long | - | users.id | 发送消息的用户ID(关联MySQL的users表) | NOT NULL | +| friend_id | 接收人ID | Long | - | users.id | 接收消息的好友ID(关联MySQL的users表) | NOT NULL | +| content_type | 内容类型 | String | - | - | 消息类型:text(文本)、query_share(查询分享)、image(图片)等 | NOT NULL | +| content | 消息内容 | Document | - | - | 动态内容:文本:{text: "Hello"};分享:{query_id: "xxx", title: "订单查询"};图片:{url: "xxx.png", size: 1024} | NOT NULL | +| send_time | 发送时间 | Date | - | - | 消息发送时间(精确到毫秒) | NOT NULL | +| is_read | 是否已读 | Boolean | - | - | true(已读)/false(未读),默认false | DEFAULT false | +| extra | 额外信息 | Document | - | - | 扩展字段(如quote_msg_id引用消息ID、is_recalled是否撤回等) | - | + +--- + +### 二、表/集合之间的关系(一对多/多对一/一对一) +#### (一)MySQL 表关系 +| 关联表1 | 关联表2 | 关系类型 | 说明 | +|---------------------------|---------------------------|----------|----------------------------------------| +| users(用户表) | roles(角色表) | 多对一 | 多个用户属于同一角色(如多个普通用户) | +| db_connections(数据库连接表) | db_types(数据库类型表) | 多对一 | 多个数据库连接属于同一类型(如多个MySQL连接) | +| table_metadata(表元数据表) | db_connections(数据库连接表) | 多对一 | 多个表属于同一数据库连接 | +| column_metadata(字段元数据表) | table_metadata(表元数据表) | 多对一 | 多个字段属于同一表 | +| user_db_permissions(用户数据权限表) | users(被授权用户) | 一对一 | 一个用户对应一条权限记录(每个用户一行) | +| user_db_permissions(用户数据权限表) | users(授权管理员) | 多对一 | 多个权限记录可由同一管理员修改 | +| friend_relations(好友关系表) | users(用户) | 多对一 | 一个用户可添加多个好友 | +| friend_relations(好友关系表) | users(好友) | 多对一 | 一个用户可被多个用户添加为好友 | +| friend_requests(好友请求表) | users(申请人) | 多对一 | 一个用户可发起多个好友请求 | +| friend_requests(好友请求表) | users(接收人) | 多对一 | 一个用户可接收多个好友请求 | +| query_shares(查询分享记录表) | users(分享人) | 多对一 | 一个用户可分享多个查询 | +| query_shares(查询分享记录表) | users(接收人) | 多对一 | 一个用户可接收多个分享 | +| llm_configs(大模型配置表) | llm_status(大模型状态表) | 多对一 | 多个大模型配置属于同一状态 | +| llm_configs(大模型配置表) | users(创建人) | 多对一 | 一个管理员可创建多个大模型配置 | +| notifications(通知表) | notification_targets(通知目标表) | 多对一 | 多个通知属于同一接收目标 | +| notifications(通知表) | priorities(通知优先级表) | 多对一 | 多个通知属于同一优先级 | +| notifications(通知表) | users(发布者) | 多对一 | 一个管理员可发布多个通知 | +| error_logs(错误分析表) | error_types(错误类型表) | 多对一 | 多个错误记录属于同一错误类型 | +| db_connection_logs(数据库连接日志表) | db_connections(数据库连接表) | 多对一 | 一个数据库连接有多个连接日志 | +| query_logs(查询日志表) | db_connections(数据库连接表) | 多对一 | 一个数据源有多个查询日志 | +| query_logs(查询日志表) | users(用户) | 多对一 | 一个用户有多个查询日志 | +| user_searches(用户搜索表) | users(用户) | 多对一 | 一个用户有多个搜索记录 | + +#### (二)MongoDB 集合关系 +| 关联集合1 | 关联集合2 | 关系类型 | 说明 | +|-----------------------------|-----------------------------|----------|----------------------------------------| +| collection_records(收藏记录表) | query_collections(收藏查询表) | 多对一 | 一个收藏组包含多个收藏记录 | +| collection_records(收藏记录表) | db_connections(数据库连接表) | 多对一 | 一个数据库连接有多个收藏记录 | +| collection_records(收藏记录表) | llm_configs(大模型配置表) | 多对一 | 一个大模型配置有多个收藏记录 | +| dialog_details(多轮对话具体内容表) | dialog_records(多轮对话列表) | 一对一 | 一个对话列表对应一个具体内容集合 | +| sql_cache(SQL缓存集合) | db_connections(数据库连接表) | 多对一 | 一个数据源有多个SQL缓存 | +| sql_cache(SQL缓存集合) | users(用户表) | 多对一 | 一个用户有多个个人SQL缓存 | +| ai_interaction_logs(AI交互日志集合) | users(用户表) | 多对一 | 一个用户有多个AI交互记录 | +| ai_interaction_logs(AI交互日志集合) | llm_configs(大模型配置表) | 多对一 | 一个大模型有多个交互记录 | +| friend_chats(好友聊天记录表) | users(用户表) | 多对一 | 一个用户可发送多条聊天消息 | +| friend_chats(好友聊天记录表) | users(用户表) | 多对一 | 一个用户可接收多条聊天消息 | + +--- + +### 三、完整索引设计(按表/集合分类) +#### (一)MySQL 数据表索引 +| 表名 | 索引类型 | 索引字段 | 索引说明 | +|-----------------------|----------------|-------------------------------------------|--------------------------------------------| +| users | 普通索引 | role_id | 按角色查询用户 | +| users | 普通索引 | status | 按账号状态筛选用户 | +| users | 唯一索引 | username | 用户名唯一,加速登录查询 | +| users | 唯一索引 | email | 邮箱唯一,加速好友添加查询 | +| users | 唯一索引 | phonenumber | 手机号唯一,加速账号验证 | +| roles | 主键索引 | id | 主键默认索引,加速角色关联查询 | +| db_types | 主键索引 | id | 主键默认索引,加速数据库类型关联查询 | +| notification_targets | 主键索引 | id | 主键默认索引,加速通知目标关联查询 | +| priorities | 普通索引 | sort | 按排序权重查询优先级 | +| priorities | 主键索引 | id | 主键默认索引,加速通知优先级关联查询 | +| error_types | 主键索引 | id | 主键默认索引,加速错误类型关联查询 | +| llm_status | 主键索引 | id | 主键默认索引,加速大模型状态关联查询 | +| db_connections | 普通索引 | db_type_id | 按数据库类型查询连接 | +| db_connections | 普通索引 | status | 按连接状态筛选连接 | +| db_connections | 普通索引 | create_user_id | 按创建人查询连接 | +| db_connections | 唯一索引 | name | 连接名称唯一,加速连接查询 | +| table_metadata | 唯一复合索引 | db_connection_id, table_name | 同一连接下表名唯一,加速表元数据查询 | +| table_metadata | 普通索引 | db_connection_id | 按数据库连接查询表 | +| column_metadata | 唯一复合索引 | table_id, column_name | 同一表下字段名唯一,加速字段元数据查询 | +| column_metadata | 普通索引 | table_id | 按表查询字段 | +| user_db_permissions | 唯一索引 | user_id | 每个用户一行权限,加速权限查询 | +| user_db_permissions | 普通索引 | last_grant_user_id | 按授权管理员查询权限记录 | +| user_db_permissions | 普通索引 | is_assigned | 按是否分配筛选权限 | +| friend_relations | 唯一复合索引 | user_id, friend_id | 避免重复添加好友 | +| friend_relations | 普通索引 | user_id | 按用户查询好友列表 | +| friend_relations | 普通索引 | friend_id | 按好友ID查询关联关系 | +| friend_requests | 唯一复合索引 | applicant_id, recipient_id | 避免重复发送好友请求 | +| friend_requests | 普通复合索引 | recipient_id, status | 接收人查询待处理请求 | +| query_shares | 普通复合索引 | receive_user_id, receive_status | 接收人查询未处理分享 | +| query_shares | 普通索引 | share_user_id | 按分享人查询分享记录 | +| system_health | 普通索引 | collect_time | 按时间查询系统健康趋势 | +| operation_logs | 普通索引 | operate_time | 按时间查询操作日志 | +| operation_logs | 普通索引 | user_id | 按用户查询操作日志 | +| operation_logs | 普通索引 | module | 按模块筛选操作日志 | +| llm_configs | 普通索引 | status_id | 按状态查询大模型配置 | +| llm_configs | 普通索引 | is_disabled | 按是否禁用筛选配置 | +| llm_configs | 唯一索引 | name | 模型名称唯一,加速模型查询 | +| notifications | 普通索引 | target_id | 按目标筛选通知 | +| notifications | 普通索引 | priority_id | 按优先级筛选通知 | +| notifications | 普通复合索引 | is_top DESC, publish_time DESC | 置顶通知优先,按时间排序 | +| token_consume | 唯一复合索引 | llm_name, consume_date | 同一模型每日一条记录,加速消耗查询 | +| token_consume | 普通索引 | consume_date | 按日期查询消耗趋势 | +| error_logs | 普通复合索引 | error_type_id, period | 按错误类型+周期查询错误记录 | +| error_logs | 普通索引 | stat_time | 按统计时间查询错误记录 | +| performance_metrics | 普通复合索引 | metric_type, metric_time | 按类型+时间查询性能趋势 | +| db_connection_logs | 普通索引 | db_connection_id | 按连接查询日志 | +| db_connection_logs | 普通索引 | connect_time | 按时间查询连接日志 | +| db_connection_logs | 普通索引 | status | 按状态筛选连接日志 | +| query_logs | 普通复合索引 | data_source_id, query_date | 统计数据源每日查询量 | +| query_logs | 普通索引 | user_id | 按用户查询查询日志 | +| query_logs | 普通索引 | dialog_id | 按对话ID查询查询日志 | +| user_searches | 普通复合索引 | user_id, last_search_time | 清理30天前的搜索记录 | +| user_searches | 普通复合索引 | user_id, search_count DESC | 查询用户常用搜索(按次数排序) | + +#### (二)MongoDB 集合索引 +| 集合名 | 索引类型 | 索引字段 | 索引说明 | +|-------------------------|----------------|-------------------------------------------|--------------------------------------------| +| query_collections | 复合索引 | { "userId": 1, "groupName": 1 } | 同用户内组名唯一,加速组查询 | +| collection_records | 复合索引 | { "queryId": 1, "userId": 1 } | 关联收藏组和用户,加速记录查询 | +| collection_records | 普通索引 | { "dbConnectionId": 1 } | 按数据库来源查询收藏记录 | +| collection_records | 普通索引 | { "llmConfigId": 1 } | 按大模型来源查询收藏记录 | +| dialog_records | 复合索引 | { "userId": 1, "lastTime": -1 } | 用户按时间查询对话列表(降序) | +| dialog_records | 唯一索引 | { "dialogId": 1 } | 对话ID唯一,加速对话定位 | +| dialog_details | 单字段索引 | { "dialogId": 1 } | 关联对话列表,加速内容查询 | +| sql_cache | 复合索引 | { "nlHash": 1, "connectionId": 1, "tableIds": 1 } | 精确匹配缓存,加速SQL复用 | +| sql_cache | TTL索引 | { "expireTime": 1 } | 自动删除过期缓存(默认7天) | +| ai_interaction_logs | 复合索引 | { "userId": 1, "createTime": -1 } | 用户按时间查询交互记录(降序) | +| ai_interaction_logs | 复合索引 | { "llmName": 1, "status": 1 } | 按模型+状态查询交互记录 | +| friend_chats | 复合索引 | { "user_id": 1, "friend_id": 1, "send_time": -1 } | 按用户+好友+时间查询聊天记录(最新在前) | +| friend_chats | 复合索引 | { "friend_id": 1, "is_read": 1 } | 查询好友未读消息 | \ No newline at end of file diff --git a/src/springboot_demo/mongodb_schema_from_last.js b/src/springboot_demo/mongodb_schema_from_last.js new file mode 100644 index 00000000..06768fe4 --- /dev/null +++ b/src/springboot_demo/mongodb_schema_from_last.js @@ -0,0 +1,482 @@ +// MongoDB数据库集合和索引创建脚本 - 严格结构定义 +// 完全按照数据库设计文档 + +db = db.getSiblingDB('natural_language_query_system'); +print("开始初始化 MongoDB 集合..."); + +try { + // 1. query_collections(收藏查询表-组) + db.createCollection("query_collections", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["userId", "groupName", "createTime"], + properties: { + userId: { + bsonType: "long", + description: "用户ID,关联users.id" + }, + groupName: { + bsonType: "string", + description: "收藏组名称(由大模型生成)" + }, + createTime: { + bsonType: "date", + description: "组创建时间" + } + } + } + } + }); + db.query_collections.createIndex({ "userId": 1, "groupName": 1 }, { unique: true }); + print("创建 query_collections 完成"); +} catch(e) { + print("query_collections 创建错误: " + e); +} + +try { + // 2. collection_records(收藏记录表-具体记录) + db.createCollection("collection_records", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["queryId", "userId", "sqlContent", "dbConnectionId", "llmConfigId", "createTime"], + properties: { + queryId: { + bsonType: "objectId", + description: "组ID,关联query_collections._id" + }, + userId: { + bsonType: "long", + description: "用户ID,关联users.id" + }, + sqlContent: { + bsonType: "string", + description: "收藏的SQL语句" + }, + queryResult: { + bsonType: "object", + description: "结果数据" + }, + dbConnectionId: { + bsonType: "long", + description: "数据库来源,关联db_connections.id" + }, + llmConfigId: { + bsonType: "long", + description: "大模型来源,关联llm_configs.id" + }, + createTime: { + bsonType: "date", + description: "收藏时间" + } + } + } + } + }); + db.collection_records.createIndex({ "queryId": 1, "userId": 1 }); + db.collection_records.createIndex({ "dbConnectionId": 1 }); + db.collection_records.createIndex({ "llmConfigId": 1 }); + print("创建 collection_records 完成"); +} catch(e) { + print("collection_records 创建错误: " + e); +} + +try { + // 3. dialog_records(多轮对话列表) + db.createCollection("dialog_records", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["dialogId", "userId", "topic", "totalRounds", "startTime", "lastTime"], + properties: { + dialogId: { + bsonType: "string", + description: "对话ID,自定义唯一标识" + }, + userId: { + bsonType: "long", + description: "用户ID,关联users.id" + }, + topic: { + bsonType: "string", + description: "对话主题(大模型生成)" + }, + totalRounds: { + bsonType: "int", + description: "对话总轮次", + minimum: 0 + }, + startTime: { + bsonType: "date", + description: "对话开始时间" + }, + lastTime: { + bsonType: "date", + description: "最后一轮对话时间" + } + } + } + } + }); + db.dialog_records.createIndex({ "userId": 1, "lastTime": -1 }); + db.dialog_records.createIndex({ "dialogId": 1 }, { unique: true }); + print("创建 dialog_records 完成"); +} catch(e) { + print("dialog_records 创建错误: " + e); +} + +try { + // 4. dialog_details(多轮对话具体内容表) + db.createCollection("dialog_details", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["dialogId", "rounds"], + properties: { + dialogId: { + bsonType: "string", + description: "对话ID,关联dialog_records.dialogId" + }, + rounds: { + bsonType: "array", + description: "对话轮次数组", + items: { + bsonType: "object", + required: ["roundNum", "userInput", "aiResponse", "generatedSql", "roundTime"], + properties: { + roundNum: { + bsonType: "int", + description: "轮次序号", + minimum: 1 + }, + userInput: { + bsonType: "string", + description: "用户输入" + }, + aiResponse: { + bsonType: "string", + description: "AI回复" + }, + generatedSql: { + bsonType: "string", + description: "生成的SQL" + }, + roundTime: { + bsonType: "date", + description: "轮次时间" + } + } + } + } + } + } + } + }); + db.dialog_details.createIndex({ "dialogId": 1 }); + print("创建 dialog_details 完成"); +} catch(e) { + print("dialog_details 创建错误: " + e); +} + +try { + // 5. sql_cache(SQL缓存集合) + db.createCollection("sql_cache", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["nlHash", "connectionId", "tableIds", "dbType", "generatedSql", "hitCount", "expireTime"], + properties: { + nlHash: { + bsonType: "string", + description: "自然语言查询的MD5哈希" + }, + userId: { + bsonType: ["long", "null"], + description: "用户ID(NULL为全局缓存)" + }, + connectionId: { + bsonType: "long", + description: "数据源ID,关联db_connections.id" + }, + tableIds: { + bsonType: "array", + description: "涉及的表ID列表", + items: { + bsonType: "long" + } + }, + dbType: { + bsonType: "string", + description: "数据库类型" + }, + generatedSql: { + bsonType: "string", + description: "缓存的SQL语句" + }, + hitCount: { + bsonType: "int", + description: "命中次数", + minimum: 0 + }, + expireTime: { + bsonType: "date", + description: "缓存过期时间" + } + } + } + } + }); + db.sql_cache.createIndex({ "nlHash": 1, "connectionId": 1, "tableIds": 1 }); + db.sql_cache.createIndex({ "expireTime": 1 }, { expireAfterSeconds: 0 }); + print("创建 sql_cache 完成"); +} catch(e) { + print("sql_cache 创建错误: " + e); +} + +try { + // 6. ai_interaction_logs(AI交互日志集合) + db.createCollection("ai_interaction_logs", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["userId", "requestType", "llmName", "requestParams", "tokenUsage", "responseTime", "status", "createTime"], + properties: { + userId: { + bsonType: "long", + description: "用户ID,关联users.id" + }, + requestType: { + bsonType: "string", + enum: ["nl2sql", "sql_optimize"], + description: "请求类型" + }, + llmName: { + bsonType: "string", + description: "模型名称,关联llm_configs.name" + }, + requestParams: { + bsonType: "object", + description: "请求参数", + properties: { + naturalLanguage: { + bsonType: "string", + description: "自然语言查询" + }, + metadata: { + bsonType: "object", + description: "元数据信息" + }, + temperature: { + bsonType: "double", + description: "温度参数", + minimum: 0, + maximum: 2 + } + } + }, + responseResult: { + bsonType: "object", + description: "响应结果", + properties: { + sql: { + bsonType: "string", + description: "生成的SQL" + }, + confidence: { + bsonType: "double", + description: "置信度", + minimum: 0, + maximum: 1 + }, + suggestion: { + bsonType: "string", + description: "优化建议" + } + } + }, + tokenUsage: { + bsonType: "object", + description: "Token消耗", + required: ["promptTokens", "completionTokens", "totalTokens"], + properties: { + promptTokens: { + bsonType: "int", + description: "输入token数", + minimum: 0 + }, + completionTokens: { + bsonType: "int", + description: "输出token数", + minimum: 0 + }, + totalTokens: { + bsonType: "int", + description: "总token数", + minimum: 0 + } + } + }, + responseTime: { + bsonType: "int", + description: "响应耗时(ms)", + minimum: 0 + }, + status: { + bsonType: "string", + enum: ["success", "fail"], + description: "交互状态" + }, + errorMsg: { + bsonType: ["string", "null"], + description: "错误信息" + }, + createTime: { + bsonType: "date", + description: "请求发起时间" + } + } + } + } + }); + db.ai_interaction_logs.createIndex({ "userId": 1, "createTime": -1 }); + db.ai_interaction_logs.createIndex({ "llmName": 1, "status": 1 }); + print("创建 ai_interaction_logs 完成"); +} catch(e) { + print("ai_interaction_logs 创建错误: " + e); +} + +try { + // 7. friend_chats(好友聊天记录表)- 完全按照设计文档 + db.createCollection("friend_chats", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["user_id", "friend_id", "content_type", "content", "send_time", "is_read"], + properties: { + user_id: { + bsonType: "long", + description: "发送人ID,关联users.id" + }, + friend_id: { + bsonType: "long", + description: "接收人ID,关联users.id" + }, + content_type: { + bsonType: "string", + enum: ["text", "query_share", "image"], + description: "消息类型" + }, + content: { + bsonType: "object", + description: "动态消息内容", + properties: { + text: { + bsonType: "string", + description: "文本内容" + }, + query_id: { + bsonType: "string", + description: "查询ID" + }, + title: { + bsonType: "string", + description: "查询标题" + }, + url: { + bsonType: "string", + description: "图片URL" + }, + size: { + bsonType: "int", + description: "文件大小", + minimum: 0 + } + } + }, + send_time: { + bsonType: "date", + description: "消息发送时间" + }, + is_read: { + bsonType: "bool", + description: "是否已读" + }, + extra: { + bsonType: "object", + description: "额外信息(扩展字段)", + properties: { + quote_msg_id: { + bsonType: "objectId", + description: "引用消息ID" + }, + is_recalled: { + bsonType: "bool", + description: "是否撤回" + } + } + } + } + } + } + }); + db.friend_chats.createIndex({ "user_id": 1, "friend_id": 1, "send_time": -1 }); + db.friend_chats.createIndex({ "friend_id": 1, "is_read": 1 }); + print("创建 friend_chats 完成"); +} catch(e) { + print("friend_chats 创建错误: " + e); +} + +// 验证创建结果 +var collections = db.getCollectionNames(); +var userCollections = collections.filter(function(name) { + return !name.startsWith('system.'); +}); + +print("\n=========================================="); +print("MongoDB 集合初始化完成"); +print("用户集合数量: " + userCollections.length); +print("集合列表: " + JSON.stringify(userCollections.sort())); +print("=========================================="); + +// 检查是否所有集合都创建成功 +var expectedCollections = [ + "query_collections", + "collection_records", + "dialog_records", + "dialog_details", + "sql_cache", + "ai_interaction_logs", + "friend_chats" +].sort(); + +var missingCollections = expectedCollections.filter(function(name) { + return userCollections.indexOf(name) === -1; +}); + +if (missingCollections.length > 0) { + print("缺失的集合: " + JSON.stringify(missingCollections)); +} else { + print("所有集合创建成功!"); + + // 显示各集合的索引信息 + print("\n📈 各集合索引详情:"); + userCollections.forEach(function(collectionName) { + var indexes = db[collectionName].getIndexes(); + var userIndexes = indexes.filter(function(index) { + return index.name !== "_id_"; + }); + print(" " + collectionName + ": " + userIndexes.length + " 个索引"); + userIndexes.forEach(function(index) { + print(" - " + index.name + ": " + JSON.stringify(index.key)); + }); + }); +} + +print("\n集合结构验证:"); +userCollections.forEach(function(collectionName) { + var stats = db[collectionName].stats(); + print(" " + collectionName + ": " + stats.count + " 个文档, " + stats.size + " 字节"); +}); + +print("\nMongoDB 严格结构定义初始化完成!"); \ No newline at end of file diff --git a/src/springboot_demo/mvnw b/src/springboot_demo/mvnw new file mode 100644 index 00000000..bd8896bf --- /dev/null +++ b/src/springboot_demo/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/src/springboot_demo/mvnw.cmd b/src/springboot_demo/mvnw.cmd new file mode 100644 index 00000000..92450f93 --- /dev/null +++ b/src/springboot_demo/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/src/springboot_demo/mysql_schema_from_last.sql b/src/springboot_demo/mysql_schema_from_last.sql new file mode 100644 index 00000000..599d697a --- /dev/null +++ b/src/springboot_demo/mysql_schema_from_last.sql @@ -0,0 +1,341 @@ +-- MySQL数据库建表脚本 +-- 基于last.md文档设计 + +-- 1. 基础信息表(用户、角色、字典) + +-- 1.1 roles(角色表)- 需要先创建,因为users依赖它 +CREATE TABLE `roles` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID', + `role_name` VARCHAR(30) NOT NULL UNIQUE COMMENT '角色名称', + `role_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '角色编码', + `description` VARCHAR(500) COMMENT '角色描述' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; + +-- 1.2 users(用户表) +CREATE TABLE `users` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', + `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', + `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密存储)', + `email` VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱', + `phonenumber` VARCHAR(50) NOT NULL UNIQUE COMMENT '手机号', + `role_id` TINYINT(2) NOT NULL COMMENT '角色ID', + `avatar_url` VARCHAR(255) DEFAULT '/default-avatar.png' COMMENT '头像URL', + `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态:0-禁用,1-正常', + `online_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '在线状态:0-离线,1-在线', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_role_id` (`role_id`), + INDEX `idx_status` (`status`), + UNIQUE INDEX `uk_username` (`username`), + UNIQUE INDEX `uk_email` (`email`), + UNIQUE INDEX `uk_phonenumber` (`phonenumber`), + FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; + +-- 1.3 db_types(数据库类型表) +CREATE TABLE `db_types` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '类型ID', + `type_name` VARCHAR(50) NOT NULL UNIQUE COMMENT '类型名称', + `type_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '类型编码', + `description` VARCHAR(500) COMMENT '类型描述' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库类型表'; + +-- 1.4 notification_targets(通知目标表) +CREATE TABLE `notification_targets` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '目标ID', + `target_name` VARCHAR(30) NOT NULL UNIQUE COMMENT '目标名称', + `target_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '目标编码', + `description` VARCHAR(200) COMMENT '目标描述' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知目标表'; + +-- 1.5 priorities(通知优先级表) +CREATE TABLE `priorities` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '优先级ID', + `priority_name` VARCHAR(20) NOT NULL UNIQUE COMMENT '优先级名称', + `priority_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '优先级编码', + `sort` INT(11) NOT NULL COMMENT '排序权重', + INDEX `idx_sort` (`sort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知优先级表'; + +-- 1.6 error_types(错误类型表) +CREATE TABLE `error_types` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '错误ID', + `error_name` VARCHAR(50) NOT NULL UNIQUE COMMENT '错误名称', + `error_code` VARCHAR(50) NOT NULL UNIQUE COMMENT '错误编码', + `description` VARCHAR(500) COMMENT '错误描述' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='错误类型表'; + +-- 1.7 llm_status(大模型状态表) +CREATE TABLE `llm_status` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '状态ID', + `status_name` VARCHAR(20) NOT NULL UNIQUE COMMENT '状态名称', + `status_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '状态编码', + `description` VARCHAR(200) COMMENT '状态描述' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大模型状态表'; + +-- 2. 数据资源表 + +-- 2.1 db_connections(数据库连接表) +CREATE TABLE `db_connections` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '连接ID', + `name` VARCHAR(100) NOT NULL UNIQUE COMMENT '连接名称', + `db_type_id` TINYINT(2) NOT NULL COMMENT '数据库类型ID', + `url` VARCHAR(255) NOT NULL COMMENT '连接地址', + `username` VARCHAR(50) NOT NULL COMMENT '数据库账号(加密存储)', + `password` VARCHAR(100) NOT NULL COMMENT '数据库密码(加密存储)', + `status` VARCHAR(20) NOT NULL DEFAULT 'disconnected' COMMENT '连接状态', + `create_user_id` BIGINT(20) NOT NULL COMMENT '创建者ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_db_type_id` (`db_type_id`), + INDEX `idx_status` (`status`), + INDEX `idx_create_user_id` (`create_user_id`), + UNIQUE INDEX `uk_name` (`name`), + FOREIGN KEY (`db_type_id`) REFERENCES `db_types`(`id`), + FOREIGN KEY (`create_user_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库连接表'; + +-- 2.2 table_metadata(表元数据表) +CREATE TABLE `table_metadata` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '表ID', + `db_connection_id` BIGINT(20) NOT NULL COMMENT '数据库连接ID', + `table_name` VARCHAR(100) NOT NULL COMMENT '表名', + `description` VARCHAR(500) COMMENT '表描述', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + UNIQUE INDEX `uk_db_connection_table` (`db_connection_id`, `table_name`), + INDEX `idx_db_connection_id` (`db_connection_id`), + FOREIGN KEY (`db_connection_id`) REFERENCES `db_connections`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表元数据表'; + +-- 2.3 column_metadata(字段元数据表) +CREATE TABLE `column_metadata` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '字段ID', + `table_id` BIGINT(20) NOT NULL COMMENT '表ID', + `column_name` VARCHAR(100) NOT NULL COMMENT '字段名', + `data_type` VARCHAR(50) NOT NULL COMMENT '数据类型', + `description` VARCHAR(500) COMMENT '字段描述', + `is_primary` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否主键:0-否,1-是', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + UNIQUE INDEX `uk_table_column` (`table_id`, `column_name`), + INDEX `idx_table_id` (`table_id`), + FOREIGN KEY (`table_id`) REFERENCES `table_metadata`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字段元数据表'; + +-- 3. 权限与关系表 + +-- 3.1 user_db_permissions(用户数据权限表) +CREATE TABLE `user_db_permissions` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID', + `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', + `permission_details` JSON NOT NULL COMMENT '权限详情', + `last_grant_user_id` BIGINT(20) NOT NULL COMMENT '最后授权管理员ID', + `is_assigned` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已分配:0-未分配,1-已分配', + `last_grant_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后授权时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + UNIQUE INDEX `uk_user_id` (`user_id`), + INDEX `idx_last_grant_user_id` (`last_grant_user_id`), + INDEX `idx_is_assigned` (`is_assigned`), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`last_grant_user_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户数据权限表'; + +-- 3.2 friend_relations(好友关系表) +CREATE TABLE `friend_relations` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '关系ID', + `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', + `friend_id` BIGINT(20) NOT NULL COMMENT '好友ID', + `friend_username` VARCHAR(50) NOT NULL COMMENT '好友用户名', + `online_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '好友在线状态:0-离线,1-在线', + `remark_name` VARCHAR(50) COMMENT '备注名', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间', + UNIQUE INDEX `uk_user_friend` (`user_id`, `friend_id`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_friend_id` (`friend_id`), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`friend_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友关系表'; + +-- 3.3 friend_requests(好友请求表) +CREATE TABLE `friend_requests` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '请求ID', + `applicant_id` BIGINT(20) NOT NULL COMMENT '申请人ID', + `recipient_id` BIGINT(20) NOT NULL COMMENT '接收人ID', + `apply_msg` VARCHAR(200) COMMENT '申请留言', + `status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '请求状态:0-待处理,1-已同意,2-已拒绝', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间', + `handle_time` DATETIME COMMENT '处理时间', + UNIQUE INDEX `uk_applicant_recipient` (`applicant_id`, `recipient_id`), + INDEX `idx_recipient_status` (`recipient_id`, `status`), + FOREIGN KEY (`applicant_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`recipient_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友请求表'; + +-- 3.4 query_shares(查询分享记录表) +CREATE TABLE `query_shares` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '分享ID', + `share_user_id` BIGINT(20) NOT NULL COMMENT '分享人ID', + `receive_user_id` BIGINT(20) NOT NULL COMMENT '接收人ID', + `dialog_id` VARCHAR(50) NOT NULL COMMENT '会话ID', + `target_rounds` JSON NOT NULL COMMENT '会话轮次数组', + `query_title` VARCHAR(200) NOT NULL COMMENT '查询标题', + `share_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '分享时间', + `receive_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '接收状态:0-未处理,1-已保存,2-已删除', + INDEX `idx_receive_user_status` (`receive_user_id`, `receive_status`), + INDEX `idx_share_user_id` (`share_user_id`), + FOREIGN KEY (`share_user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`receive_user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='查询分享记录表'; + +-- 4. 系统日志与监控表 + +-- 4.1 system_health(系统健康表) +CREATE TABLE `system_health` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', + `db_delay` INT(11) NOT NULL COMMENT '数据库延迟(ms)', + `cache_delay` INT(11) NOT NULL COMMENT '缓存延迟(ms)', + `llm_delay` INT(11) NOT NULL COMMENT '大模型延迟(ms)', + `storage_usage` DECIMAL(5,2) NOT NULL COMMENT '存储使用率(%)', + `collect_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '采集时间', + INDEX `idx_collect_time` (`collect_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统健康表'; + +-- 4.2 operation_logs(系统操作日志表) +CREATE TABLE `operation_logs` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', + `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', + `username` VARCHAR(50) NOT NULL COMMENT '用户名', + `operation` VARCHAR(100) NOT NULL COMMENT '操作名称', + `module` VARCHAR(50) NOT NULL COMMENT '操作模块', + `related_llm` VARCHAR(50) COMMENT '涉及模型', + `ip_address` VARCHAR(50) NOT NULL COMMENT 'IP地址', + `operate_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', + `result` TINYINT(1) NOT NULL COMMENT '操作结果:0-失败,1-成功', + `error_msg` TEXT COMMENT '错误信息', + INDEX `idx_operate_time` (`operate_time`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_module` (`module`), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表'; + +-- 4.3 llm_configs(大模型配置表) +CREATE TABLE `llm_configs` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '配置ID', + `name` VARCHAR(50) NOT NULL UNIQUE COMMENT '模型名称', + `version` VARCHAR(20) NOT NULL COMMENT '模型版本', + `api_key` VARCHAR(200) NOT NULL COMMENT 'API密钥(AES加密)', + `api_url` VARCHAR(255) NOT NULL COMMENT 'API地址', + `status_id` TINYINT(2) NOT NULL COMMENT '状态ID', + `is_disabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否禁用:0-启用,1-禁用', + `timeout` INT(11) NOT NULL COMMENT '超时时间(ms)', + `create_user_id` BIGINT(20) NOT NULL COMMENT '创建人ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_status_id` (`status_id`), + INDEX `idx_is_disabled` (`is_disabled`), + UNIQUE INDEX `uk_name` (`name`), + FOREIGN KEY (`status_id`) REFERENCES `llm_status`(`id`), + FOREIGN KEY (`create_user_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大模型配置表'; + +-- 4.4 notifications(通知表) +CREATE TABLE `notifications` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '通知ID', + `title` VARCHAR(100) NOT NULL COMMENT '通知标题', + `content` TEXT NOT NULL COMMENT '通知内容', + `target_id` TINYINT(2) NOT NULL COMMENT '目标ID', + `priority_id` TINYINT(2) NOT NULL COMMENT '优先级ID', + `publisher_id` BIGINT(20) NOT NULL COMMENT '发布者ID', + `is_top` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶:0-否,1-是', + `publish_time` DATETIME COMMENT '发布时间', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `Latest_updateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上次更新时间', + INDEX `idx_target_id` (`target_id`), + INDEX `idx_priority_id` (`priority_id`), + INDEX `idx_is_top_publish_time` (`is_top` DESC, `publish_time` DESC), + FOREIGN KEY (`target_id`) REFERENCES `notification_targets`(`id`), + FOREIGN KEY (`priority_id`) REFERENCES `priorities`(`id`), + FOREIGN KEY (`publisher_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知表'; + +-- 4.5 token_consume(token消耗表) +CREATE TABLE `token_consume` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', + `llm_name` VARCHAR(50) NOT NULL COMMENT '模型名称', + `total_tokens` BIGINT(11) NOT NULL COMMENT '总消耗', + `prompt_tokens` BIGINT(11) NOT NULL COMMENT '输入消耗', + `completion_tokens` BIGINT(11) NOT NULL COMMENT '输出消耗', + `consume_date` DATE NOT NULL COMMENT '消耗日期', + `growth_rate` DECIMAL(5,2) COMMENT '增长率(%)', + UNIQUE INDEX `uk_llm_date` (`llm_name`, `consume_date`), + INDEX `idx_consume_date` (`consume_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='token消耗表'; + +-- 4.6 error_logs(错误分析表) +CREATE TABLE `error_logs` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', + `error_type_id` TINYINT(2) NOT NULL COMMENT '错误类型ID', + `error_count` INT(11) NOT NULL COMMENT '错误次数', + `error_rate` DECIMAL(5,2) COMMENT '错误率(%)', + `period` VARCHAR(20) NOT NULL COMMENT '统计周期', + `stat_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '统计时间', + INDEX `idx_error_type_period` (`error_type_id`, `period`), + INDEX `idx_stat_time` (`stat_time`), + FOREIGN KEY (`error_type_id`) REFERENCES `error_types`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='错误分析表'; + +-- 4.7 performance_metrics(性能趋势表) +CREATE TABLE `performance_metrics` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '指标ID', + `metric_type` VARCHAR(20) NOT NULL COMMENT '指标类型', + `metric_value` DECIMAL(10,2) NOT NULL COMMENT '指标值', + `metric_time` DATETIME NOT NULL COMMENT '指标时间', + `trend` TINYINT(1) COMMENT '趋势标识:0-下降,1-上升', + INDEX `idx_metric_type_time` (`metric_type`, `metric_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='性能趋势表'; + +-- 4.8 db_connection_logs(数据库连接日志表) +CREATE TABLE `db_connection_logs` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', + `db_connection_id` BIGINT(20) NOT NULL COMMENT '数据库连接ID', + `db_name` VARCHAR(100) NOT NULL COMMENT '数据库名称', + `connect_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '连接时间', + `status` VARCHAR(20) NOT NULL COMMENT '连接状态', + `remark` TEXT COMMENT '备注信息', + `handler_id` BIGINT(20) COMMENT '处理人ID', + INDEX `idx_db_connection_id` (`db_connection_id`), + INDEX `idx_connect_time` (`connect_time`), + INDEX `idx_status` (`status`), + FOREIGN KEY (`db_connection_id`) REFERENCES `db_connections`(`id`), + FOREIGN KEY (`handler_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库连接日志表'; + +-- 4.9 query_logs(查询日志表) +CREATE TABLE `query_logs` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', + `dialog_id` VARCHAR(50) NOT NULL COMMENT '对话ID', + `data_source_id` BIGINT(20) NOT NULL COMMENT '数据源ID', + `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', + `query_date` DATE NOT NULL COMMENT '查询日期', + `query_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '查询时间', + `execute_result` TINYINT(1) NOT NULL COMMENT '执行结果:0-失败,1-成功', + INDEX `idx_data_source_date` (`data_source_id`, `query_date`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_dialog_id` (`dialog_id`), + FOREIGN KEY (`data_source_id`) REFERENCES `db_connections`(`id`), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='查询日志表'; + +-- 4.10 user_searches(用户搜索表) +CREATE TABLE `user_searches` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '搜索ID', + `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', + `sql_content` TEXT NOT NULL COMMENT 'SQL语句', + `query_title` VARCHAR(200) NOT NULL COMMENT '查询标题', + `search_count` INT(11) NOT NULL DEFAULT 1 COMMENT '搜索次数', + `last_search_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后搜索时间', + INDEX `idx_user_last_search` (`user_id`, `last_search_time`), + INDEX `idx_user_search_count` (`user_id`, `search_count` DESC), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户搜索表'; + diff --git a/src/springboot_demo/package-lock.json b/src/springboot_demo/package-lock.json new file mode 100644 index 00000000..536952d1 --- /dev/null +++ b/src/springboot_demo/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "springboot_demo", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/src/springboot_demo/pom.xml b/src/springboot_demo/pom.xml new file mode 100644 index 00000000..ef3b9334 --- /dev/null +++ b/src/springboot_demo/pom.xml @@ -0,0 +1,127 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + com.example + springboot_demo + 0.0.1-SNAPSHOT + springboot_demo + springboot_demo + + + + + + + + + + + + + + + 21 + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.mysql + mysql-connector-j + runtime + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + 3.5.9 + + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.projectlombok + lombok + true + + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.43 + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-security + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java new file mode 100644 index 00000000..60a1168e --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java @@ -0,0 +1,13 @@ +package com.example.springboot_demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringbootDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringbootDemoApplication.class, args); + } + +} diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/common/Result.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/common/Result.java new file mode 100644 index 00000000..ecac05d4 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/common/Result.java @@ -0,0 +1,37 @@ +package com.example.springboot_demo.common; + +import lombok.Data; + +@Data +public class Result { + private Integer code; + private String message; + private T data; + + public static Result success() { + return success(null); + } + + public static Result success(T data) { + Result result = new Result<>(); + result.setCode(200); + result.setMessage("success"); + result.setData(data); + return result; + } + + public static Result error(String message) { + Result result = new Result<>(); + result.setCode(500); + result.setMessage(message); + return result; + } + + public static Result error(Integer code, String message) { + Result result = new Result<>(); + result.setCode(code); + result.setMessage(message); + return result; + } +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/CorsConfig.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/CorsConfig.java new file mode 100644 index 00000000..e6c93e4d --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/CorsConfig.java @@ -0,0 +1,26 @@ +package com.example.springboot_demo.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOriginPattern("*"); + config.setAllowCredentials(true); + config.addAllowedMethod("*"); + config.addAllowedHeader("*"); + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java new file mode 100644 index 00000000..e1a6df5a --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java @@ -0,0 +1,42 @@ +package com.example.springboot_demo.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +import com.example.springboot_demo.utils.JwtUtil; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class JwtInterceptor implements HandlerInterceptor { + + @Autowired + private JwtUtil jwtUtil; + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { + // Allow OPTIONS requests (CORS preflight) + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return true; + } + + String authHeader = request.getHeader("Authorization"); + if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + if (jwtUtil.validateToken(token)) { + // Optionally set user info in request attributes + request.setAttribute("userId", jwtUtil.getUserIdFromToken(token)); + request.setAttribute("username", jwtUtil.getUsernameFromToken(token)); + return true; + } + } + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/SecurityConfig.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/SecurityConfig.java new file mode 100644 index 00000000..2b306532 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/SecurityConfig.java @@ -0,0 +1,33 @@ +package com.example.springboot_demo.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) // Disable CSRF for REST APIs + .authorizeHttpRequests(auth -> auth + .requestMatchers("/**").permitAll() // Allow all requests for now, we'll handle auth with Interceptor for specific paths or refine this later. + // Note: The plan mentioned "Initially disable Spring Security's default login page". + // Permitting all here effectively bypasses Spring Security's default auth flow, relying on our custom JWT flow. + ); + return http.build(); + } +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java new file mode 100644 index 00000000..7dc940d8 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java @@ -0,0 +1,27 @@ +package com.example.springboot_demo.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + private JwtInterceptor jwtInterceptor; + + @Override + public void addInterceptors(@NonNull InterceptorRegistry registry) { + registry.addInterceptor(jwtInterceptor) + .addPathPatterns("/**") + .excludePathPatterns( + "/auth/**", + "/role", // 仅 POST /role 免认证 + "/user", // 用户注册免认证 + "/error" + ); + } +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/AuthController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/AuthController.java new file mode 100644 index 00000000..ef3ab1fa --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/AuthController.java @@ -0,0 +1,31 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.dto.LoginDTO; +import com.example.springboot_demo.service.AuthService; +import com.example.springboot_demo.vo.LoginVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Autowired + private AuthService authService; + + @PostMapping("/login") + public Result login(@RequestBody LoginDTO loginDTO) { + try { + LoginVO loginVO = authService.login(loginDTO); + return Result.success(loginVO); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java new file mode 100644 index 00000000..13ba8982 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java @@ -0,0 +1,76 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.ColumnMetadata; +import com.example.springboot_demo.service.ColumnMetadataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/column-metadata") +public class ColumnMetadataController { + + @Autowired + private ColumnMetadataService columnMetadataService; + + /** + * 查询所有字段元数据 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(columnMetadataService.list()); + } + + /** + * 根据表ID查询字段元数据 + */ + @GetMapping("/list/{tableId}") + public Result> listByTable(@PathVariable Long tableId) { + return Result.success(columnMetadataService.listByTableId(tableId)); + } + + /** + * 根据ID查询字段元数据 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(columnMetadataService.getById(id)); + } + + /** + * 添加字段元数据 + */ + @PostMapping + public Result save(@RequestBody ColumnMetadata columnMetadata) { + columnMetadata.setCreateTime(LocalDateTime.now()); + if (columnMetadata.getIsPrimary() == null) { + columnMetadata.setIsPrimary(0); + } + columnMetadataService.save(columnMetadata); + return Result.success(columnMetadata); + } + + /** + * 更新字段元数据 + */ + @PutMapping + public Result update(@RequestBody ColumnMetadata columnMetadata) { + columnMetadataService.updateById(columnMetadata); + return Result.success(columnMetadata); + } + + /** + * 删除字段元数据 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + columnMetadataService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java new file mode 100644 index 00000000..bcb3282f --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java @@ -0,0 +1,86 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.DbConnection; +import com.example.springboot_demo.service.DbConnectionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/db-connection") +public class DbConnectionController { + + @Autowired + private DbConnectionService dbConnectionService; + + /** + * 查询所有数据库连接 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(dbConnectionService.list()); + } + + /** + * 根据创建者ID查询数据库连接 + */ + @GetMapping("/list/{createUserId}") + public Result> listByUser(@PathVariable Long createUserId) { + return Result.success(dbConnectionService.listByCreateUserId(createUserId)); + } + + /** + * 根据ID查询数据库连接 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(dbConnectionService.getById(id)); + } + + /** + * 添加数据库连接 + */ + @PostMapping + public Result save(@RequestBody DbConnection dbConnection) { + dbConnection.setCreateTime(LocalDateTime.now()); + dbConnection.setUpdateTime(LocalDateTime.now()); + if (dbConnection.getStatus() == null) { + dbConnection.setStatus("disconnected"); + } + dbConnectionService.save(dbConnection); + return Result.success(dbConnection); + } + + /** + * 更新数据库连接 + */ + @PutMapping + public Result update(@RequestBody DbConnection dbConnection) { + dbConnection.setUpdateTime(LocalDateTime.now()); + dbConnectionService.updateById(dbConnection); + return Result.success(dbConnection); + } + + /** + * 删除数据库连接 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + dbConnectionService.removeById(id); + return Result.success(); + } + + /** + * 测试数据库连接 + */ + @GetMapping("/test/{id}") + public Result testConnection(@PathVariable Long id) { + boolean result = dbConnectionService.testConnection(id); + return Result.success(result); + } +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbTypeController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbTypeController.java new file mode 100644 index 00000000..6dc048f5 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbTypeController.java @@ -0,0 +1,71 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.DbType; +import com.example.springboot_demo.service.DbTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/db-type") +public class DbTypeController { + + @Autowired + private DbTypeService dbTypeService; + + /** + * 查询所有数据库类型 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(dbTypeService.list()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Integer id) { + return Result.success(dbTypeService.getById(id)); + } + + /** + * 根据类型编码查询 + */ + @GetMapping("/code/{typeCode}") + public Result getByTypeCode(@PathVariable String typeCode) { + return Result.success(dbTypeService.getByTypeCode(typeCode)); + } + + /** + * 添加数据库类型 + */ + @PostMapping + public Result save(@RequestBody DbType dbType) { + dbTypeService.save(dbType); + return Result.success(dbType); + } + + /** + * 更新数据库类型 + */ + @PutMapping + public Result update(@RequestBody DbType dbType) { + dbTypeService.updateById(dbType); + return Result.success(dbType); + } + + /** + * 删除数据库类型 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + dbTypeService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DialogController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DialogController.java new file mode 100644 index 00000000..9010bb95 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DialogController.java @@ -0,0 +1,34 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mongodb.DialogRecord; +import com.example.springboot_demo.service.DialogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/dialog") +public class DialogController { + + @Autowired + private DialogService dialogService; + + @GetMapping("/list") + public Result> getUserDialogs(@RequestHeader(value = "userId", defaultValue = "1") Long userId) { + List dialogs = dialogService.getUserDialogs(userId); + return Result.success(dialogs); + } + + @GetMapping("/{dialogId}") + public Result getDialogById(@PathVariable String dialogId) { + DialogRecord dialog = dialogService.getDialogById(dialogId); + if (dialog != null) { + return Result.success(dialog); + } + return Result.error("对话不存在"); + } +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java new file mode 100644 index 00000000..294b93c4 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java @@ -0,0 +1,86 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.ErrorLog; +import com.example.springboot_demo.service.ErrorLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/error-log") +public class ErrorLogController { + + @Autowired + private ErrorLogService errorLogService; + + /** + * 查询所有错误日志 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(errorLogService.list()); + } + + /** + * 根据错误类型查询 + */ + @GetMapping("/list/type/{errorTypeId}") + public Result> listByErrorType(@PathVariable Integer errorTypeId) { + return Result.success(errorLogService.listByErrorTypeId(errorTypeId)); + } + + /** + * 根据统计周期查询 + */ + @GetMapping("/list/period/{period}") + public Result> listByPeriod(@PathVariable String period) { + return Result.success(errorLogService.listByPeriod(period)); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(errorLogService.getById(id)); + } + + /** + * 添加错误日志 + */ + @PostMapping + public Result save(@RequestBody ErrorLog errorLog) { + if (errorLog.getStatTime() == null) { + errorLog.setStatTime(LocalDateTime.now()); + } + errorLogService.save(errorLog); + return Result.success(errorLog); + } + + /** + * 更新错误日志 + */ + @PutMapping + public Result update(@RequestBody ErrorLog errorLog) { + errorLogService.updateById(errorLog); + return Result.success(errorLog); + } + + /** + * 删除错误日志 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + errorLogService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java new file mode 100644 index 00000000..979d7938 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java @@ -0,0 +1,74 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.ErrorType; +import com.example.springboot_demo.service.ErrorTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/error-type") +public class ErrorTypeController { + + @Autowired + private ErrorTypeService errorTypeService; + + /** + * 查询所有错误类型 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(errorTypeService.list()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Integer id) { + return Result.success(errorTypeService.getById(id)); + } + + /** + * 根据错误编码查询 + */ + @GetMapping("/code/{errorCode}") + public Result getByErrorCode(@PathVariable String errorCode) { + return Result.success(errorTypeService.getByErrorCode(errorCode)); + } + + /** + * 添加错误类型 + */ + @PostMapping + public Result save(@RequestBody ErrorType errorType) { + errorTypeService.save(errorType); + return Result.success(errorType); + } + + /** + * 更新错误类型 + */ + @PutMapping + public Result update(@RequestBody ErrorType errorType) { + errorTypeService.updateById(errorType); + return Result.success(errorType); + } + + /** + * 删除错误类型 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + errorTypeService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java new file mode 100644 index 00000000..b8be3cf0 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java @@ -0,0 +1,91 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.LlmConfig; +import com.example.springboot_demo.service.LlmConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/llm-config") +public class LlmConfigController { + + @Autowired + private LlmConfigService llmConfigService; + + /** + * 查询所有大模型配置 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(llmConfigService.list()); + } + + /** + * 查询所有可用的大模型配置 + */ + @GetMapping("/list/available") + public Result> listAvailable() { + return Result.success(llmConfigService.listAvailable()); + } + + /** + * 根据ID查询大模型配置 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(llmConfigService.getById(id)); + } + + /** + * 添加大模型配置 + */ + @PostMapping + public Result save(@RequestBody LlmConfig llmConfig) { + llmConfig.setCreateTime(LocalDateTime.now()); + llmConfig.setUpdateTime(LocalDateTime.now()); + if (llmConfig.getIsDisabled() == null) { + llmConfig.setIsDisabled(0); + } + llmConfigService.save(llmConfig); + return Result.success(llmConfig); + } + + /** + * 更新大模型配置 + */ + @PutMapping + public Result update(@RequestBody LlmConfig llmConfig) { + llmConfig.setUpdateTime(LocalDateTime.now()); + llmConfigService.updateById(llmConfig); + return Result.success(llmConfig); + } + + /** + * 删除大模型配置 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + llmConfigService.removeById(id); + return Result.success(); + } + + /** + * 禁用/启用大模型配置 + */ + @PutMapping("/{id}/toggle") + public Result toggle(@PathVariable Long id) { + LlmConfig config = llmConfigService.getById(id); + if (config != null) { + config.setIsDisabled(config.getIsDisabled() == 0 ? 1 : 0); + config.setUpdateTime(LocalDateTime.now()); + llmConfigService.updateById(config); + } + return Result.success(); + } +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java new file mode 100644 index 00000000..e7437954 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java @@ -0,0 +1,71 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.LlmStatus; +import com.example.springboot_demo.service.LlmStatusService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/llm-status") +public class LlmStatusController { + + @Autowired + private LlmStatusService llmStatusService; + + /** + * 查询所有大模型状态 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(llmStatusService.list()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Integer id) { + return Result.success(llmStatusService.getById(id)); + } + + /** + * 根据状态编码查询 + */ + @GetMapping("/code/{statusCode}") + public Result getByStatusCode(@PathVariable String statusCode) { + return Result.success(llmStatusService.getByStatusCode(statusCode)); + } + + /** + * 添加大模型状态 + */ + @PostMapping + public Result save(@RequestBody LlmStatus llmStatus) { + llmStatusService.save(llmStatus); + return Result.success(llmStatus); + } + + /** + * 更新大模型状态 + */ + @PutMapping + public Result update(@RequestBody LlmStatus llmStatus) { + llmStatusService.updateById(llmStatus); + return Result.success(llmStatus); + } + + /** + * 删除大模型状态 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + llmStatusService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationController.java new file mode 100644 index 00000000..71f65e69 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationController.java @@ -0,0 +1,125 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.Notification; +import com.example.springboot_demo.service.NotificationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/notification") +public class NotificationController { + + @Autowired + private NotificationService notificationService; + + /** + * 查询所有通知 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(notificationService.list()); + } + + /** + * 查询已发布的通知 + */ + @GetMapping("/list/published") + public Result> listPublished() { + return Result.success(notificationService.listPublished()); + } + + /** + * 查询草稿 + */ + @GetMapping("/list/drafts") + public Result> listDrafts() { + return Result.success(notificationService.listDrafts()); + } + + /** + * 根据目标ID查询通知 + */ + @GetMapping("/list/target/{targetId}") + public Result> listByTarget(@PathVariable Integer targetId) { + return Result.success(notificationService.listByTargetId(targetId)); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(notificationService.getById(id)); + } + + /** + * 添加通知(草稿) + */ + @PostMapping + public Result save(@RequestBody Notification notification) { + notification.setCreateTime(LocalDateTime.now()); + notification.setLatestUpdateTime(LocalDateTime.now()); + if (notification.getIsTop() == null) { + notification.setIsTop(0); + } + notificationService.save(notification); + return Result.success(notification); + } + + /** + * 更新通知 + */ + @PutMapping + public Result update(@RequestBody Notification notification) { + notification.setLatestUpdateTime(LocalDateTime.now()); + notificationService.updateById(notification); + return Result.success(notification); + } + + /** + * 发布通知 + */ + @PutMapping("/{id}/publish") + public Result publish(@PathVariable Long id) { + Notification notification = notificationService.getById(id); + if (notification != null) { + notification.setPublishTime(LocalDateTime.now()); + notification.setLatestUpdateTime(LocalDateTime.now()); + notificationService.updateById(notification); + } + return Result.success(); + } + + /** + * 置顶/取消置顶 + */ + @PutMapping("/{id}/toggle-top") + public Result toggleTop(@PathVariable Long id) { + Notification notification = notificationService.getById(id); + if (notification != null) { + notification.setIsTop(notification.getIsTop() == 0 ? 1 : 0); + notification.setLatestUpdateTime(LocalDateTime.now()); + notificationService.updateById(notification); + } + return Result.success(); + } + + /** + * 删除通知 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + notificationService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java new file mode 100644 index 00000000..4053b71a --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java @@ -0,0 +1,74 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.NotificationTarget; +import com.example.springboot_demo.service.NotificationTargetService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/notification-target") +public class NotificationTargetController { + + @Autowired + private NotificationTargetService notificationTargetService; + + /** + * 查询所有通知目标 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(notificationTargetService.list()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Integer id) { + return Result.success(notificationTargetService.getById(id)); + } + + /** + * 根据目标编码查询 + */ + @GetMapping("/code/{targetCode}") + public Result getByTargetCode(@PathVariable String targetCode) { + return Result.success(notificationTargetService.getByTargetCode(targetCode)); + } + + /** + * 添加通知目标 + */ + @PostMapping + public Result save(@RequestBody NotificationTarget notificationTarget) { + notificationTargetService.save(notificationTarget); + return Result.success(notificationTarget); + } + + /** + * 更新通知目标 + */ + @PutMapping + public Result update(@RequestBody NotificationTarget notificationTarget) { + notificationTargetService.updateById(notificationTarget); + return Result.success(notificationTarget); + } + + /** + * 删除通知目标 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + notificationTargetService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/OperationLogController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/OperationLogController.java new file mode 100644 index 00000000..7046f985 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/OperationLogController.java @@ -0,0 +1,82 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.OperationLog; +import com.example.springboot_demo.service.OperationLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/operation-log") +public class OperationLogController { + + @Autowired + private OperationLogService operationLogService; + + /** + * 查询所有操作日志 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(operationLogService.list()); + } + + /** + * 根据用户ID查询操作日志 + */ + @GetMapping("/list/user/{userId}") + public Result> listByUser(@PathVariable Long userId) { + return Result.success(operationLogService.listByUserId(userId)); + } + + /** + * 根据模块查询操作日志 + */ + @GetMapping("/list/module/{module}") + public Result> listByModule(@PathVariable String module) { + return Result.success(operationLogService.listByModule(module)); + } + + /** + * 查询失败的操作日志 + */ + @GetMapping("/list/failed") + public Result> listFailed() { + return Result.success(operationLogService.listFailed()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(operationLogService.getById(id)); + } + + /** + * 添加操作日志 + */ + @PostMapping + public Result save(@RequestBody OperationLog operationLog) { + if (operationLog.getOperateTime() == null) { + operationLog.setOperateTime(LocalDateTime.now()); + } + operationLogService.save(operationLog); + return Result.success(operationLog); + } + + /** + * 删除操作日志 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + operationLogService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/PriorityController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/PriorityController.java new file mode 100644 index 00000000..7912c334 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/PriorityController.java @@ -0,0 +1,74 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.Priority; +import com.example.springboot_demo.service.PriorityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/priority") +public class PriorityController { + + @Autowired + private PriorityService priorityService; + + /** + * 查询所有优先级(按排序) + */ + @GetMapping("/list") + public Result> list() { + return Result.success(priorityService.listOrderBySort()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Integer id) { + return Result.success(priorityService.getById(id)); + } + + /** + * 根据优先级编码查询 + */ + @GetMapping("/code/{priorityCode}") + public Result getByPriorityCode(@PathVariable String priorityCode) { + return Result.success(priorityService.getByPriorityCode(priorityCode)); + } + + /** + * 添加优先级 + */ + @PostMapping + public Result save(@RequestBody Priority priority) { + priorityService.save(priority); + return Result.success(priority); + } + + /** + * 更新优先级 + */ + @PutMapping + public Result update(@RequestBody Priority priority) { + priorityService.updateById(priority); + return Result.success(priority); + } + + /** + * 删除优先级 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + priorityService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryController.java new file mode 100644 index 00000000..78d2f7a3 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryController.java @@ -0,0 +1,29 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.dto.QueryRequestDTO; +import com.example.springboot_demo.service.QueryService; +import com.example.springboot_demo.vo.QueryResponseVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/query") +public class QueryController { + + @Autowired + private QueryService queryService; + + @PostMapping("/execute") + public Result executeQuery(@RequestBody QueryRequestDTO request, + @RequestHeader(value = "userId", defaultValue = "1") Long userId) { + try { + QueryResponseVO response = queryService.executeQuery(request, userId); + return Result.success(response); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryLogController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryLogController.java new file mode 100644 index 00000000..22f789c3 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryLogController.java @@ -0,0 +1,78 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.QueryLog; +import com.example.springboot_demo.service.QueryLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/query-log") +public class QueryLogController { + + @Autowired + private QueryLogService queryLogService; + + /** + * 查询所有查询日志 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(queryLogService.list()); + } + + /** + * 根据用户ID查询查询日志 + */ + @GetMapping("/list/user/{userId}") + public Result> listByUser(@PathVariable Long userId) { + return Result.success(queryLogService.listByUserId(userId)); + } + + /** + * 根据对话ID查询查询日志 + */ + @GetMapping("/list/dialog/{dialogId}") + public Result> listByDialog(@PathVariable String dialogId) { + return Result.success(queryLogService.listByDialogId(dialogId)); + } + + /** + * 根据ID查询查询日志 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(queryLogService.getById(id)); + } + + /** + * 添加查询日志 + */ + @PostMapping + public Result save(@RequestBody QueryLog queryLog) { + if (queryLog.getQueryDate() == null) { + queryLog.setQueryDate(LocalDate.now()); + } + if (queryLog.getQueryTime() == null) { + queryLog.setQueryTime(LocalDateTime.now()); + } + queryLogService.save(queryLog); + return Result.success(queryLog); + } + + /** + * 删除查询日志 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + queryLogService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/RoleController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/RoleController.java new file mode 100644 index 00000000..b6756740 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/RoleController.java @@ -0,0 +1,28 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.Role; +import com.example.springboot_demo.service.RoleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/role") +public class RoleController { + + @Autowired + private RoleService roleService; + + @PostMapping + public Result add(@RequestBody Role role) { + roleService.save(role); + return Result.success(role); + } + + @GetMapping("/list") + public Result> list() { + return Result.success(roleService.list()); + } +} diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java new file mode 100644 index 00000000..0c7429bd --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java @@ -0,0 +1,77 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.SystemHealth; +import com.example.springboot_demo.service.SystemHealthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/system-health") +public class SystemHealthController { + + @Autowired + private SystemHealthService systemHealthService; + + /** + * 查询所有健康记录 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(systemHealthService.list()); + } + + /** + * 查询最新的健康记录 + */ + @GetMapping("/latest") + public Result getLatest() { + return Result.success(systemHealthService.getLatest()); + } + + /** + * 查询最近N条健康记录 + */ + @GetMapping("/recent/{limit}") + public Result> listRecent(@PathVariable int limit) { + return Result.success(systemHealthService.listRecent(limit)); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(systemHealthService.getById(id)); + } + + /** + * 添加健康记录 + */ + @PostMapping + public Result save(@RequestBody SystemHealth systemHealth) { + if (systemHealth.getCollectTime() == null) { + systemHealth.setCollectTime(LocalDateTime.now()); + } + systemHealthService.save(systemHealth); + return Result.success(systemHealth); + } + + /** + * 删除健康记录 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + systemHealthService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java new file mode 100644 index 00000000..f6f77f72 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java @@ -0,0 +1,75 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.TableMetadata; +import com.example.springboot_demo.service.TableMetadataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/table-metadata") +public class TableMetadataController { + + @Autowired + private TableMetadataService tableMetadataService; + + /** + * 查询所有表元数据 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(tableMetadataService.list()); + } + + /** + * 根据数据库连接ID查询表元数据 + */ + @GetMapping("/list/{dbConnectionId}") + public Result> listByDbConnection(@PathVariable Long dbConnectionId) { + return Result.success(tableMetadataService.listByDbConnectionId(dbConnectionId)); + } + + /** + * 根据ID查询表元数据 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(tableMetadataService.getById(id)); + } + + /** + * 添加表元数据 + */ + @PostMapping + public Result save(@RequestBody TableMetadata tableMetadata) { + tableMetadata.setCreateTime(LocalDateTime.now()); + tableMetadata.setUpdateTime(LocalDateTime.now()); + tableMetadataService.save(tableMetadata); + return Result.success(tableMetadata); + } + + /** + * 更新表元数据 + */ + @PutMapping + public Result update(@RequestBody TableMetadata tableMetadata) { + tableMetadata.setUpdateTime(LocalDateTime.now()); + tableMetadataService.updateById(tableMetadata); + return Result.success(tableMetadata); + } + + /** + * 删除表元数据 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + tableMetadataService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TestController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TestController.java new file mode 100644 index 00000000..7e9efbd8 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TestController.java @@ -0,0 +1,107 @@ +package com.example.springboot_demo.controller; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/test") +public class TestController { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private MongoTemplate mongoTemplate; + + @Autowired + private StringRedisTemplate redisTemplate; + + @GetMapping("/hello") + public String hello() { + return "Hello, Spring Boot!"; + } + + @GetMapping("/mysql") + public Map testMySQL() { + Map result = new HashMap<>(); + try { + // 查询 MySQL 版本 + String version = jdbcTemplate.queryForObject("SELECT VERSION()", String.class); + // 查询数据库名 + String database = jdbcTemplate.queryForObject("SELECT DATABASE()", String.class); + // 查询表数量 + Integer tableCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ?", + Integer.class, + database + ); + + result.put("status", "success"); + result.put("version", version); + result.put("database", database); + result.put("tableCount", tableCount); + } catch (Exception e) { + result.put("status", "error"); + result.put("message", e.getMessage()); + } + return result; + } + + @GetMapping("/mongodb") + public Map testMongoDB() { + Map result = new HashMap<>(); + try { + // 获取数据库名 + String dbName = mongoTemplate.getDb().getName(); + // 获取集合数量 + int collectionCount = mongoTemplate.getDb().listCollectionNames().into(new java.util.ArrayList<>()).size(); + + result.put("status", "success"); + result.put("database", dbName); + result.put("collectionCount", collectionCount); + } catch (Exception e) { + result.put("status", "error"); + result.put("message", e.getMessage()); + } + return result; + } + + @GetMapping("/redis") + public Map testRedis() { + Map result = new HashMap<>(); + try { + // 测试写入 + redisTemplate.opsForValue().set("test:key", "test_value"); + // 测试读取 + String value = redisTemplate.opsForValue().get("test:key"); + // 删除测试数据 + redisTemplate.delete("test:key"); + + result.put("status", "success"); + result.put("message", "Redis 连接正常"); + result.put("testValue", value); + } catch (Exception e) { + result.put("status", "error"); + result.put("message", e.getMessage()); + } + return result; + } + + @GetMapping("/all") + public Map testAll() { + Map result = new HashMap<>(); + result.put("mysql", testMySQL()); + result.put("mongodb", testMongoDB()); + result.put("redis", testRedis()); + return result; + } +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserController.java new file mode 100644 index 00000000..77e982fb --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserController.java @@ -0,0 +1,87 @@ +package com.example.springboot_demo.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.User; +import com.example.springboot_demo.service.UserService; + +@RestController +@RequestMapping("/user") +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + User user = userService.getById(id); + if (user != null) { + return Result.success(user); + } + return Result.error("用户不存在"); + } + + @GetMapping("/list") + public Result> list() { + List users = userService.list(); + return Result.success(users); + } + + @GetMapping("/page") + public Result> page( + @RequestParam(defaultValue = "1") int current, + @RequestParam(defaultValue = "10") int size) { + Page page = userService.page(current, size); + return Result.success(page); + } + + @PostMapping + public Result save(@RequestBody User user) { + boolean success = userService.save(user); + if (success) { + return Result.success("添加成功"); + } + return Result.error("添加失败"); + } + + @PutMapping + public Result update(@RequestBody User user) { + boolean success = userService.updateById(user); + if (success) { + return Result.success("更新成功"); + } + return Result.error("更新失败"); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + boolean success = userService.removeById(id); + if (success) { + return Result.success("删除成功"); + } + return Result.error("删除失败"); + } + + @GetMapping("/username/{username}") + public Result getByUsername(@PathVariable String username) { + User user = userService.getByUsername(username); + if (user != null) { + return Result.success(user); + } + return Result.error("用户不存在"); + } +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java new file mode 100644 index 00000000..c91565ab --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java @@ -0,0 +1,95 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.UserDbPermission; +import com.example.springboot_demo.service.UserDbPermissionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/user-db-permission") +public class UserDbPermissionController { + + @Autowired + private UserDbPermissionService userDbPermissionService; + + /** + * 查询所有权限 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(userDbPermissionService.list()); + } + + /** + * 查询已分配权限的用户 + */ + @GetMapping("/list/assigned") + public Result> listAssigned() { + return Result.success(userDbPermissionService.listAssigned()); + } + + /** + * 查询未分配权限的用户 + */ + @GetMapping("/list/unassigned") + public Result> listUnassigned() { + return Result.success(userDbPermissionService.listUnassigned()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(userDbPermissionService.getById(id)); + } + + /** + * 根据用户ID查询权限 + */ + @GetMapping("/user/{userId}") + public Result getByUserId(@PathVariable Long userId) { + return Result.success(userDbPermissionService.getByUserId(userId)); + } + + /** + * 添加或更新权限 + */ + @PostMapping + public Result save(@RequestBody UserDbPermission permission) { + permission.setLastGrantTime(LocalDateTime.now()); + permission.setUpdateTime(LocalDateTime.now()); + if (permission.getIsAssigned() == null) { + permission.setIsAssigned(1); + } + userDbPermissionService.save(permission); + return Result.success(permission); + } + + /** + * 更新权限 + */ + @PutMapping + public Result update(@RequestBody UserDbPermission permission) { + permission.setLastGrantTime(LocalDateTime.now()); + permission.setUpdateTime(LocalDateTime.now()); + userDbPermissionService.updateById(permission); + return Result.success(permission); + } + + /** + * 删除权限 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + userDbPermissionService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/LoginDTO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/LoginDTO.java new file mode 100644 index 00000000..bc48a109 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/LoginDTO.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.dto; + +import lombok.Data; + +@Data +public class LoginDTO { + private String username; + private String password; +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java new file mode 100644 index 00000000..dff2b8c9 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java @@ -0,0 +1,13 @@ +package com.example.springboot_demo.dto; + +import lombok.Data; + +@Data +public class QueryRequestDTO { + private String userPrompt; // 用户的自然语言查询 + private String model; // 使用的大模型 + private String database; // 使用的数据库 + private String conversationId; // 对话ID(用于多轮对话) +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java new file mode 100644 index 00000000..e72179ef --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java @@ -0,0 +1,29 @@ +package com.example.springboot_demo.entity.mongodb; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Data +@Document(collection = "dialog_records") +public class DialogRecord { + + @Id + private String id; + + private String dialogId; + + private Long userId; + + private String topic; + + private Integer totalRounds; + + private LocalDateTime startTime; + + private LocalDateTime lastTime; +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java new file mode 100644 index 00000000..32f43878 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java @@ -0,0 +1,30 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("column_metadata") +public class ColumnMetadata { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long tableId; + + private String columnName; + + private String dataType; + + private String description; + + private Integer isPrimary; + + private LocalDateTime createTime; +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java new file mode 100644 index 00000000..6b3274ca --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java @@ -0,0 +1,35 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("db_connections") +public class DbConnection { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String name; + + private Integer dbTypeId; + + private String url; + + private String username; + + private String password; + + private String status; + + private Long createUserId; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java new file mode 100644 index 00000000..bcbff5ef --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("db_types") +public class DbType { + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + private String typeName; + + private String typeCode; + + private String description; +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java new file mode 100644 index 00000000..6a708bf0 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java @@ -0,0 +1,29 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@TableName("error_logs") +public class ErrorLog { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Integer errorTypeId; + + private Integer errorCount; + + private BigDecimal errorRate; + + private String period; + + private LocalDateTime statTime; +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java new file mode 100644 index 00000000..548e619e --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("error_types") +public class ErrorType { + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + private String errorName; + + private String errorCode; + + private String description; +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java new file mode 100644 index 00000000..7fba5c66 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java @@ -0,0 +1,37 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("llm_configs") +public class LlmConfig { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String name; + + private String version; + + private String apiKey; + + private String apiUrl; + + private Integer statusId; + + private Integer isDisabled; + + private Integer timeout; + + private Long createUserId; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java new file mode 100644 index 00000000..834d787f --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("llm_status") +public class LlmStatus { + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + private String statusName; + + private String statusCode; + + private String description; +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java new file mode 100644 index 00000000..d9dac720 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java @@ -0,0 +1,39 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("notifications") +public class Notification { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String title; + + private String content; + + private Integer targetId; + + private Integer priorityId; + + private Long publisherId; + + private Integer isTop; + + private LocalDateTime publishTime; + + private LocalDateTime createTime; + + private LocalDateTime latestUpdateTime; +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java new file mode 100644 index 00000000..47f0d16f --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("notification_targets") +public class NotificationTarget { + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + private String targetName; + + private String targetCode; + + private String description; +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java new file mode 100644 index 00000000..449a8f10 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java @@ -0,0 +1,36 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("operation_logs") +public class OperationLog { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long userId; + + private String username; + + private String operation; + + private String module; + + private String relatedLlm; + + private String ipAddress; + + private LocalDateTime operateTime; + + private Integer result; + + private String errorMsg; +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java new file mode 100644 index 00000000..1800e3f5 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("priorities") +public class Priority { + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + private String priorityName; + + private String priorityCode; + + private Integer sort; +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java new file mode 100644 index 00000000..b192dbfe --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java @@ -0,0 +1,31 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@TableName("query_logs") +public class QueryLog { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String dialogId; + + private Long dataSourceId; + + private Long userId; + + private LocalDate queryDate; + + private LocalDateTime queryTime; + + private Integer executeResult; +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Role.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Role.java new file mode 100644 index 00000000..2685fbef --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Role.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("roles") +public class Role { + + @TableId(type = IdType.AUTO) + private Integer id; + + private String roleName; + + private String roleCode; + + private String description; +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java new file mode 100644 index 00000000..949bfff0 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java @@ -0,0 +1,32 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@TableName("system_health") +public class SystemHealth { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Integer dbDelay; + + private Integer cacheDelay; + + private Integer llmDelay; + + private BigDecimal storageUsage; + + private LocalDateTime collectTime; +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java new file mode 100644 index 00000000..537e2607 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java @@ -0,0 +1,27 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("table_metadata") +public class TableMetadata { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long dbConnectionId; + + private String tableName; + + private String description; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/User.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/User.java new file mode 100644 index 00000000..6bf4b10f --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/User.java @@ -0,0 +1,38 @@ +package com.example.springboot_demo.entity.mysql; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +@Data +@TableName("users") +public class User { + + @TableId(type = IdType.AUTO) + private Long id; + + private String username; + + private String password; + + private String email; + + private String phonenumber; + + private Integer roleId; + + private String avatarUrl; + + private Integer status; + + private Integer onlineStatus; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java new file mode 100644 index 00000000..77395ad1 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java @@ -0,0 +1,30 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("user_db_permissions") +public class UserDbPermission { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long userId; + + private String permissionDetails; // JSON格式字符串 + + private Long lastGrantUserId; + + private Integer isAssigned; + + private LocalDateTime lastGrantTime; + + private LocalDateTime updateTime; +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java new file mode 100644 index 00000000..1dc43695 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.ColumnMetadata; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ColumnMetadataMapper extends BaseMapper { +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java new file mode 100644 index 00000000..2fe5045d --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.DbConnection; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DbConnectionMapper extends BaseMapper { +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java new file mode 100644 index 00000000..09d97b6e --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.DbType; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DbTypeMapper extends BaseMapper { +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java new file mode 100644 index 00000000..df160331 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.ErrorLog; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ErrorLogMapper extends BaseMapper { +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java new file mode 100644 index 00000000..b9148bca --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.ErrorType; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ErrorTypeMapper extends BaseMapper { +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java new file mode 100644 index 00000000..53198bf5 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.LlmConfig; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface LlmConfigMapper extends BaseMapper { +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java new file mode 100644 index 00000000..b03abe84 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.LlmStatus; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface LlmStatusMapper extends BaseMapper { +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java new file mode 100644 index 00000000..30259ac6 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.Notification; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface NotificationMapper extends BaseMapper { +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java new file mode 100644 index 00000000..f228f075 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.NotificationTarget; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface NotificationTargetMapper extends BaseMapper { +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java new file mode 100644 index 00000000..0c7c52c6 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.OperationLog; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OperationLogMapper extends BaseMapper { +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java new file mode 100644 index 00000000..a3a8149f --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.Priority; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PriorityMapper extends BaseMapper { +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java new file mode 100644 index 00000000..7f335438 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.QueryLog; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface QueryLogMapper extends BaseMapper { +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java new file mode 100644 index 00000000..4ea0159a --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.Role; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface RoleMapper extends BaseMapper { +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java new file mode 100644 index 00000000..30b80f6e --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.SystemHealth; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SystemHealthMapper extends BaseMapper { +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java new file mode 100644 index 00000000..1796fd36 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.TableMetadata; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TableMetadataMapper extends BaseMapper { +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java new file mode 100644 index 00000000..83b35f82 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.UserDbPermission; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserDbPermissionMapper extends BaseMapper { +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserMapper.java new file mode 100644 index 00000000..cf537ad9 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.User; + +@Mapper +public interface UserMapper extends BaseMapper { +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java new file mode 100644 index 00000000..10a74819 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.repository; + +import com.example.springboot_demo.entity.mongodb.DialogRecord; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface DialogRecordRepository extends MongoRepository { + List findByUserIdOrderByLastTimeDesc(Long userId); + DialogRecord findByDialogId(String dialogId); +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/AuthService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/AuthService.java new file mode 100644 index 00000000..2b09304f --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/AuthService.java @@ -0,0 +1,10 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.dto.LoginDTO; +import com.example.springboot_demo.vo.LoginVO; + +public interface AuthService { + LoginVO login(LoginDTO loginDTO); +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java new file mode 100644 index 00000000..fb20e175 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java @@ -0,0 +1,16 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.ColumnMetadata; + +import java.util.List; + +public interface ColumnMetadataService extends IService { + /** + * 根据表ID查询字段元数据列表 + */ + List listByTableId(Long tableId); +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbConnectionService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbConnectionService.java new file mode 100644 index 00000000..cc488b7a --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbConnectionService.java @@ -0,0 +1,20 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.DbConnection; + +import java.util.List; + +public interface DbConnectionService extends IService { + /** + * 根据创建者ID查询数据库连接列表 + */ + List listByCreateUserId(Long createUserId); + + /** + * 测试数据库连接 + */ + boolean testConnection(Long id); +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbTypeService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbTypeService.java new file mode 100644 index 00000000..bf73ad08 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbTypeService.java @@ -0,0 +1,14 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.DbType; + +public interface DbTypeService extends IService { + /** + * 根据类型编码查询 + */ + DbType getByTypeCode(String typeCode); +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DialogService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DialogService.java new file mode 100644 index 00000000..1820ac01 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DialogService.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mongodb.DialogRecord; + +import java.util.List; + +public interface DialogService { + List getUserDialogs(Long userId); + DialogRecord getDialogById(String dialogId); +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorLogService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorLogService.java new file mode 100644 index 00000000..7e52e227 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorLogService.java @@ -0,0 +1,24 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.ErrorLog; + +import java.util.List; + +public interface ErrorLogService extends IService { + /** + * 根据错误类型查询 + */ + List listByErrorTypeId(Integer errorTypeId); + + /** + * 根据统计周期查询 + */ + List listByPeriod(String period); +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java new file mode 100644 index 00000000..7d333874 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java @@ -0,0 +1,17 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.ErrorType; + +public interface ErrorTypeService extends IService { + /** + * 根据错误编码查询 + */ + ErrorType getByErrorCode(String errorCode); +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmConfigService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmConfigService.java new file mode 100644 index 00000000..ee679470 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmConfigService.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.LlmConfig; + +import java.util.List; + +public interface LlmConfigService extends IService { + /** + * 获取所有可用的大模型配置 + */ + List listAvailable(); +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmService.java new file mode 100644 index 00000000..6823b868 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmService.java @@ -0,0 +1,20 @@ +package com.example.springboot_demo.service; + +import java.util.Map; + +/** + * 大模型调用服务接口 + */ +public interface LlmService { + /** + * 调用大模型生成SQL和结果 + * @param prompt 用户提示词 + * @param modelName 模型名称 + * @param databaseName 数据库名称 + * @return 包含SQL、表格数据和图表数据的Map + */ + Map generateQuery(String prompt, String modelName, String databaseName); +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmStatusService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmStatusService.java new file mode 100644 index 00000000..ab52b85d --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmStatusService.java @@ -0,0 +1,14 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.LlmStatus; + +public interface LlmStatusService extends IService { + /** + * 根据状态编码查询 + */ + LlmStatus getByStatusCode(String statusCode); +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationService.java new file mode 100644 index 00000000..6cbea2f0 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationService.java @@ -0,0 +1,29 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.Notification; + +import java.util.List; + +public interface NotificationService extends IService { + /** + * 查询所有已发布的通知(置顶优先,按时间排序) + */ + List listPublished(); + + /** + * 查询所有草稿 + */ + List listDrafts(); + + /** + * 根据目标ID查询通知 + */ + List listByTargetId(Integer targetId); +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java new file mode 100644 index 00000000..4cc954cf --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java @@ -0,0 +1,17 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.NotificationTarget; + +public interface NotificationTargetService extends IService { + /** + * 根据目标编码查询 + */ + NotificationTarget getByTargetCode(String targetCode); +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/OperationLogService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/OperationLogService.java new file mode 100644 index 00000000..b15542ff --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/OperationLogService.java @@ -0,0 +1,26 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.OperationLog; + +import java.util.List; + +public interface OperationLogService extends IService { + /** + * 根据用户ID查询操作日志 + */ + List listByUserId(Long userId); + + /** + * 根据模块查询操作日志 + */ + List listByModule(String module); + + /** + * 查询失败的操作日志 + */ + List listFailed(); +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/PriorityService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/PriorityService.java new file mode 100644 index 00000000..fadedad1 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/PriorityService.java @@ -0,0 +1,24 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.Priority; + +import java.util.List; + +public interface PriorityService extends IService { + /** + * 根据优先级编码查询 + */ + Priority getByPriorityCode(String priorityCode); + + /** + * 按排序权重查询所有优先级 + */ + List listOrderBySort(); +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryLogService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryLogService.java new file mode 100644 index 00000000..7aba0403 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryLogService.java @@ -0,0 +1,21 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.QueryLog; + +import java.util.List; + +public interface QueryLogService extends IService { + /** + * 根据用户ID查询查询日志 + */ + List listByUserId(Long userId); + + /** + * 根据对话ID查询查询日志 + */ + List listByDialogId(String dialogId); +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryService.java new file mode 100644 index 00000000..c70897c1 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryService.java @@ -0,0 +1,10 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.dto.QueryRequestDTO; +import com.example.springboot_demo.vo.QueryResponseVO; + +public interface QueryService { + QueryResponseVO executeQuery(QueryRequestDTO request, Long userId); +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/RoleService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/RoleService.java new file mode 100644 index 00000000..f9a5d4ae --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/RoleService.java @@ -0,0 +1,7 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.Role; + +public interface RoleService extends IService { +} diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/SystemHealthService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/SystemHealthService.java new file mode 100644 index 00000000..5377752b --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/SystemHealthService.java @@ -0,0 +1,24 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.SystemHealth; + +import java.util.List; + +public interface SystemHealthService extends IService { + /** + * 查询最新的健康记录 + */ + SystemHealth getLatest(); + + /** + * 查询最近N条健康记录 + */ + List listRecent(int limit); +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/TableMetadataService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/TableMetadataService.java new file mode 100644 index 00000000..7d66bc37 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/TableMetadataService.java @@ -0,0 +1,14 @@ +package com.example.springboot_demo.service; + +import java.util.List; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.TableMetadata; + +public interface TableMetadataService extends IService { + /** + * 根据数据库连接ID查询表元数据列表 + */ + List listByDbConnectionId(Long dbConnectionId); +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java new file mode 100644 index 00000000..e776639d --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java @@ -0,0 +1,26 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.UserDbPermission; + +import java.util.List; + +public interface UserDbPermissionService extends IService { + /** + * 根据用户ID查询权限 + */ + UserDbPermission getByUserId(Long userId); + + /** + * 查询所有已分配权限的用户 + */ + List listAssigned(); + + /** + * 查询所有未分配权限的用户 + */ + List listUnassigned(); +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserService.java new file mode 100644 index 00000000..dae70f44 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserService.java @@ -0,0 +1,24 @@ +package com.example.springboot_demo.service; + +import java.util.List; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.example.springboot_demo.entity.mysql.User; + +public interface UserService { + + User getById(Long id); + + List list(); + + Page page(int current, int size); + + boolean save(User user); + + boolean updateById(User user); + + boolean removeById(Long id); + + User getByUsername(String username); +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java new file mode 100644 index 00000000..8f57905d --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java @@ -0,0 +1,70 @@ +package com.example.springboot_demo.service.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.example.springboot_demo.dto.LoginDTO; +import com.example.springboot_demo.entity.mysql.Role; +import com.example.springboot_demo.entity.mysql.User; +import com.example.springboot_demo.mapper.RoleMapper; +import com.example.springboot_demo.mapper.UserMapper; +import com.example.springboot_demo.service.AuthService; +import com.example.springboot_demo.utils.JwtUtil; +import com.example.springboot_demo.vo.LoginVO; + +@Service +public class AuthServiceImpl implements AuthService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private RoleMapper roleMapper; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public LoginVO login(LoginDTO loginDTO) { + // 查询用户 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getUsername, loginDTO.getUsername()); + User user = userMapper.selectOne(wrapper); + + if (user == null) { + throw new RuntimeException("用户不存在"); + } + + // 验证密码 + if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) { + throw new RuntimeException("密码错误"); + } + + // 检查账号状态 + if (user.getStatus() == 0) { + throw new RuntimeException("账号已被禁用"); + } + + // 查询角色信息 + Role role = roleMapper.selectById(user.getRoleId()); + + // 构造返回数据 + LoginVO loginVO = new LoginVO(); + loginVO.setToken(jwtUtil.generateToken(user.getId(), user.getUsername())); + loginVO.setUserId(user.getId()); + loginVO.setUsername(user.getUsername()); + loginVO.setEmail(user.getEmail()); + loginVO.setRoleId(user.getRoleId()); + loginVO.setRoleName(role != null ? role.getRoleName() : null); + loginVO.setAvatarUrl(user.getAvatarUrl()); + + return loginVO; + } +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java new file mode 100644 index 00000000..7cee6d47 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java @@ -0,0 +1,27 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.ColumnMetadata; +import com.example.springboot_demo.mapper.ColumnMetadataMapper; +import com.example.springboot_demo.service.ColumnMetadataService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ColumnMetadataServiceImpl extends ServiceImpl implements ColumnMetadataService { + + @Override + public List listByTableId(Long tableId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ColumnMetadata::getTableId, tableId); + // 主键字段优先 + wrapper.orderByDesc(ColumnMetadata::getIsPrimary); + wrapper.orderByAsc(ColumnMetadata::getColumnName); + return list(wrapper); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java new file mode 100644 index 00000000..b6782dfa --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java @@ -0,0 +1,36 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.DbConnection; +import com.example.springboot_demo.mapper.DbConnectionMapper; +import com.example.springboot_demo.service.DbConnectionService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class DbConnectionServiceImpl extends ServiceImpl implements DbConnectionService { + + @Override + public List listByCreateUserId(Long createUserId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DbConnection::getCreateUserId, createUserId); + wrapper.orderByDesc(DbConnection::getCreateTime); + return list(wrapper); + } + + @Override + public boolean testConnection(Long id) { + // TODO: 实现真实的数据库连接测试逻辑 + // 暂时返回 Mock 结果 + DbConnection connection = getById(id); + if (connection == null) { + return false; + } + // 这里应该根据 db_type_id 和连接信息实际测试连接 + return true; + } +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java new file mode 100644 index 00000000..a4dea719 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.DbType; +import com.example.springboot_demo.mapper.DbTypeMapper; +import com.example.springboot_demo.service.DbTypeService; +import org.springframework.stereotype.Service; + +@Service +public class DbTypeServiceImpl extends ServiceImpl implements DbTypeService { + + @Override + public DbType getByTypeCode(String typeCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DbType::getTypeCode, typeCode); + return getOne(wrapper); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java new file mode 100644 index 00000000..655f4303 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java @@ -0,0 +1,28 @@ +package com.example.springboot_demo.service.impl; + +import com.example.springboot_demo.entity.mongodb.DialogRecord; +import com.example.springboot_demo.repository.DialogRecordRepository; +import com.example.springboot_demo.service.DialogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class DialogServiceImpl implements DialogService { + + @Autowired + private DialogRecordRepository dialogRecordRepository; + + @Override + public List getUserDialogs(Long userId) { + return dialogRecordRepository.findByUserIdOrderByLastTimeDesc(userId); + } + + @Override + public DialogRecord getDialogById(String dialogId) { + return dialogRecordRepository.findByDialogId(dialogId); + } +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java new file mode 100644 index 00000000..97779cac --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java @@ -0,0 +1,36 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.ErrorLog; +import com.example.springboot_demo.mapper.ErrorLogMapper; +import com.example.springboot_demo.service.ErrorLogService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ErrorLogServiceImpl extends ServiceImpl implements ErrorLogService { + + @Override + public List listByErrorTypeId(Integer errorTypeId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ErrorLog::getErrorTypeId, errorTypeId); + wrapper.orderByDesc(ErrorLog::getStatTime); + return list(wrapper); + } + + @Override + public List listByPeriod(String period) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ErrorLog::getPeriod, period); + wrapper.orderByDesc(ErrorLog::getStatTime); + return list(wrapper); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java new file mode 100644 index 00000000..40971a86 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.ErrorType; +import com.example.springboot_demo.mapper.ErrorTypeMapper; +import com.example.springboot_demo.service.ErrorTypeService; +import org.springframework.stereotype.Service; + +@Service +public class ErrorTypeServiceImpl extends ServiceImpl implements ErrorTypeService { + + @Override + public ErrorType getByErrorCode(String errorCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ErrorType::getErrorCode, errorCode); + return getOne(wrapper); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java new file mode 100644 index 00000000..36257976 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java @@ -0,0 +1,24 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.LlmConfig; +import com.example.springboot_demo.mapper.LlmConfigMapper; +import com.example.springboot_demo.service.LlmConfigService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class LlmConfigServiceImpl extends ServiceImpl implements LlmConfigService { + + @Override + public List listAvailable() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(LlmConfig::getIsDisabled, 0); + wrapper.orderByDesc(LlmConfig::getCreateTime); + return list(wrapper); + } +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java new file mode 100644 index 00000000..345e87ab --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java @@ -0,0 +1,380 @@ +package com.example.springboot_demo.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.example.springboot_demo.service.LlmService; +import org.springframework.stereotype.Service; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.*; + +/** + * 大模型调用服务实现 + */ +@Service +public class LlmServiceImpl implements LlmService { + + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .build(); + + @Override + public Map generateQuery(String prompt, String modelName, String databaseName) { + String lowerModelName = modelName.toLowerCase(); + + try { + if (lowerModelName.contains("gemini")) { + return callGemini(prompt, modelName, databaseName); + } else if (lowerModelName.contains("gpt")) { + return callOpenAI(prompt, modelName, databaseName); + } else if (lowerModelName.contains("glm")) { + return callGLM(prompt, modelName, databaseName); + } else if (lowerModelName.contains("qwen")) { + return callQwen(prompt, modelName, databaseName); + } else if (lowerModelName.contains("kimi")) { + return callKimi(prompt, modelName, databaseName); + } else { + throw new RuntimeException("不支持的模型: " + modelName); + } + } catch (Exception e) { + throw new RuntimeException("模型调用失败: " + e.getMessage(), e); + } + } + + /** + * 生成统一的Prompt + */ + private String generatePrompt(String prompt, String databaseName) { + return String.format( + "你是数据查询助手,需将用户请求转换为指定JSON格式。\n" + + "连接的数据库为\"%s\",仅生成该数据库的SQL。\n" + + "响应必须是单个有效的JSON对象,不包含任何额外文本或格式(如```json)。\n\n" + + "用户请求:\"%s\"\n\n" + + "规则:\n" + + "- 数据查询(可SQL回答):success=true,生成SQL、表格数据和图表数据\n" + + "- 非数据查询:success=false,表格数据用[\"Message\"]和[\"抱歉,仅支持数据查询\"]\n\n" + + "返回JSON格式:\n" + + "{\n" + + " \"success\": true/false,\n" + + " \"sqlQuery\": \"SQL语句\",\n" + + " \"tableData\": {\n" + + " \"headers\": [\"列1\", \"列2\"],\n" + + " \"rows\": [[\"值1\", \"值2\"]]\n" + + " },\n" + + " \"chartData\": {\n" + + " \"type\": \"bar/line/pie\",\n" + + " \"labels\": [\"标签1\"],\n" + + " \"datasets\": [{\n" + + " \"label\": \"数据标签\",\n" + + " \"data\": [1, 2, 3],\n" + + " \"backgroundColor\": \"rgba(22, 93, 255, 0.6)\"\n" + + " }]\n" + + " }\n" + + "}", + databaseName, prompt + ); + } + + /** + * 调用Gemini模型 + */ + private Map callGemini(String prompt, String modelName, String databaseName) throws Exception { + // 从配置中获取API Key(这里简化处理,实际应该从数据库读取) + String apiKey = System.getenv("GEMINI_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("Gemini API密钥未配置"); + } + + String url = "https://generativelanguage.googleapis.com/v1beta/models/" + modelName + ":generateContent?key=" + apiKey; + + JSONObject requestBody = new JSONObject(); + requestBody.put("contents", Arrays.asList(Map.of( + "parts", Arrays.asList(Map.of("text", generatePrompt(prompt, databaseName))) + ))); + requestBody.put("generationConfig", Map.of( + "responseMimeType", "application/json" + )); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .timeout(Duration.ofSeconds(60)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Gemini API调用失败: " + response.statusCode()); + } + + JSONObject jsonResponse = JSON.parseObject(response.body()); + + // 检查响应结构 + if (!jsonResponse.containsKey("candidates") || jsonResponse.getJSONArray("candidates").isEmpty()) { + throw new RuntimeException("Gemini API响应格式错误:缺少candidates"); + } + + JSONObject candidate = jsonResponse.getJSONArray("candidates").getJSONObject(0); + if (!candidate.containsKey("content")) { + throw new RuntimeException("Gemini API响应格式错误:缺少content"); + } + + JSONObject contentObj = candidate.getJSONObject("content"); + if (!contentObj.containsKey("parts") || contentObj.getJSONArray("parts").isEmpty()) { + throw new RuntimeException("Gemini API响应格式错误:缺少parts"); + } + + String content = contentObj.getJSONArray("parts") + .getJSONObject(0) + .getString("text"); + + if (content == null || content.isEmpty()) { + throw new RuntimeException("Gemini API返回内容为空"); + } + + return parseJsonResponse(content); + } + + /** + * 调用OpenAI模型 + */ + private Map callOpenAI(String prompt, String modelName, String databaseName) throws Exception { + String apiKey = System.getenv("OPENAI_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("OpenAI API密钥未配置"); + } + + String url = "https://api.openai.com/v1/chat/completions"; + + JSONObject requestBody = new JSONObject(); + requestBody.put("model", modelName); + requestBody.put("messages", Arrays.asList(Map.of( + "role", "user", + "content", generatePrompt(prompt, databaseName) + ))); + requestBody.put("response_format", Map.of("type", "json_object")); + requestBody.put("temperature", 0.0); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .timeout(Duration.ofSeconds(60)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("OpenAI API调用失败: " + response.statusCode()); + } + + JSONObject jsonResponse = JSON.parseObject(response.body()); + + // 检查响应结构 + if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { + throw new RuntimeException("OpenAI API响应格式错误:缺少choices"); + } + + JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); + if (!choice.containsKey("message")) { + throw new RuntimeException("OpenAI API响应格式错误:缺少message"); + } + + String content = choice.getJSONObject("message").getString("content"); + if (content == null || content.isEmpty()) { + throw new RuntimeException("OpenAI API返回内容为空"); + } + + String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); + return parseJsonResponse(cleanedContent); + } + + /** + * 调用GLM模型 + */ + private Map callGLM(String prompt, String modelName, String databaseName) throws Exception { + String apiKey = System.getenv("GLM_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("GLM API密钥未配置"); + } + + String url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"; + + JSONObject requestBody = new JSONObject(); + requestBody.put("model", modelName); + requestBody.put("messages", Arrays.asList(Map.of( + "role", "user", + "content", generatePrompt(prompt, databaseName) + ))); + requestBody.put("response_format", Map.of("type", "json_object")); + requestBody.put("temperature", 0.0); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .timeout(Duration.ofSeconds(60)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("GLM API调用失败: " + response.statusCode()); + } + + JSONObject jsonResponse = JSON.parseObject(response.body()); + + // 检查响应结构 + if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { + throw new RuntimeException("GLM API响应格式错误:缺少choices"); + } + + JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); + if (!choice.containsKey("message")) { + throw new RuntimeException("GLM API响应格式错误:缺少message"); + } + + String content = choice.getJSONObject("message").getString("content"); + if (content == null || content.isEmpty()) { + throw new RuntimeException("GLM API返回内容为空"); + } + + String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); + return parseJsonResponse(cleanedContent); + } + + /** + * 调用Qwen模型 + */ + private Map callQwen(String prompt, String modelName, String databaseName) throws Exception { + String apiKey = System.getenv("QWEN_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("Qwen API密钥未配置"); + } + + String url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; + + JSONObject requestBody = new JSONObject(); + requestBody.put("model", modelName); + requestBody.put("messages", Arrays.asList(Map.of( + "role", "user", + "content", generatePrompt(prompt, databaseName) + ))); + requestBody.put("response_format", Map.of("type", "json_object")); + requestBody.put("temperature", 0.0); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .timeout(Duration.ofSeconds(60)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Qwen API调用失败: " + response.statusCode()); + } + + JSONObject jsonResponse = JSON.parseObject(response.body()); + + // 检查响应结构 + if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { + throw new RuntimeException("Qwen API响应格式错误:缺少choices"); + } + + JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); + if (!choice.containsKey("message")) { + throw new RuntimeException("Qwen API响应格式错误:缺少message"); + } + + String content = choice.getJSONObject("message").getString("content"); + if (content == null || content.isEmpty()) { + throw new RuntimeException("Qwen API返回内容为空"); + } + + String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); + return parseJsonResponse(cleanedContent); + } + + /** + * 调用Kimi模型 + */ + private Map callKimi(String prompt, String modelName, String databaseName) throws Exception { + String apiKey = System.getenv("KIMI_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("Kimi API密钥未配置"); + } + + String url = "https://api.moonshot.cn/v1/chat/completions"; + + JSONObject requestBody = new JSONObject(); + requestBody.put("model", modelName); + requestBody.put("messages", Arrays.asList(Map.of( + "role", "user", + "content", generatePrompt(prompt, databaseName) + ))); + requestBody.put("response_format", Map.of("type", "json_object")); + requestBody.put("temperature", 0.0); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .timeout(Duration.ofSeconds(60)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Kimi API调用失败: " + response.statusCode()); + } + + JSONObject jsonResponse = JSON.parseObject(response.body()); + + // 检查响应结构 + if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { + throw new RuntimeException("Kimi API响应格式错误:缺少choices"); + } + + JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); + if (!choice.containsKey("message")) { + throw new RuntimeException("Kimi API响应格式错误:缺少message"); + } + + String content = choice.getJSONObject("message").getString("content"); + if (content == null || content.isEmpty()) { + throw new RuntimeException("Kimi API返回内容为空"); + } + + String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); + return parseJsonResponse(cleanedContent); + } + + /** + * 解析JSON响应 + */ + private Map parseJsonResponse(String jsonContent) { + try { + JSONObject json = JSON.parseObject(jsonContent); + Map result = new HashMap<>(); + result.put("success", json.getBooleanValue("success")); + result.put("sqlQuery", json.getString("sqlQuery")); + result.put("tableData", json.getJSONObject("tableData")); + result.put("chartData", json.getJSONObject("chartData")); + return result; + } catch (Exception e) { + throw new RuntimeException("解析模型响应失败: " + e.getMessage(), e); + } + } +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java new file mode 100644 index 00000000..c7744390 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.LlmStatus; +import com.example.springboot_demo.mapper.LlmStatusMapper; +import com.example.springboot_demo.service.LlmStatusService; +import org.springframework.stereotype.Service; + +@Service +public class LlmStatusServiceImpl extends ServiceImpl implements LlmStatusService { + + @Override + public LlmStatus getByStatusCode(String statusCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(LlmStatus::getStatusCode, statusCode); + return getOne(wrapper); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java new file mode 100644 index 00000000..a086a63f --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java @@ -0,0 +1,48 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.Notification; +import com.example.springboot_demo.mapper.NotificationMapper; +import com.example.springboot_demo.service.NotificationService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class NotificationServiceImpl extends ServiceImpl implements NotificationService { + + @Override + public List listPublished() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.isNotNull(Notification::getPublishTime); + // 置顶优先,然后按发布时间降序 + wrapper.orderByDesc(Notification::getIsTop); + wrapper.orderByDesc(Notification::getPublishTime); + return list(wrapper); + } + + @Override + public List listDrafts() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.isNull(Notification::getPublishTime); + wrapper.orderByDesc(Notification::getLatestUpdateTime); + return list(wrapper); + } + + @Override + public List listByTargetId(Integer targetId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Notification::getTargetId, targetId); + wrapper.isNotNull(Notification::getPublishTime); + wrapper.orderByDesc(Notification::getIsTop); + wrapper.orderByDesc(Notification::getPublishTime); + return list(wrapper); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java new file mode 100644 index 00000000..78b1a76a --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.NotificationTarget; +import com.example.springboot_demo.mapper.NotificationTargetMapper; +import com.example.springboot_demo.service.NotificationTargetService; +import org.springframework.stereotype.Service; + +@Service +public class NotificationTargetServiceImpl extends ServiceImpl implements NotificationTargetService { + + @Override + public NotificationTarget getByTargetCode(String targetCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(NotificationTarget::getTargetCode, targetCode); + return getOne(wrapper); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java new file mode 100644 index 00000000..71011c81 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java @@ -0,0 +1,41 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.OperationLog; +import com.example.springboot_demo.mapper.OperationLogMapper; +import com.example.springboot_demo.service.OperationLogService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class OperationLogServiceImpl extends ServiceImpl implements OperationLogService { + + @Override + public List listByUserId(Long userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OperationLog::getUserId, userId); + wrapper.orderByDesc(OperationLog::getOperateTime); + return list(wrapper); + } + + @Override + public List listByModule(String module) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OperationLog::getModule, module); + wrapper.orderByDesc(OperationLog::getOperateTime); + return list(wrapper); + } + + @Override + public List listFailed() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OperationLog::getResult, 0); + wrapper.orderByDesc(OperationLog::getOperateTime); + return list(wrapper); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java new file mode 100644 index 00000000..1a67b1ab --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java @@ -0,0 +1,34 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.Priority; +import com.example.springboot_demo.mapper.PriorityMapper; +import com.example.springboot_demo.service.PriorityService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class PriorityServiceImpl extends ServiceImpl implements PriorityService { + + @Override + public Priority getByPriorityCode(String priorityCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Priority::getPriorityCode, priorityCode); + return getOne(wrapper); + } + + @Override + public List listOrderBySort() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByAsc(Priority::getSort); + return list(wrapper); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java new file mode 100644 index 00000000..1ea9557b --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java @@ -0,0 +1,33 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.QueryLog; +import com.example.springboot_demo.mapper.QueryLogMapper; +import com.example.springboot_demo.service.QueryLogService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class QueryLogServiceImpl extends ServiceImpl implements QueryLogService { + + @Override + public List listByUserId(Long userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QueryLog::getUserId, userId); + wrapper.orderByDesc(QueryLog::getQueryTime); + return list(wrapper); + } + + @Override + public List listByDialogId(String dialogId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QueryLog::getDialogId, dialogId); + wrapper.orderByAsc(QueryLog::getQueryTime); + return list(wrapper); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java new file mode 100644 index 00000000..08e538a2 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java @@ -0,0 +1,247 @@ +package com.example.springboot_demo.service.impl; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.example.springboot_demo.dto.QueryRequestDTO; +import com.example.springboot_demo.entity.mongodb.DialogRecord; +import com.example.springboot_demo.repository.DialogRecordRepository; +import com.example.springboot_demo.service.LlmService; +import com.example.springboot_demo.service.QueryService; +import com.example.springboot_demo.vo.QueryResponseVO; +import com.example.springboot_demo.vo.TableDataVO; +import com.example.springboot_demo.vo.ChartDataVO; +import com.example.springboot_demo.vo.DatasetVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class QueryServiceImpl implements QueryService { + + @Autowired + private DialogRecordRepository dialogRecordRepository; + + @Autowired + private LlmService llmService; + + @Override + public QueryResponseVO executeQuery(QueryRequestDTO request, Long userId) { + long startTime = System.currentTimeMillis(); + + // 生成或获取对话ID + String conversationId = request.getConversationId(); + if (conversationId == null || conversationId.isEmpty()) { + conversationId = "conv_" + UUID.randomUUID().toString().substring(0, 8); + // 创建新对话记录 + DialogRecord dialogRecord = new DialogRecord(); + dialogRecord.setDialogId(conversationId); + dialogRecord.setUserId(userId); + dialogRecord.setTopic(request.getUserPrompt().substring(0, Math.min(20, request.getUserPrompt().length()))); + dialogRecord.setTotalRounds(1); + dialogRecord.setStartTime(LocalDateTime.now()); + dialogRecord.setLastTime(LocalDateTime.now()); + dialogRecordRepository.save(dialogRecord); + } else { + // 更新对话记录 + DialogRecord dialogRecord = dialogRecordRepository.findByDialogId(conversationId); + if (dialogRecord != null) { + dialogRecord.setTotalRounds(dialogRecord.getTotalRounds() + 1); + dialogRecord.setLastTime(LocalDateTime.now()); + dialogRecordRepository.save(dialogRecord); + } + } + + // 调用大模型API生成SQL和结果 + Map llmResult = llmService.generateQuery( + request.getUserPrompt(), + request.getModel(), + request.getDatabase() + ); + + // 计算执行时间 + long endTime = System.currentTimeMillis(); + String executionTime = String.format("%.1f秒", (endTime - startTime) / 1000.0); + + // 构建响应 + QueryResponseVO response = new QueryResponseVO(); + response.setId("query_" + UUID.randomUUID().toString().substring(0, 8)); + response.setUserPrompt(request.getUserPrompt()); + response.setSqlQuery((String) llmResult.getOrDefault("sqlQuery", "")); + response.setConversationId(conversationId); + response.setQueryTime(LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)); + response.setExecutionTime(executionTime); + response.setDatabase(request.getDatabase()); + response.setModel(request.getModel()); + + // 解析表格数据 + Object tableDataObj = llmResult.get("tableData"); + if (tableDataObj != null) { + TableDataVO tableData = parseTableData(tableDataObj); + response.setTableData(tableData); + } + + // 解析图表数据 + Object chartDataObj = llmResult.get("chartData"); + if (chartDataObj != null) { + ChartDataVO chartData = parseChartData(chartDataObj); + response.setChartData(chartData); + } + + return response; + } + + /** + * 解析表格数据 + */ + @SuppressWarnings("unchecked") + private TableDataVO parseTableData(Object tableDataObj) { + TableDataVO tableData = new TableDataVO(); + + if (tableDataObj instanceof Map) { + Map map = (Map) tableDataObj; + + // 解析headers + Object headersObj = map.get("headers"); + if (headersObj instanceof List) { + List headers = ((List) headersObj).stream() + .map(String::valueOf) + .collect(Collectors.toList()); + tableData.setHeaders(headers); + } + + // 解析rows + Object rowsObj = map.get("rows"); + if (rowsObj instanceof List) { + List> rows = new ArrayList<>(); + for (Object row : (List) rowsObj) { + if (row instanceof List) { + List rowList = ((List) row).stream() + .map(String::valueOf) + .collect(Collectors.toList()); + rows.add(rowList); + } else { + rows.add(Collections.emptyList()); + } + } + tableData.setRows(rows); + } + } else if (tableDataObj instanceof JSONObject) { + JSONObject json = (JSONObject) tableDataObj; + JSONArray headersArray = json.getJSONArray("headers"); + JSONArray rowsArray = json.getJSONArray("rows"); + + List headers = headersArray != null ? + headersArray.toJavaList(String.class) : Collections.emptyList(); + + List> rows = new ArrayList<>(); + if (rowsArray != null) { + for (Object row : rowsArray) { + if (row instanceof JSONArray) { + rows.add(((JSONArray) row).toJavaList(String.class)); + } else { + rows.add(Collections.emptyList()); + } + } + } + + tableData.setHeaders(headers); + tableData.setRows(rows); + } + + return tableData; + } + + /** + * 解析图表数据 + */ + @SuppressWarnings("unchecked") + private ChartDataVO parseChartData(Object chartDataObj) { + ChartDataVO chartData = new ChartDataVO(); + + if (chartDataObj instanceof Map) { + Map map = (Map) chartDataObj; + chartData.setType((String) map.getOrDefault("type", "bar")); + + Object labelsObj = map.get("labels"); + if (labelsObj instanceof List) { + List labels = ((List) labelsObj).stream() + .map(String::valueOf) + .collect(Collectors.toList()); + chartData.setLabels(labels); + } + + Object datasetsObj = map.get("datasets"); + if (datasetsObj instanceof List) { + List datasets = ((List) datasetsObj).stream() + .map(datasetObj -> { + DatasetVO dataset = new DatasetVO(); + if (datasetObj instanceof Map) { + Map datasetMap = (Map) datasetObj; + dataset.setLabel((String) datasetMap.get("label")); + + Object dataObj = datasetMap.get("data"); + if (dataObj instanceof List) { + List data = ((List) dataObj).stream() + .map(item -> { + if (item instanceof Number) { + return ((Number) item).doubleValue(); + } + return 0.0; + }) + .collect(Collectors.toList()); + dataset.setData(data); + } + + dataset.setBackgroundColor((String) datasetMap.get("backgroundColor")); + } + return dataset; + }) + .collect(Collectors.toList()); + chartData.setDatasets(datasets); + } + } else if (chartDataObj instanceof JSONObject) { + JSONObject json = (JSONObject) chartDataObj; + chartData.setType(json.getString("type")); + + JSONArray labelsArray = json.getJSONArray("labels"); + if (labelsArray != null) { + chartData.setLabels(labelsArray.toJavaList(String.class)); + } + + JSONArray datasetsArray = json.getJSONArray("datasets"); + if (datasetsArray != null) { + List datasets = datasetsArray.stream() + .map(item -> { + JSONObject datasetJson = (JSONObject) item; + DatasetVO dataset = new DatasetVO(); + dataset.setLabel(datasetJson.getString("label")); + + JSONArray dataArray = datasetJson.getJSONArray("data"); + if (dataArray != null) { + List data = dataArray.stream() + .map(obj -> { + if (obj instanceof Number) { + return ((Number) obj).doubleValue(); + } + return 0.0; + }) + .collect(Collectors.toList()); + dataset.setData(data); + } + + dataset.setBackgroundColor(datasetJson.getString("backgroundColor")); + return dataset; + }) + .collect(Collectors.toList()); + chartData.setDatasets(datasets); + } + } + + return chartData; + } +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java new file mode 100644 index 00000000..5873277e --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.service.impl; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.Role; +import com.example.springboot_demo.mapper.RoleMapper; +import com.example.springboot_demo.service.RoleService; + +@Service +public class RoleServiceImpl extends ServiceImpl implements RoleService { +} diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java new file mode 100644 index 00000000..6c245698 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java @@ -0,0 +1,36 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.SystemHealth; +import com.example.springboot_demo.mapper.SystemHealthMapper; +import com.example.springboot_demo.service.SystemHealthService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class SystemHealthServiceImpl extends ServiceImpl implements SystemHealthService { + + @Override + public SystemHealth getLatest() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByDesc(SystemHealth::getCollectTime); + wrapper.last("LIMIT 1"); + return getOne(wrapper); + } + + @Override + public List listRecent(int limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByDesc(SystemHealth::getCollectTime); + wrapper.last("LIMIT " + limit); + return list(wrapper); + } +} + + + + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java new file mode 100644 index 00000000..66eb9c73 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.TableMetadata; +import com.example.springboot_demo.mapper.TableMetadataMapper; +import com.example.springboot_demo.service.TableMetadataService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class TableMetadataServiceImpl extends ServiceImpl implements TableMetadataService { + + @Override + public List listByDbConnectionId(Long dbConnectionId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TableMetadata::getDbConnectionId, dbConnectionId); + wrapper.orderByAsc(TableMetadata::getTableName); + return list(wrapper); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java new file mode 100644 index 00000000..7a7548de --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java @@ -0,0 +1,39 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.UserDbPermission; +import com.example.springboot_demo.mapper.UserDbPermissionMapper; +import com.example.springboot_demo.service.UserDbPermissionService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserDbPermissionServiceImpl extends ServiceImpl implements UserDbPermissionService { + + @Override + public UserDbPermission getByUserId(Long userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserDbPermission::getUserId, userId); + return getOne(wrapper); + } + + @Override + public List listAssigned() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserDbPermission::getIsAssigned, 1); + wrapper.orderByDesc(UserDbPermission::getLastGrantTime); + return list(wrapper); + } + + @Override + public List listUnassigned() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserDbPermission::getIsAssigned, 0); + return list(wrapper); + } +} + + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java new file mode 100644 index 00000000..38148be3 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java @@ -0,0 +1,69 @@ +package com.example.springboot_demo.service.impl; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.example.springboot_demo.entity.mysql.User; +import com.example.springboot_demo.mapper.UserMapper; +import com.example.springboot_demo.service.UserService; + +@Service +public class UserServiceImpl implements UserService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public User getById(Long id) { + return userMapper.selectById(id); + } + + @Override + public List list() { + return userMapper.selectList(null); + } + + @Override + public Page page(int current, int size) { + Page page = new Page<>(current, size); + return userMapper.selectPage(page, null); + } + + @Override + public boolean save(User user) { + if (StringUtils.hasText(user.getPassword())) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + } + return userMapper.insert(user) > 0; + } + + @Override + public boolean updateById(User user) { + if (StringUtils.hasText(user.getPassword())) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + } + return userMapper.updateById(user) > 0; + } + + @Override + public boolean removeById(Long id) { + return userMapper.deleteById(id) > 0; + } + + @Override + public User getByUsername(String username) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getUsername, username); + return userMapper.selectOne(wrapper); + } +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/utils/JwtUtil.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/utils/JwtUtil.java new file mode 100644 index 00000000..13206046 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/utils/JwtUtil.java @@ -0,0 +1,57 @@ +package com.example.springboot_demo.utils; + +import java.security.Key; +import java.util.Date; + +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +@Component +public class JwtUtil { + + // Use a secure key for HS256 + private static final Key KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256); + private static final long EXPIRATION_TIME = 86400000; // 24 hours + + public String generateToken(Long userId, String username) { + return Jwts.builder() + .setSubject(username) + .claim("userId", userId) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) + .signWith(KEY) + .compact(); + } + + public Claims getClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(KEY) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public boolean validateToken(String token) { + try { + getClaimsFromToken(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + public Long getUserIdFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.get("userId", Long.class); + } + + public String getUsernameFromToken(String token) { + return getClaimsFromToken(token).getSubject(); + } +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java new file mode 100644 index 00000000..f4f687e0 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java @@ -0,0 +1,13 @@ +package com.example.springboot_demo.vo; + +import lombok.Data; +import java.util.List; + +@Data +public class ChartDataVO { + private String type; // bar, line, pie + private List labels; + private List datasets; +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/DatasetVO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/DatasetVO.java new file mode 100644 index 00000000..135458dc --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/DatasetVO.java @@ -0,0 +1,13 @@ +package com.example.springboot_demo.vo; + +import lombok.Data; +import java.util.List; + +@Data +public class DatasetVO { + private String label; + private List data; + private Object backgroundColor; // 可以是字符串或数组 +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/LoginVO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/LoginVO.java new file mode 100644 index 00000000..ba21ed6c --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/LoginVO.java @@ -0,0 +1,16 @@ +package com.example.springboot_demo.vo; + +import lombok.Data; + +@Data +public class LoginVO { + private String token; + private Long userId; + private String username; + private String email; + private Integer roleId; + private String roleName; + private String avatarUrl; +} + + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java new file mode 100644 index 00000000..7ca36218 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java @@ -0,0 +1,18 @@ +package com.example.springboot_demo.vo; + +import lombok.Data; + +@Data +public class QueryResponseVO { + private String id; + private String userPrompt; + private String sqlQuery; + private String conversationId; + private String queryTime; + private String executionTime; + private TableDataVO tableData; + private ChartDataVO chartData; + private String database; + private String model; +} + diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/TableDataVO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/TableDataVO.java new file mode 100644 index 00000000..446dd465 --- /dev/null +++ b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/TableDataVO.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.vo; + +import lombok.Data; +import java.util.List; + +@Data +public class TableDataVO { + private List headers; + private List> rows; +} + + diff --git a/src/springboot_demo/src/main/resources/application.yml b/src/springboot_demo/src/main/resources/application.yml new file mode 100644 index 00000000..0df01fb4 --- /dev/null +++ b/src/springboot_demo/src/main/resources/application.yml @@ -0,0 +1,75 @@ +server: + port: 8080 + +spring: + application: + name: springboot_demo + + # MySQL 数据源配置 + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/natural_language_query_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: root123456 + hikari: + minimum-idle: 5 + maximum-pool-size: 20 + idle-timeout: 600000 + max-lifetime: 1800000 + connection-timeout: 30000 + + # MongoDB 配置 + data: + mongodb: + host: 127.0.0.1 + port: 27017 + database: natural_language_query_system + username: admin + password: admin123456 + authentication-database: admin + + # Redis 配置(可选) + redis: + host: localhost + port: 6379 + password: + database: 0 + timeout: 3000ms + lettuce: + pool: + max-active: 8 + max-wait: -1ms + max-idle: 8 + min-idle: 0 + +# MyBatis Plus 配置 +mybatis-plus: + # Mapper XML 文件位置 + mapper-locations: classpath:mapper/**/*.xml + # 实体类包路径 + type-aliases-package: com.example.springboot_demo.entity.mysql + configuration: + # 日志输出 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 驼峰命名转换 + map-underscore-to-camel-case: true + # 缓存 + cache-enabled: true + global-config: + db-config: + # 主键策略:自增 + id-type: auto + # 逻辑删除字段 + logic-delete-field: deleted + logic-delete-value: 1 + logic-not-delete-value: 0 + +# 日志配置 +logging: + level: + root: INFO + com.example.springboot_demo: DEBUG + com.example.springboot_demo.mapper: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + diff --git a/src/springboot_demo/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java b/src/springboot_demo/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java new file mode 100644 index 00000000..4ba0d8fa --- /dev/null +++ b/src/springboot_demo/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.springboot_demo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SpringbootDemoApplicationTests { + + @Test + void contextLoads() { + } + +} -- 2.34.1 From 0b22bb325be6ce1a22fc5fa7d3306a82157a408a Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Mon, 1 Dec 2025 12:11:04 +0800 Subject: [PATCH 05/14] =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/springboot_demo/.gitattributes | 2 - src/springboot_demo/.gitignore | 33 - .../.mvn/wrapper/maven-wrapper.properties | 3 - src/springboot_demo/198.18.0.22443 | 0 src/springboot_demo/API_INTERFACE.md | 575 ---- src/springboot_demo/DATABASE_SETUP.md | 202 -- src/springboot_demo/ERROR_FIXES.md | 46 - src/springboot_demo/PROJECT_STRUCTURE.md | 80 - src/springboot_demo/README_CONNECTION.md | 129 - src/springboot_demo/TESTING_GUIDE.md | 323 -- src/springboot_demo/api-test.http | 636 ---- src/springboot_demo/docker-compose.yml | 123 - src/springboot_demo/frontend/.gitignore | 24 - src/springboot_demo/frontend/App.tsx | 732 ----- src/springboot_demo/frontend/README.md | 20 - .../frontend/components/AccountPage.tsx | 222 -- .../frontend/components/ChatMessage.tsx | 40 - .../frontend/components/ChatModal.tsx | 384 --- .../frontend/components/ComparisonModal.tsx | 232 -- .../frontend/components/DataAdminPage.tsx | 123 - .../frontend/components/DataAdminSidebar.tsx | 93 - .../frontend/components/Dropdown.tsx | 63 - .../frontend/components/FriendsPage.tsx | 728 ----- .../frontend/components/HistoryPage.tsx | 455 --- .../frontend/components/HistorySidebar.tsx | 130 - .../frontend/components/LoginPage.tsx | 211 -- .../frontend/components/Modal.tsx | 64 - .../frontend/components/NotificationsPage.tsx | 156 - .../frontend/components/PlaceholderPage.tsx | 18 - .../frontend/components/QueryPage.tsx | 260 -- .../frontend/components/QueryResult.tsx | 264 -- .../frontend/components/RightSidebar.tsx | 83 - .../frontend/components/Sidebar.tsx | 94 - .../frontend/components/SysAdminPage.tsx | 43 - .../frontend/components/SysAdminSidebar.tsx | 90 - .../frontend/components/TopHeader.tsx | 267 -- .../components/admin/AdminAccountPage.tsx | 92 - .../frontend/components/admin/AdminModal.tsx | 28 - .../components/admin/DashboardPage.tsx | 243 -- .../components/admin/LLMConfigPage.tsx | 357 --- .../admin/NotificationManagementPage.tsx | 129 - .../components/admin/SystemLogPage.tsx | 130 - .../components/admin/UserManagementPage.tsx | 263 -- .../data-admin/ConnectionLogPage.tsx | 169 -- .../data-admin/DataAdminDashboardPage.tsx | 126 - .../data-admin/DataAdminNotificationPage.tsx | 107 - .../data-admin/DataSourceManagementPage.tsx | 172 -- .../data-admin/UserPermissionPage.tsx | 365 --- src/springboot_demo/frontend/constants.ts | 270 -- src/springboot_demo/frontend/env.d.ts | 14 - src/springboot_demo/frontend/index.html | 66 - src/springboot_demo/frontend/index.tsx | 16 - src/springboot_demo/frontend/metadata.json | 5 - .../frontend/package-lock.json | 2630 ----------------- src/springboot_demo/frontend/package.json | 25 - src/springboot_demo/frontend/services/api.ts | 246 -- src/springboot_demo/frontend/tsconfig.json | 30 - src/springboot_demo/frontend/types.ts | 193 -- src/springboot_demo/frontend/vite.config.ts | 35 - src/springboot_demo/last.md | 507 ---- .../mongodb_schema_from_last.js | 482 --- src/springboot_demo/mvnw | 295 -- src/springboot_demo/mvnw.cmd | 189 -- .../mysql_schema_from_last.sql | 341 --- src/springboot_demo/package-lock.json | 6 - src/springboot_demo/pom.xml | 127 - .../SpringbootDemoApplication.java | 13 - .../springboot_demo/common/Result.java | 37 - .../springboot_demo/config/CorsConfig.java | 26 - .../config/JwtInterceptor.java | 42 - .../config/SecurityConfig.java | 33 - .../springboot_demo/config/WebMvcConfig.java | 27 - .../controller/AuthController.java | 31 - .../controller/ColumnMetadataController.java | 76 - .../controller/DbConnectionController.java | 86 - .../controller/DbTypeController.java | 71 - .../controller/DialogController.java | 34 - .../controller/ErrorLogController.java | 86 - .../controller/ErrorTypeController.java | 74 - .../controller/LlmConfigController.java | 91 - .../controller/LlmStatusController.java | 71 - .../controller/NotificationController.java | 125 - .../NotificationTargetController.java | 74 - .../controller/OperationLogController.java | 82 - .../controller/PriorityController.java | 74 - .../controller/QueryController.java | 29 - .../controller/QueryLogController.java | 78 - .../controller/RoleController.java | 28 - .../controller/SystemHealthController.java | 77 - .../controller/TableMetadataController.java | 75 - .../controller/TestController.java | 107 - .../controller/UserController.java | 87 - .../UserDbPermissionController.java | 95 - .../example/springboot_demo/dto/LoginDTO.java | 11 - .../springboot_demo/dto/QueryRequestDTO.java | 13 - .../entity/mongodb/DialogRecord.java | 29 - .../entity/mysql/ColumnMetadata.java | 30 - .../entity/mysql/DbConnection.java | 35 - .../springboot_demo/entity/mysql/DbType.java | 22 - .../entity/mysql/ErrorLog.java | 29 - .../entity/mysql/ErrorType.java | 25 - .../entity/mysql/LlmConfig.java | 37 - .../entity/mysql/LlmStatus.java | 22 - .../entity/mysql/Notification.java | 39 - .../entity/mysql/NotificationTarget.java | 25 - .../entity/mysql/OperationLog.java | 36 - .../entity/mysql/Priority.java | 25 - .../entity/mysql/QueryLog.java | 31 - .../springboot_demo/entity/mysql/Role.java | 22 - .../entity/mysql/SystemHealth.java | 32 - .../entity/mysql/TableMetadata.java | 27 - .../springboot_demo/entity/mysql/User.java | 38 - .../entity/mysql/UserDbPermission.java | 30 - .../mapper/ColumnMetadataMapper.java | 12 - .../mapper/DbConnectionMapper.java | 11 - .../springboot_demo/mapper/DbTypeMapper.java | 12 - .../mapper/ErrorLogMapper.java | 15 - .../mapper/ErrorTypeMapper.java | 15 - .../mapper/LlmConfigMapper.java | 11 - .../mapper/LlmStatusMapper.java | 12 - .../mapper/NotificationMapper.java | 15 - .../mapper/NotificationTargetMapper.java | 15 - .../mapper/OperationLogMapper.java | 12 - .../mapper/PriorityMapper.java | 15 - .../mapper/QueryLogMapper.java | 12 - .../springboot_demo/mapper/RoleMapper.java | 11 - .../mapper/SystemHealthMapper.java | 15 - .../mapper/TableMetadataMapper.java | 11 - .../mapper/UserDbPermissionMapper.java | 12 - .../springboot_demo/mapper/UserMapper.java | 11 - .../repository/DialogRecordRepository.java | 15 - .../springboot_demo/service/AuthService.java | 10 - .../service/ColumnMetadataService.java | 16 - .../service/DbConnectionService.java | 20 - .../service/DbTypeService.java | 14 - .../service/DialogService.java | 12 - .../service/ErrorLogService.java | 24 - .../service/ErrorTypeService.java | 17 - .../service/LlmConfigService.java | 15 - .../springboot_demo/service/LlmService.java | 20 - .../service/LlmStatusService.java | 14 - .../service/NotificationService.java | 29 - .../service/NotificationTargetService.java | 17 - .../service/OperationLogService.java | 26 - .../service/PriorityService.java | 24 - .../service/QueryLogService.java | 21 - .../springboot_demo/service/QueryService.java | 10 - .../springboot_demo/service/RoleService.java | 7 - .../service/SystemHealthService.java | 24 - .../service/TableMetadataService.java | 14 - .../service/UserDbPermissionService.java | 26 - .../springboot_demo/service/UserService.java | 24 - .../service/impl/AuthServiceImpl.java | 70 - .../impl/ColumnMetadataServiceImpl.java | 27 - .../service/impl/DbConnectionServiceImpl.java | 36 - .../service/impl/DbTypeServiceImpl.java | 22 - .../service/impl/DialogServiceImpl.java | 28 - .../service/impl/ErrorLogServiceImpl.java | 36 - .../service/impl/ErrorTypeServiceImpl.java | 25 - .../service/impl/LlmConfigServiceImpl.java | 24 - .../service/impl/LlmServiceImpl.java | 380 --- .../service/impl/LlmStatusServiceImpl.java | 22 - .../service/impl/NotificationServiceImpl.java | 48 - .../impl/NotificationTargetServiceImpl.java | 25 - .../service/impl/OperationLogServiceImpl.java | 41 - .../service/impl/PriorityServiceImpl.java | 34 - .../service/impl/QueryLogServiceImpl.java | 33 - .../service/impl/QueryServiceImpl.java | 247 -- .../service/impl/RoleServiceImpl.java | 12 - .../service/impl/SystemHealthServiceImpl.java | 36 - .../impl/TableMetadataServiceImpl.java | 25 - .../impl/UserDbPermissionServiceImpl.java | 39 - .../service/impl/UserServiceImpl.java | 69 - .../springboot_demo/utils/JwtUtil.java | 57 - .../springboot_demo/vo/ChartDataVO.java | 13 - .../example/springboot_demo/vo/DatasetVO.java | 13 - .../example/springboot_demo/vo/LoginVO.java | 16 - .../springboot_demo/vo/QueryResponseVO.java | 18 - .../springboot_demo/vo/TableDataVO.java | 12 - .../src/main/resources/application.yml | 75 - .../SpringbootDemoApplicationTests.java | 13 - src/test | 1 + 182 files changed, 1 insertion(+), 19005 deletions(-) delete mode 100644 src/springboot_demo/.gitattributes delete mode 100644 src/springboot_demo/.gitignore delete mode 100644 src/springboot_demo/.mvn/wrapper/maven-wrapper.properties delete mode 100644 src/springboot_demo/198.18.0.22443 delete mode 100644 src/springboot_demo/API_INTERFACE.md delete mode 100644 src/springboot_demo/DATABASE_SETUP.md delete mode 100644 src/springboot_demo/ERROR_FIXES.md delete mode 100644 src/springboot_demo/PROJECT_STRUCTURE.md delete mode 100644 src/springboot_demo/README_CONNECTION.md delete mode 100644 src/springboot_demo/TESTING_GUIDE.md delete mode 100644 src/springboot_demo/api-test.http delete mode 100644 src/springboot_demo/docker-compose.yml delete mode 100644 src/springboot_demo/frontend/.gitignore delete mode 100644 src/springboot_demo/frontend/App.tsx delete mode 100644 src/springboot_demo/frontend/README.md delete mode 100644 src/springboot_demo/frontend/components/AccountPage.tsx delete mode 100644 src/springboot_demo/frontend/components/ChatMessage.tsx delete mode 100644 src/springboot_demo/frontend/components/ChatModal.tsx delete mode 100644 src/springboot_demo/frontend/components/ComparisonModal.tsx delete mode 100644 src/springboot_demo/frontend/components/DataAdminPage.tsx delete mode 100644 src/springboot_demo/frontend/components/DataAdminSidebar.tsx delete mode 100644 src/springboot_demo/frontend/components/Dropdown.tsx delete mode 100644 src/springboot_demo/frontend/components/FriendsPage.tsx delete mode 100644 src/springboot_demo/frontend/components/HistoryPage.tsx delete mode 100644 src/springboot_demo/frontend/components/HistorySidebar.tsx delete mode 100644 src/springboot_demo/frontend/components/LoginPage.tsx delete mode 100644 src/springboot_demo/frontend/components/Modal.tsx delete mode 100644 src/springboot_demo/frontend/components/NotificationsPage.tsx delete mode 100644 src/springboot_demo/frontend/components/PlaceholderPage.tsx delete mode 100644 src/springboot_demo/frontend/components/QueryPage.tsx delete mode 100644 src/springboot_demo/frontend/components/QueryResult.tsx delete mode 100644 src/springboot_demo/frontend/components/RightSidebar.tsx delete mode 100644 src/springboot_demo/frontend/components/Sidebar.tsx delete mode 100644 src/springboot_demo/frontend/components/SysAdminPage.tsx delete mode 100644 src/springboot_demo/frontend/components/SysAdminSidebar.tsx delete mode 100644 src/springboot_demo/frontend/components/TopHeader.tsx delete mode 100644 src/springboot_demo/frontend/components/admin/AdminAccountPage.tsx delete mode 100644 src/springboot_demo/frontend/components/admin/AdminModal.tsx delete mode 100644 src/springboot_demo/frontend/components/admin/DashboardPage.tsx delete mode 100644 src/springboot_demo/frontend/components/admin/LLMConfigPage.tsx delete mode 100644 src/springboot_demo/frontend/components/admin/NotificationManagementPage.tsx delete mode 100644 src/springboot_demo/frontend/components/admin/SystemLogPage.tsx delete mode 100644 src/springboot_demo/frontend/components/admin/UserManagementPage.tsx delete mode 100644 src/springboot_demo/frontend/components/data-admin/ConnectionLogPage.tsx delete mode 100644 src/springboot_demo/frontend/components/data-admin/DataAdminDashboardPage.tsx delete mode 100644 src/springboot_demo/frontend/components/data-admin/DataAdminNotificationPage.tsx delete mode 100644 src/springboot_demo/frontend/components/data-admin/DataSourceManagementPage.tsx delete mode 100644 src/springboot_demo/frontend/components/data-admin/UserPermissionPage.tsx delete mode 100644 src/springboot_demo/frontend/constants.ts delete mode 100644 src/springboot_demo/frontend/env.d.ts delete mode 100644 src/springboot_demo/frontend/index.html delete mode 100644 src/springboot_demo/frontend/index.tsx delete mode 100644 src/springboot_demo/frontend/metadata.json delete mode 100644 src/springboot_demo/frontend/package-lock.json delete mode 100644 src/springboot_demo/frontend/package.json delete mode 100644 src/springboot_demo/frontend/services/api.ts delete mode 100644 src/springboot_demo/frontend/tsconfig.json delete mode 100644 src/springboot_demo/frontend/types.ts delete mode 100644 src/springboot_demo/frontend/vite.config.ts delete mode 100644 src/springboot_demo/last.md delete mode 100644 src/springboot_demo/mongodb_schema_from_last.js delete mode 100644 src/springboot_demo/mvnw delete mode 100644 src/springboot_demo/mvnw.cmd delete mode 100644 src/springboot_demo/mysql_schema_from_last.sql delete mode 100644 src/springboot_demo/package-lock.json delete mode 100644 src/springboot_demo/pom.xml delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/common/Result.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/config/CorsConfig.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/config/SecurityConfig.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/AuthController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbTypeController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DialogController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/OperationLogController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/PriorityController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryLogController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/RoleController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TestController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/dto/LoginDTO.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Role.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/User.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserMapper.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/AuthService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbConnectionService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbTypeService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/DialogService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorLogService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmConfigService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmStatusService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/OperationLogService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/PriorityService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryLogService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/RoleService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/SystemHealthService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/TableMetadataService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserService.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/utils/JwtUtil.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/vo/DatasetVO.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/vo/LoginVO.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java delete mode 100644 src/springboot_demo/src/main/java/com/example/springboot_demo/vo/TableDataVO.java delete mode 100644 src/springboot_demo/src/main/resources/application.yml delete mode 100644 src/springboot_demo/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java create mode 160000 src/test diff --git a/src/springboot_demo/.gitattributes b/src/springboot_demo/.gitattributes deleted file mode 100644 index 3b41682a..00000000 --- a/src/springboot_demo/.gitattributes +++ /dev/null @@ -1,2 +0,0 @@ -/mvnw text eol=lf -*.cmd text eol=crlf diff --git a/src/springboot_demo/.gitignore b/src/springboot_demo/.gitignore deleted file mode 100644 index 667aaef0..00000000 --- a/src/springboot_demo/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -HELP.md -target/ -.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ - -### STS ### -.apt_generated -.classpath -.factorypath -.project -.settings -.springBeans -.sts4-cache - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ -build/ -!**/src/main/**/build/ -!**/src/test/**/build/ - -### VS Code ### -.vscode/ diff --git a/src/springboot_demo/.mvn/wrapper/maven-wrapper.properties b/src/springboot_demo/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index c0bcafe9..00000000 --- a/src/springboot_demo/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,3 +0,0 @@ -wrapperVersion=3.3.4 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/src/springboot_demo/198.18.0.22443 b/src/springboot_demo/198.18.0.22443 deleted file mode 100644 index e69de29b..00000000 diff --git a/src/springboot_demo/API_INTERFACE.md b/src/springboot_demo/API_INTERFACE.md deleted file mode 100644 index d82ce444..00000000 --- a/src/springboot_demo/API_INTERFACE.md +++ /dev/null @@ -1,575 +0,0 @@ -# 前后端接口文档 - -## 基础配置 - -### API 基础地址 -- **默认地址**: `http://localhost:8080` -- **前端配置**: 通过环境变量 `VITE_API_BASE_URL` 配置 -- **配置文件**: `frontend/services/api.ts` - -### 认证方式 -- **认证类型**: JWT (JSON Web Token) -- **Token 位置**: HTTP Header `Authorization: Bearer {token}` -- **Token 存储**: 前端 localStorage -- **用户ID**: HTTP Header `userId` (部分接口需要) - -### 统一响应格式 - -所有接口返回统一的 `Result` 格式: - -```typescript -interface Result { - code: number; // 200: 成功, 500: 失败 - message: string; // 响应消息 - data: T; // 响应数据 -} -``` - -**前端自动处理**: `frontend/services/api.ts` 中的 `request()` 函数会自动解析 `Result` 格式,直接返回 `data` 部分。 - ---- - -## 一、认证接口 - -### 1.1 用户登录 - -**接口**: `POST /auth/login` - -**认证要求**: ❌ 无需认证(公开接口) - -**请求体**: -```typescript -{ - username: string; // 用户名 - password: string; // 密码 -} -``` - -**响应**: -```typescript -{ - token: string; // JWT token - userId: number; // 用户ID - username: string; // 用户名 - email: string; // 邮箱 - roleId: number; // 角色ID (1:系统管理员, 2:数据管理员, 3:普通用户) - roleName: string; // 角色名称 - avatarUrl: string; // 头像URL -} -``` - -**前端调用**: -```typescript -import { authApi } from './services/api'; - -const response = await authApi.login({ - username: 'admin', - password: '123456' -}); -// 登录成功后,token会自动保存到localStorage -``` - -**后端实现**: `AuthController.java` - ---- - -## 二、查询接口 - -### 2.1 执行自然语言查询 - -**接口**: `POST /query/execute` - -**认证要求**: ✅ 需要JWT认证 - -**请求头**: -- `Authorization: Bearer {token}` -- `userId: {userId}` (从localStorage获取) - -**请求体**: -```typescript -{ - userPrompt: string; // 用户自然语言查询 - model: string; // 大模型名称 (如: "gemini-2.5-pro") - database: string; // 数据库名称 (如: "销售数据库") - conversationId?: string; // 对话ID (可选,用于多轮对话) -} -``` - -**响应**: -```typescript -{ - id: string; // 查询ID - userPrompt: string; // 用户查询 - sqlQuery: string; // 生成的SQL语句 - conversationId: string; // 对话ID - queryTime: string; // 查询时间 (ISO格式) - executionTime: string; // 执行耗时 (如: "2.5秒") - database: string; // 数据库名称 - model: string; // 使用的模型 - tableData: { // 表格数据 - headers: string[]; // 表头 - rows: string[][]; // 数据行 - }; - chartData?: { // 图表数据 (可选) - type: string; // 图表类型: "bar" | "line" | "pie" - labels: string[]; // 标签 - datasets: Array<{ - label: string; - data: number[]; - backgroundColor?: string | string[]; - }>; - }; -} -``` - -**前端调用**: -```typescript -import { queryApi } from './services/api'; - -const result = await queryApi.execute({ - userPrompt: '展示2023年各季度的订单量', - model: 'gemini-2.5-pro', - database: '销售数据库', - conversationId: 'conv_12345678' // 可选 -}); -``` - -**后端实现**: `QueryController.java` - ---- - -## 三、对话接口 - -### 3.1 获取用户对话列表 - -**接口**: `GET /dialog/list` - -**认证要求**: ✅ 需要JWT认证 - -**请求头**: -- `Authorization: Bearer {token}` -- `userId: {userId}` - -**响应**: -```typescript -DialogRecord[] { - dialogId: string; // 对话ID - userId: number; // 用户ID - topic: string; // 对话主题 - totalRounds: number; // 对话轮次 - startTime: string; // 开始时间 - lastTime: string; // 最后更新时间 -}[] -``` - -**前端调用**: -```typescript -import { dialogApi } from './services/api'; - -const dialogs = await dialogApi.getList(); -``` - -**后端实现**: `DialogController.java` - -### 3.2 获取对话详情 - -**接口**: `GET /dialog/{dialogId}` - -**认证要求**: ✅ 需要JWT认证 - -**路径参数**: -- `dialogId`: 对话ID - -**响应**: 同 3.1 的单个 `DialogRecord` 对象 - -**前端调用**: -```typescript -const dialog = await dialogApi.getById('conv_12345678'); -``` - ---- - -## 四、用户管理接口 - -### 4.1 获取用户列表 - -**接口**: `GET /user/list` - -**认证要求**: ✅ 需要JWT认证 - -**响应**: -```typescript -User[] { - id: number; - username: string; - email: string; - phonenumber: string; - roleId: number; - avatarUrl: string; - status: number; // 0: 禁用, 1: 正常 -}[] -``` - -**前端调用**: -```typescript -import { userApi } from './services/api'; - -const users = await userApi.getList(); -``` - -**后端实现**: `UserController.java` - -### 4.2 根据ID获取用户 - -**接口**: `GET /user/{id}` - -**认证要求**: ✅ 需要JWT认证 - -**路径参数**: `id` - 用户ID - -**响应**: 单个 `User` 对象 - -### 4.3 根据用户名获取用户 - -**接口**: `GET /user/username/{username}` - -**认证要求**: ✅ 需要JWT认证 - -**路径参数**: `username` - 用户名 - -**响应**: 单个 `User` 对象 - -### 4.4 分页查询用户 - -**接口**: `GET /user/page?current=1&size=10` - -**认证要求**: ✅ 需要JWT认证 - -**查询参数**: -- `current`: 当前页码 (默认: 1) -- `size`: 每页大小 (默认: 10) - -**响应**: 分页对象 - -### 4.5 创建用户 - -**接口**: `POST /user` - -**认证要求**: ✅ 需要JWT认证 - -**请求体**: `User` 对象(密码会自动BCrypt加密) - -### 4.6 更新用户 - -**接口**: `PUT /user` - -**认证要求**: ✅ 需要JWT认证 - -**请求体**: `User` 对象 - -### 4.7 删除用户 - -**接口**: `DELETE /user/{id}` - -**认证要求**: ✅ 需要JWT认证 - -**路径参数**: `id` - 用户ID - ---- - -## 五、数据库连接管理接口 - -### 5.1 获取数据库连接列表 - -**接口**: `GET /db-connection/list` - -**认证要求**: ✅ 需要JWT认证 - -**响应**: -```typescript -DbConnection[] { - id: number; - name: string; - dbTypeId: number; - url: string; - username: string; - password?: string; - status: string; // "connected" | "disconnected" | "error" - createUserId: number; -}[] -``` - -**前端调用**: -```typescript -import { dbConnectionApi } from './services/api'; - -const connections = await dbConnectionApi.getList(); -``` - -**后端实现**: `DbConnectionController.java` - -### 5.2 根据ID获取数据库连接 - -**接口**: `GET /db-connection/{id}` - -**认证要求**: ✅ 需要JWT认证 - -### 5.3 根据创建者获取数据库连接列表 - -**接口**: `GET /db-connection/list/{createUserId}` - -**认证要求**: ✅ 需要JWT认证 - -### 5.4 创建数据库连接 - -**接口**: `POST /db-connection` - -**认证要求**: ✅ 需要JWT认证 - -**请求体**: `DbConnection` 对象 - -### 5.5 更新数据库连接 - -**接口**: `PUT /db-connection` - -**认证要求**: ✅ 需要JWT认证 - -### 5.6 删除数据库连接 - -**接口**: `DELETE /db-connection/{id}` - -**认证要求**: ✅ 需要JWT认证 - -### 5.7 测试数据库连接 - -**接口**: `GET /db-connection/test/{id}` - -**认证要求**: ✅ 需要JWT认证 - -**响应**: `boolean` - 连接是否成功 - -**前端调用**: -```typescript -const isConnected = await dbConnectionApi.test(1); -``` - ---- - -## 六、大模型配置接口 - -### 6.1 获取大模型配置列表 - -**接口**: `GET /llm-config/list` - -**认证要求**: ✅ 需要JWT认证 - -**响应**: -```typescript -LlmConfig[] { - id: number; - name: string; // 模型名称 - version: string; // 版本号 - apiKey?: string; // API密钥 (可能不返回) - apiUrl: string; // API地址 - statusId: number; // 状态ID - isDisabled: number; // 0: 启用, 1: 禁用 - timeout: number; // 超时时间(毫秒) -}[] -``` - -**前端调用**: -```typescript -import { llmConfigApi } from './services/api'; - -const configs = await llmConfigApi.getList(); -``` - -**后端实现**: `LlmConfigController.java` - -### 6.2 获取可用的大模型配置 - -**接口**: `GET /llm-config/list/available` - -**认证要求**: ✅ 需要JWT认证 - -**响应**: 仅返回 `isDisabled = 0` 的配置列表 - -**前端调用**: -```typescript -const availableConfigs = await llmConfigApi.getAvailable(); -``` - -### 6.3 根据ID获取大模型配置 - -**接口**: `GET /llm-config/{id}` - -**认证要求**: ✅ 需要JWT认证 - -### 6.4 创建大模型配置 - -**接口**: `POST /llm-config` - -**认证要求**: ✅ 需要JWT认证 - -### 6.5 更新大模型配置 - -**接口**: `PUT /llm-config` - -**认证要求**: ✅ 需要JWT认证 - -### 6.6 删除大模型配置 - -**接口**: `DELETE /llm-config/{id}` - -**认证要求**: ✅ 需要JWT认证 - -### 6.7 启用/禁用大模型配置 - -**接口**: `PUT /llm-config/{id}/toggle` - -**认证要求**: ✅ 需要JWT认证 - -**功能**: 切换 `isDisabled` 状态 (0 ↔ 1) - ---- - -## 七、其他接口 - -### 7.1 测试接口(无需认证) - -**接口**: `GET /test/hello` - -**响应**: `"Hello, Spring Boot!"` - -**接口**: `GET /test/all` - -**响应**: 数据库连接测试结果 -```json -{ - "mysql": { "status": "success", ... }, - "mongodb": { "status": "success", ... }, - "redis": { "status": "success", ... } -} -``` - ---- - -## 八、认证拦截器配置 - -### 免认证接口 - -以下接口无需JWT认证(在 `WebMvcConfig.java` 中配置): - -- `/auth/**` - 所有认证相关接口 -- `/role` - 角色接口(仅POST) -- `/user` - 用户注册接口 -- `/error` - 错误处理接口 - -### 认证流程 - -1. **登录**: 调用 `/auth/login` 获取token -2. **保存**: 前端自动保存token到localStorage -3. **携带**: 后续请求自动在Header中添加 `Authorization: Bearer {token}` -4. **验证**: 后端 `JwtInterceptor` 验证token有效性 -5. **提取**: 从token中提取userId并设置到request attribute - ---- - -## 九、前端API服务使用示例 - -### 完整示例 - -```typescript -import { authApi, queryApi, dialogApi } from './services/api'; - -// 1. 登录 -try { - const loginResponse = await authApi.login({ - username: 'admin', - password: '123456' - }); - console.log('登录成功:', loginResponse); -} catch (error) { - console.error('登录失败:', error.message); -} - -// 2. 执行查询 -try { - const queryResult = await queryApi.execute({ - userPrompt: '展示2023年各季度的订单量', - model: 'gemini-2.5-pro', - database: '销售数据库' - }); - console.log('查询结果:', queryResult); -} catch (error) { - console.error('查询失败:', error.message); -} - -// 3. 获取对话列表 -try { - const dialogs = await dialogApi.getList(); - console.log('对话列表:', dialogs); -} catch (error) { - console.error('获取对话列表失败:', error.message); -} - -// 4. 登出 -authApi.logout(); -``` - -### 错误处理 - -所有API调用都会自动处理错误: - -```typescript -try { - const result = await queryApi.execute({...}); -} catch (error) { - // error.message 包含后端返回的错误信息 - console.error('请求失败:', error.message); -} -``` - ---- - -## 十、接口状态码说明 - -### HTTP状态码 -- `200`: 请求成功 -- `401`: 未认证(token无效或过期) -- `500`: 服务器内部错误 - -### Result.code -- `200`: 业务成功 -- `500`: 业务失败 - ---- - -## 十一、注意事项 - -1. **Token过期**: 如果token过期,需要重新登录 -2. **CORS配置**: 后端已配置CORS,允许前端跨域访问 -3. **userId Header**: 部分接口需要 `userId` header,前端会自动添加 -4. **密码加密**: 创建/更新用户时,密码会自动BCrypt加密 -5. **时间格式**: 所有时间字段使用ISO 8601格式 (如: `2024-01-01T10:00:00`) - ---- - -## 十二、接口文件位置 - -### 前端 -- **API服务**: `frontend/services/api.ts` -- **类型定义**: `frontend/services/api.ts` (接口中定义) - -### 后端 -- **控制器**: `src/main/java/com/example/springboot_demo/controller/` -- **DTO**: `src/main/java/com/example/springboot_demo/dto/` -- **VO**: `src/main/java/com/example/springboot_demo/vo/` -- **统一响应**: `src/main/java/com/example/springboot_demo/common/Result.java` - ---- - -## 更新日志 - -- **2024-01-XX**: 初始版本,包含认证、查询、对话、用户管理等核心接口 - diff --git a/src/springboot_demo/DATABASE_SETUP.md b/src/springboot_demo/DATABASE_SETUP.md deleted file mode 100644 index 5d06f510..00000000 --- a/src/springboot_demo/DATABASE_SETUP.md +++ /dev/null @@ -1,202 +0,0 @@ -# 数据库配置说明 - -## 是否必须使用Docker? - -**答案:不是必须的!** 你可以使用以下任一方式: - -1. ✅ **使用Docker Compose**(推荐,最简单) -2. ✅ **使用本地已安装的数据库** -3. ✅ **使用远程数据库服务器** - -## 方式一:使用Docker Compose(推荐) - -### 优点 -- 一键启动所有数据库服务 -- 自动初始化数据库和表结构 -- 环境隔离,不影响本地其他项目 -- 配置已预设好,无需额外配置 - -### 启动步骤 -```bash -# 在项目根目录执行 -docker-compose up -d - -# 验证服务启动 -docker-compose ps -``` - -## 方式二:使用本地已安装的数据库 - -### 前提条件 -- 本地已安装 MySQL 8.4+ -- 本地已安装 MongoDB 8.2+ -- 本地已安装 Redis(可选) - -### 配置步骤 - -#### 1. 创建MySQL数据库 -```sql -CREATE DATABASE natural_language_query_system - CHARACTER SET utf8mb4 - COLLATE utf8mb4_unicode_ci; - --- 创建用户(可选) -CREATE USER 'nlq_user'@'localhost' IDENTIFIED BY 'nlq_pass123'; -GRANT ALL PRIVILEGES ON natural_language_query_system.* TO 'nlq_user'@'localhost'; -FLUSH PRIVILEGES; -``` - -#### 2. 初始化MySQL表结构 -```bash -# 执行SQL脚本 -mysql -u root -p natural_language_query_system < mysql_schema_from_last.sql -``` - -#### 3. 配置MongoDB -```javascript -// 连接到MongoDB -mongosh - -// 创建数据库和用户 -use natural_language_query_system; -db.createUser({ - user: "admin", - pwd: "admin123456", - roles: [{ role: "readWrite", db: "natural_language_query_system" }] -}); -``` - -#### 4. 初始化MongoDB集合 -```bash -# 执行初始化脚本 -mongosh -u admin -p admin123456 --authenticationDatabase admin < mongodb_schema_from_last.js -``` - -#### 5. 修改 application.yml(如果需要) - -如果本地数据库的用户名、密码或端口与配置不同,修改 `src/main/resources/application.yml`: - -```yaml -spring: - datasource: - url: jdbc:mysql://localhost:3306/natural_language_query_system?... - username: your_username - password: your_password - - data: - mongodb: - host: 127.0.0.1 - port: 27017 - username: your_mongo_username - password: your_mongo_password -``` - -## 方式三:使用远程数据库服务器 - -### 修改 application.yml - -```yaml -spring: - datasource: - url: jdbc:mysql://your-mysql-server:3306/natural_language_query_system?... - username: remote_user - password: remote_password - - data: - mongodb: - host: your-mongo-server - port: 27017 - username: remote_mongo_user - password: remote_mongo_password -``` - -## 启动后端(不依赖Docker) - -### 如果数据库已就绪 - -```bash -cd springboot_demo -mvn spring-boot:run -``` - -### 如果数据库未启动 - -Spring Boot会在启动时尝试连接数据库。如果连接失败,**默认情况下应用会启动失败**。 - -### 可选:配置延迟初始化(不推荐用于生产环境) - -如果你想在数据库不可用时也能启动应用(仅用于开发测试),可以修改 `application.yml`: - -```yaml -spring: - datasource: - # 添加连接失败时的处理 - hikari: - connection-timeout: 30000 - # 允许启动时数据库不可用(仅开发环境) - initialization-fail-timeout: -1 - - # MongoDB延迟初始化 - data: - mongodb: - # 如果MongoDB不可用,应用仍可启动(但相关功能会失败) -``` - -**注意**:这种方式虽然可以让应用启动,但数据库相关的功能(登录、查询等)将无法正常工作。 - -## 数据库连接检查 - -启动后端后,可以通过以下接口检查数据库连接: - -```bash -# 检查所有数据库 -GET http://localhost:8080/test/all - -# 单独检查 -GET http://localhost:8080/test/mysql -GET http://localhost:8080/test/mongodb -GET http://localhost:8080/test/redis -``` - -## 常见问题 - -### 1. MySQL连接失败 - -**错误信息**:`Communications link failure` - -**解决方案**: -- 检查MySQL服务是否启动:`mysql -u root -p` -- 检查端口3306是否被占用 -- 检查用户名密码是否正确 -- 检查数据库是否存在 - -### 2. MongoDB连接失败 - -**错误信息**:`Exception authenticating MongoCredential` - -**解决方案**: -- 检查MongoDB服务是否启动:`mongosh` -- 检查端口27017是否被占用 -- 检查认证信息是否正确 -- 检查authentication-database配置 - -### 3. Redis连接失败(可选) - -Redis是可选的,如果未启动Redis,应用仍可正常启动,只是缓存功能不可用。 - -## 推荐方案 - -- **开发环境**:使用Docker Compose(最简单) -- **生产环境**:使用独立的数据库服务器 -- **本地测试**:可以使用本地已安装的数据库 - -## 快速启动检查清单 - -- [ ] MySQL服务运行中(端口3306) -- [ ] MongoDB服务运行中(端口27017) -- [ ] Redis服务运行中(端口6379,可选) -- [ ] 数据库已创建并初始化表结构 -- [ ] application.yml配置正确 -- [ ] 后端可以成功启动 - - diff --git a/src/springboot_demo/ERROR_FIXES.md b/src/springboot_demo/ERROR_FIXES.md deleted file mode 100644 index 5b6c2a0e..00000000 --- a/src/springboot_demo/ERROR_FIXES.md +++ /dev/null @@ -1,46 +0,0 @@ -# 错误修复清单 - -## 已修复的错误 - -### 1. ✅ 前端API服务命名冲突 -**文件**: `frontend/services/api.ts` -**问题**: `request` 参数名与 `request()` 函数名冲突 -**修复**: 将参数名从 `request` 改为 `queryRequest` - -### 2. ✅ 后端API响应解析缺少错误检查 -**文件**: `src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java` -**问题**: 所有大模型API调用缺少响应结构验证,可能导致空指针异常 -**修复**: -- Gemini API: 添加 candidates、content、parts 结构检查 -- OpenAI API: 添加 choices、message 结构检查 -- GLM API: 添加 choices、message 结构检查 -- Qwen API: 添加 choices、message 结构检查 -- Kimi API: 添加 choices、message 结构检查 -- 所有API: 添加内容为空检查 - -### 3. ✅ LoginPage语法错误 -**文件**: `frontend/components/LoginPage.tsx` -**问题**: 多余的 `};` 导致语法错误 -**修复**: 删除多余的闭合括号 - -## 代码质量改进 - -### 错误处理增强 -- 所有API调用现在都有完整的响应结构验证 -- 提供更清晰的错误消息,便于调试 -- 防止空指针异常 - -### 类型安全 -- 修复了TypeScript类型错误 -- 确保所有函数参数类型正确 - -## 建议的后续改进 - -1. **统一错误处理**: 考虑创建统一的异常处理类 -2. **日志记录**: 添加详细的日志记录,便于排查问题 -3. **重试机制**: 为API调用添加重试逻辑 -4. **超时处理**: 优化超时时间配置 -5. **响应缓存**: 考虑添加响应缓存机制 - - - diff --git a/src/springboot_demo/PROJECT_STRUCTURE.md b/src/springboot_demo/PROJECT_STRUCTURE.md deleted file mode 100644 index 7b962b49..00000000 --- a/src/springboot_demo/PROJECT_STRUCTURE.md +++ /dev/null @@ -1,80 +0,0 @@ -# 项目结构说明 - -## 项目架构 - -``` -springboot_demo/ -├── frontend/ # 前端项目(React + TypeScript) -│ ├── components/ # React组件 -│ │ ├── admin/ # 系统管理员页面组件 -│ │ ├── data-admin/ # 数据管理员页面组件 -│ │ └── *.tsx # 通用组件 -│ ├── services/ # API服务层 -│ │ └── api.ts # 后端API封装 -│ ├── constants.ts # 常量定义(Mock数据) -│ ├── types.ts # TypeScript类型定义 -│ ├── App.tsx # 应用主组件 -│ └── index.tsx # 入口文件 -│ -├── src/main/java/ # 后端Java代码 -│ └── com/example/springboot_demo/ -│ ├── common/ # 通用类(Result等) -│ ├── config/ # 配置类(CORS、JWT、Security等) -│ ├── controller/ # REST控制器 -│ ├── service/ # 业务服务层 -│ │ └── impl/ # 服务实现 -│ ├── entity/ # 实体类 -│ │ ├── mysql/ # MySQL实体 -│ │ └── mongodb/ # MongoDB实体 -│ ├── mapper/ # MyBatis Mapper接口 -│ ├── repository/ # MongoDB Repository -│ ├── dto/ # 数据传输对象 -│ ├── vo/ # 视图对象 -│ └── utils/ # 工具类 -│ -├── src/main/resources/ # 资源文件 -│ ├── application.yml # 应用配置 -│ └── mapper/ # MyBatis XML映射文件 -│ -├── docker-compose.yml # Docker Compose配置 -├── pom.xml # Maven配置 -└── README_CONNECTION.md # 前后端连接说明 -``` - -## 已删除的冗余文件 - -1. ✅ `frontend/services/geminiService.ts` - 前端不再直接调用大模型API -2. ✅ `frontend/login.html` - 已被React组件LoginPage替代 -3. ✅ `frontend/DataAdminPage.tsx` - 与components目录下重复 -4. ✅ `frontend/DataAdminSidebar.tsx` - 与components目录下重复 - -## 保留的测试/工具文件 - -- `api-test.http` - HTTP测试文件,用于API测试 -- `src/.../controller/TestController.java` - 数据库连接测试接口 -- `last.md` - 数据库Schema文档(参考用) - -## 核心功能模块 - -### 前端 -- **认证**: LoginPage组件,调用后端 `/auth/login` -- **查询**: QueryPage组件,调用后端 `/query/execute` -- **对话管理**: HistorySidebar组件,调用后端 `/dialog/*` -- **API服务**: `services/api.ts` 统一封装所有后端接口 - -### 后端 -- **认证服务**: AuthService + JWT -- **查询服务**: QueryService + LlmService(大模型调用) -- **对话服务**: DialogService(MongoDB) -- **用户管理**: UserService -- **数据源管理**: DbConnectionService -- **大模型配置**: LlmConfigService - -## 数据存储 - -- **MySQL**: 用户、角色、数据源、配置等结构化数据 -- **MongoDB**: 对话记录、查询详情等非结构化数据 -- **Redis**: 缓存(可选) - - - diff --git a/src/springboot_demo/README_CONNECTION.md b/src/springboot_demo/README_CONNECTION.md deleted file mode 100644 index cc241007..00000000 --- a/src/springboot_demo/README_CONNECTION.md +++ /dev/null @@ -1,129 +0,0 @@ -# 前后端连接说明 - -## 已完成的工作 - -### 1. 前端API服务层 -- 创建了 `frontend/services/api.ts`,封装了所有后端API调用 -- 支持认证token自动管理 -- 包含登录、查询、对话、用户管理等接口 - -### 2. 前端登录功能 -- 修改了 `LoginPage.tsx`,现在调用后端 `/auth/login` 接口 -- 登录成功后自动保存JWT token和用户信息到localStorage -- 支持错误提示 - -### 3. 前端查询功能 -- 修改了 `QueryPage.tsx`,现在调用后端 `/query/execute` 接口 -- 移除了直接调用大模型API的逻辑 -- 支持对话ID管理和多轮对话 - -### 4. 后端大模型服务 -- 创建了 `LlmService` 接口和 `LlmServiceImpl` 实现 -- 支持多种大模型:Gemini、OpenAI、GLM、Qwen、Kimi -- 统一处理模型响应格式 - -### 5. 后端查询服务 -- 更新了 `QueryServiceImpl`,集成大模型调用 -- 实现了对话记录管理(MongoDB) -- 解析并返回表格和图表数据 - -## 配置说明 - -### 前端配置 - -1. **API基础URL** - - 默认:`http://localhost:8080` - - 可通过环境变量 `VITE_API_BASE_URL` 配置 - - 在 `frontend/services/api.ts` 中设置 - -2. **环境变量** - - 前端不再需要大模型API密钥(已迁移到后端) - - 只需配置后端API地址 - -### 后端配置 - -1. **大模型API密钥** - - 通过系统环境变量配置: - - `GEMINI_API_KEY` - - `OPENAI_API_KEY` - - `GLM_API_KEY` - - `QWEN_API_KEY` - - `KIMI_API_KEY` - -2. **数据库配置** - - MySQL: 在 `application.yml` 中配置 - - MongoDB: 在 `application.yml` 中配置 - -## 使用步骤 - -### 1. 启动后端 -```bash -cd springboot_demo -mvn spring-boot:run -``` - -### 2. 配置环境变量(后端) -在系统环境变量或启动脚本中设置大模型API密钥: -```bash -export GEMINI_API_KEY=your_key_here -export OPENAI_API_KEY=your_key_here -# ... 其他密钥 -``` - -### 3. 启动前端 -```bash -cd springboot_demo/frontend -npm install -npm run dev -``` - -### 4. 访问系统 -- 前端:http://localhost:3000 -- 后端:http://localhost:8080 - -## API接口 - -### 认证接口 -- `POST /auth/login` - 用户登录 - -### 查询接口 -- `POST /query/execute` - 执行自然语言查询 - -### 对话接口 -- `GET /dialog/list` - 获取用户对话列表 -- `GET /dialog/{dialogId}` - 获取对话详情 - -### 用户接口 -- `GET /user/list` - 获取用户列表 -- `GET /user/{id}` - 获取用户详情 - -## 注意事项 - -1. **CORS配置** - - 后端已配置CORS,允许前端跨域访问 - - 配置在 `CorsConfig.java` 中 - -2. **JWT认证** - - 登录后token保存在localStorage - - API请求自动携带token - - 后端通过 `JwtInterceptor` 验证token - -3. **大模型调用** - - 所有大模型调用现在在后端进行 - - API密钥安全存储在服务器端 - - 前端不再直接访问大模型API - -4. **错误处理** - - 前端会显示后端返回的错误信息 - - 网络错误会自动提示 - -## 后续优化建议 - -1. 实现真正的SQL执行功能(连接数据库执行SQL) -2. 添加SQL安全检查和验证 -3. 实现查询结果缓存 -4. 添加更多错误处理和日志记录 -5. 优化大模型调用性能 - - - diff --git a/src/springboot_demo/TESTING_GUIDE.md b/src/springboot_demo/TESTING_GUIDE.md deleted file mode 100644 index f22d1354..00000000 --- a/src/springboot_demo/TESTING_GUIDE.md +++ /dev/null @@ -1,323 +0,0 @@ -# 测试验证指南 - -## 前置准备 - -### 1. 环境要求 -- Java 21+ -- Maven 3.6+ -- Node.js 18+ -- Docker & Docker Compose(用于数据库) - -### 2. 启动数据库服务 - -```bash -# 在项目根目录执行 -docker-compose up -d - -# 验证服务启动 -docker-compose ps -``` - -应该看到以下服务运行: -- MySQL (端口 3306) -- MongoDB (端口 27017) -- Redis (端口 6379) -- Mongo Express (端口 8081) - 可选 -- Adminer (端口 8082) - 可选 - -### 3. 配置环境变量 - -#### 后端环境变量(系统环境变量或启动脚本) -```bash -export GEMINI_API_KEY=your_gemini_api_key -export OPENAI_API_KEY=your_openai_api_key -export GLM_API_KEY=your_glm_api_key -export QWEN_API_KEY=your_qwen_api_key -export KIMI_API_KEY=your_kimi_api_key -``` - -#### 前端环境变量(可选) -创建 `frontend/.env` 文件: -```env -VITE_API_BASE_URL=http://localhost:8080 -``` - -## 启动服务 - -### 1. 启动后端 - -```bash -cd springboot_demo -mvn clean install -mvn spring-boot:run -``` - -验证后端启动: -- 访问 http://localhost:8080/test/hello 应返回 "Hello, Spring Boot!" -- 访问 http://localhost:8080/test/all 应返回数据库连接测试结果 - -### 2. 启动前端 - -```bash -cd springboot_demo/frontend -npm install -npm run dev -``` - -前端应运行在 http://localhost:3000 - -## 功能测试 - -### 1. 数据库连接测试 - -**测试接口**: `GET http://localhost:8080/test/all` - -**预期结果**: -```json -{ - "mysql": { - "status": "success", - "version": "8.4.x", - "database": "natural_language_query_system", - "tableCount": 17 - }, - "mongodb": { - "status": "success", - "database": "natural_language_query_system", - "collectionCount": 7 - }, - "redis": { - "status": "success", - "message": "Redis 连接正常" - } -} -``` - -### 2. 用户登录测试 - -**步骤**: -1. 访问 http://localhost:3000 -2. 输入用户名和密码(需要先在数据库中创建用户) -3. 选择角色(系统管理员/数据管理员/普通用户) -4. 点击"安全登录" - -**验证点**: -- ✅ 登录成功后跳转到主页面 -- ✅ localStorage中保存了token和用户信息 -- ✅ 根据角色显示对应的侧边栏 - -**API测试**: -```http -POST http://localhost:8080/auth/login -Content-Type: application/json - -{ - "username": "admin", - "password": "123456" -} -``` - -**预期响应**: -```json -{ - "code": 200, - "data": { - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "userId": 1, - "username": "admin", - "email": "admin@example.com", - "roleId": 1, - "roleName": "系统管理员", - "avatarUrl": "/default-avatar.png" - } -} -``` - -### 3. 自然语言查询测试 - -**步骤**: -1. 登录系统 -2. 进入"数据查询"页面 -3. 选择模型(如:gemini-2.5-pro) -4. 选择数据库(如:销售数据库) -5. 输入查询:"展示2023年各季度的订单量" -6. 点击"发送" - -**验证点**: -- ✅ 显示加载状态 -- ✅ 返回SQL查询语句 -- ✅ 显示表格数据 -- ✅ 显示图表数据(如果有) -- ✅ 对话记录保存到MongoDB - -**API测试**: -```http -POST http://localhost:8080/query/execute -Content-Type: application/json -Authorization: Bearer {your_token} -userId: 1 - -{ - "userPrompt": "展示2023年各季度的订单量", - "model": "gemini-2.5-pro", - "database": "销售数据库" -} -``` - -**预期响应**: -```json -{ - "code": 200, - "data": { - "id": "query_xxxxx", - "userPrompt": "展示2023年各季度的订单量", - "sqlQuery": "SELECT ...", - "conversationId": "conv_xxxxx", - "queryTime": "2024-01-01T10:00:00", - "executionTime": "2.5秒", - "database": "销售数据库", - "model": "gemini-2.5-pro", - "tableData": { - "headers": ["季度", "订单量"], - "rows": [["2023-Q1", "1200"], ...] - }, - "chartData": { - "type": "bar", - "labels": ["2023-Q1", "2023-Q2", ...], - "datasets": [...] - } - } -} -``` - -### 4. 对话历史测试 - -**步骤**: -1. 执行多次查询 -2. 点击左侧历史记录图标 -3. 查看对话列表 - -**验证点**: -- ✅ 显示所有对话记录 -- ✅ 可以切换对话 -- ✅ 可以创建新对话 -- ✅ 可以删除对话 - -**API测试**: -```http -GET http://localhost:8080/dialog/list -Authorization: Bearer {your_token} -userId: 1 -``` - -### 5. 用户管理测试(系统管理员) - -**步骤**: -1. 以系统管理员身份登录 -2. 进入"用户管理"页面 -3. 测试增删改查功能 - -**API测试**: -```http -# 获取用户列表 -GET http://localhost:8080/user/list -Authorization: Bearer {your_token} - -# 创建用户 -POST http://localhost:8080/user -Content-Type: application/json -Authorization: Bearer {your_token} - -{ - "username": "testuser", - "password": "123456", - "email": "test@example.com", - "phonenumber": "13800138000", - "roleId": 3, - "status": 1 -} -``` - -### 6. 数据源管理测试(数据管理员) - -**步骤**: -1. 以数据管理员身份登录 -2. 进入"数据源管理"页面 -3. 添加数据库连接 -4. 测试连接 - -**API测试**: -```http -# 获取数据源列表 -GET http://localhost:8080/db-connection/list -Authorization: Bearer {your_token} - -# 测试连接 -GET http://localhost:8080/db-connection/test/1 -Authorization: Bearer {your_token} -``` - -## 常见问题排查 - -### 1. 后端启动失败 - -**检查项**: -- ✅ Java版本是否为21+ -- ✅ MySQL、MongoDB、Redis是否启动 -- ✅ 数据库连接配置是否正确(application.yml) -- ✅ 端口8080是否被占用 - -### 2. 前端无法连接后端 - -**检查项**: -- ✅ 后端是否正常运行 -- ✅ CORS配置是否正确 -- ✅ API_BASE_URL配置是否正确 -- ✅ 浏览器控制台是否有错误 - -### 3. 大模型调用失败 - -**检查项**: -- ✅ 环境变量是否配置 -- ✅ API密钥是否有效 -- ✅ 网络是否可访问模型API -- ✅ 模型名称是否正确 - -### 4. 登录失败 - -**检查项**: -- ✅ 数据库中是否有用户数据 -- ✅ 密码是否正确(BCrypt加密) -- ✅ JWT配置是否正确 - -## 测试检查清单 - -- [ ] 数据库服务启动正常 -- [ ] 后端服务启动正常(8080端口) -- [ ] 前端服务启动正常(3000端口) -- [ ] 用户登录功能正常 -- [ ] JWT token生成和验证正常 -- [ ] 自然语言查询功能正常 -- [ ] 大模型API调用正常 -- [ ] 对话历史保存和读取正常 -- [ ] 用户管理功能正常(系统管理员) -- [ ] 数据源管理功能正常(数据管理员) -- [ ] 权限控制正常 -- [ ] 错误处理正常 - -## 性能测试建议 - -1. **并发查询测试**: 使用工具(如JMeter)模拟多用户同时查询 -2. **大模型响应时间**: 监控不同模型的响应时间 -3. **数据库连接池**: 检查连接池使用情况 -4. **缓存效果**: 测试Redis缓存是否生效 - -## 安全测试建议 - -1. **JWT验证**: 测试无效token是否被拒绝 -2. **SQL注入**: 测试SQL注入防护 -3. **XSS攻击**: 测试前端XSS防护 -4. **CORS配置**: 测试跨域请求是否被正确控制 - - - diff --git a/src/springboot_demo/api-test.http b/src/springboot_demo/api-test.http deleted file mode 100644 index 80801dfb..00000000 --- a/src/springboot_demo/api-test.http +++ /dev/null @@ -1,636 +0,0 @@ -### 测试 Hello -GET http://localhost:8080/test/hello - -### 测试所有数据库 -GET http://localhost:8080/test/all - -### ==================== 认证接口 ==================== - -### 登录 -POST http://localhost:8080/auth/login -Content-Type: application/json - -{ - "username": "admin", - "password": "123456" -} - -### ==================== 查询执行接口 ==================== - -### 执行查询(新对话) -POST http://localhost:8080/query/execute -Content-Type: application/json -userId: 1 - -{ - "userPrompt": "查询所有用户信息", - "model": "gemini-2.5-pro", - "database": "销售数据库" -} - -### 执行查询(继续对话) -POST http://localhost:8080/query/execute -Content-Type: application/json -userId: 1 - -{ - "userPrompt": "按订单量排序", - "model": "gemini-2.5-pro", - "database": "销售数据库", - "conversationId": "conv_12345678" -} - -### ==================== 对话历史接口 ==================== - -### 获取用户对话列表 -GET http://localhost:8080/dialog/list -userId: 1 - -### 获取对话详情 -GET http://localhost:8080/dialog/conv_12345678 - -### ==================== 用户管理接口 ==================== - -### 查询所有用户 -GET http://localhost:8080/user/list - -### 添加用户 -POST http://localhost:8080/user -Content-Type: application/json - -{ - "username": "admin", - "password": "123456", - "email": "admin@example.com", - "phonenumber": "13800138000", - "roleId": 1, - "status": 1 -} - -### 分页查询 -GET http://localhost:8080/user/page?current=1&size=10 - -### 根据ID查询 -GET http://localhost:8080/user/3 - -### 更新用户 -PUT http://localhost:8080/user -Content-Type: application/json - -{ - "id": 3, - "username": "admin", - "email": "newemail@example.com", - "status": 1 -} - -### 删除用户 -DELETE http://localhost:8080/user/3 - -### 根据用户名查询 -GET http://localhost:8080/user/username/admin - -### ==================== 数据库连接管理接口 ==================== - -### 查询所有数据库连接 -GET http://localhost:8080/db-connection/list - -### 根据创建者查询数据库连接 -GET http://localhost:8080/db-connection/list/4 - -### 根据ID查询数据库连接 -GET http://localhost:8080/db-connection/1 - -### 添加数据库连接 -POST http://localhost:8080/db-connection -Content-Type: application/json - -{ - "name": "测试MySQL连接", - "dbTypeId": 1, - "url": "127.0.0.1:3306/test_db", - "username": "root", - "password": "password", - "status": "disconnected", - "createUserId": 4 -} - -### 更新数据库连接 -PUT http://localhost:8080/db-connection -Content-Type: application/json - -{ - "id": 1, - "name": "更新后的连接名称", - "status": "connected" -} - -### 测试数据库连接 -GET http://localhost:8080/db-connection/test/1 - -### 删除数据库连接 -DELETE http://localhost:8080/db-connection/1 - -### ==================== 大模型配置接口 ==================== - -### 查询所有大模型配置 -GET http://localhost:8080/llm-config/list - -### 查询可用的大模型配置 -GET http://localhost:8080/llm-config/list/available - -### 根据ID查询大模型配置 -GET http://localhost:8080/llm-config/1 - -### 添加大模型配置 -POST http://localhost:8080/llm-config -Content-Type: application/json - -{ - "name": "智谱AI", - "version": "4.0", - "apiKey": "your-api-key-here", - "apiUrl": "https://api.zhipuai.com/v4/chat/completions", - "statusId": 1, - "isDisabled": 0, - "timeout": 5000, - "createUserId": 4 -} - -### 更新大模型配置 -PUT http://localhost:8080/llm-config -Content-Type: application/json - -{ - "id": 1, - "name": "智谱AI", - "version": "4.5", - "timeout": 8000 -} - -### 禁用/启用大模型配置 -PUT http://localhost:8080/llm-config/1/toggle - -### 删除大模型配置 -DELETE http://localhost:8080/llm-config/1 - -### ==================== 数据表元数据接口 ==================== - -### 查询所有表元数据 -GET http://localhost:8080/table-metadata/list - -### 根据数据库连接ID查询表元数据 -GET http://localhost:8080/table-metadata/list/1 - -### 根据ID查询表元数据 -GET http://localhost:8080/table-metadata/1 - -### 添加表元数据 -POST http://localhost:8080/table-metadata -Content-Type: application/json - -{ - "dbConnectionId": 1, - "tableName": "orders", - "description": "订单表,存储所有订单信息" -} - -### 更新表元数据 -PUT http://localhost:8080/table-metadata -Content-Type: application/json - -{ - "id": 1, - "tableName": "orders", - "description": "订单表(已更新)" -} - -### 删除表元数据 -DELETE http://localhost:8080/table-metadata/1 - -### ==================== 字段元数据接口 ==================== - -### 查询所有字段元数据 -GET http://localhost:8080/column-metadata/list - -### 根据表ID查询字段元数据 -GET http://localhost:8080/column-metadata/list/1 - -### 根据ID查询字段元数据 -GET http://localhost:8080/column-metadata/1 - -### 添加字段元数据 -POST http://localhost:8080/column-metadata -Content-Type: application/json - -{ - "tableId": 1, - "columnName": "order_id", - "dataType": "bigint(20)", - "description": "订单唯一标识", - "isPrimary": 1 -} - -### 更新字段元数据 -PUT http://localhost:8080/column-metadata -Content-Type: application/json - -{ - "id": 1, - "columnName": "order_id", - "description": "订单ID(主键)" -} - -### 删除字段元数据 -DELETE http://localhost:8080/column-metadata/1 - -### ==================== 查询日志接口 ==================== - -### 查询所有查询日志 -GET http://localhost:8080/query-log/list - -### 根据用户ID查询查询日志 -GET http://localhost:8080/query-log/list/user/4 - -### 根据对话ID查询查询日志 -GET http://localhost:8080/query-log/list/dialog/conv_12345678 - -### 根据ID查询查询日志 -GET http://localhost:8080/query-log/1 - -### 添加查询日志 -POST http://localhost:8080/query-log -Content-Type: application/json - -{ - "dialogId": "conv_12345678", - "dataSourceId": 1, - "userId": 4, - "executeResult": 1 -} - -### 删除查询日志 -DELETE http://localhost:8080/query-log/1 - -### ==================== 数据库类型字典接口 ==================== - -### 查询所有数据库类型 -GET http://localhost:8080/db-type/list - -### 根据ID查询数据库类型 -GET http://localhost:8080/db-type/1 - -### 根据类型编码查询 -GET http://localhost:8080/db-type/code/mysql - -### 添加数据库类型 -POST http://localhost:8080/db-type -Content-Type: application/json - -{ - "typeName": "MySQL", - "typeCode": "mysql", - "description": "关系型数据库" -} - -### 更新数据库类型 -PUT http://localhost:8080/db-type -Content-Type: application/json - -{ - "id": 1, - "typeName": "MySQL", - "description": "关系型数据库(已更新)" -} - -### 删除数据库类型 -DELETE http://localhost:8080/db-type/1 - -### ==================== 大模型状态字典接口 ==================== - -### 查询所有大模型状态 -GET http://localhost:8080/llm-status/list - -### 根据ID查询大模型状态 -GET http://localhost:8080/llm-status/1 - -### 根据状态编码查询 -GET http://localhost:8080/llm-status/code/available - -### 添加大模型状态 -POST http://localhost:8080/llm-status -Content-Type: application/json - -{ - "statusName": "可用", - "statusCode": "available", - "description": "API成功率≥95%" -} - -### 更新大模型状态 -PUT http://localhost:8080/llm-status -Content-Type: application/json - -{ - "id": 1, - "statusName": "可用", - "description": "API成功率≥98%" -} - -### 删除大模型状态 -DELETE http://localhost:8080/llm-status/1 - -### ==================== 用户数据权限接口 ==================== - -### 查询所有权限 -GET http://localhost:8080/user-db-permission/list - -### 查询已分配权限的用户 -GET http://localhost:8080/user-db-permission/list/assigned - -### 查询未分配权限的用户 -GET http://localhost:8080/user-db-permission/list/unassigned - -### 根据ID查询权限 -GET http://localhost:8080/user-db-permission/1 - -### 根据用户ID查询权限 -GET http://localhost:8080/user-db-permission/user/4 - -### 添加用户权限 -POST http://localhost:8080/user-db-permission -Content-Type: application/json - -{ - "userId": 4, - "permissionDetails": "[{\"db_connection_id\":1,\"table_ids\":[1,2,3]}]", - "lastGrantUserId": 4, - "isAssigned": 1 -} - -### 更新用户权限 -PUT http://localhost:8080/user-db-permission -Content-Type: application/json - -{ - "id": 1, - "userId": 4, - "permissionDetails": "[{\"db_connection_id\":1,\"table_ids\":[1,2,3,4,5]}]", - "lastGrantUserId": 4 -} - -### 删除用户权限 -DELETE http://localhost:8080/user-db-permission/1 - -### ==================== 系统操作日志接口 ==================== - -### 查询所有操作日志 -GET http://localhost:8080/operation-log/list - -### 根据用户ID查询操作日志 -GET http://localhost:8080/operation-log/list/user/4 - -### 根据模块查询操作日志 -GET http://localhost:8080/operation-log/list/module/数据源管理 - -### 查询失败的操作日志 -GET http://localhost:8080/operation-log/list/failed - -### 根据ID查询操作日志 -GET http://localhost:8080/operation-log/1 - -### 添加操作日志 -POST http://localhost:8080/operation-log -Content-Type: application/json - -{ - "userId": 4, - "username": "admin", - "operation": "创建数据库连接", - "module": "数据源管理", - "relatedLlm": null, - "ipAddress": "127.0.0.1", - "result": 1, - "errorMsg": null -} - -### 删除操作日志 -DELETE http://localhost:8080/operation-log/1 - -### ==================== 错误日志接口 ==================== - -### 查询所有错误日志 -GET http://localhost:8080/error-log/list - -### 根据错误类型查询 -GET http://localhost:8080/error-log/list/type/1 - -### 根据统计周期查询 -GET http://localhost:8080/error-log/list/period/today - -### 根据ID查询错误日志 -GET http://localhost:8080/error-log/1 - -### 添加错误日志 -POST http://localhost:8080/error-log -Content-Type: application/json - -{ - "errorTypeId": 1, - "errorCount": 5, - "errorRate": 2.5, - "period": "today" -} - -### 更新错误日志 -PUT http://localhost:8080/error-log -Content-Type: application/json - -{ - "id": 1, - "errorCount": 10, - "errorRate": 5.0 -} - -### 删除错误日志 -DELETE http://localhost:8080/error-log/1 - -### ==================== 错误类型字典接口 ==================== - -### 查询所有错误类型 -GET http://localhost:8080/error-type/list - -### 根据ID查询错误类型 -GET http://localhost:8080/error-type/1 - -### 根据错误编码查询 -GET http://localhost:8080/error-type/code/llm_timeout - -### 添加错误类型 -POST http://localhost:8080/error-type -Content-Type: application/json - -{ - "errorName": "模型调用超时", - "errorCode": "llm_timeout", - "description": "大模型API响应时间超过设定阈值" -} - -### 更新错误类型 -PUT http://localhost:8080/error-type -Content-Type: application/json - -{ - "id": 1, - "errorName": "模型调用超时", - "description": "大模型API响应时间超过5秒" -} - -### 删除错误类型 -DELETE http://localhost:8080/error-type/1 - -### ==================== 通知目标字典接口 ==================== - -### 查询所有通知目标 -GET http://localhost:8080/notification-target/list - -### 根据ID查询通知目标 -GET http://localhost:8080/notification-target/1 - -### 根据目标编码查询 -GET http://localhost:8080/notification-target/code/all - -### 添加通知目标 -POST http://localhost:8080/notification-target -Content-Type: application/json - -{ - "targetName": "所有用户", - "targetCode": "all", - "description": "发送给系统所有用户" -} - -### 更新通知目标 -PUT http://localhost:8080/notification-target -Content-Type: application/json - -{ - "id": 1, - "targetName": "所有用户", - "description": "发送给系统所有注册用户" -} - -### 删除通知目标 -DELETE http://localhost:8080/notification-target/1 - -### ==================== 优先级字典接口 ==================== - -### 查询所有优先级(按排序) -GET http://localhost:8080/priority/list - -### 根据ID查询优先级 -GET http://localhost:8080/priority/1 - -### 根据优先级编码查询 -GET http://localhost:8080/priority/code/urgent - -### 添加优先级 -POST http://localhost:8080/priority -Content-Type: application/json - -{ - "priorityName": "紧急", - "priorityCode": "urgent", - "sort": 1 -} - -### 更新优先级 -PUT http://localhost:8080/priority -Content-Type: application/json - -{ - "id": 1, - "priorityName": "紧急", - "sort": 0 -} - -### 删除优先级 -DELETE http://localhost:8080/priority/1 - -### ==================== 通知管理接口 ==================== - -### 查询所有通知 -GET http://localhost:8080/notification/list - -### 查询已发布的通知 -GET http://localhost:8080/notification/list/published - -### 查询草稿 -GET http://localhost:8080/notification/list/drafts - -### 根据目标ID查询通知 -GET http://localhost:8080/notification/list/target/1 - -### 根据ID查询通知 -GET http://localhost:8080/notification/1 - -### 添加通知(草稿) -POST http://localhost:8080/notification -Content-Type: application/json - -{ - "title": "系统维护通知", - "content": "系统将于今晚22:00-24:00进行维护,请提前保存数据", - "targetId": 1, - "priorityId": 1, - "publisherId": 4, - "isTop": 0 -} - -### 更新通知 -PUT http://localhost:8080/notification -Content-Type: application/json - -{ - "id": 1, - "title": "系统维护通知(已更新)", - "content": "系统将于今晚22:00-次日00:30进行维护,请提前保存数据" -} - -### 发布通知 -PUT http://localhost:8080/notification/1/publish - -### 置顶/取消置顶 -PUT http://localhost:8080/notification/1/toggle-top - -### 删除通知 -DELETE http://localhost:8080/notification/1 - -### ==================== 系统健康监控接口 ==================== - -### 查询所有健康记录 -GET http://localhost:8080/system-health/list - -### 查询最新的健康记录 -GET http://localhost:8080/system-health/latest - -### 查询最近10条健康记录 -GET http://localhost:8080/system-health/recent/10 - -### 根据ID查询健康记录 -GET http://localhost:8080/system-health/1 - -### 添加健康记录 -POST http://localhost:8080/system-health -Content-Type: application/json - -{ - "dbDelay": 10, - "cacheDelay": 2, - "llmDelay": 150, - "storageUsage": 75.5 -} - -### 删除健康记录 -DELETE http://localhost:8080/system-health/1 - diff --git a/src/springboot_demo/docker-compose.yml b/src/springboot_demo/docker-compose.yml deleted file mode 100644 index d4a846fb..00000000 --- a/src/springboot_demo/docker-compose.yml +++ /dev/null @@ -1,123 +0,0 @@ -services: - # MySQL 8.4 服务 - mysql: - image: mysql:8.4 - container_name: nlq_mysql - restart: always - environment: - MYSQL_ROOT_PASSWORD: root123456 - MYSQL_DATABASE: natural_language_query_system - MYSQL_USER: nlq_user - MYSQL_PASSWORD: nlq_pass123 - TZ: Asia/Shanghai - ports: - - "3306:3306" - volumes: - # 挂载初始化脚本(MySQL会自动执行/docker-entrypoint-initdb.d/目录下的.sql文件) - - ./mysql_schema_from_last.sql:/docker-entrypoint-initdb.d/init.sql - # 数据持久化 - - mysql_data:/var/lib/mysql - command: - - --character-set-server=utf8mb4 - - --collation-server=utf8mb4_unicode_ci - - --mysql-native-password=ON - networks: - - nlq_network - healthcheck: - test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123456"] - interval: 10s - timeout: 5s - retries: 5 - - # MongoDB 8.2 服务 - mongodb: - image: mongo:8.2 - container_name: nlq_mongodb - restart: always - environment: - MONGO_INITDB_ROOT_USERNAME: admin - MONGO_INITDB_ROOT_PASSWORD: admin123456 - MONGO_INITDB_DATABASE: natural_language_query_system - TZ: Asia/Shanghai - ports: - - "27017:27017" - volumes: - # 挂载初始化脚本 - - ./mongodb_schema_from_last.js:/docker-entrypoint-initdb.d/init.js - # 数据持久化 - - mongodb_data:/data/db - - mongodb_config:/data/configdb - networks: - - nlq_network - healthcheck: - test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] - interval: 10s - timeout: 5s - retries: 5 - - # 可选:Mongo Express (MongoDB 的 Web 管理界面) - mongo-express: - image: mongo-express:latest - container_name: nlq_mongo_express - restart: always - ports: - - "8081:8081" - environment: - ME_CONFIG_MONGODB_ADMINUSERNAME: admin - ME_CONFIG_MONGODB_ADMINPASSWORD: admin123456 - ME_CONFIG_MONGODB_URL: mongodb://admin:admin123456@mongodb:27017/ - ME_CONFIG_BASICAUTH_USERNAME: admin - ME_CONFIG_BASICAUTH_PASSWORD: admin - depends_on: - - mongodb - networks: - - nlq_network - - # 可选:Adminer (MySQL 的 Web 管理界面) - adminer: - image: adminer:latest - container_name: nlq_adminer - restart: always - ports: - - "8082:8080" - environment: - ADMINER_DEFAULT_SERVER: mysql - depends_on: - - mysql - networks: - - nlq_network - - # Redis 缓存服务 - redis: - image: redis:7-alpine - container_name: nlq_redis - restart: always - ports: - - "6379:6379" - volumes: - - redis_data:/data - command: redis-server --appendonly yes - networks: - - nlq_network - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 10s - timeout: 3s - retries: 5 - -# 数据卷(数据持久化) -volumes: - mysql_data: - driver: local - mongodb_data: - driver: local - mongodb_config: - driver: local - redis_data: - driver: local - -# 网络配置 -networks: - nlq_network: - driver: bridge - diff --git a/src/springboot_demo/frontend/.gitignore b/src/springboot_demo/frontend/.gitignore deleted file mode 100644 index a547bf36..00000000 --- a/src/springboot_demo/frontend/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/src/springboot_demo/frontend/App.tsx b/src/springboot_demo/frontend/App.tsx deleted file mode 100644 index 2310b52f..00000000 --- a/src/springboot_demo/frontend/App.tsx +++ /dev/null @@ -1,732 +0,0 @@ -import React, { useState, useEffect } from 'react'; -// 页面组件导入 -import { LoginPage } from './components/LoginPage'; -import { Sidebar } from './components/Sidebar'; -import { QueryPage } from './components/QueryPage'; -import { HistoryPage } from './components/HistoryPage'; -import { NotificationsPage } from './components/NotificationsPage'; -import { AccountPage } from './components/AccountPage'; -import { FriendsPage } from './components/FriendsPage'; -import { SysAdminSidebar } from './components/SysAdminSidebar'; -import { DataAdminSidebar } from './components/DataAdminSidebar'; -import { SysAdminPage } from './components/SysAdminPage'; -import { DataAdminPage } from './components/DataAdminPage'; -import { TopHeader } from './components/TopHeader'; -import { ComparisonModal } from './components/ComparisonModal'; -import { Modal } from './components/Modal'; - -// 模拟数据导入 -import { - MOCK_INITIAL_CONVERSATION, - MOCK_SAVED_QUERIES, - MOCK_USER_PROFILE, - MOCK_NOTIFICATIONS, - MOCK_QUERY_SHARES, - MOCK_FRIENDS_LIST -} from './constants'; - -// 类型定义导入 -import { - Conversation, - MessageRole, - Page, - QueryResultData, - UserRole, - SysAdminPageType, - DataAdminPageType, - QueryShare, - Notification -} from './types'; - -/** - * 各角色侧边栏配置项 - * 按角色划分功能菜单,与对应Sidebar组件菜单结构保持一致 - */ -// 普通用户侧边栏项目 - 基础数据查询与个人中心功能 -const normalUserSidebarItems = [ - { href: 'query', label: '数据查询' }, - { href: 'history', label: '收藏夹' }, - { href: 'notifications', label: '通知中心' }, - { href: 'friends', label: '好友管理' }, - { href: 'account', label: '账户管理' }, -] as const; - -// 数据管理员侧边栏项目 - 包含数据管理与基础功能 -const dataAdminSidebarItems = [ - { href: 'query', label: '数据查询' }, - { href: 'history', label: '收藏夹' }, - { href: 'datasource', label: '数据源管理' }, - { href: 'dashboard', label: '数据源概览' }, - { href: 'user-permission', label: '用户权限管理' }, - { href: 'notification-management', label: '通知管理(数据员)' }, - { href: 'connection-log', label: '数据源连接日志' }, - { href: 'notifications', label: '通知中心' }, - { href: 'account', label: '我的账户' }, - { href: 'friends', label: '好友管理' }, -] as const; - -// 系统管理员侧边栏项目 - 系统配置与管理功能 -const sysAdminSidebarItems = [ - { href: 'dashboard', label: '系统概览' }, - { href: 'user-management', label: '用户管理' }, - { href: 'account', label: '我的账户' }, - { href: 'system-log', label: '系统日志' }, - { href: 'llm-config', label: '大模型配置' }, - { href: 'notification-management', label: '通知管理' }, -] as const; - -/** - * 应用根组件 - 全局状态管理与角色路由控制 - * 负责用户身份验证、页面切换、数据状态维护 - */ -const App: React.FC = () => { - // ===== 全局核心状态 ===== - // 当前用户角色(null表示未登录) - const [userRole, setUserRole] = useState(null); - // 所有对话列表 - const [conversations, setConversations] = useState([MOCK_INITIAL_CONVERSATION]); - // 当前激活的对话ID - const [currentConversationId, setCurrentConversationId] = useState(MOCK_INITIAL_CONVERSATION.id); - // 历史对话面板显示状态(仅query页面生效) - const [isHistoryOpen, setIsHistoryOpen] = useState(false); - // 普通用户当前激活页面 - const [activePage, setActivePage] = useState('query'); - // 已保存的查询结果列表 - const [savedQueries, setSavedQueries] = useState(MOCK_SAVED_QUERIES); - // 新对话初始提示语(用于重新运行查询场景) - const [initialPrompt, setInitialPrompt] = useState(undefined); - // 控制弹窗显示/隐藏 - const [isCompareModalOpen, setIsCompareModalOpen] = useState(false); - // 存储要对比的查询 - const [compareQueries, setCompareQueries] = useState<{ oldQuery: QueryResultData; newQuery: QueryResultData } | null>(null); - // 待对比的查询ID组合(旧查询ID, 新查询ID) - const [comparisonQueryIds, setComparisonQueryIds] = useState<[string, string] | null>(null); - // 对话不存在提示弹窗显示状态 - const [notFoundModalOpen, setNotFoundModalOpen] = useState(false); - // 查询分享记录列表 - const [queryShares, setQueryShares] = useState(MOCK_QUERY_SHARES); - // 通知消息列表 - const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS); - - // ===== 管理员专属状态 ===== - // 系统管理员当前激活页面 - const [activeSysAdminPage, setActiveSysAdminPage] = useState('dashboard'); - // 数据管理员当前激活页面 - const [activeDataAdminPage, setActiveDataAdminPage] = useState('dashboard'); - - // 当前激活的对话详情(通过currentConversationId从列表中匹配) - const currentConversation = conversations.find(c => c.id === currentConversationId); - - // ===== 辅助函数 ===== - /** - * 获取侧边栏当前页面名称 - * 根据用户角色和激活页面,匹配对应的菜单名称 - * @returns 当前页面的显示名称(空字符串表示无需显示) - */ - const getSidebarPageName = () => { - if (userRole === 'normal-user') { - // 普通用户query页面不显示侧边栏名称 - if (activePage === 'query') return ''; - return normalUserSidebarItems.find(item => item.href === activePage)?.label || ''; - } - - if (userRole === 'data-admin') { - // 数据管理员query页面不显示侧边栏名称 - if (activeDataAdminPage === 'query') return ''; - return dataAdminSidebarItems.find(item => item.href === activeDataAdminPage)?.label || ''; - } - - if (userRole === 'sys-admin') { - return sysAdminSidebarItems.find(item => item.href === activeSysAdminPage)?.label || ''; - } - - return ''; - }; - - /** - * 获取顶部导航栏标题 - * 规则:query页面显示对话标题,其他页面显示侧边栏菜单名称 - * @returns 顶部导航栏显示标题 - */ - const getHeaderTitle = () => { - // 判定是否为query页面(跨所有角色) - const isQueryPage = ( - (userRole === 'normal-user' && activePage === 'query') || - (userRole === 'data-admin' && activeDataAdminPage === 'query') || - (userRole === 'sys-admin' && activeSysAdminPage === 'query') - ); - - // query页面优先显示对话标题,无则显示默认文本 - if (isQueryPage) { - return currentConversation?.title || '新对话'; - } - - // 非query页面显示侧边栏菜单名称 - return getSidebarPageName(); - }; - - // 顶部导航栏最终显示标题 - const headerTitle = getHeaderTitle(); - - // ===== 事件处理函数 ===== - /** - * 登录处理函数 - * 优先使用传入的角色,无则从sessionStorage读取保存的角色 - * @param role - 登录选择的用户角色 - */ - const handleLogin = (role: UserRole) => { - const savedRole = sessionStorage.getItem('userRole') as UserRole; - const roleToSet = role || savedRole; - if (roleToSet) { - setUserRole(roleToSet); - } - }; - - /** - * 组件挂载时初始化登录状态 - * 从sessionStorage读取保存的角色,自动登录 - */ - useEffect(() => { - handleLogin(userRole); - }, []); - - /** - * 退出登录处理函数 - * 清除sessionStorage中的角色信息,重置所有状态到初始值 - */ - const handleLogout = () => { - sessionStorage.removeItem('userRole'); - setUserRole(null); - setActivePage('query'); - setActiveSysAdminPage('dashboard'); - setActiveDataAdminPage('dashboard'); - }; - - /** - * 添加对话消息 - * 向当前激活的对话中追加新消息,首次用户消息自动生成对话标题 - * @param role - 消息发送者角色(user/ai) - * @param content - 消息内容(文本或查询结果数据) - */ - const handleAddMessage = (role: MessageRole, content: string | QueryResultData) => { - // 优先使用传入的目标对话ID,无则使用当前激活对话ID - const convId = currentConversationId; - if (!convId) return; - - setConversations(prev => prev.map(conv => { - // 只更新当前激活的对话 - if (conv.id === currentConversationId) { - const newMessages = [...conv.messages, { role, content }]; - let newTitle = conv.title; - - // 首次用户消息(对话初始状态),截取前20字作为对话标题 - if (conv.messages.length === 1 && role === 'user' && typeof content === 'string') { - newTitle = content.substring(0, 20); - } - - return { ...conv, title: newTitle, messages: newMessages }; - } - return conv; - })); - }; - - /** - * 创建新对话 - * 优先复用现有空对话,无则创建全新对话,自动切换到query页面 - */ - const handleNewConversation = () => { - // 判断对话是否为空(仅包含AI初始问候语) - const isEmptyConv = (conv: Conversation) => { - return conv.messages.length === 1 && - conv.messages[0].role === 'ai' && - typeof conv.messages[0].content === 'string' && - conv.messages[0].content.includes('您好!我是数据查询助手'); - }; - - // 查找现有空对话 - const existingEmptyConv = conversations.find(isEmptyConv); - - if (existingEmptyConv) { - // 复用空对话 - setCurrentConversationId(existingEmptyConv.id); - } else { - // 创建新对话 - const newConv: Conversation = { - id: 'conv-' + Date.now(), // 时间戳生成唯一ID - title: '新对话', - messages: [{ - role: 'ai', - content: '您好!我是数据查询助手,您可以通过自然语言描述您的查询需求(例如:"展示2023年各季度的订单量"),我会为您生成相应的结果。' - }], - createTime: new Date().toISOString(), - }; - // 添加到对话列表头部 - setConversations(prev => [newConv, ...prev]); - setCurrentConversationId(newConv.id); - } - - // 关闭历史对话面板 - setIsHistoryOpen(false); - - // 切换到query页面(区分角色) - if (userRole === 'data-admin') { - setActiveDataAdminPage('query'); - } else { - setActivePage('query'); - } - }; - - /** - * 切换对话 - * 从历史对话列表中选择已有对话,自动切换到query页面 - * @param id - 目标对话ID - */ - const handleSwitchConversation = (id: string) => { - setCurrentConversationId(id); - setIsHistoryOpen(true); - setActivePage('query'); - }; - - /** - * 删除对话 - * 从对话列表中移除目标对话,若删除的是当前对话则自动切换到首个对话(无则创建新对话) - * @param deleteId - 待删除对话ID - */ - const handleDeleteConversation = (deleteId: string) => { - const updatedConversations = conversations.filter(conv => conv.id !== deleteId); - - // 若删除的是当前激活的对话 - if (currentConversationId === deleteId) { - if (updatedConversations.length > 0) { - // 切换到剩余对话的首个 - setCurrentConversationId(updatedConversations[0].id); - } else { - // 无剩余对话时创建新对话 - const newConv: Conversation = { - id: 'conv-' + Date.now(), - title: '新对话', - messages: [{ - role: 'ai', - content: '您好!我是数据查询助手,您可以通过自然语言描述您的查询需求...' - }], - createTime: new Date().toISOString(), - }; - updatedConversations.unshift(newConv); - setCurrentConversationId(newConv.id); - } - } - - setConversations(updatedConversations); - }; - - /** - * 保存查询结果 - * 将查询结果添加到收藏夹,避免重复保存 - * @param query - 待保存的查询结果数据 - */ - const handleSaveQuery = (query: QueryResultData) => { - setSavedQueries(prev => { - if (prev.some(q => q.id === query.id)) return prev; - return [query, ...prev]; - }); - }; - - /** - * 删除收藏的查询结果 - * 从收藏夹中移除目标查询 - * @param id - 待删除查询的ID - */ - const handleDeleteSavedQuery = (id: string) => { - setSavedQueries(prev => prev.filter(q => q.id !== id)); - }; - - /** - * 在对话中查看收藏的查询 - * 匹配对应的对话,存在则切换到该对话,不存在则显示提示弹窗 - * @param conversationId - 收藏查询关联的对话ID - */ - const handleViewInChat = (conversationId: string) => { - const convExists = conversations.some(c => c.id === conversationId); - if (convExists) { - setCurrentConversationId(conversationId); - // 切换到query页面(区分角色) - if (userRole === 'data-admin') { - setActiveDataAdminPage('query'); - } else { - setActivePage('query'); - } - } else { - setNotFoundModalOpen(true); - } - }; - - /** - * 重新运行查询 - * 创建新对话并带入原始查询提示语 - * @param prompt - 原始查询提示语 - */ - const handleRerunQuery = (prompt: string) => { - handleNewConversation(); - setInitialPrompt(prompt); - }; - - - /** - * 对比两个查询结果 - * 按查询时间排序(旧在前,新在后),打开对比弹窗 - * @param queryId1 - 第一个查询ID - * @param queryId2 - 第二个查询ID - */ - const handleCompareQueries = (queryId1: string, queryId2: string) => { - const query1 = savedQueries.find(q => q.id === queryId1); - const query2 = savedQueries.find(q => q.id === queryId2); - if (!query1 || !query2) return; - - // 按查询时间排序,确保旧查询在前 - const date1 = new Date(query1.queryTime); - const date2 = new Date(query2.queryTime); - const oldQuery = date1 < date2 ? query1 : query2; - const newQuery = date1 < date2 ? query2 : query1; - - //设置弹窗数据并打开弹窗 - setCompareQueries({ oldQuery, newQuery }); - setIsCompareModalOpen(true); -}; - - /** - * 分享查询结果给好友 - * 创建分享记录并生成对应通知 - * @param queryId - 待分享的查询ID - * @param friendId - 接收分享的好友ID - */ - const handleShareQuery = (queryId: string, friendId: string) => { - const queryToShare = savedQueries.find(q => q.id === queryId); - const friend = MOCK_FRIENDS_LIST.find(f => f.id === friendId); - - if (queryToShare && friend) { - // 创建分享记录 - const newShare: QueryShare = { - id: `share-${Date.now()}`, - sender: { ...MOCK_USER_PROFILE, isOnline: true }, // 发送者为当前用户 - recipientId: friend.id, // 接收者为选中的好友 - querySnapshot: queryToShare, // 分享的查询快照 - timestamp: new Date().toISOString(), - status: 'unread', // 初始状态为未读 - }; - setQueryShares(prev => [newShare, ...prev]); - - // 生成分享通知(模拟) - const newNotification: Notification = { - id: `notif-${Date.now()}`, - type: 'share', - title: `${MOCK_USER_PROFILE.name} 分享了一个查询给你`, - content: `"${queryToShare.userPrompt}"`, - timestamp: new Date().toISOString(), - isRead: false, - isPinned: false, - fromUser: { name: MOCK_USER_PROFILE.name, avatarUrl: MOCK_USER_PROFILE.avatarUrl }, - relatedShareId: newShare.id, - }; - setNotifications(prev => [newNotification, ...prev]); - } - }; - - /** - * 标记分享记录为已读 - * 更新目标分享记录的状态 - * @param shareId - 待标记的分享ID - */ - const handleMarkShareAsRead = (shareId: string) => { - setQueryShares(prev => prev.map(s => s.id === shareId ? { ...s, status: 'read' } : s)); - }; - - /** - * 删除分享记录 - * 从分享列表中移除目标记录 - * @param shareId - 待删除的分享ID - */ - const handleDeleteShare = (shareId: string) => { - setQueryShares(prev => prev.filter(s => s.id !== shareId)); - }; - - /** - * 点击通知图标处理 - * 切换到通知中心页面(区分角色) - */ - const handleNotificationClick = () => { - if (userRole === 'normal-user') { - setActivePage('notifications'); - } else if (userRole === 'data-admin') { - setActiveDataAdminPage('notifications'); - } - }; - - /** - * 点击头像处理 - * 切换到个人中心页面(区分角色) - */ - const handleAvatarClick = () => { - if (userRole === 'normal-user') { - setActivePage('account'); - } else if (userRole === 'data-admin') { - setActiveDataAdminPage('account'); - } else if (userRole === 'sys-admin') { - setActiveSysAdminPage('account'); - } - }; - - // ===== 页面渲染函数 ===== - /** - * 普通用户页面渲染 - * 根据当前激活页面,渲染对应的功能页面 - * @param page - 普通用户当前激活页面 - * @returns 对应的功能页面组件 - */ - const renderNormalUserPage = (page: Page) => { - switch (page) { - case 'query': - return ( - setIsHistoryOpen(!isHistoryOpen)} - isHistoryOpen={isHistoryOpen} - onAddMessage={handleAddMessage} - onSaveQuery={handleSaveQuery} - onShareQuery={handleShareQuery} - savedQueries={savedQueries} - initialPrompt={initialPrompt} - onClearInitialPrompt={() => setInitialPrompt(undefined)} - conversations={conversations} - currentConversationId={currentConversationId || ''} - onSwitchConversation={handleSwitchConversation} - onNewConversation={handleNewConversation} - onDeleteConversation={handleDeleteConversation} - /> - ); - case 'history': - return ( - - ); - case 'notifications': - return ; - case 'account': - return ; - case 'friends': - return ( - - ); - default: - return
    Page not found
    ; - } - }; - - // ===== 角色路由渲染 ===== - // 未登录状态 - 渲染登录页面 - if (!userRole) { - return ; - } - - // 系统管理员 - 渲染系统管理页面 - if (userRole === 'sys-admin') { - return ( -
    - -
    - !n.isRead).length} - notifications={notifications} - onNotificationClick={handleNotificationClick} - onAvatarClick={handleAvatarClick} - currentConversationName={headerTitle} - /> - -
    -
    - ); - } - - // 数据管理员 - 渲染数据管理页面 - if (userRole === 'data-admin') { - return ( - <> -
    - -
    - !n.isRead).length} - notifications={notifications} - onNotificationClick={handleNotificationClick} - showHistoryToggle={activeDataAdminPage === 'query'} // 仅query页面显示历史面板切换按钮 - onToggleHistory={() => setIsHistoryOpen(!isHistoryOpen)} - onAvatarClick={handleAvatarClick} - onNewConversation={handleNewConversation} - currentConversationName={headerTitle} - /> - setIsHistoryOpen(!isHistoryOpen)} - isHistoryOpen={isHistoryOpen} - onAddMessage={handleAddMessage} - onSaveQuery={handleSaveQuery} - onShareQuery={handleShareQuery} - savedQueries={savedQueries} - initialPrompt={initialPrompt} - onClearInitialPrompt={() => setInitialPrompt(undefined)} - conversations={conversations} - currentConversationId={currentConversationId || ''} - onSwitchConversation={handleSwitchConversation} - onNewConversation={handleNewConversation} - onDelete={handleDeleteSavedQuery} - onViewInChat={handleViewInChat} - onRerun={handleRerunQuery} - onCompare={handleCompareQueries} - shares={queryShares} - onMarkShareAsRead={handleMarkShareAsRead} - onDeleteShare={handleDeleteShare} - /> -
    -
    - - {/* 对话不存在提示弹窗 */} - setNotFoundModalOpen(false)} - title="操作提示" - > -
    -
    - -
    -

    对话记录不存在或已被删除。

    -

    这可能是由于它已被手动删除或系统自动清理。

    -
    - -
    -
    -
    - {/* 2. 对比弹窗(全局浮层,独立于其他弹窗!) */} - {compareQueries && ( - { - setIsCompareModalOpen(false); - setCompareQueries(null); - }} - oldQuery={compareQueries.oldQuery} - newQuery={compareQueries.newQuery} - /> - )} - - ); - } - - // 普通用户 - 渲染基础功能页面 - return ( - <> -
    - -
    - !n.isRead).length} - notifications={notifications} - onNotificationClick={handleNotificationClick} - showHistoryToggle={activePage === 'query'} // 仅query页面显示历史面板切换按钮 - onToggleHistory={() => setIsHistoryOpen(!isHistoryOpen)} - onAvatarClick={handleAvatarClick} - onNewConversation={handleNewConversation} - currentConversationName={headerTitle} - /> - {renderNormalUserPage(activePage)} -
    -
    - - {/* 对话不存在提示弹窗 */} - setNotFoundModalOpen(false)} - title="操作提示" - > -
    -
    - -
    -

    对话记录不存在或已被删除。

    -

    这可能是由于它已被手动删除或系统自动清理。

    -
    - -
    -
    -
    - {/* 2. 对比弹窗(全局浮层,独立于其他弹窗!) */} - {compareQueries && ( - { - setIsCompareModalOpen(false); - setCompareQueries(null); - }} - oldQuery={compareQueries.oldQuery} - newQuery={compareQueries.newQuery} - /> - )} - - ); -}; - -export default App; \ No newline at end of file diff --git a/src/springboot_demo/frontend/README.md b/src/springboot_demo/frontend/README.md deleted file mode 100644 index 94bfbe13..00000000 --- a/src/springboot_demo/frontend/README.md +++ /dev/null @@ -1,20 +0,0 @@ -
    -GHBanner -
    - -# Run and deploy your AI Studio app - -This contains everything you need to run your app locally. - -View your app in AI Studio: https://ai.studio/apps/drive/1l5dsPVkxm5nl4yN-ckKSgRGs0nlklvz9 - -## Run Locally - -**Prerequisites:** Node.js - - -1. Install dependencies: - `npm install` -2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key -3. Run the app: - `npm run dev` diff --git a/src/springboot_demo/frontend/components/AccountPage.tsx b/src/springboot_demo/frontend/components/AccountPage.tsx deleted file mode 100644 index f640d37a..00000000 --- a/src/springboot_demo/frontend/components/AccountPage.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import React, { useState, useRef } from 'react'; -import { MOCK_USER_PROFILE } from '../constants'; -import { UserProfile } from '../types'; -import { Modal } from './Modal'; - -const InfoField: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => ( -
    -
    {label}
    -
    {value}
    -
    -); - -const maskPhoneNumber = (phone: string) => { - if (phone.length === 11) { - return `${phone.substring(0, 3)}****${phone.substring(7)}`; - } - return phone; -}; - -export const AccountPage: React.FC = () => { - const [user, setUser] = useState(MOCK_USER_PROFILE); - const [isEditModalOpen, setEditModalOpen] = useState(false); - const [isPasswordModalOpen, setPasswordModalOpen] = useState(false); - const [isPhoneModalOpen, setPhoneModalOpen] = useState(false); - const [editForm, setEditForm] = useState({ name: '', email: '', phoneNumber: '' }); - const [avatarPreview, setAvatarPreview] = useState(user.avatarUrl); - const fileInputRef = useRef(null); - - const handleOpenEditModal = () => { - setEditForm({ - name: user.name, - email: user.email, - phoneNumber: user.phoneNumber, - }); - setEditModalOpen(true); - }; - - const handleSaveProfile = () => { - setUser(prev => ({ - ...prev, - name: editForm.name, - email: editForm.email, - phoneNumber: editForm.phoneNumber, - avatarUrl: avatarPreview - })); - setEditModalOpen(false); - }; - - const handleAvatarChange = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onloadend = () => { - setAvatarPreview(reader.result as string); - }; - reader.readAsDataURL(file); - } - }; - - return ( -
    -
    -
    -
    - {/* Personal Info Card */} -
    -
    - {/* Left side: Avatar */} -
    - User Avatar - - -
    - - {/* Right side: Info */} -
    -
    -

    个人基本信息

    -
    -
    - - - - - - - {user.accountStatus === 'normal' ? '正常' : '禁用'} - - } - /> -
    -
    - -
    -
    -
    -
    - - {/* Security Settings Card */} -
    -

    安全设置

    -
    -
    -
    -

    修改密码

    -

    上次修改时间:2025-05-10

    -
    - -
    -
    -
    -

    绑定手机

    -

    已绑定:{maskPhoneNumber(user.phoneNumber)}

    -
    - -
    -
    -
    -
    - - {/* Edit Profile Modal */} - setEditModalOpen(false)} title="编辑个人信息"> -
    -
    - - setEditForm(prev => ({...prev, name: e.target.value}))} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary/30 focus:border-primary" - /> -
    -
    - - setEditForm(prev => ({...prev, email: e.target.value}))} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary/30 focus:border-primary" - /> -
    -
    - - setEditForm(prev => ({...prev, phoneNumber: e.target.value}))} - className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary/30 focus:border-primary" - /> -
    -
    -
    - - -
    -
    - - {/* Password Change Modal */} - setPasswordModalOpen(false)} title="修改密码"> -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - -
    -
    - - {/* Phone Change Modal */} - setPhoneModalOpen(false)} title="更换绑定手机"> -
    -
    - - -
    -
    - -
    - - -
    -
    -
    -
    - -
    -
    - -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/ChatMessage.tsx b/src/springboot_demo/frontend/components/ChatMessage.tsx deleted file mode 100644 index e3109870..00000000 --- a/src/springboot_demo/frontend/components/ChatMessage.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { Message, QueryResultData } from '../types'; -import { QueryResult } from './QueryResult'; - -interface ChatMessageProps { - message: Message; - onSaveQuery: (query: QueryResultData) => void; - onShareQuery: (queryId: string, friendId: string) => void; - savedQueries: QueryResultData[]; -} - -export const ChatMessage: React.FC = ({ message, onSaveQuery, onShareQuery, savedQueries }) => { - const { role, content } = message; - - const isUser = role === 'user'; - const wrapperClass = `flex items-start gap-3 ${isUser ? 'flex-row-reverse' : ''}`; - const avatarClass = `w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 text-lg ${isUser ? 'bg-primary text-white' : 'bg-primary/10 text-primary'}`; - const iconClass = `fa ${isUser ? 'fa-user' : 'fa-robot'}`; - const bubbleClass = `p-4 shadow-sm max-w-[85%] md:max-w-[80%] text-sm rounded-lg ${isUser ? 'bg-primary text-white rounded-tr-none' : 'bg-white border border-gray-200 rounded-tl-none'}`; - - return ( -
    -
    - -
    -
    - {typeof content === 'string' ? ( -

    {content}

    - ) : ( - - )} -
    -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/ChatModal.tsx b/src/springboot_demo/frontend/components/ChatModal.tsx deleted file mode 100644 index 5731f9c0..00000000 --- a/src/springboot_demo/frontend/components/ChatModal.tsx +++ /dev/null @@ -1,384 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { Modal } from './Modal'; -import { Friend, QueryResultData } from '../types'; - -// 聊天消息接口定义:描述单条聊天消息的结构规范 -interface Message { - id: string; // 消息唯一标识(时间戳+随机字符串生成) - content: string; // 消息文本内容 - isSent: boolean; // 发送者标识(true:当前用户发送;false:好友发送) - timestamp: Date; // 消息发送时间 - isRead: boolean; // 消息已读状态(true:已读;false:未读) -} - -// 聊天模态框属性接口:定义组件接收的外部参数 -interface ChatModalProps { - isOpen: boolean; // 控制聊天弹窗显示/隐藏 - onClose: () => void; // 弹窗关闭回调函数 - friend: Friend; // 聊天对象(好友)的基础信息 - savedQueries: QueryResultData[]; // 可分享的查询记录列表(来自收藏夹) - currentUnreadCount: number; // 当前好友的未读消息数 - updateUnreadCount: (friendId: string, count: number) => void; // 更新未读消息数的回调 - messages: Message[]; // 聊天记录列表(从父组件传入,双向绑定) - updateMessages: (newMessages: Message[]) => void; // 更新聊天记录的回调(通知父组件同步) -} - -export const ChatModal: React.FC = ({ - isOpen, onClose, friend, savedQueries, - currentUnreadCount, updateUnreadCount, - messages, - updateMessages -}) => { - // 消息输入框内容状态:绑定输入框的值 - const [inputText, setInputText] = useState(''); - // 消息展示容器的DOM引用:核心用于滚动控制(定位到消息底部) - const messagesContainerRef = useRef(null); - // 输入框的DOM引用:用于手动控制输入框聚焦 - const inputRef = useRef(null); - // 分享查询弹窗的显示状态:false=关闭,true=打开 - const [isShareModalOpen, setShareModalOpen] = useState(false); - // 选中待分享的查询记录ID:null=未选中,存储选中查询的唯一标识 - const [selectedQueryId, setSelectedQueryId] = useState(null); - - // 模拟好友回复的随机文本池:避免回复内容重复,提升交互自然度 - const randomReplies = [ - '收到,我看看~', - '好的,我研究一下', - '这个问题我得仔细瞧瞧', - '明白,我马上处理', - '哦,这个我知道怎么弄', - '稍等,我查一下资料', - ]; - - // ===== 终极修复:每次打开弹窗都滚到底(不管关闭多少次)===== - useEffect(() => { - // 只在弹窗打开时执行(isOpen为true) - if (isOpen) { - // 延迟200ms:给Modal动画+DOM重新渲染留足时间(关闭再打开时DOM要重新生成) - const scrollTimer = setTimeout(() => { - // 用requestAnimationFrame确保DOM完全更新后再滚动(解决scrollHeight计算不准确的问题) - requestAnimationFrame(() => { - if (messagesContainerRef.current) { - const container = messagesContainerRef.current; - // 强制滚到底部(和HTML的window.scrollTo逻辑完全一致) - container.scrollTop = container.scrollHeight; - // 双重保险:如果第一次没滚到(距离底部超过10px),再补一次滚动 - if (Math.abs(container.scrollHeight - (container.scrollTop + container.clientHeight)) > 10) { - container.scrollTop = container.scrollHeight; - } - } - }); - }, 200); - - // 弹窗关闭时清除定时器:避免定时器残留导致无效滚动或内存泄漏 - return () => clearTimeout(scrollTimer); - } - }, [isOpen]); // 只依赖isOpen状态:确保每次打开弹窗都触发滚动,不受其他状态影响 - - // ===== 其他初始化逻辑(不变)===== - useEffect(() => { - if (isOpen) { - // 1. 打开弹窗时,清空当前好友的未读消息数(通知父组件更新) - updateUnreadCount(friend.id, 0); - // 2. 标记好友发送的未读消息为"已读"(遍历消息列表,修改未读状态后同步给父组件) - const updatedMessages = messages.map(msg => - !msg.isSent && !msg.isRead ? { ...msg, isRead: true } : msg - ); - updateMessages(updatedMessages); - - // 延迟200ms聚焦输入框:和滚动延迟同步,确保DOM渲染完成后再聚焦 - setTimeout(() => inputRef.current?.focus(), 200); - } else { - // 关闭弹窗时,清空输入框内容(避免下次打开残留上次输入) - setInputText(''); - } - }, [isOpen, friend.id, updateUnreadCount, messages, updateMessages]); - - // ===== 发送消息(不变,保留滚动)===== - const handleSendMessage = () => { - // 1. 获取输入框内容(去首尾空格,避免发送空消息) - const currentValue = inputRef.current?.value.trim() || ''; - if (!currentValue) return; // 内容为空则不执行发送逻辑 - - // 2. 生成当前用户发送的新消息(唯一ID由时间戳+随机字符串组成,确保不重复) - const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - const newMessage: Message = { - id: uniqueId, - content: currentValue, - isSent: true, - timestamp: new Date(), - isRead: false, - }; - - // 3. 更新消息列表,拼接新消息后通知父组件同步 - const updatedMessages = [...messages, newMessage]; - updateMessages(updatedMessages); - - // 4. 清空输入框(同时更新状态和DOM,适配中文输入法等特殊场景) - setInputText(''); - if (inputRef.current) inputRef.current.value = ''; - - // 发送后自动滚到底部:确保用户能看到自己发送的最新消息 - requestAnimationFrame(() => { - if (messagesContainerRef.current) { - messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; - } - }); - - // 模拟好友1秒后回复消息(延迟1000ms,模拟实时聊天交互) - setTimeout(() => { - // 1. 先标记用户发送的消息为"已读"(模拟好友已查看) - const messagesWithRead = updatedMessages.map(msg => - msg.isSent && !msg.isRead ? { ...msg, isRead: true } : msg - ); - // 2. 生成好友回复消息(随机选择回复文本,生成唯一ID) - const replyId = `${Date.now()}-reply-${Math.random().toString(36).slice(2, 8)}`; - const randomReply = randomReplies[Math.floor(Math.random() * randomReplies.length)]; - const replyMessage: Message = { - id: replyId, - content: randomReply, - isSent: false, - timestamp: new Date(), - isRead: false, - }; - // 3. 一次更新:同时包含"已读标记"和"好友回复",同步给父组件 - const finalMessages = [...messagesWithRead, replyMessage]; - updateMessages(finalMessages); - - // 回复后自动滚到底部:确保用户能看到好友的最新回复 - requestAnimationFrame(() => { - if (messagesContainerRef.current) { - messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; - } - }); - }, 1000); - }; - - // ===== 事件:打开"分享查询"弹窗 ===== - const handleOpenShareModal = () => { - setSelectedQueryId(null); // 重置选中状态(避免残留上次选中的查询ID) - setShareModalOpen(true); // 显示分享查询弹窗 - }; - - // ===== 事件:确认分享查询给好友 ===== - const handleConfirmShare = () => { - // 1. 校验:未选择查询记录时,不执行分享逻辑 - if (!selectedQueryId) return; - // 根据选中的ID找到对应的查询记录 - const selectedQuery = savedQueries.find(q => q.id === selectedQueryId); - if (!selectedQuery) return; - - // 2. 生成"分享查询"的消息(作为一条聊天消息发送) - const shareId = `${Date.now()}-share-${Math.random().toString(36).slice(2, 8)}`; - const shareMessage: Message = { - id: shareId, - content: `分享了查询:${selectedQuery.userPrompt}`, - isSent: true, - timestamp: new Date(), - isRead: false, - }; - - // 3. 更新消息列表,添加分享消息后通知父组件同步 - const updatedMessages = [...messages, shareMessage]; - updateMessages(updatedMessages); - - // 4. 关闭分享弹窗,清空输入框 - setInputText(''); - setShareModalOpen(false); - - // 分享后自动滚到底部:确保分享消息显示在最新位置 - requestAnimationFrame(() => { - if (messagesContainerRef.current) { - messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; - } - }); - - // 模拟好友1.5秒后回复分享内容(延迟1500ms,适配分享场景的交互节奏) - setTimeout(() => { - // 1. 先标记分享消息为"已读"(模拟好友已查看分享) - const messagesWithRead = updatedMessages.map(msg => - msg.isSent && !msg.isRead ? { ...msg, isRead: true } : msg - ); - // 2. 生成好友回复消息(替换关键词,让回复更贴合分享场景) - const replyShareId = `${Date.now()}-share-reply-${Math.random().toString(36).slice(2, 8)}`; - const randomShareReply = randomReplies[Math.floor(Math.random() * randomReplies.length)].replace('看看', '研究'); - const replyMessage: Message = { - id: replyShareId, - content: randomShareReply || '我看到了,这个查询很有用!', - isSent: false, - timestamp: new Date(), - isRead: false, - }; - // 3. 一次更新:同时包含"已读标记"和"好友回复",同步给父组件 - const finalMessages = [...messagesWithRead, replyMessage]; - updateMessages(finalMessages); - - // 回复后自动滚到底部:确保好友的回复显示在最新位置 - requestAnimationFrame(() => { - if (messagesContainerRef.current) { - messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; - } - }); - }, 1500); - }; - - // ===== 事件:回车键发送消息 ===== - const handleKeyDown = (e: React.KeyboardEvent) => { - // 监听回车键,触发发送消息逻辑 - if (e.key === 'Enter') { - e.preventDefault(); // 阻止输入框默认的换行行为(单行输入框无需换行) - handleSendMessage(); // 调用发送消息函数 - } - }; - - // ===== 渲染(不变)===== - return ( - <> - {/* 1. 聊天主弹窗:展示聊天记录、输入框、分享按钮 */} - -
    - {/* 消息展示区域:可滚动,自动定位到底部 */} -
    - {/* 渲染所有聊天消息:遍历messages数组,每条消息对应一个DOM节点 */} - {messages.map((message) => ( -
    - {/* 头像:自己用默认头像,好友用其专属头像(无则用默认) */} - {message.isSent - - {/* 消息内容+时间戳容器:对齐方式与消息一致 */} -
    - {/* 消息气泡:区分自己和好友的样式(颜色、圆角、边框) */} -
    -

    {message.content}

    -
    - - {/* 时间戳+已读状态:小字体弱化显示,区分自己和好友的文本颜色 */} -
    - {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} - {message.isRead ? '已读' : '未读'} -
    -
    -
    - ))} -
    - - {/* 消息输入区域:包含分享按钮、输入框、发送按钮 */} -
    -
    - {/* 分享查询按钮:点击打开分享弹窗 */} - - {/* 消息输入框:绑定输入状态,支持回车发送 */} - setInputText(e.target.value.trimStart())} - onKeyDown={handleKeyDown} - className="flex-1 px-4 py-3 border border-gray-300 rounded-full focus:ring-2 focus:ring-primary/30 outline-none" - readOnly={false} - /> - {/* 发送按钮:空消息时禁用,点击发送消息 */} - -
    -
    -
    -
    - - {/* 2. 分享查询弹窗:选择要分享的查询记录 */} - setShareModalOpen(false)} title="分享查询结果"> -
    -

    从您的历史记录中选择一个查询结果分享给 {friend.name}。

    - {/* 可分享查询列表:滚动展示,单选模式 */} -
    - {savedQueries.length > 0 ? ( - savedQueries.map(q => ( - - )) - ) : ( -

    没有收藏的查询记录

    - )} -
    -
    - {/* 分享弹窗底部按钮:取消/确认分享 */} -
    - - -
    -
    - - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/ComparisonModal.tsx b/src/springboot_demo/frontend/components/ComparisonModal.tsx deleted file mode 100644 index 0bc5e5d3..00000000 --- a/src/springboot_demo/frontend/components/ComparisonModal.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { Chart as ChartJS, registerables } from 'chart.js'; -import { Chart } from 'react-chartjs-2'; -import { QueryResultData } from '../types'; -import { Modal } from './Modal'; // 引入你的Modal组件(确保路径正确) - -// 注册ChartJS所需的所有组件 -ChartJS.register(...registerables); - -// 表格行差异结果类型定义:包含新增、删除、修改和共同的行数据 -type DiffResult = { - added: string[][]; // 新增的行 - deleted: string[][]; // 删除的行 - modified: { oldRow: string[], newRow: string[] }[]; // 修改的行(包含旧行和新行) - common: string[][]; // 未变化的共同行 -}; - -// 查找两个表格数据集之间行差异的辅助函数 -// 参数:旧行数据、新行数据;返回:差异结果对象 -const findRowChanges = (oldRows: string[][], newRows: string[][]): DiffResult => { - // 以每行第一个元素为键,构建旧行和新行的映射表(便于快速查找) - const oldMap = new Map(oldRows.map(row => [row[0], row])); - const newMap = new Map(newRows.map(row => [row[0], row])); - - // 初始化差异结果对象 - const result: DiffResult = { added: [], deleted: [], modified: [], common: [] }; - - // 遍历旧行映射,检查每行在新行中的状态(删除/修改/共同) - oldMap.forEach((oldRow, key) => { - if (!newMap.has(key)) { - result.deleted.push(oldRow); // 新行中无此键:标记为删除 - } else { - const newRow = newMap.get(key)!; - // 行数据字符串化后不等:标记为修改 - if (JSON.stringify(oldRow) !== JSON.stringify(newRow)) { - result.modified.push({ oldRow, newRow }); - } else { - result.common.push(oldRow); // 行数据相同:标记为共同 - } - } - }); - - // 遍历新行映射,检查新增的行(旧行中无此键) - newMap.forEach((newRow, key) => { - if (!oldMap.has(key)) { - result.added.push(newRow); - } - }); - - return result; -}; - -// 表格对比组件:展示两个查询结果的表格差异(新增/删除/修改行) -// 参数:旧查询结果、新查询结果、差异结果 -const TableComparison: React.FC<{ oldQuery: QueryResultData; newQuery: QueryResultData, changes: DiffResult }> = ({ oldQuery, newQuery, changes }) => { - // 渲染单元格内容:若新旧单元格内容不同,高亮显示新内容并标注旧内容 - const renderCell = (oldCell: string, newCell: string) => { - if (oldCell !== newCell) { - return ( - - {newCell} {oldCell} - - ); - } - return newCell; - }; - - return ( -
    - {/* 旧数据表格 */} -
    -

    旧数据 ({new Date(oldQuery.queryTime).toLocaleString()})

    -
    - - {oldQuery.tableData.headers.map(h => )} - - {/* 显示删除的行(红色背景+删除线) */} - {changes.deleted.map((row, i) => )} - {/* 显示修改的旧行 */} - {changes.modified.map(({ oldRow }, i) => ( - - {oldRow.map((cell, j) => )} - - ))} - {/* 显示未变化的共同行 */} - {changes.common.map((row, i) => {row.map((cell, j) => )})} - -
    {h}
    {row.join(' | ')}
    {cell}
    {cell}
    -
    -
    - - {/* 新数据表格 */} -
    -

    新数据 ({new Date(newQuery.queryTime).toLocaleString()})

    -
    - - {newQuery.tableData.headers.map(h => )} - - {/* 显示新增的行(绿色背景) */} - {changes.added.map((row, i) => {row.map((cell, j) => )})} - {/* 显示修改的新行(高亮变化的单元格) */} - {changes.modified.map(({ oldRow, newRow }, i) => ( - - {newRow.map((cell, j) => )} - - ))} - {/* 显示未变化的共同行 */} - {changes.common.map((row, i) => {row.map((cell, j) => )})} - -
    {h}
    {cell}
    {renderCell(oldRow[j], cell)}
    {cell}
    -
    -
    -
    - ); -}; - -// 图表对比组件:在同一图表中展示新旧查询结果的图表数据 -// 参数:旧查询结果、新查询结果 -const ChartComparison: React.FC<{ oldQuery: QueryResultData; newQuery: QueryResultData }> = ({ oldQuery, newQuery }) => { - // 构建图表数据:合并新旧标签,分别配置新旧数据集样式 - const chartData = { - labels: [...new Set([...oldQuery.chartData.labels, ...newQuery.chartData.labels])], // 合并去重标签 - datasets: [ - { - ...oldQuery.chartData.datasets[0], - label: `${oldQuery.chartData.datasets[0].label} (旧)`, - backgroundColor: 'rgba(22, 93, 255, 0.3)', - borderColor: 'rgba(22, 93, 255, 0.5)', - borderDash: [5, 5], // 旧数据用虚线 - }, - { - ...newQuery.chartData.datasets[0], - label: `${newQuery.chartData.datasets[0].label} (新)`, - backgroundColor: 'rgba(54, 162, 235, 0.6)', - borderColor: 'rgba(54, 162, 235, 1)', // 新数据用实线 - } - ], - }; - - return ( -
    -
    - -
    -
    - ); -}; - -// 弹窗式对比组件属性接口:新增弹窗控制参数,保留原有对比参数 -interface ComparisonModalProps { - oldQuery: QueryResultData; - newQuery: QueryResultData; - isOpen: boolean; // 控制弹窗显示/隐藏 - onClose: () => void; // 弹窗关闭回调 -} - -// 弹窗式对比组件:用Modal包裹原有对比内容,适配弹窗形态 -export const ComparisonModal: React.FC = ({ oldQuery, newQuery, isOpen, onClose }) => { - // 视图切换状态:表格视图/图表视图 - const [activeView, setActiveView] = useState<'table' | 'chart'>('table'); - // 计算表格行差异(使用useMemo避免重复计算) - const changes = useMemo(() => findRowChanges(oldQuery.tableData.rows, newQuery.tableData.rows), [oldQuery, newQuery]); - - // 对比摘要统计项:新增/删除/变更行数量 - const summaryItems = [ - { icon: 'fa-plus-circle', color: 'text-green-500', value: changes.added.length, label: '新增行' }, - { icon: 'fa-minus-circle', color: 'text-red-500', value: changes.deleted.length, label: '删除行' }, - { icon: 'fa-pencil-square-o', color: 'text-yellow-500', value: changes.modified.length, label: '变更行' }, - ]; - - return ( - - {/* 弹窗内容区域:限制高度+滚动 */} -
    - {/* 对比说明 */} -
    -

    对比查询 "{oldQuery.userPrompt}" 的两个不同版本。

    -
    - - {/* 对比摘要卡片 */} -
    -

    对比摘要

    -
    - {summaryItems.map(item => ( -
    - -
    -

    {item.value}

    -

    {item.label}

    -
    -
    - ))} -
    -
    - - {/* 对比视图切换区域 */} -
    -
    -
    - - -
    -
    - {/* 根据当前视图状态渲染对应的对比组件 */} - {activeView === 'table' - ? - : - } -
    -
    -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/DataAdminPage.tsx b/src/springboot_demo/frontend/components/DataAdminPage.tsx deleted file mode 100644 index 95706be8..00000000 --- a/src/springboot_demo/frontend/components/DataAdminPage.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from 'react'; -import { DataAdminPageType, Page, Conversation, QueryResultData, MessageRole, QueryShare } from '../types'; -import { DataSourceManagementPage } from './data-admin/DataSourceManagementPage'; -import { UserPermissionPage } from './data-admin/UserPermissionPage'; -import { ConnectionLogPage } from './data-admin/ConnectionLogPage'; -import { QueryPage } from './QueryPage'; -import { HistoryPage } from './HistoryPage'; -import { AccountPage } from './AccountPage'; -import { FriendsPage } from './FriendsPage'; -import { NotificationsPage } from './NotificationsPage'; -import { DataAdminNotificationPage } from './data-admin/DataAdminNotificationPage'; -import { ComparisonModal } from './ComparisonModal'; -import { DataAdminDashboardPage } from './data-admin/DataAdminDashboardPage'; - -interface DataAdminPageProps { - activePage: DataAdminPageType; - setActivePage: (page: DataAdminPageType) => void; - // Props for QueryPage and others - currentConversation: Conversation | undefined; - onToggleHistory: () => void; - isHistoryOpen: boolean; - onAddMessage: (role: MessageRole, content: string | QueryResultData) => void; - onSaveQuery: (query: QueryResultData) => void; - onShareQuery: (queryId: string, friendId: string) => void; - savedQueries: QueryResultData[]; - initialPrompt?: string; - onClearInitialPrompt: () => void; - // Props for HistoryPage - conversations: Conversation[]; - currentConversationId: string; - onSwitchConversation: (id: string) => void; - onNewConversation: () => void; - onDelete: (id: string) => void; - onViewInChat: (conversationId: string) => void; - onRerun: (prompt: string) => void; - onCompare: (queryId1: string, queryId2: string) => void; - // Props for FriendsPage - shares: QueryShare[]; - onMarkShareAsRead: (shareId: string) => void; - onDeleteShare: (shareId: string) => void; - onDeleteConversation: (id: string) => void; -} - -export const DataAdminPage: React.FC = (props) => { - const { activePage } = props; - - // Duplicated from App.tsx to render shared pages - const renderNormalUserPage = (page: Page) => { - switch (page) { - case 'query': - return ( - - ); - case 'history': - return ( - - ); - case 'notifications': - return ; - case 'account': - return ; - case 'friends': - return ( - - ); - default: - // This case handles 'comparison', which is rendered by the parent switch - return
    Page not found
    ; - } - }; - - switch (activePage) { - // Admin specific pages - case 'dashboard': - return ; - case 'datasource': - return ; - case 'user-permission': - return ; - case 'notification-management': - return ; - case 'connection-log': - return ; - case 'query': - case 'history': - case 'notifications': - case 'account': - case 'friends': - return renderNormalUserPage(activePage); - default: - const exhaustiveCheck: never = activePage; - return
    Page not found
    ; - } -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/DataAdminSidebar.tsx b/src/springboot_demo/frontend/components/DataAdminSidebar.tsx deleted file mode 100644 index ea2a521d..00000000 --- a/src/springboot_demo/frontend/components/DataAdminSidebar.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import { DataAdminPageType } from '../types'; - -interface SidebarItemProps { - href: DataAdminPageType; - icon: string; - label: string; - isActive: boolean; - onClick: (page: DataAdminPageType) => void; -} - -const SidebarItem: React.FC = ({ href, icon, label, isActive, onClick }) => { - const activeClass = 'bg-primary/10 text-primary border-l-4 border-primary'; - const inactiveClass = 'text-gray-600 hover:bg-gray-50'; - - return ( -
  • - { - e.preventDefault(); - onClick(href); - }} - > - - {label} - -
  • - ); -}; - -interface SidebarProps { - activePage: DataAdminPageType; - setActivePage: (page: DataAdminPageType) => void; - onLogout: () => void; -} - -export const DataAdminSidebar: React.FC = ({ activePage, setActivePage, onLogout }) => { - const queryItems = [ - { href: 'query', icon: 'fa-search', label: '数据查询' }, - { href: 'history', icon: 'fa-star', label: '收藏夹' }, - ] as const; - - const managementItems = [ - { href: 'dashboard', icon: 'fa-tachometer', label: '仪表盘' }, - { href: 'datasource', icon: 'fa-plug', label: '数据源管理' }, - { href: 'user-permission', icon: 'fa-key', label: '用户权限管理' }, - { href: 'notification-management', icon: 'fa-bullhorn', label: '通知管理' }, - { href: 'connection-log', icon: 'fa-link', label: '连接日志' }, - ] as const; - - const personalItems = [ - { href: 'notifications', icon: 'fa-bell', label: '通知中心' }, - { href: 'account', icon: 'fa-user', label: '账户管理' }, - { href: 'friends', icon: 'fa-users', label: '好友管理' }, - ] as const; - - return ( - - ); -}; diff --git a/src/springboot_demo/frontend/components/Dropdown.tsx b/src/springboot_demo/frontend/components/Dropdown.tsx deleted file mode 100644 index 4611cee1..00000000 --- a/src/springboot_demo/frontend/components/Dropdown.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { ModelOption } from '../types'; - -interface DropdownProps { - options: ModelOption[]; - selected: string; - setSelected: (option: string) => void; - icon: string; -} - -export const Dropdown: React.FC = ({ options, selected, setSelected, icon }) => { - const [isOpen, setIsOpen] = useState(false); - const dropdownRef = useRef(null); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setIsOpen(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - const handleSelect = (option: ModelOption) => { - if (option.disabled) return; - setSelected(option.name); - setIsOpen(false); - }; - - return ( -
    - - {isOpen && ( -
    - {options.map(option => ( - - ))} -
    - )} -
    - ); -}; diff --git a/src/springboot_demo/frontend/components/FriendsPage.tsx b/src/springboot_demo/frontend/components/FriendsPage.tsx deleted file mode 100644 index c77a78a0..00000000 --- a/src/springboot_demo/frontend/components/FriendsPage.tsx +++ /dev/null @@ -1,728 +0,0 @@ -import React, { useState, useMemo, useEffect } from 'react'; -import { MOCK_FRIENDS_LIST, MOCK_FRIEND_REQUESTS } from '../constants'; -import { Friend, FriendRequest, QueryResultData, QueryShare } from '../types'; -import { Modal } from './Modal'; -import { ChatModal } from './ChatModal'; -import { QueryResult } from './QueryResult'; - -// 补充Message接口定义(若types文件已包含可删除) -interface Message { - id: string; - content: string; - isSent: boolean; - timestamp: Date; - isRead: boolean; -} - -// 标签类型定义 -type ActiveTab = 'friends' | 'requests' | 'shares'; - -// 1. 好友卡片组件(新增备注显示+备注/主页按钮) -const FriendCard: React.FC<{ - friend: Friend; - onChat: (friend: Friend) => void; - onDelete: (friend: Friend) => void; - onRemark: (friend: Friend) => void; // 新增:触发备注弹窗 - onViewProfile: (friend: Friend) => void; // 新增:触发查看主页 - unreadCount: number; -}> = ({ friend, onChat, onDelete, onRemark, onViewProfile, unreadCount }) => ( -
    -
    -
    - {friend.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} - /> - {/* 在线状态标记 */} - {friend.isOnline && ( - - )} -
    -
    -
    - {/* 有备注显示备注,无备注显示原名(作为主要标识) */} -

    - {friend.remark || friend.name} -

    - {/* 未读消息提示 */} - {unreadCount > 0 && ( - - {unreadCount} - - )} -
    - {/* 有备注时才显示原名(作为补充),无备注则不显示 */} - {friend.remark && ( -

    「{friend.name}」

    - )} -
    -
    -
    - {/* 聊天按钮(原有) */} - - {/* 新增:备注按钮 */} - - {/* 新增:访问主页按钮 */} - - {/* 删除好友按钮(原有) */} - -
    -
    -); - -// 2. 好友请求卡片组件(原有功能不变) -const RequestCard: React.FC<{ - request: FriendRequest; - onAccept: (request: FriendRequest) => void; - onReject: (requestId: string) => void; -}> = ({ request, onAccept, onReject }) => ( -
    -
    - {request.fromUser.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} - /> -
    -

    {request.fromUser.name}

    -

    {request.timestamp}

    -
    -
    -
    - - -
    -
    -); - -// 3. 分享记录卡片组件(原有功能不变) -const ShareRecordCard: React.FC<{ - share: QueryShare; - onView: () => void; - onRerun: () => void; - onDelete: () => void; - onSave: () => void; -}> = ({ share, onView, onRerun, onDelete, onSave }) => ( -
    -
    - {share.sender.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} - /> -
    -

    {share.sender.name}分享了一个查询:

    -

    "{share.querySnapshot.userPrompt}"

    -

    {new Date(share.timestamp).toLocaleString()}

    -
    -
    -
    -
    - - - - -
    -
    -
    -); - -// 4. 标签按钮组件(原有功能不变) -const TabButton: React.FC<{ - tab: ActiveTab; - label: string; - count?: number; - activeTab: ActiveTab; - onTabChange: (tab: ActiveTab) => void; -}> = ({ tab, label, count, activeTab, onTabChange }) => ( - -); - -// 页面Props定义(原有功能不变) -interface FriendsPageProps { - savedQueries: QueryResultData[]; - shares: QueryShare[]; - onMarkShareAsRead: (shareId: string) => void; - onDeleteShare: (shareId: string) => void; - onRerunQuery: (prompt: string) => void; - onSaveQuery: (query: QueryResultData) => void; -} - -export const FriendsPage: React.FC = ({ - savedQueries, shares, onMarkShareAsRead, - onDeleteShare, onRerunQuery, onSaveQuery -}) => { - // 页面状态 - const [friends, setFriends] = useState( - // 初始化好友数据:给原有Mock数据添加默认空备注 - MOCK_FRIENDS_LIST.map(friend => ({ ...friend, remark: '' })) - ); - const [requests, setRequests] = useState(MOCK_FRIEND_REQUESTS); - const [isAddFriendModalOpen, setAddFriendModalOpen] = useState(false); - const [chattingWith, setChattingWith] = useState(null); - const [searchTerm, setSearchTerm] = useState(''); - const [friendToDelete, setFriendToDelete] = useState(null); - const [activeTab, setActiveTab] = useState('friends'); - const [viewingShare, setViewingShare] = useState(null); - const [activeModal, setActiveModal] = useState(null); - - // 新增:备注相关状态 - const [isRemarkModalOpen, setIsRemarkModalOpen] = useState(false); - const [currentRemarkFriend, setCurrentRemarkFriend] = useState(null); - const [remarkInput, setRemarkInput] = useState(''); - - // 新增:好友主页相关状态 - const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); - const [currentProfileFriend, setCurrentProfileFriend] = useState(null); - - // 未读消息状态 - const [unreadMessages, setUnreadMessages] = useState>({}); - - // 聊天记录状态 - const [friendMessages, setFriendMessages] = useState>(() => ({ - 'friend-1': [ - { - id: `${Date.now()}-init-1`, - content: '你好!最近在忙什么?', - isSent: false, - timestamp: new Date(Date.now() - 3600000), - isRead: true, - }, - { - id: `${Date.now()}-init-2`, - content: '我在处理一个数据查询,挺复杂的~', - isSent: true, - timestamp: new Date(Date.now() - 3500000), - isRead: false, - } - ], - 'friend-2': [ - { - id: `${Date.now()}-init-3`, - content: '在吗?上次分享的查询结果很有用!', - isSent: false, - timestamp: new Date(Date.now() - 86400000), - isRead: true, - } - ] - })); - - // 初始化未读消息 - useEffect(() => { - setUnreadMessages({ - 'friend-1': 2, - 'friend-2': 1 - }); - }, []); - - // 新增:打开备注弹窗时初始化输入值 - useEffect(() => { - if (currentRemarkFriend) { - setRemarkInput(currentRemarkFriend.remark || ''); - } - }, [currentRemarkFriend]); - - // 更新未读消息数量 - const updateUnreadCount = (friendId: string, count: number) => { - setUnreadMessages(prev => ({ - ...prev, - [friendId]: count - })); - }; - - // 更新聊天记录 - const updateFriendMessages = (friendId: string, newMessages: Message[]) => { - setFriendMessages(prev => ({ - ...prev, - [friendId]: newMessages - })); - }; - - // 打开聊天 - const handleOpenChat = (friend: Friend) => { - setChattingWith(friend); - if (!friendMessages[friend.id]) { - setFriendMessages(prev => ({ - ...prev, - [friend.id]: [ - { - id: `${Date.now()}-init-empty`, - content: `你好!开始和${friend.name}聊天吧~`, - isSent: false, - timestamp: new Date(), - isRead: true, - } - ] - })); - } - }; - - // 模拟新消息(测试用) - const mockNewMessage = (friendId: string) => { - setUnreadMessages(prev => ({ - ...prev, - [friendId]: (prev[friendId] || 0) + 1 - })); - - const friend = friends.find(f => f.id === friendId); - if (friend) { - const randomTexts = [ - "你好!在忙吗?", - "这个查询结果我觉得很有用", - "我们下次什么时候再讨论一下?", - "你之前分享的内容帮了我大忙", - "有空可以看看我刚发的查询" - ]; - const randomText = randomTexts[Math.floor(Math.random() * randomTexts.length)]; - - const newMessage: Message = { - id: `mock-${Date.now()}`, - content: randomText, - isSent: false, - timestamp: new Date(), - isRead: false - }; - - setFriendMessages(prev => ({ - ...prev, - [friendId]: prev[friendId] - ? [...prev[friendId], newMessage] - : [newMessage] - })); - } - }; - - // 筛选好友 - const filteredFriends = useMemo(() => - friends.filter(friend => - friend.name.toLowerCase().includes(searchTerm.toLowerCase()) || - (friend.remark && friend.remark.toLowerCase().includes(searchTerm.toLowerCase())) // 支持按备注搜索 - ), - [friends, searchTerm] - ); - - // 删除好友逻辑 - const leteRequest = (friend: Friend) => setFriendToDelete(friend); - const handleConfirmDelete = () => { - if (friendToDelete) { - setFriends(prev => prev.filter(f => f.id !== friendToDelete.id)); - setUnreadMessages(prev => { - const newUnread = { ...prev }; - delete newUnread[friendToDelete.id]; - return newUnread; - }); - setFriendMessages(prev => { - const newMessages = { ...prev }; - delete newMessages[friendToDelete.id]; - return newMessages; - }); - setFriendToDelete(null); - } - }; - - // 好友请求处理 - const handleAcceptRequest = (request: FriendRequest) => { - const newFriend: Friend = { - id: `friend-${Date.now()}`, - name: request.fromUser.name, - avatarUrl: request.fromUser.avatarUrl, - isOnline: false, - remark: '', - email:'' - }; - setFriends(prev => [newFriend, ...prev]); - setRequests(prev => prev.filter(req => req.id !== request.id)); - }; - const handleRejectRequest = (requestId: string) => { - setRequests(prev => prev.filter(req => req.id !== requestId)); - }; - - // 分享记录处理 - const handleViewShare = (share: QueryShare) => { - setViewingShare(share); - onMarkShareAsRead(share.id); - }; - const handleRerunShare = (share: QueryShare) => { - onRerunQuery(share.querySnapshot.userPrompt); - }; - const handleSaveShare = (share: QueryShare) => { - onSaveQuery(share.querySnapshot); - setActiveModal('saveSuccess'); - }; - const [shareToDelete, setShareToDelete] = useState(null); - // 新增:备注功能相关函数 - const handleOpenRemarkModal = (friend: Friend) => { - setCurrentRemarkFriend(friend); - setIsRemarkModalOpen(true); - }; - const handleSaveRemark = () => { - if (currentRemarkFriend) { - setFriends(prev => prev.map(friend => - friend.id === currentRemarkFriend.id - ? { ...friend, remark: remarkInput.trim() } - : friend - )); - setIsRemarkModalOpen(false); - setCurrentRemarkFriend(null); - } - }; - - // 新增:好友主页功能相关函数 - const handleOpenProfileModal = (friend: Friend) => { - console.log('打开主页的好友数据:', friend); - setCurrentProfileFriend(friend); - setIsProfileModalOpen(true); - }; - // 新增:处理分享删除请求(触发确认弹窗) -const handleDeleteShareRequest = (share: QueryShare) => { - setShareToDelete(share); -}; - -// 新增:确认删除分享 -const handleConfirmDeleteShare = () => { - if (shareToDelete) { - onDeleteShare(shareToDelete.id); - setShareToDelete(null); - } -}; - return ( -
    - {/* 页面标题栏 */} -
    -

    - -

    -
    - - -
    -
    - - {/* 标签栏 */} -
    -
    - - - s.status === 'unread').length} - activeTab={activeTab} - onTabChange={setActiveTab} - /> -
    -
    - - {/* 内容区域 */} -
    - {/* 1. 好友列表标签 */} - {activeTab === 'friends' && ( -
    - {/* 搜索框(支持搜索名称/备注) */} -
    -
    - - setSearchTerm(e.target.value)} - className="w-full pl-9 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/30" - /> -
    -
    - - {/* 好友列表 */} - {filteredFriends.length > 0 ? ( -
    - {filteredFriends.map(friend => ( - - ))} -
    - ) : ( -
    - -

    未找到匹配的好友

    -
    - )} -
    - )} - - {/* 2. 好友请求标签(原有不变) */} - {activeTab === 'requests' && ( -
    - {requests.length > 0 ? ( - requests.map(req => ( - - )) - ) : ( -
    - -

    没有新的好友请求

    -
    - )} -
    - )} - - {/* 3. 分享记录标签(原有不变) */} - {activeTab === 'shares' && ( -
    - {shares.length > 0 ? ( - shares.map(share => ( - handleViewShare(share)} - onRerun={() => handleRerunShare(share)} - onDelete={() => handleDeleteShareRequest(share)} - onSave={() => handleSaveShare(share)} - /> - )) - ) : ( -
    - -

    没有收到任何分享

    -
    - )} -
    - )} -
    - - {/* 原有模态框:添加好友 */} - setAddFriendModalOpen(false)} title="添加好友"> -
    -
    - - -
    -
    -
    - - -
    -
    - - {/* 原有模态框:删除好友确认 */} - setFriendToDelete(null)} title="删除好友"> -

    您确定要删除好友 "{friendToDelete?.name}" 吗?此操作无法撤销。

    -
    - - -
    -
    - - {/* 原有模态框:聊天窗口 */} - {chattingWith && ( - setChattingWith(null)} - savedQueries={savedQueries} - currentUnreadCount={unreadMessages[chattingWith.id] || 0} - updateUnreadCount={updateUnreadCount} - messages={friendMessages[chattingWith.id] || []} - updateMessages={(newMessages) => updateFriendMessages(chattingWith.id, newMessages)} - /> - )} - - {/* 原有模态框:查看分享结果 */} - setViewingShare(null)} title={`查看分享: "${viewingShare?.querySnapshot.userPrompt}"`}> - {viewingShare && ( -
    - -
    - )} -
    - - {/* 原有模态框:保存成功提示 */} - setActiveModal(null)} hideTitle> -
    -
    - -
    -

    保存成功

    -

    该查询结果已保存至您的历史记录中。

    - -
    -
    - - {/* 新增:备注好友模态框 */} - setIsRemarkModalOpen(false)} title="备注好友"> -
    -
    - - setRemarkInput(e.target.value)} - className="w-full px-4 py-2 border border-gray-300 rounded-lg" - maxLength={20} // 限制备注长度 - /> -

    备注将显示在好友名称下方,最多20个字符

    -
    -
    -
    - - -
    -
    - - {/* 新增:好友主页模态框 */} - setIsProfileModalOpen(false)} title="好友主页" hideTitle={false}> - {currentProfileFriend && ( -
    - {/* 好友头像+基本信息 */} -
    -
    - {currentProfileFriend.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} - /> -
    -

    {currentProfileFriend.name}

    - {currentProfileFriend.remark && ( -

    备注:{currentProfileFriend.remark}

    - )} -

    - - {currentProfileFriend?.email || '未设置邮箱'} -

    - - {currentProfileFriend.isOnline ? '在线' : '离线'} - - -
    - - {/* 操作按钮 */} -
    - - -
    -
    - )} -
    - {/* 新增:删除分享确认模态框 */} - setShareToDelete(null)} title="删除分享"> -

    - 您确定要删除 "{shareToDelete?.sender.name}" 分享的查询吗?此操作无法撤销。 -

    -
    - - -
    -
    -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/HistoryPage.tsx b/src/springboot_demo/frontend/components/HistoryPage.tsx deleted file mode 100644 index 5ba30779..00000000 --- a/src/springboot_demo/frontend/components/HistoryPage.tsx +++ /dev/null @@ -1,455 +0,0 @@ -import React, { useState, useMemo, useCallback } from 'react'; -import { Conversation, QueryResultData } from '../types'; -import { Modal } from './Modal'; -import { QueryResult } from './QueryResult'; -import { MODEL_OPTIONS, DATABASE_OPTIONS } from '../constants'; // 引入真实选项数据 - -// 日期筛选类型定义:全部、今天、近7天、近30天 -type FilterType = 'all' | 'today' | '7days' | '30days'; -// 确认弹窗操作类型定义:单条删除、重新执行、批量删除 -type ConfirmAction = 'delete' | 'rerun' | 'bulkDelete'; - -// 历史页面属性接口定义 -interface HistoryPageProps { - savedQueries: QueryResultData[]; - conversations: Conversation[]; - onDelete: (id: string) => void; - onViewInChat: (conversationId: string) => void; - onRerun: (prompt: string) => void; - onCompare: (queryId1: string, queryId2: string) => void; -} - -// 校验日期是否在指定天数范围内(含起始日期) -const isDateInRage = (date: Date, days: number): boolean => { - const today = new Date(); - const pastDate = new Date(); - if (days > 0) { - pastDate.setDate(today.getDate() - (days - 1)); - } - today.setHours(23, 59, 59, 999); - pastDate.setHours(0, 0, 0, 0); - return date >= pastDate && date <= today; -}; - -// 历史查询页面组件 -export const HistoryPage: React.FC = ({ savedQueries, conversations, onDelete, onViewInChat, onRerun, onCompare }) => { - // 搜索关键词状态 - const [searchTerm, setSearchTerm] = useState(''); - // 当前激活的筛选条件状态 - const [activeFilters, setActiveFilters] = useState({ - date: 'all' as FilterType, - model: 'all', - database: 'all' - }); - // 展开的查询分组标识状态(按用户查询提示语分组) - const [expandedGroup, setExpandedGroup] = useState(null); - // 选中的查询记录ID集合(用于批量操作或对比) - const [selectedIds, setSelectedIds] = useState>(new Set()); - // 正在查看详情的查询记录状态 - const [viewingQuery, setViewingQuery] = useState(null); - - // 确认弹窗状态管理:控制弹窗显示、操作类型及目标数据 - const [confirmModalState, setConfirmModalState] = useState<{ - isOpen: boolean; - action: ConfirmAction | null; - targetId: string | null; - targetPrompt: string | null; - targetIds: string[] | null; - }>({ - isOpen: false, - action: null, - targetId: null, - targetPrompt: null, - targetIds: null, - }); - - // 根据对话ID获取对应的对话标题 - const getConversationTitle = (id: string) => { - return conversations.find(c => c.id === id)?.title || '未知对话'; - }; - - // 切换查询分组的展开/收起状态,切换时清空已选中的查询记录 - const handleToggleGroup = (prompt: string) => { - setExpandedGroup(prev => (prev === prompt ? null : prompt)); - setSelectedIds(new Set()); - }; - - // 切换单个查询记录的选中状态(最多支持100条选中) - const handleSelectSnapshot = (id: string) => { - setSelectedIds(prev => { - const newSet = new Set(prev); - if (newSet.has(id)) { - newSet.delete(id); - } else if (newSet.size < 100) { - newSet.add(id); - } - return newSet; - }); - }; - - // 执行两条选中查询记录的对比操作 - const handleCompare = () => { - if (selectedIds.size !== 2) return; - const [id1, id2] = Array.from(selectedIds); - onCompare(id1, id2); - setSelectedIds(new Set()); - }; - - // 打开单条查询记录的删除确认弹窗 - const openDeleteConfirm = useCallback((id: string) => { - setConfirmModalState({ - isOpen: true, - action: 'delete', - targetId: id, - targetPrompt: null, - targetIds: null, - }); - }, []); - - // 打开查询的重新执行确认弹窗 - const openRerunConfirm = useCallback((prompt: string) => { - setConfirmModalState({ - isOpen: true, - action: 'rerun', - targetId: null, - targetPrompt: prompt, - targetIds: null, - }); - }, []); - - // 打开批量删除确认弹窗 - const openBulkDeleteConfirm = useCallback(() => { - setConfirmModalState({ - isOpen: true, - action: 'bulkDelete', - targetId: null, - targetPrompt: null, - targetIds: Array.from(selectedIds), - }); - }, [selectedIds]); - - // 确认弹窗的操作执行逻辑(根据不同操作类型分发处理) - const handleConfirmAction = () => { - if (confirmModalState.action === 'delete' && confirmModalState.targetId) { - onDelete(confirmModalState.targetId); - } else if (confirmModalState.action === 'rerun' && confirmModalState.targetPrompt) { - onRerun(confirmModalState.targetPrompt); - } else if (confirmModalState.action === 'bulkDelete' && confirmModalState.targetIds) { - confirmModalState.targetIds.forEach(id => onDelete(id)); - setSelectedIds(new Set()); - } - setConfirmModalState({ isOpen: false, action: null, targetId: null, targetPrompt: null, targetIds: null }); - }; - - // 取消确认弹窗操作 - const handleCancelConfirm = () => { - setConfirmModalState({ isOpen: false, action: null, targetId: null, targetPrompt: null, targetIds: null }); - }; - - // 切换筛选条件 - const handleFilterChange = (type: 'date' | 'model' | 'database', value: string) => { - setActiveFilters(prev => ({ - ...prev, - [type]: value - })); - }; - - // 重置所有筛选条件 - const handleResetFilters = () => { - setActiveFilters({ - date: 'all', - model: 'all', - database: 'all' - }); - setSearchTerm(''); - }; - - // 按用户查询提示语分组查询记录,每组内按执行时间倒序排序 - const queryGroups = useMemo(() => { - const groups: Record = {}; - - savedQueries.forEach(query => { - if (!groups[query.userPrompt]) { - groups[query.userPrompt] = []; - } - groups[query.userPrompt].push(query); - }); - - Object.values(groups).forEach(snapshots => { - snapshots.sort((a, b) => new Date(b.queryTime).getTime() - new Date(a.queryTime).getTime()); - }); - return groups; - }, [savedQueries]); - - // 根据搜索关键词和所有筛选条件,过滤查询分组 - const filteredGroups = useMemo(() => { - return Object.entries(queryGroups) - .filter(([prompt, snapshots]) => { - const matchesSearch = prompt.toLowerCase().includes(searchTerm.toLowerCase()); - if (!matchesSearch) return false; - - // 过滤出符合所有筛选条件的记录 - const filteredSnapshots = snapshots.filter(query => { - // 日期筛选 - if (activeFilters.date !== 'all') { - const queryDate = new Date(query.queryTime); - const dateMatch = - activeFilters.date === 'today' ? isDateInRage(queryDate, 1) : - activeFilters.date === '7days' ? isDateInRage(queryDate, 7) : - activeFilters.date === '30days' ? isDateInRage(queryDate, 30) : - false; - if (!dateMatch) return false; - } - - // 大模型筛选(使用真实model字段) - if (activeFilters.model !== 'all' && query.model !== activeFilters.model) { - return false; - } - - // 数据库筛选(使用真实database字段) - if (activeFilters.database !== 'all' && query.database !== activeFilters.database) { - return false; - } - - return true; - }); - - return filteredSnapshots.length > 0; - }); - }, [queryGroups, searchTerm, activeFilters]); - - // 渲染下拉筛选组件(复用逻辑) - const renderFilterDropdown = ( - label: string, - type: 'date' | 'model' | 'database', - options: { value: string; label: string }[] - ) => ( -
    - -
    - ); - - - // 大模型筛选选项(从MODEL_OPTIONS提取,去重并添加"全部") - const modelOptions = useMemo(() => { - const uniqueModels = Array.from(new Set(MODEL_OPTIONS.map(option => option.name))); - return [ - { value: 'all', label: '全部大模型' }, - ...uniqueModels.map(model => ({ value: model, label: model })) - ]; - }, []); - - // 数据库筛选选项(从DATABASE_OPTIONS提取,去重并添加"全部") - const databaseOptions = useMemo(() => { - const uniqueDatabases = Array.from(new Set(DATABASE_OPTIONS.map(option => option.name))); - return [ - { value: 'all', label: '全部数据库' }, - ...uniqueDatabases.map(db => ({ value: db, label: db })) - ]; - }, []); - - - // 日期筛选选项 - const dateOptions = [ - { value: 'all', label: '全部日期' }, - { value: 'today', label: '今天' }, - { value: '7days', label: '近7天' }, - { value: '30days', label: '近30天' } - ]; - - // 根据确认弹窗的操作类型,生成对应的弹窗内容(标题、提示信息、按钮文本及样式) - const confirmModalContent = useMemo(() => { - if (confirmModalState.action === 'delete') { - return { - title: '确认删除查询记录?', - message: '此操作将永久删除该条查询快照。请确认是否继续?', - buttonText: '确认删除', - buttonClass: 'bg-red-600 hover:bg-red-700', - }; - } - if (confirmModalState.action === 'rerun') { - return { - title: '确认重新执行查询?', - message: `您确定要重新执行查询:"${confirmModalState.targetPrompt}" 吗? 这将消耗新的计算资源。`, - buttonText: '重新执行', - buttonClass: 'bg-primary hover:bg-primary/90', - }; - } - if (confirmModalState.action === 'bulkDelete' && confirmModalState.targetIds) { - return { - title: '确认批量删除?', - message: `您确定要永久删除选中的 ${confirmModalState.targetIds.length} 条查询记录吗?此操作不可恢复。`, - buttonText: '批量删除', - buttonClass: 'bg-red-600 hover:bg-red-700', - }; - } - return { title: '', message: '', buttonText: '', buttonClass: '' }; - }, [confirmModalState.action, confirmModalState.targetPrompt, confirmModalState.targetIds]); - - return ( -
    - -
    -
    - {/* 搜索框 - 左侧缩小 */} -
    - - setSearchTerm(e.target.value)} - className="w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/30" - /> -
    - - {/* 筛选区域 - 右侧横向分布 */} -
    - {renderFilterDropdown('大模型', 'model', modelOptions)} - {renderFilterDropdown('数据库', 'database', databaseOptions)} - {renderFilterDropdown('日期', 'date', dateOptions)} - - {/* 重置按钮 */} - - - {/* 批量删除按钮*/} - -
    -
    -
    - -
    - {filteredGroups.length > 0 ? ( - filteredGroups.map(([prompt, snapshots]) => ( -
    -
    handleToggleGroup(prompt)}> -
    -

    {prompt}

    -

    {snapshots.length} 次执行

    -
    -
    - {snapshots.length > 1 && expandedGroup === prompt && ( - - )} - -
    -
    - {expandedGroup === prompt && ( -
    - {snapshots.map(query => ( -
    -
    -
    - handleSelectSnapshot(query.id)} - className="mr-4 h-4 w-4 text-primary focus:ring-primary/50 border-gray-300 rounded" - /> -

    执行于: {new Date(query.queryTime).toLocaleString()}

    -
    - {/* 展示真实的查询详情信息 */} -
    - 耗时: {query.executionTime} - 大模型: {query.model} - 数据库: {query.database} - 所属对话: "{query.conversationId}" -
    -
    -
    - - - - -
    -
    - ))} -
    - )} -
    - )) - ) : ( -
    - -

    未找到匹配的查询记录

    -
    - )} -
    - - {/* 查询详情弹窗 */} - setViewingQuery(null)} - > - {viewingQuery && ( -
    - -
    - )} -
    - - {/* 通用确认弹窗(支持单删/批量删/重新执行) */} - -

    {confirmModalContent.message}

    - -
    - - -
    -
    -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/HistorySidebar.tsx b/src/springboot_demo/frontend/components/HistorySidebar.tsx deleted file mode 100644 index 93d8f634..00000000 --- a/src/springboot_demo/frontend/components/HistorySidebar.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useState } from 'react'; -import { Conversation } from '../types'; - -interface HistorySidebarProps { - isOpen: boolean; - onClose: () => void; - conversations: Conversation[]; - currentConversationId: string; - onSwitchConversation: (id: string) => void; - onNewConversation: () => void; - onDeleteConversation: (id: string) => void; -} - -export const HistorySidebar: React.FC = ({ - isOpen, - onClose, - conversations, - currentConversationId, - onSwitchConversation, - onNewConversation, - onDeleteConversation -}) => { - // 新增:删除确认弹窗状态 - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const [deleteTargetId, setDeleteTargetId] = useState(null); - - const sidebarClasses = ` - bg-white border-l border-gray-200 h-full flex flex-col - transition-transform duration-300 ease-in-out - fixed top-0 right-0 z-40 transform - ${isOpen ? 'translate-x-0' : 'translate-x-full'} - w-80 shadow-lg - `; - - const innerContentClasses = `w-80 h-full flex flex-col overflow-hidden`; - - return ( - <> - {isOpen &&
    } - - - - {/* 新增:删除确认弹窗 */} - {showDeleteConfirm && ( -
    -
    -

    确认删除

    -

    - 确定要删除这条对话吗?删除后无法恢复。 -

    -
    - - -
    -
    -
    - )} - - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/LoginPage.tsx b/src/springboot_demo/frontend/components/LoginPage.tsx deleted file mode 100644 index 9238d268..00000000 --- a/src/springboot_demo/frontend/components/LoginPage.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import React, { useState } from 'react'; -import { UserRole } from '../types'; -import { authApi, LoginRequest } from '../services/api'; - -interface LoginPageProps { - onLogin: (role: UserRole) => void; -} - -const roles: { id: UserRole; name: string; icon: string }[] = [ - { id: 'sys-admin', name: '系统管理员', icon: 'fa-shield' }, - { id: 'data-admin', name: '数据管理员', icon: 'fa-database' }, - { id: 'normal-user', name: '普通用户', icon: 'fa-user' }, -]; - - -export const LoginPage: React.FC = ({ onLogin }) => { - const [selectedRole, setSelectedRole] = useState('sys-admin'); - const [isForgotModalOpen, setForgotModalOpen] = useState(false); - const [resetEmailSent, setResetEmailSent] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [error, setError] = useState(null); - - const handleLogin = async (e: React.MouseEvent) => { - e.preventDefault(); - setError(null); - - if (!username.trim() || !password.trim()) { - setError('请输入用户名和密码'); - return; - } - - setIsLoading(true); - try { - const loginRequest: LoginRequest = { - username: username.trim(), - password: password, - }; - - const response = await authApi.login(loginRequest); - - // 根据角色ID映射到前端角色 - // 假设: 1=系统管理员, 2=数据管理员, 3=普通用户 - let role: UserRole = 'normal-user'; - if (response.roleId === 1) { - role = 'sys-admin'; - } else if (response.roleId === 2) { - role = 'data-admin'; - } else if (response.roleId === 3) { - role = 'normal-user'; - } - - onLogin(role); - } catch (err) { - setError(err instanceof Error ? err.message : '登录失败,请检查用户名和密码'); - } finally { - setIsLoading(false); - } - }; - - const handleRequestReset = (e: React.FormEvent) => { - e.preventDefault(); - setResetEmailSent(true); - setTimeout(() => { - handleCloseForgotModal(); - }, 3000); - }; - - const handleCloseForgotModal = () => { - setForgotModalOpen(false); - setTimeout(() => setResetEmailSent(false), 300); - }; - - const illustrationSvg = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbD0iI0VBRUlGRiIgZD0iTTQzMyAxNDFhMTQgMTQgMCAwIDAtMTQgMTQuMDQ0djE5OC45MTJBMTQgMTQgMCAwIDAgNDMzIDM2OGgxNFYxNDFoLTE0eiIvPjxwYXRoIGZpbGw9IiNDRUQ4RkYiIGQ9Ik0zMzUgMTg0YTQgNCAwIDAgMC00IDR2MTM2YTQgNCAwIDAgMCA4IDBWMTg4YTQgNCAwIDAgMC00LTR6TTI1NSA5N2E0IDQgMCAwIDAtNCA0djI1MGE0IDQgMCAwIDAgOCAwdl0yNTBhNCA0IDAgMCAwLTQtNHpNMzc1IDIyN2E0IDQgMCAwIDAtNCA0djg5YTQgNCAwIDAgMCA4IDB2LTg5YTQgNCAwIDAgMC00LTR6Ii8+PHBhdGggZmlsbD0iI0VBRUlGRiIgZD0iTTgxIDIwMWExNCAxNCAwIDAgMC0xNCAxNC4wNDR2MTM3LjkyQTE0IDE0IDAgMCAwIDgxIDM2OGgxNFYyMDFIODF6Ii8+PHBhdGggZmlsbD0iI0NFRDhGRiIgZD0iTTE5NSA2MWExNCAxNCAwIDAgMC0xNCAxNC4wNDR2MjgwLjkyQTE0IDE0IDAgMCAwIDE5NSAzNjhIMTlWMjYwaDE2NHYtNTZIMTlWOTFoMTc2VjYxaC0xNHptLTE2NCAxODVWMjEzaDE2NFYxNTVIMzF2NDh6bTE2NC05NVY5OWgtMTZWNzdhNCA0IDAgMCAwLTQtNFY2MWgtNDB2MTJhNCA0IDAgMCAwLTQgN3YxNkgzMXY0MGgxNjR6Ii8+PHBhdGggZmlsbD0iI0VBRUlGRiIgZD0iTTI5NiAyNTlhMTQgMTQgMCAwIDAtMTQgMTQuMDQ0djk0LjkyQTE0IDE0IDAgMCAwIDI5NiAzODJoMTRWMjU5aC0xNHpNMzU2IDMwOWExNCAxNCAwIDAgMC0xNCAxNC4wNDR2NTUuOTJBMTQgMTQgMCAwIDAgMzU2IDM5MWgxNFYzMDloLTM1eiIvPjxwYXRoIGZpbGw9IiNDRUQ4RkYiIGQ9Ik0xMzUgMTI0YTQgNCAwIDAgMC00IDR2MjE3YTQgNCAwIDAgMCA4IDBWMTE4YTQgNCAwIDAgMC00LTR6Ii8+PHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTQyMyAzODRINjlhMTQgMTQgMCAwIDAtMTQgMTR2NTRoMzg1VjM5OGExNCAxNCAwIDAgMC0xNC0xNHoiLz48cGF0aCBmaWxsPSIjQ0VEOEZGIiBkPSJNMzY4IDQyNEgxNDR2MTZoMjI0di0xNnpNMzYxIDQwNUg4NmExNCAxNCAwIDAgMCAwIDI4aDI3NWExNCAxNCAwIDAgMCAwLTI4eiIvPjxwYXRoIGZpbGw9IiNGNEY5RkYiIGQ9Ik01NSAzOTIuMTY0VjM5OGExNCAxNCAwIDAgMCAxNCAxNGg1NDFBMTQgMTQgMCAwIDEgNDM3IDQyNkg1OFY0MDZoMzAzdjEyaDI0di0xMmgyOHYxMmgyNHYtMTJoLTh2LTEySDU4djEyLjE2NEExMy45IDEzLjkgMCAwIDEgNTUgMzkyLjE2NHptMCAzMS44MzZWMzk4YTE0IDE0IDAgMCAxIDE0LTE0aDM1N2EzMyAzMyAwIDAgMSAzMyAzM2gtNDIydi0zMmgzMDN2LTguMTY0QTEzLjkgMTMuOSAwIDAgMSA0MjQgNDI0eiIvPjxwYXRoIGZpbGw9IiM0Q0I2RkYiIGQ9Ik0yMTEgMjg4aC0zM2wtMzUtOTBoLTI4bDQ3IDEyM2g0OGwxNi00NGg0NmwtOCA0NGg0Mmw0Ny0xMjNoLTM4bC0yMyA2M2gtNDVsMTYtNDVaIi8+PHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTk1IDQ1MmgyNHYyNEg5NXptMjA4IDBoMjR2MjRIMzAzem0xMjAgMGgyNHYyNGgtMjR6Ii8+PC9zdmc+"; - - return ( -
    -
    -
    -
    -
    - -

    自然语言查询系统

    -
    -

    欢迎回来

    -

    请选择您的角色并登录。

    -
    - -
    -
    - -
    - {roles.map((role) => ( - - ))} -
    -
    -
    -
    - - setUsername(e.target.value)} - className="w-full px-4 py-3 pl-10 bg-gray-50 text-dark border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary placeholder-gray-400" - /> -
    -
    -
    -
    - - setPassword(e.target.value)} - onKeyPress={(e) => e.key === 'Enter' && handleLogin(e as any)} - className="w-full px-4 py-3 pl-10 bg-gray-50 text-dark border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary placeholder-gray-400" - /> -
    -
    - {error && ( -
    - {error} -
    - )} -
    - - -
    -
    -
    - © 2024 Your Company. All Rights Reserved. -
    -
    - -
    -
    -

    智能数据,一语洞穿

    -

    - 无需复杂的SQL,只需用您最熟悉的自然语言提问,即可获得精准的数据洞察、直观的可视化图表,并轻松与团队分享。 -

    -
      -
    • 自然语言查询
    • -
    • 智能图表生成
    • -
    • 结果轻松分享
    • -
    -
    -
    - Data Analysis Illustration -
    -
    -
    - - {/* 忘记密码模态框 */} - {isForgotModalOpen && ( -
    -
    e.stopPropagation()}> - {!resetEmailSent ? ( - <> -

    重置密码

    -
    -

    请输入您注册时使用的邮箱地址,我们将向您发送一封密码重置邮件。

    -
    - - -
    -
    - - -
    -
    - - ) : ( -
    -
    - -
    -

    已发送

    -

    如果该邮箱地址已注册,一封包含密码重置链接的邮件已经发送到您的邮箱。

    -
    - )} -
    -
    - )} -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/Modal.tsx b/src/springboot_demo/frontend/components/Modal.tsx deleted file mode 100644 index 16850394..00000000 --- a/src/springboot_demo/frontend/components/Modal.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useEffect, useState } from 'react'; - -interface ModalProps { - isOpen: boolean; - onClose: () => void; - title?: string; - hideTitle?: boolean; - children: React.ReactNode; - contentClassName?: string; // 外部自定义样式(会覆盖默认) -} - -export const Modal: React.FC = ({ - isOpen, - onClose, - title, - hideTitle = false, - children, - contentClassName = "" -}) => { - const [isRendered, setIsRendered] = useState(isOpen); - - useEffect(() => { - if (isOpen) { - setIsRendered(true); - } else { - const timer = setTimeout(() => setIsRendered(false), 300); - return () => clearTimeout(timer); - } - }, [isOpen]); - - if (!isRendered) { - return null; - } - - // 容器样式(保持居中逻辑不变) - const modalContainerClass = `fixed inset-0 bg-black/50 flex flex-col items-center justify-center z-50 transition-opacity duration-300 ${ - isOpen ? 'opacity-100' : 'opacity-0' - }`; - - // 核心修改:添加默认宽度 max-w-md(和你原来的一样),同时让 contentClassName 能覆盖它 - // 注意类的顺序:默认样式在前,自定义样式在后(后定义的会覆盖前定义的) - const modalContentClass = ` - bg-white rounded-xl p-6 w-full mx-4 my-auto transition-all duration-300 - max-w-md // 默认宽度 - max-h-[90vh] // 弹窗最大高度(限制上限) - height: 100% // 让内容容器高度充满可用空间 - ${isOpen ? 'scale-100 opacity-100' : 'scale-95 opacity-0'} - ${contentClassName} - `; - - return ( -
    -
    e.stopPropagation()}> - {!hideTitle && ( -
    -

    {title}

    - -
    - )} - {children} -
    -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/NotificationsPage.tsx b/src/springboot_demo/frontend/components/NotificationsPage.tsx deleted file mode 100644 index fee2ee8f..00000000 --- a/src/springboot_demo/frontend/components/NotificationsPage.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import React, { useState } from 'react'; -import { MOCK_NOTIFICATIONS } from '../constants'; -import { Notification } from '../types'; -import { Modal } from './Modal'; // 引入Modal组件 - -const NotificationIcon: React.FC<{ type: 'share' | 'system' }> = ({ type }) => { - switch (type) { - case 'share': - return ; - case 'system': - return ; - default: - return null; - } -}; - -const NotificationItem: React.FC<{ notification: Notification; onToggleRead: (id: string) => void; onDelete: (id: string) => void; }> = ({ notification, onToggleRead, onDelete }) => { - const { id, type, title, content, timestamp, isRead, isPinned } = notification; - - return ( -
  • -
    - - {isPinned && } -
    -
    -

    {title}

    -

    {content}

    - {new Date(timestamp).toLocaleString()} -
    -
    - - {!isPinned && ( - - )} -
    -
  • - ); -}; - - -export const NotificationsPage: React.FC = () => { - const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS); - // 新增:删除确认弹窗状态 - const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); - const [deleteTargetId, setDeleteTargetId] = useState(null); - - const pinnedNotifications = notifications.filter(n => n.isPinned); - const regularNotifications = notifications.filter(n => !n.isPinned); - - const handleToggleRead = (id: string) => { - setNotifications( - notifications.map(n => n.id === id ? { ...n, isRead: !n.isRead } : n) - ); - }; - - const handleMarkAllRead = () => { - setNotifications(notifications.map(n => ({ ...n, isRead: true }))); - }; - - // 修改:点击删除按钮时显示弹窗 - const handleDelete = (id: string) => { - setDeleteTargetId(id); - setShowDeleteConfirm(true); - }; - - // 新增:确认删除后执行的逻辑 - const handleConfirmDelete = () => { - if (deleteTargetId) { - setNotifications(notifications.filter(n => n.id !== deleteTargetId)); - } - setShowDeleteConfirm(false); - setDeleteTargetId(null); - }; - - const handleClearAll = () => { - setNotifications(notifications.filter(n => n.isPinned)); - }; - - return ( -
    -
    -
    - - -
    -
    - - {pinnedNotifications.length > 0 && ( -
    -

    置顶通知

    -
    -
      - {pinnedNotifications.map(n => ( - - ))} -
    -
    -
    - )} - -
    -

    - {pinnedNotifications.length > 0 ? '普通通知' : ''} -

    -
    -
      - {regularNotifications.length > 0 ? regularNotifications.map(n => ( - - )) : ( -
      - -

      没有新的通知

      -
      - )} -
    -
    -
    - - {/* 新增:删除确认弹窗 */} - { - setShowDeleteConfirm(false); - setDeleteTargetId(null); - }} - title="确认删除" - > -
    -

    确定要删除这条通知吗?删除后无法恢复。

    -
    - - -
    -
    -
    -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/PlaceholderPage.tsx b/src/springboot_demo/frontend/components/PlaceholderPage.tsx deleted file mode 100644 index 11c299f7..00000000 --- a/src/springboot_demo/frontend/components/PlaceholderPage.tsx +++ /dev/null @@ -1,18 +0,0 @@ - -import React from 'react'; - -interface PlaceholderPageProps { - title: string; -} - -export const PlaceholderPage: React.FC = ({ title }) => { - return ( -
    -
    - -

    {title}

    -

    此页面正在建设中。

    -
    -
    - ); -}; diff --git a/src/springboot_demo/frontend/components/QueryPage.tsx b/src/springboot_demo/frontend/components/QueryPage.tsx deleted file mode 100644 index e0ff69f9..00000000 --- a/src/springboot_demo/frontend/components/QueryPage.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import { Conversation, MessageRole, QueryResultData } from '../types'; -import { ChatMessage } from './ChatMessage'; -import { Dropdown } from './Dropdown'; -import { MODEL_OPTIONS, DATABASE_OPTIONS } from '../constants'; -import { HistorySidebar } from './HistorySidebar'; -import { RightSidebar } from './RightSidebar'; -import { queryApi, QueryResponse } from '../services/api'; - -interface QueryPageProps { - currentConversation: Conversation | undefined; - onToggleHistory: () => void; - isHistoryOpen: boolean; - onAddMessage: (role: MessageRole, content: string | QueryResultData) => void; - onSaveQuery: (query: QueryResultData) => void; - onShareQuery: (queryId: string, friendId: string) => void; - savedQueries: QueryResultData[]; - initialPrompt?: string; - onClearInitialPrompt: () => void; - conversations: Conversation[]; - currentConversationId: string; // 当前激活的对话ID(关键) - onSwitchConversation: (id: string) => void; - onNewConversation: () => void; - onDeleteConversation: (id: string) => void; -} - -export const QueryPage: React.FC = ({ - currentConversation, - onToggleHistory, - isHistoryOpen, - onAddMessage, - onSaveQuery, - onShareQuery, - savedQueries, - initialPrompt, - onClearInitialPrompt, - conversations, - currentConversationId, // 接收当前对话ID - onSwitchConversation, - onNewConversation, - onDeleteConversation, -}) => { - const [prompt, setPrompt] = useState(''); - const [selectedModel, setSelectedModel] = useState(MODEL_OPTIONS[0].name); - const [selectedDatabase, setSelectedDatabase] = useState(DATABASE_OPTIONS[0].name); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [abortController, setAbortController] = useState(null); - // 新增:记录当前正在处理的请求对应的对话ID - const [pendingConversationId, setPendingConversationId] = useState(null); - const chatContainerRef = useRef(null); - - // 自动滚动到底部 - useEffect(() => { - if (chatContainerRef.current) { - chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; - } - }, [currentConversation?.messages]); - - // 处理初始提示 - useEffect(() => { - if (initialPrompt) { - setPrompt(initialPrompt); - const syntheticEvent = { preventDefault: () => {} } as React.FormEvent; - handleSubmit(syntheticEvent, initialPrompt); - onClearInitialPrompt(); - } - }, [initialPrompt, onClearInitialPrompt]); - - // 关键修改1:当对话ID变化时,自动中断当前请求 - useEffect(() => { - // 如果正在加载中,且当前对话ID与请求时的ID不一致,中断请求 - if (isLoading && pendingConversationId && pendingConversationId !== currentConversationId) { - handleStop(); - } - }, [currentConversationId, isLoading, pendingConversationId]); - - const handleSubmit = async (e: React.FormEvent, customPrompt?: string) => { - e.preventDefault(); - const finalPrompt = customPrompt || prompt; - if (!finalPrompt.trim() || isLoading) return; - - // 1. 记录当前请求对应的对话ID(关键) - const requestConversationId = currentConversationId; - setPendingConversationId(requestConversationId); - - // 2. 创建中断控制器 - const controller = new AbortController(); - setAbortController(controller); - - onAddMessage('user', finalPrompt); - setPrompt(''); - setIsLoading(true); - setError(null); - - try { - if (!currentConversation) throw new Error("No active conversation."); - - // 3. 调用后端API - const response: QueryResponse = await queryApi.execute({ - userPrompt: finalPrompt, - model: selectedModel, - database: selectedDatabase, - conversationId: currentConversation.id !== 'conv-1' ? currentConversation.id : undefined, - }); - - // 4. 将后端响应转换为前端格式 - const result: QueryResultData = { - id: response.id, - userPrompt: response.userPrompt, - sqlQuery: response.sqlQuery, - conversationId: response.conversationId, - queryTime: response.queryTime, - executionTime: response.executionTime, - database: response.database, - model: response.model, - tableData: response.tableData, - chartData: response.chartData ? { - type: (response.chartData.type || 'bar') as 'bar' | 'line' | 'pie', - labels: response.chartData.labels || [], - datasets: response.chartData.datasets || [], - } : undefined, - }; - - // 5. 关键校验:只有当前对话仍为请求时的对话,才添加AI回复 - if (currentConversationId === requestConversationId) { - onAddMessage('ai', result); - } else { - // 对话已切换,丢弃回复(可选:添加日志便于调试) - console.log(`AI回复已丢弃(目标对话已切换):原对话ID=${requestConversationId},新对话ID=${currentConversationId}`); - } - } catch (err) { - // 6. 错误处理也需校验对话ID - if (err instanceof Error && err.name === 'AbortError') { - // 仅在原对话中显示"已停止"提示 - if (currentConversationId === requestConversationId) { - onAddMessage('ai', '查询已被手动停止'); - } - } else { - const errorMessage = err instanceof Error ? err.message : '查询失败,请稍后重试'; - // 仅在原对话中显示错误 - if (currentConversationId === requestConversationId) { - setError(errorMessage); - onAddMessage('ai', errorMessage); - } - } - } finally { - // 7. 仅在原对话中重置状态 - if (currentConversationId === requestConversationId) { - setIsLoading(false); - setAbortController(null); - setPendingConversationId(null); - } - } - }; - - // 停止请求 - const handleStop = () => { - if (abortController) { - abortController.abort(); // 触发中断 - setIsLoading(false); - setAbortController(null); - setPendingConversationId(null); - } - }; - - const handleRecommendationClick = (recommendation: string) => { - const fakeEvent = { preventDefault: () => {} } as React.FormEvent; - handleSubmit(fakeEvent, recommendation); - }; - - return ( -
    -
    -
    - {/* Chat Area */} -
    - {currentConversation?.messages.map((msg, index) => ( - - ))} - {isLoading && ( -
    -
    - -
    -
    -
    -
    - 正在生成结果... -
    -
    -
    - )} -
    - - {/* Input Area */} -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - - - - setModal(null)} title="确认删除通知"> -

    您确定要删除通知 "{currentItem?.title}" 吗?

    -
    -
    -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/admin/SystemLogPage.tsx b/src/springboot_demo/frontend/components/admin/SystemLogPage.tsx deleted file mode 100644 index b2fbc6f6..00000000 --- a/src/springboot_demo/frontend/components/admin/SystemLogPage.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import React, { useState, useMemo, useEffect } from 'react'; -import { AdminModal } from './AdminModal'; -import { SystemLog } from '../../types'; -import { MOCK_SYSTEM_LOGS } from '../../constants'; - -interface SystemLogPageProps { - initialStatusFilter: string; - clearInitialFilter: () => void; -} - -export const SystemLogPage: React.FC = ({ initialStatusFilter, clearInitialFilter }) => { - const [logs, setLogs] = useState(MOCK_SYSTEM_LOGS); - const [filters, setFilters] = useState({ startDate: '', endDate: '', user: '', action: '', status: initialStatusFilter }); - const [isExportModalOpen, setExportModalOpen] = useState(false); - const [viewingLog, setViewingLog] = useState(null); - - useEffect(() => { - // Clear the initial filter from the parent so it's not reapplied on re-renders - if (initialStatusFilter) { - clearInitialFilter(); - } - }, [initialStatusFilter, clearInitialFilter]); - - const filteredLogs = useMemo(() => { - return logs.filter(log => - (!filters.startDate || log.time >= filters.startDate) && - (!filters.endDate || log.time <= `${filters.endDate} 23:59:59`) && - (!filters.user || log.user.toLowerCase().includes(filters.user.toLowerCase())) && - (!filters.action || log.action.toLowerCase().includes(filters.action.toLowerCase())) && - (!filters.status || log.status === filters.status) - ); - }, [logs, filters]); - - const handleFilterChange = (e: React.ChangeEvent) => { - setFilters(prev => ({ ...prev, [e.target.name]: e.target.value })); - }; - - const resetFilters = () => { - setFilters({ startDate: '', endDate: '', user: '', action: '', status: '' }); - }; - - const handleExport = () => { - alert('日志已开始导出...'); - setExportModalOpen(false); - } - - return ( -
    -
    - -
    -
    -
    -
    - -
    -
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    -
    -
    - - - - {filteredLogs.map(log => ( - - - - - - - - - - - ))} - -
    ID操作时间操作用户操作内容涉及模型IP地址状态操作
    {log.id}{log.time}{log.user}{log.action}{log.model}{log.ip}{log.status === 'success' ? '成功' : '失败'} - {log.status === 'failure' && log.details && ( - - )} -
    -
    -

    显示 {filteredLogs.length} 条,共 {logs.length} 条

    -
    - - setExportModalOpen(false)} title="导出系统日志"> -
    { e.preventDefault(); handleExport(); }} className="space-y-4"> -
    - -
    -
    -
    - -
    -
    -
    - - -
    -
    -
    - - setViewingLog(null)} title={`日志详情 (ID: ${viewingLog?.id})`}> -
    -

    错误信息

    -
    -                        {viewingLog?.details}
    -                    
    -
    -
    - -
    -
    -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/admin/UserManagementPage.tsx b/src/springboot_demo/frontend/components/admin/UserManagementPage.tsx deleted file mode 100644 index fc232973..00000000 --- a/src/springboot_demo/frontend/components/admin/UserManagementPage.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import React, { useState, useMemo, useCallback } from 'react'; -import { AdminModal } from './AdminModal'; -import { AdminUser, UserRole } from '../../types'; - -const MOCK_USERS: AdminUser[] = [ - { id: 1, username: 'admin', role: 'sys-admin', email: 'admin@example.com', regTime: '2024-01-15', status: 'active' }, - { id: 2, username: 'zhangsan', role: 'normal-user', email: 'zhangsan@example.com', regTime: '2024-03-22', status: 'active' }, - { id: 3, username: 'lisi', role: 'data-admin', email: 'lisi@example.com', regTime: '2024-05-10', status: 'disabled' }, - { id: 4, username: 'wangwu', role: 'normal-user', email: 'wangwu@example.com', regTime: '2024-06-01', status: 'active' }, - { id: 5, username: 'zhaoliu', role: 'data-admin', email: 'zhaoliu@example.com', regTime: '2024-07-11', status: 'active' }, -]; - -export const UserManagementPage: React.FC = () => { - const [users, setUsers] = useState(MOCK_USERS); - const [filters, setFilters] = useState({ search: '', role: '', status: '' }); - const [selectedUserIds, setSelectedUserIds] = useState>(new Set()); - const [modal, setModal] = useState<'add' | 'edit' | 'confirmDelete' | 'confirmBatch' | 'confirmResetPassword' | 'confirmToggleStatus' | null>(null); - const [userToProcess, setUserToProcess] = useState(null); - const [batchAction, setBatchAction] = useState<'enable' | 'disable' | 'delete' | ''>(''); - - const filteredUsers = useMemo(() => { - return users.filter(user => - (user.username.toLowerCase().includes(filters.search.toLowerCase()) || user.email.toLowerCase().includes(filters.search.toLowerCase())) && - (filters.role ? user.role === filters.role : true) && - (filters.status ? user.status === filters.status : true) - ); - }, [users, filters]); - - const handleFilterChange = (e: React.ChangeEvent) => { - setFilters(prev => ({ ...prev, [e.target.name]: e.target.value })); - }; - - const handleSelectAll = (e: React.ChangeEvent) => { - if (e.target.checked) { - setSelectedUserIds(new Set(filteredUsers.map(u => u.id))); - } else { - setSelectedUserIds(new Set()); - } - }; - - const handleSelectUser = (id: number, checked: boolean) => { - const newSet = new Set(selectedUserIds); - if (checked) { - newSet.add(id); - } else { - newSet.delete(id); - } - setSelectedUserIds(newSet); - }; - - const requestDeleteUser = useCallback((user: AdminUser) => { - setUserToProcess(user); - setModal('confirmDelete'); - }, []); - - const confirmDeleteUser = () => { - if (userToProcess) { - setUsers(prev => prev.filter(u => u.id !== userToProcess.id)); - } - setModal(null); - }; - - const requestToggleUserStatus = useCallback((user: AdminUser) => { - setUserToProcess(user); - setModal('confirmToggleStatus'); - }, []); - - const confirmToggleUserStatus = () => { - if(userToProcess) { - setUsers(prev => prev.map(u => u.id === userToProcess.id ? { ...u, status: u.status === 'active' ? 'disabled' : 'active' } : u)); - } - setModal(null); - }; - - const handleApplyBatchAction = () => { - if (!batchAction || selectedUserIds.size === 0) return; - setModal('confirmBatch'); - }; - - const confirmBatchAction = () => { - if (batchAction === 'delete') { - setUsers(prev => prev.filter(u => !selectedUserIds.has(u.id))); - } else { - setUsers(prev => prev.map(u => selectedUserIds.has(u.id) ? { ...u, status: batchAction === 'enable' ? 'active' : 'disabled' } : u)); - } - setSelectedUserIds(new Set()); - setModal(null); - }; - - const requestEditUser = useCallback((user: AdminUser) => { - setUserToProcess(user); - setModal('edit'); - }, []); - - const requestAddUser = () => { - setUserToProcess(null); - setModal('add'); - }; - - const handleSaveUser = useCallback((e: React.FormEvent) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - - if (modal === 'add') { - const password = formData.get('password') as string; - if (password.length < 6) { - alert('密码至少需要6位!'); - return; - } - const newUser: AdminUser = { - id: Date.now(), - username: formData.get('username') as string, - email: formData.get('email') as string, - role: formData.get('role') as UserRole, - regTime: new Date().toISOString().split('T')[0], - status: 'active', - }; - setUsers(prev => [newUser, ...prev]); - - } else if (modal === 'edit' && userToProcess) { - const updatedUser = { - ...userToProcess, - username: formData.get('username') as string, - email: formData.get('email') as string, - role: formData.get('role') as UserRole, - }; - setUsers(prev => prev.map(u => u.id === updatedUser.id ? updatedUser : u)); - } - - setModal(null); - }, [modal, userToProcess]); - - const requestResetPassword = useCallback((user: AdminUser) => { - setUserToProcess(user); - setModal('confirmResetPassword'); - }, []); - - const confirmResetPassword = () => { - if (userToProcess) { - alert(`已向用户 ${userToProcess.username} (${userToProcess.email}) 发送密码重置邮件。`); - } - setModal(null); - }; - - const getRoleName = (role: UserRole) => ({ 'sys-admin': '系统管理员', 'data-admin': '数据管理员', 'normal-user': '普通用户' }[role]); - const getRoleClass = (role: UserRole) => ({ 'sys-admin': 'bg-primary/10 text-primary', 'data-admin': 'bg-success/10 text-success', 'normal-user': 'bg-secondary/10 text-secondary' }[role]); - - return ( -
    -
    -
    - -
    -
    -
    -
    -
    -
    -
    - - -
    -
    - - - -
    -
    - -
    -
    - - - - - - - - - - - - - - {filteredUsers.map(user => ( - - - - - - - - - - ))} - -
    0 && selectedUserIds.size === filteredUsers.length} className="w-4 h-4 text-primary focus:ring-primary/30" />用户名角色邮箱注册时间状态操作
    handleSelectUser(user.id, e.target.checked)} className="w-4 h-4 text-primary focus:ring-primary/30" />{user.username}{getRoleName(user.role)}{user.email}{user.regTime}{user.status === 'active' ? '正常' : '禁用'} -
    - - - - -
    -
    -
    -

    显示 {filteredUsers.length} 条,共 {users.length} 条

    -
    - - setModal(null)} title={modal === 'add' ? '添加新用户' : '编辑用户信息'}> -
    -
    -
    - {modal === 'add' && <> -
    - } -
    - - -
    -
    - - -
    -
    -
    - - setModal(null)} title="确认删除用户"> -

    您确定要删除用户 "{userToProcess?.username}" 吗?此操作不可撤销。

    -
    - - -
    -
    - - setModal(null)} title="确认重置密码"> -

    您确定要为用户 "{userToProcess?.username}" 重置密码吗?系统将向其邮箱发送一封密码重置邮件。

    -
    - - -
    -
    - - setModal(null)} title={`确认${userToProcess?.status === 'active' ? '禁用' : '启用'}用户`}> -

    您确定要{userToProcess?.status === 'active' ? '禁用' : '启用'}用户 "{userToProcess?.username}" 吗?

    -
    - - -
    -
    - - setModal(null)} title="确认批量操作"> -

    您确定要对选中的 {selectedUserIds.size} 个用户执行 "{ {enable: '启用', disable: '禁用', delete: '删除'}[batchAction] }" 操作吗?

    -
    - - -
    -
    -
    - ); -}; diff --git a/src/springboot_demo/frontend/components/data-admin/ConnectionLogPage.tsx b/src/springboot_demo/frontend/components/data-admin/ConnectionLogPage.tsx deleted file mode 100644 index ae3f16ca..00000000 --- a/src/springboot_demo/frontend/components/data-admin/ConnectionLogPage.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { AdminModal } from '../admin/AdminModal'; -import { ConnectionLog } from '../../types'; -import { MOCK_CONNECTION_LOGS } from '../../constants'; - -export const ConnectionLogPage: React.FC = () => { - const [logs, setLogs] = useState(MOCK_CONNECTION_LOGS); - const [isExportModalOpen, setExportModalOpen] = useState(false); - const [viewingLog, setViewingLog] = useState(null); - // 新增搜索相关状态 - const [searchTerm, setSearchTerm] = useState(''); - const [searchType, setSearchType] = useState<'all' | 'time' | 'datasource' | 'status'>('all'); - - const getStatusClass = (status: '成功' | '失败') => { - return status === '成功' ? 'text-success' : 'text-danger'; - }; - - const handleExport = () => { - alert('日志已开始导出...'); - setExportModalOpen(false); - }; - - // 搜索筛选逻辑 - const filteredLogs = useMemo(() => { - if (!searchTerm.trim()) return logs; - - const term = searchTerm.toLowerCase(); - return logs.filter(log => { - switch (searchType) { - case 'time': - return new Date(log.time).toLocaleString().toLowerCase().includes(term); - case 'datasource': - return log.datasource.toLowerCase().includes(term); - case 'status': - return log.status.toLowerCase().includes(term); - default: // 'all' - return ( - new Date(log.time).toLocaleString().toLowerCase().includes(term) || - log.datasource.toLowerCase().includes(term) || - log.status.toLowerCase().includes(term) - ); - } - }); - }, [logs, searchTerm, searchType]); - - return ( -
    - {/* 修改顶部区域,添加搜索功能 */} -
    -
    - setSearchTerm(e.target.value)} - className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50" - /> - -
    - {/* 导出按钮靠右放置 */} - -
    - -
    -
    - - - - - - - - - - - {/* 使用筛选后的日志列表 */} - {filteredLogs.map(log => ( - - - - - - - ))} - {/* 无搜索结果时显示 */} - {filteredLogs.length === 0 && ( - - - - )} - -
    时间数据源状态操作
    {new Date(log.time).toLocaleString()}{log.datasource}{log.status} - {log.status === '失败' && log.details && ( - - )} -
    - 没有找到匹配的日志记录 -
    -
    -
    - - setExportModalOpen(false)} title="导出连接日志"> -
    { e.preventDefault(); handleExport(); }} className="space-y-4"> -
    - -
    - - -
    -
    -
    - -
    - - -
    -
    -
    - - -
    -
    -
    - - setViewingLog(null)} title={`连接失败详情 (${viewingLog?.datasource})`}> -
    -

    错误信息

    -
    -                        {viewingLog?.details}
    -                    
    -
    -
    - -
    -
    -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/data-admin/DataAdminDashboardPage.tsx b/src/springboot_demo/frontend/components/data-admin/DataAdminDashboardPage.tsx deleted file mode 100644 index 3d168b60..00000000 --- a/src/springboot_demo/frontend/components/data-admin/DataAdminDashboardPage.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react'; -import { DataAdminPageType } from '../../types'; -import { MOCK_DATASOURCES, MOCK_CONNECTION_LOGS, MOCK_PERMISSION_LOGS, MOCK_QUERY_LOAD } from '../../constants'; -import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'; -import { Pie, Bar } from 'react-chartjs-2'; - -ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement); - -// Reusable StatCard component specific for this dashboard -const StatCard: React.FC<{ title: string; value: string; icon: string; color: string; onClick: () => void; }> = ({ title, value, icon, color, onClick }) => ( -
    -
    -
    -

    {title}

    -

    {value}

    -
    -
    - -
    -
    -
    -); - -// Helper to format timestamp into a relative string -const formatTimeAgo = (timestamp: string): string => { - const now = new Date(); - const then = new Date(timestamp); - const diffInSeconds = Math.round((now.getTime() - then.getTime()) / 1000); - - if (diffInSeconds < 60) return `${diffInSeconds}秒前`; - const diffInMinutes = Math.round(diffInSeconds / 60); - if (diffInMinutes < 60) return `${diffInMinutes}分钟前`; - const diffInHours = Math.round(diffInMinutes / 60); - if (diffInHours < 24) return `${diffInHours}小时前`; - const diffInDays = Math.round(diffInHours / 24); - return `${diffInDays}天前`; -}; - -export const DataAdminDashboardPage: React.FC<{ setActivePage: (page: DataAdminPageType) => void; }> = ({ setActivePage }) => { - // Data processing for cards and charts - const connectedCount = MOCK_DATASOURCES.filter(ds => ds.status === 'connected').length; - const errorCount = MOCK_CONNECTION_LOGS.filter(log => log.status === '失败').length; - const pendingRequests = 3; // Mock value - - const healthStatusCounts = MOCK_DATASOURCES.reduce((acc, ds) => { - const statusMap = { connected: '已连接', disconnected: '未连接', error: '错误', testing: '测试中', disabled: '已禁用' }; - const status = statusMap[ds.status]; - acc[status] = (acc[status] || 0) + 1; - return acc; - }, {} as Record); - - const healthStatusData = { - labels: Object.keys(healthStatusCounts), - datasets: [{ - data: Object.values(healthStatusCounts), - backgroundColor: ['#00B42A', '#86909C', '#F53F3F', '#36BFFA', '#C9CDD4'], - borderWidth: 0, - }], - }; - - const queryLoadData = { - labels: MOCK_QUERY_LOAD.labels, - datasets: [{ - label: '查询量', - data: MOCK_QUERY_LOAD.data, - backgroundColor: '#165DFF', - borderRadius: 4, - }], - }; - - const recentFailures = MOCK_CONNECTION_LOGS.filter(log => log.status === '失败').slice(0, 5); - - return ( -
    - -
    - setActivePage('datasource')} /> - setActivePage('datasource')} /> - setActivePage('connection-log')} /> - setActivePage('user-permission')} /> -
    - -
    -
    -

    数据源健康状态

    -
    -
    -
    -

    数据源查询量 Top 5

    -
    -
    -
    - -
    -
    -

    近期连接失败日志

    -
    - {recentFailures.length > 0 ? recentFailures.map(log => ( -
    -
    -

    {log.datasource}: {log.note}

    -

    {log.time}

    -
    - -
    - )) :

    无失败记录

    } -
    -
    -
    -

    近期权限变更动态

    -
    - {MOCK_PERMISSION_LOGS.slice(0, 4).map(log => ( -
    - -
    -

    -

    {formatTimeAgo(log.timestamp)}

    -
    -
    - ))} -
    -
    -
    -
    - ); -}; diff --git a/src/springboot_demo/frontend/components/data-admin/DataAdminNotificationPage.tsx b/src/springboot_demo/frontend/components/data-admin/DataAdminNotificationPage.tsx deleted file mode 100644 index f32c01e9..00000000 --- a/src/springboot_demo/frontend/components/data-admin/DataAdminNotificationPage.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { AdminModal } from '../admin/AdminModal'; -import { AdminNotification } from '../../types'; -import { MOCK_DATA_ADMIN_NOTIFICATIONS } from '../../constants'; - -export const DataAdminNotificationPage: React.FC = () => { - const [notifications, setNotifications] = useState(MOCK_DATA_ADMIN_NOTIFICATIONS); - const [modal, setModal] = useState<'add' | 'edit' | 'delete' | null>(null); - const [currentItem, setCurrentItem] = useState(null); - - const sortedNotifications = useMemo(() => { - return [...notifications].sort((a, b) => (b.pinned ? 1 : -1) - (a.pinned ? 1 : -1) || new Date(b.publishTime).getTime() - new Date(a.publishTime).getTime()); - }, [notifications]); - - const openModal = (type: 'add' | 'edit' | 'delete', item?: AdminNotification) => { - setCurrentItem(item || null); - setModal(type); - }; - - const handleTogglePin = (item: AdminNotification) => { - setNotifications(prev => prev.map(n => n.id === item.id ? { ...n, pinned: !n.pinned } : n)); - }; - - const handleSave = (e: React.FormEvent) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - - if (modal === 'add') { - const newNotif: AdminNotification = { - id: Date.now(), - title: formData.get('title') as string, - content: formData.get('content') as string, - dataSourceTopic: formData.get('dataSourceTopic') as string, - role: 'all', // Data admins typically notify all or data-related roles - priority: 'normal', - pinned: formData.get('pinned') === 'on', - publisher: '数据管理员', - publishTime: new Date().toISOString().split('T')[0], - status: 'published', - }; - setNotifications(prev => [newNotif, ...prev]); - } else if (modal === 'edit' && currentItem) { - const updated = { - ...currentItem, - title: formData.get('title') as string, - content: formData.get('content') as string, - dataSourceTopic: formData.get('dataSourceTopic') as string, - pinned: formData.get('pinned') === 'on', - }; - setNotifications(prev => prev.map(n => n.id === currentItem.id ? updated : n)); - } - setModal(null); - }; - - const confirmDelete = () => { - if (currentItem) { - setNotifications(prev => prev.filter(n => n.id !== currentItem.id)); - } - setModal(null); - }; - - return ( -
    -
    - -
    - -
    -
    - - - - {sortedNotifications.map(n => ( - - - - - - - - ))} - -
    标题数据源主题发布者发布时间操作
    {n.title} {n.pinned && }{n.dataSourceTopic}{n.publisher}{n.publishTime} - - - -
    -
    -
    - - setModal(null)} title={modal === 'add' ? '发布新通知' : '编辑通知'}> -
    -
    -
    -
    -
    -
    -
    -
    - - setModal(null)} title="确认删除通知"> -

    您确定要删除通知 "{currentItem?.title}" 吗?

    -
    -
    -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/data-admin/DataSourceManagementPage.tsx b/src/springboot_demo/frontend/components/data-admin/DataSourceManagementPage.tsx deleted file mode 100644 index a3a9928d..00000000 --- a/src/springboot_demo/frontend/components/data-admin/DataSourceManagementPage.tsx +++ /dev/null @@ -1,172 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { AdminModal } from '../admin/AdminModal'; -import { DataSource } from '../../types'; -import { MOCK_DATASOURCES } from '../../constants'; - -export const DataSourceManagementPage: React.FC = () => { - const [dataSources, setDataSources] = useState(MOCK_DATASOURCES); - const [filters, setFilters] = useState({ search: '', type: '', status: '' }); - const [modal, setModal] = useState<'add' | 'edit' | 'delete' | 'confirmDisable' | null>(null); - const [currentItem, setCurrentItem] = useState(null); - - const filteredDataSources = useMemo(() => { - return dataSources.filter(ds => - ds.name.toLowerCase().includes(filters.search.toLowerCase()) && - (filters.type ? ds.type === filters.type : true) && - (filters.status ? ds.status === filters.status : true) - ); - }, [dataSources, filters]); - - const handleFilterChange = (e: React.ChangeEvent) => { - setFilters(prev => ({ ...prev, [e.target.name]: e.target.value })); - }; - - const resetFilters = () => setFilters({ search: '', type: '', status: '' }); - - const openModal = (type: 'add' | 'edit' | 'delete', item?: DataSource) => { - setCurrentItem(item || null); - setModal(type); - }; - - const handleSave = (e: React.FormEvent) => { - e.preventDefault(); - const formData = new FormData(e.currentTarget); - const data = Object.fromEntries(formData.entries()); - - if (modal === 'add') { - const newDataSource: DataSource = { - id: `ds-${Date.now()}`, - name: data.name as string, - type: data.type as any, - address: `${data.host}:${data.port}`, - status: 'disconnected', - }; - setDataSources(prev => [newDataSource, ...prev]); - } else if (modal === 'edit' && currentItem) { - setDataSources(prev => prev.map(ds => ds.id === currentItem.id ? { - ...ds, - name: data.name as string, - type: data.type as any, - address: `${data.host}:${data.port}`, - } : ds)); - } - setModal(null); - }; - - const confirmDelete = () => { - if (currentItem) { - setDataSources(prev => prev.filter(ds => ds.id !== currentItem.id)); - } - setModal(null); - }; - - const handleTestConnection = (id: string) => { - setDataSources(prev => prev.map(ds => ds.id === id ? { ...ds, status: 'testing' } : ds)); - setTimeout(() => { - setDataSources(prev => prev.map(ds => { - if (ds.id === id) { - const statuses: DataSource['status'][] = ['connected', 'error']; - return { ...ds, status: statuses[Math.floor(Math.random() * 2)] }; - } - return ds; - })); - }, 2000); - }; - - const requestToggleDisable = (dataSource: DataSource) => { - setCurrentItem(dataSource); - setModal('confirmDisable'); - }; - - const confirmToggleDisable = () => { - if (currentItem) { - setDataSources(prev => prev.map(ds => { - if (ds.id === currentItem.id) { - return { ...ds, status: ds.status === 'disabled' ? 'disconnected' : 'disabled' }; - } - return ds; - })); - } - setModal(null); - }; - - const getStatusChip = (status: DataSource['status']) => { - const styles = { - connected: 'bg-success/10 text-success', - disconnected: 'bg-gray-100 text-gray-600', - error: 'bg-danger/10 text-danger', - testing: 'bg-blue-100 text-blue-600 animate-pulse', - disabled: 'bg-gray-200 text-gray-700' - }; - const text = { - connected: '已连接', - disconnected: '未连接', - error: '连接错误', - testing: '测试中...', - disabled: '已禁用' - }; - return {text[status]} - }; - - return ( -
    - - -
    -
    -
    -
    -
    -
    - -
    -
    - -
    -
    - - - - {filteredDataSources.map(ds => ( - - - - - ))} - -
    数据源名称数据库类型连接地址状态操作
    {ds.name}{ds.type}{ds.address}{getStatusChip(ds.status)} - - - - -
    -
    -
    - - setModal(null)} title={modal === 'add' ? '添加数据源' : '编辑数据源'}> -
    -
    -
    -
    -
    -
    -
    -
    - - setModal(null)} title="确认删除数据源"> -

    您确定要删除数据源 "{currentItem?.name}" 吗?此操作不可撤销。

    -
    -
    - - setModal(null)} title={`确认${currentItem?.status === 'disabled' ? '启用' : '禁用'}数据源`}> -

    您确定要{currentItem?.status === 'disabled' ? '启用' : '禁用'}数据源 "{currentItem?.name}" 吗?

    -
    - - -
    -
    -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/components/data-admin/UserPermissionPage.tsx b/src/springboot_demo/frontend/components/data-admin/UserPermissionPage.tsx deleted file mode 100644 index 4bbab325..00000000 --- a/src/springboot_demo/frontend/components/data-admin/UserPermissionPage.tsx +++ /dev/null @@ -1,365 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { AdminModal } from '../admin/AdminModal'; -import { UserPermissionAssignment, UnassignedUser, DataSourcePermission } from '../../types'; - -const MOCK_UNASSIGNED_USERS: UnassignedUser[] = [ - { id: 'user-3', username: '赵文琪', email: 'zhaoliu@example.com', regTime: '2024-08-01' }, - { id: 'user-4', username: '孙七', email: 'sunqi@example.com', regTime: '2024-08-05' }, -]; - -const MOCK_ASSIGNED_PERMISSIONS: UserPermissionAssignment[] = [ - { - id: 'perm-1', - userId: 'user-1', - username: '李瑜清', - permissions: [ - { dataSourceId: 'ds-1', dataSourceName: '销售数据库', tables: ['orders', 'customers'] }, - { dataSourceId: 'ds-2', dataSourceName: '产品数据库', tables: ['products'] }, - ] - }, - { - id: 'perm-2', - userId: 'user-2', - username: '马芳琼', - permissions: [ - { dataSourceId: 'ds-1', dataSourceName: '销售数据库', tables: ['orders'] } - ] - }, -]; - -const MOCK_DATASOURCES = [ - { id: 'ds-1', name: '销售数据库', tables: ['orders', 'customers', 'sales_report', 'inventory'] }, - { id: 'ds-2', name: '产品数据库', tables: ['products', 'categories', 'reviews'] }, - { id: 'ds-3', name: '用户数据库', tables: ['users', 'profiles', 'login_history'] }, -]; - -// 定义支持的搜索类别 -type SearchCategory = 'all' | 'username' | 'email' | 'datasource' | 'table'; - -export const UserPermissionPage: React.FC = () => { - const [unassignedUsers, setUnassignedUsers] = useState(MOCK_UNASSIGNED_USERS); - const [assignedPermissions, setAssignedPermissions] = useState(MOCK_ASSIGNED_PERMISSIONS); - const [selectedUserIds, setSelectedUserIds] = useState>(new Set()); - const [modal, setModal] = useState<'assign' | 'manage' | null>(null); - const [currentItem, setCurrentItem] = useState(null); - const [usersToAssign, setUsersToAssign] = useState([]); - const [searchKeyword, setSearchKeyword] = useState(''); - const [searchCategory, setSearchCategory] = useState('all'); - - // 2. 优化:按「搜索类别+关键词」过滤待分配用户 - const filteredUnassignedUsers = useMemo(() => { - const keyword = searchKeyword.toLowerCase().trim(); - if (!keyword) return unassignedUsers; - - return unassignedUsers.filter(user => { - switch (searchCategory) { - case 'username': - return user.username.toLowerCase().includes(keyword); - case 'email': - return user.email.toLowerCase().includes(keyword); - case 'all': - return user.username.toLowerCase().includes(keyword) || user.email.toLowerCase().includes(keyword); - default: - return false; - } - }); - }, [unassignedUsers, searchKeyword, searchCategory]); - - // 3. 优化:按「搜索类别+关键词」过滤已分配用户 - const filteredAssignedPermissions = useMemo(() => { - const keyword = searchKeyword.toLowerCase().trim(); - if (!keyword) return assignedPermissions; - - return assignedPermissions.filter(assignment => { - switch (searchCategory) { - case 'username': - return assignment.username.toLowerCase().includes(keyword); - case 'email': // 已分配用户无邮箱字段,不匹配 - return false; - case 'datasource': - return assignment.permissions.some(perm => perm.dataSourceName.toLowerCase().includes(keyword)); - case 'table': - return assignment.permissions.some(perm => perm.tables.some(table => table.toLowerCase().includes(keyword))); - case 'all': // 全部:匹配用户名/数据源/表名 - return assignment.username.toLowerCase().includes(keyword) || - assignment.permissions.some(perm => perm.dataSourceName.toLowerCase().includes(keyword)) || - assignment.permissions.some(perm => perm.tables.some(table => table.toLowerCase().includes(keyword))); - default: - return false; - } - }); - }, [assignedPermissions, searchKeyword, searchCategory]); - - const handleSelectAll = (e: React.ChangeEvent) => { - if (e.target.checked) { - setSelectedUserIds(new Set(filteredUnassignedUsers.map(u => u.id))); - } else { - setSelectedUserIds(new Set()); - } - }; - - const handleSelectUser = (id: string, checked: boolean) => { - const newSet = new Set(selectedUserIds); - if (checked) newSet.add(id); - else newSet.delete(id); - setSelectedUserIds(newSet); - }; - - const openAssignModal = (users: UnassignedUser[]) => { - if (users.length === 0) return; - setUsersToAssign(users); - setCurrentItem(null); - setModal('assign'); - }; - - const openManageModal = (permission: UserPermissionAssignment) => { - setCurrentItem(permission); - setModal('manage'); - }; - - const handleSavePermissions = (userIds: string[], permissions: DataSourcePermission[]) => { - if (modal === 'assign') { - const newAssignments: UserPermissionAssignment[] = userIds.map(id => { - const user = unassignedUsers.find(u => u.id === id); - return { - id: `perm-${Date.now()}-${id}`, - userId: id, - username: user?.username || '未知用户', - permissions: permissions.filter(p => p.tables.length > 0) - }; - }); - - setAssignedPermissions(prev => [...prev, ...newAssignments]); - setUnassignedUsers(prev => prev.filter(u => !userIds.includes(u.id))); - setSelectedUserIds(new Set()); - } else if (modal === 'manage' && currentItem) { - setAssignedPermissions(prev => prev.map(p => p.id === currentItem.id ? { - ...p, - permissions: permissions.filter(p => p.tables.length > 0) - } : p)); - } - - setModal(null); - }; - - const PermissionModal: React.FC<{ - users: { id: string, username: string }[]; - existingPermissions?: DataSourcePermission[]; - onSave: (userIds: string[], permissions: DataSourcePermission[]) => void; - onClose: () => void; - }> = ({ users, existingPermissions = [], onSave, onClose }) => { - const [perms, setPerms] = useState( - MOCK_DATASOURCES.map(ds => { - const existing = existingPermissions.find(p => p.dataSourceId === ds.id); - return { - dataSourceId: ds.id, - dataSourceName: ds.name, - tables: existing ? [...existing.tables] : [] - }; - }) - ); - - const handleTableToggle = (dsId: string, table: string, checked: boolean) => { - setPerms(prev => prev.map(p => { - if (p.dataSourceId === dsId) { - const newTables = new Set(p.tables); - if (checked) newTables.add(table); - else newTables.delete(table); - return { ...p, tables: Array.from(newTables) }; - } - return p; - })); - }; - - const handleSelectAllTables = (dsId: string, checked: boolean) => { - const ds = MOCK_DATASOURCES.find(d => d.id === dsId); - if(!ds) return; - setPerms(prev => prev.map(p => p.dataSourceId === dsId ? { ...p, tables: checked ? ds.tables : [] } : p)); - }; - - const title = users.length > 1 ? `为 ${users.length} 位用户分配权限` : `为 ${users[0].username} 分配权限`; - const isEditing = existingPermissions.length > 0; - - return ( - -
    - {MOCK_DATASOURCES.map(ds => { - const currentPerm = perms.find(p => p.dataSourceId === ds.id); - const allTablesForDs = ds.tables; - const allSelected = currentPerm ? currentPerm.tables.length === allTablesForDs.length : false; - - return ( -
    -

    {ds.name}

    -
    - -
    - {allTablesForDs.map(table => ( - - ))} -
    -
    -
    - ) - })} -
    -
    - - -
    -
    - ); - }; - - return ( -
    -
    - - setSearchKeyword(e.target.value)} - className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50" - /> -
    - -
    -
    -

    待分配权限用户 ({filteredUnassignedUsers.length})

    - -
    -
    - - - - - - - - - - - - {filteredUnassignedUsers.map(user => ( - - - - - - - - ))} - {filteredUnassignedUsers.length === 0 && ( - - - - )} - -
    - 0 && selectedUserIds.size === filteredUnassignedUsers.length} - /> - 用户名邮箱注册时间操作
    - handleSelectUser(user.id, e.target.checked)} - /> - {user.username}{user.email}{user.regTime} - -
    未找到匹配的待分配用户
    -
    -
    - -
    -

    已分配权限用户 ({filteredAssignedPermissions.length})

    -
    - - - - - - - - - - {filteredAssignedPermissions.map(p => ( - - - - - - ))} - {filteredAssignedPermissions.length === 0 && ( - - - - )} - -
    用户名数据源权限操作
    {p.username} - {p.permissions.map(perm => ( -
    - {perm.dataSourceName}: - {perm.tables.join(', ')} -
    - ))} -
    - -
    未找到匹配的已分配权限用户
    -
    -
    - - {(modal === 'assign' && usersToAssign.length > 0) && ( - setModal(null)} - /> - )} - - {(modal === 'manage' && currentItem) && ( - setModal(null)} - /> - )} -
    - ); -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/constants.ts b/src/springboot_demo/frontend/constants.ts deleted file mode 100644 index 560457e5..00000000 --- a/src/springboot_demo/frontend/constants.ts +++ /dev/null @@ -1,270 +0,0 @@ -import { Conversation, QueryResultData, Notification, UserProfile, Friend, FriendRequest, ModelOption, AdminNotification, SystemLog, DataSource, ConnectionLog, PermissionLog, QueryShare } from './types'; - -// Used in QueryPage.tsx -export const MODEL_OPTIONS: ModelOption[] = [ - { name: 'gemini-2.5-pro', disabled: false, description: '最强大的模型,用于复杂查询' }, - { name: 'gemini-2.5-flash', disabled: false, description: '速度最快的模型,用于快速响应' }, - { name: 'GPT-4', disabled: false, description: 'OpenAI 的先进模型' }, - { name: 'GLM-4.6', disabled: false, description: '智谱AI GLM-4 (即将支持)' }, - { name: 'qwen3-max', disabled: false, description: '阿里通义千问 (即将支持)' }, - { name: 'kimi-k2-0905-preview', disabled: false, description: 'kimi-k2-0905-preview K2 (即将支持)' }, -]; - -export const DATABASE_OPTIONS: ModelOption[] = [ - { name: '销售数据库', disabled: false, description: '包含订单、客户和销售数据' }, - { name: '用户数据库', disabled: false, description: '包含用户信息和活动日志' }, - { name: '产品数据库', disabled: false, description: '包含产品目录和库存信息' }, -]; - - -// Mocks for normal user view -export const MOCK_INITIAL_CONVERSATION: Conversation = { - id: 'conv-1', - title: '', - messages: [{ - role: 'ai', - content: '您好!我是数据查询助手,您可以通过自然语言描述您的查询需求(例如:"展示2023年各季度的订单量"),我会为您生成相应的结果。' - }], - createTime: new Date().toISOString(), -}; - -const MOCK_QUERY_RESULT_1: QueryResultData = { - id: 'query-1', - userPrompt: '展示2023年各季度的订单量', - sqlQuery: "SELECT strftime('%Y-Q', order_date) as quarter, COUNT(order_id) as order_count FROM orders WHERE strftime('%Y', order_date) = '2023' GROUP BY quarter ORDER BY quarter;", - conversationId: 'conv-1', - queryTime: new Date('2023-11-20T10:30:00Z').toISOString(), - executionTime: '0.8秒', - tableData: { - headers: ['季度', '订单量', '同比增长'], - rows: [ - ['2023-Q1', '1,200', '+15%'], - ['2023-Q2', '1,550', '+18%'], - ['2023-Q3', '1,400', '+12%'], - ['2023-Q4', '1,850', '+25%'] - ] - }, - chartData: { - type: 'bar', - labels: ['2023-Q1', '2023-Q2', '2023-Q3', '2023-Q4'], - datasets: [{ - label: '订单量', - data: [1200, 1550, 1400, 1850], - backgroundColor: 'rgba(22, 93, 255, 0.6)', - }] - }, - database:"销售数据库", - model:"gemini-2.5-pro", -}; - -const MOCK_QUERY_RESULT_2: QueryResultData = { - id: 'query-2', - userPrompt: '展示2023年各季度的订单量', // Same prompt - sqlQuery: "SELECT strftime('%Y-Q', order_date) as quarter, COUNT(order_id) as order_count FROM orders WHERE strftime('%Y', order_date) = '2023' GROUP BY quarter ORDER BY quarter;", - conversationId: 'conv-2', // Different conversation - queryTime: new Date('2023-11-21T11:00:00Z').toISOString(), // Later time - executionTime: '0.9秒', - tableData: { - headers: ['季度', '订单量', '同比增长'], - rows: [ - // ['2023-Q1', '1,200', '+15%'] is now deleted - ['2023-Q2', '1,600', '+20%'], // Changed value - ['2023-Q3', '1,400', '+12%'], // Same value - ['2023-Q4', '1,850', '+25%'], // Same value - ['2023-Q5 (预测)', '2,100', '+30%'] // Added row - ] - }, - chartData: { - type: 'bar', - labels: ['2023-Q2', '2023-Q3', '2023-Q4', '2023-Q5 (预测)'], // Changed labels - datasets: [{ - label: '订单量', - data: [1600, 1400, 1850, 2100], // Changed data - backgroundColor: 'rgba(54, 162, 235, 0.6)', - }] - }, - database:"销售数据库", - model:"qwen3-max", -}; - -const MOCK_QUERY_RESULT_3: QueryResultData = { - id: 'query-3', - userPrompt: '统计每月新增用户数', // A different prompt - sqlQuery: "SELECT strftime('%Y-%m', registration_date) as month, COUNT(user_id) as new_users FROM users GROUP BY month;", - conversationId: 'conv-3', - queryTime: new Date('2023-11-22T09:00:00Z').toISOString(), - executionTime: '1.1秒', - tableData: { - headers: ['月份', '新增用户数'], - rows: [ - ['2023-09', '5,200'], - ['2023-10', '6,100'], - ] - }, - chartData: { - type: 'line', - labels: ['2023-09', '2023-10'], - datasets: [{ - label: '新增用户数', - data: [5200, 6100], - backgroundColor: 'rgba(75, 192, 192, 0.6)', - }] - }, - database:"产品数据库", - model:"qwen3-max", -} - -const MOCK_QUERY_RESULT_4: QueryResultData = { - id: 'query-4', - userPrompt: '各产品线销售额占比', - sqlQuery: "SELECT product_line, SUM(sales) as total_sales FROM sales_by_product_line GROUP BY product_line;", - conversationId: 'conv-4', - queryTime: new Date('2023-11-23T14:00:00Z').toISOString(), - executionTime: '0.7秒', - tableData: { - headers: ['产品线', '销售额', '占比'], - rows: [ - ['电子产品', '550,000', '55%'], - ['家居用品', '250,000', '25%'], - ['服装配饰', '200,000', '20%'] - ] - }, - chartData: { - type: 'pie', - labels: ['电子产品', '家居用品', '服装配饰'], - datasets: [{ - label: '销售额', - data: [550000, 250000, 200000], - backgroundColor: [ - 'rgba(22, 93, 255, 0.7)', - 'rgba(54, 162, 235, 0.7)', - 'rgba(255, 206, 86, 0.7)', - ], - }] - }, - database:"用户数据库", - model:"qwen3-max", - -}; - - -export const MOCK_SAVED_QUERIES: QueryResultData[] = [MOCK_QUERY_RESULT_1, MOCK_QUERY_RESULT_2, MOCK_QUERY_RESULT_3, MOCK_QUERY_RESULT_4]; - - -export const MOCK_NOTIFICATIONS: Notification[] = [ - { id: '3', type: 'system', title: '系统将在今晚2点进行维护。', content: '系统将在今晚2点进行维护。', timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), isRead: false, isPinned: true }, - { id: '1', type: 'system', title: '新用户 王小明 已注册。', content: '新用户 王小明 已注册。', timestamp: new Date(Date.now() - 5 * 60 * 1000).toISOString(), isRead: false, isPinned: false }, - { id: '2', type: 'system', title: '模型 Gemini 连接失败。', content: '模型 Gemini 连接失败。', timestamp: new Date(Date.now() - 60 * 60 * 1000).toISOString(), isRead: false, isPinned: false }, - { id: 'share-1', type: 'share', title: '李琪雯 分享了一个查询给你', content: '"展示2023年各季度的订单量"', timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), isRead: false, isPinned: false, fromUser: { name: '李琪雯', avatarUrl: 'https://i.pravatar.cc/150?u=li-si' }, relatedShareId: 'share-1' }, -]; - -export const MOCK_USER_PROFILE: UserProfile = { - id: 'user-001', - userId: 'zhangsan', - name: '李瑜清', - email: 'zhangsan@example.com', - phoneNumber: '13812345678', - avatarUrl: 'https://i.pravatar.cc/150?u=zhang-san', - registrationDate: '2024-03-22', - accountStatus: 'normal', - preferences: { - defaultModel: 'gemini-2.5-pro', - defaultDatabase: '销售数据库', - }, -}; - -export const MOCK_FRIENDS_LIST: Friend[] = [{ id: 'friend-1', name: '李琪雯', avatarUrl: 'https://i.pravatar.cc/150?u=li-si', isOnline: true,email:'Maem12129@gmail.com'} - , - { id: 'friend-2', name: '马芳琼', avatarUrl: 'https://i.pravatar.cc/150?u=wang-wu', isOnline: false,email:'DonQuixote@gmail.com' }, -]; - -export const MOCK_FRIEND_REQUESTS: FriendRequest[] = [ - { id: 'req-1', fromUser: { name: '赵文琪', avatarUrl: 'https://i.pravatar.cc/150?u=zhao-liu' }, timestamp: '2小时前' } -]; - -export const MOCK_QUERY_SHARES: QueryShare[] = [ - { - id: 'share-1', - sender: { id: 'friend-1', name: '李琪雯', avatarUrl: 'https://i.pravatar.cc/150?u=li-si', isOnline: true ,email:'Maem12129@gmail.com'}, - recipientId: 'user-001', - querySnapshot: MOCK_QUERY_RESULT_1, - timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), - status: 'unread', - }, - { - id: 'share-2', - sender: { id: 'friend-2', name: '马芳琼', avatarUrl: 'https://i.pravatar.cc/150?u=wang-wu', isOnline: false ,email:'DonQuixote@gmail.com'}, - recipientId: 'user-001', - querySnapshot: MOCK_QUERY_RESULT_3, - timestamp: new Date(Date.now() - 28 * 60 * 60 * 1000).toISOString(), - status: 'read', - } -]; - -// Used in RightSidebar.tsx -export const COMMON_RECOMMENDATIONS = [ - '近7天用户增长趋势', - '上个季度各产品线销售额对比', - '查询华东地区销量最高的产品', - '统计每月新增用户数' -]; - -export const MOCK_SUCCESS_SUGGESTIONS = [ - '按地区细分订单量', - '与去年同期数据进行对比', - '分析各季度订单的平均金额', -]; - -export const MOCK_FAILURE_SUGGESTIONS = [ - '换一种更简单的问法', - '检查是否选择了正确的数据源', - '尝试询问“你能做什么?”', -]; - -// Mocks for Admin Panel -export const MOCK_ADMIN_NOTIFICATIONS: AdminNotification[] = [ - { id: 1, title: '系统将于今晚23:00进行升级维护', content: '...', role: 'all', priority: 'urgent', pinned: true, publisher: '系统管理员', publishTime: '2025-10-28 18:00', status: 'published' }, - { id: 2, title: '【草稿】新功能发布预告', content: '...', role: 'normal-user', priority: 'normal', pinned: false, publisher: '系统管理员', publishTime: '2025-10-27 09:00', status: 'draft' }, -]; - -export const MOCK_DATA_ADMIN_NOTIFICATIONS: AdminNotification[] = [ - { id: 1, title: '销售数据库表结构变更', content: '`orders` 表新增 `discount_rate` 字段。', role: 'data-admin', priority: 'important', pinned: false, publisher: '李琪雯', publishTime: '2025-10-28 10:00', status: 'published', dataSourceTopic: '销售数据库' }, - { id: 2, title: '用户数据库计划下线旧表', content: '`users_old` 表将于11月30日下线,请及时迁移。', role: 'all', priority: 'normal', pinned: false, publisher: '李琪雯', publishTime: '2025-10-26 15:00', status: 'published', dataSourceTopic: '用户数据库' }, -]; - -export const MOCK_SYSTEM_LOGS: SystemLog[] = [ - { id: '#LOG001', time: '2025-10-29 14:32:18', user: '李瑜清', action: '执行自然语言查询', model: 'gemini-2.5-pro', ip: '192.168.1.102', status: 'success' }, - { id: '#LOG002', time: '2025-10-29 14:28:45', user: '李琪雯', action: '添加MySQL数据源', model: '-', ip: '192.168.1.105', status: 'success' }, - { id: '#LOG003', time: '2025-10-29 14:25:10', user: '李瑜清', action: '执行自然语言查询', model: 'GPT-4', ip: '192.168.1.102', status: 'failure', details: 'Error: API call to OpenAI failed with status 429 - Too Many Requests. Please check your plan and billing details.' }, - { id: '#LOG004', time: '2025-10-29 13:50:21', user: '未知用户', action: '尝试登录系统', model: '-', ip: '203.0.113.45', status: 'failure', details: 'Authentication failed: Invalid credentials provided for user "unknown".' }, - { id: '#LOG005', time: '2025-10-29 12:15:05', user: 'admin', action: '更新用户角色', model: '-', ip: '127.0.0.1', status: 'success' }, -]; - -// Mocks for Data Admin Panel -export const MOCK_DATASOURCES: DataSource[] = [ - { id: 'ds-1', name: '销售数据库', type: 'MySQL', address: '192.168.1.101:3306', status: 'connected' }, - { id: 'ds-2', name: '用户数据库', type: 'PostgreSQL', address: '192.168.1.102:5432', status: 'connected' }, - { id: 'ds-3', name: '产品数据库', type: 'MySQL', address: '192.168.1.103:3306', status: 'error' }, - { id: 'ds-4', name: '日志数据库', type: 'SQL Server', address: '192.168.1.104:1433', status: 'disconnected' }, - { id: 'ds-5', name: '库存数据库', type: 'Oracle', address: '192.168.1.105:1521', status: 'disabled' }, -]; - -export const MOCK_CONNECTION_LOGS: ConnectionLog[] = [ - { id: 'log-1', time: '2023-06-15 16:42:30', datasource: '销售数据库', status: '成功' }, - { id: 'log-2', time: '2023-06-15 14:20:15', datasource: '产品数据库', status: '失败', details: "Error: Connection timed out after 15000ms. Could not connect to 192.168.1.103:3306. Please check network connectivity and firewall rules." }, - { id: 'log-3', time: '2023-06-14 11:05:42', datasource: '用户数据库', status: '成功' }, - { id: 'log-4', time: '2023-06-13 09:30:18', datasource: '日志数据库', status: '失败', details: "SQLSTATE[28000]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Login failed for user 'log_reader'." }, - { id: 'log-5', time: new Date(Date.now() - 2 * 60 * 1000).toISOString(), datasource: '产品数据库', status: '失败', details: "Error: Access denied for user 'prod_user'@'localhost' (using password: YES)" } -]; - -export const MOCK_PERMISSION_LOGS: PermissionLog[] = [ - { id: 'plog-1', timestamp: new Date(Date.now() - 5 * 60 * 1000).toISOString(), text: '管理员 李琪雯 授予 李瑜清 "销售数据库" 的访问权限。' }, - { id: 'plog-2', timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), text: '管理员 李琪雯 撤销了 马芳琼 对 "产品数据库" 的所有权限。' }, - { id: 'plog-3', timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), text: '管理员 李琪雯 修改了 李瑜清 对 "销售数据库" 的 orders 表权限。' }, - { id: 'plog-4', timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), text: '系统自动为新用户 赵文琪 分配了默认权限。' }, -]; - -export const MOCK_QUERY_LOAD = { - labels: ['销售数据库', '用户数据库', '产品数据库', '日志数据库', '库存数据库'], - data: [1250, 890, 650, 320, 150] -}; \ No newline at end of file diff --git a/src/springboot_demo/frontend/env.d.ts b/src/springboot_demo/frontend/env.d.ts deleted file mode 100644 index 391b89c9..00000000 --- a/src/springboot_demo/frontend/env.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -/// - -interface ImportMetaEnv { - // 与 .env.local 中的变量一一对应 - readonly VITE_GEMINI_API_KEY: string; - readonly VITE_OPENAI_API_KEY: string; - readonly VITE_GLM_API_KEY: string; - readonly VITE_QWEN_API_KEY: string; - readonly VITE_KIMI_API_KEY: string; -} - -interface ImportMeta { - readonly env: ImportMetaEnv; -} \ No newline at end of file diff --git a/src/springboot_demo/frontend/index.html b/src/springboot_demo/frontend/index.html deleted file mode 100644 index 749fd6d4..00000000 --- a/src/springboot_demo/frontend/index.html +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - 自然语言数据库查询系统 - - - - - - - - -
    - - - \ No newline at end of file diff --git a/src/springboot_demo/frontend/index.tsx b/src/springboot_demo/frontend/index.tsx deleted file mode 100644 index aaa0c6e4..00000000 --- a/src/springboot_demo/frontend/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ - -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; - -const rootElement = document.getElementById('root'); -if (!rootElement) { - throw new Error("Could not find root element to mount to"); -} - -const root = ReactDOM.createRoot(rootElement); -root.render( - - - -); diff --git a/src/springboot_demo/frontend/metadata.json b/src/springboot_demo/frontend/metadata.json deleted file mode 100644 index c73209e9..00000000 --- a/src/springboot_demo/frontend/metadata.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "Natural Language Database Query System", - "description": "A web application that allows users to query databases using natural language. It provides a chat-based interface, displays results in tables and charts, and manages conversation history.", - "requestFramePermissions": [] -} \ No newline at end of file diff --git a/src/springboot_demo/frontend/package-lock.json b/src/springboot_demo/frontend/package-lock.json deleted file mode 100644 index 1ec8261f..00000000 --- a/src/springboot_demo/frontend/package-lock.json +++ /dev/null @@ -1,2630 +0,0 @@ -{ - "name": "natural-language-database-query-system", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "natural-language-database-query-system", - "version": "0.0.0", - "dependencies": { - "@google/genai": "^1.28.0", - "chart.js": "^4.5.1", - "openai": "^6.8.1", - "react": "^19.2.0", - "react-chartjs-2": "^5.3.1", - "react-dom": "^19.2.0" - }, - "devDependencies": { - "@types/node": "^22.14.0", - "@vitejs/plugin-react": "^5.0.0", - "typescript": "~5.8.2", - "vite": "^6.2.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@google/genai": { - "version": "1.28.0", - "resolved": "https://registry.npmmirror.com/@google/genai/-/genai-1.28.0.tgz", - "integrity": "sha512-0pfZ1EWQsM9kINsL+mFKJvpzM6NRHS9t360S1MzKq4JtIwTj/RbsPpC/K5wpKiPy9PC+J+bsz/9gvaL51++KrA==", - "license": "Apache-2.0", - "dependencies": { - "google-auth-library": "^10.3.0", - "ws": "^8.18.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.20.1" - }, - "peerDependenciesMeta": { - "@modelcontextprotocol/sdk": { - "optional": true - } - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@kurkle/color": { - "version": "0.3.4", - "resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.4.tgz", - "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", - "license": "MIT" - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.43", - "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz", - "integrity": "sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", - "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", - "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", - "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", - "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", - "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", - "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", - "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", - "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", - "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", - "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", - "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", - "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", - "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", - "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", - "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", - "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", - "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", - "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", - "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", - "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", - "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", - "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmmirror.com/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmmirror.com/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "22.19.0", - "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.0.tgz", - "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "5.1.0", - "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-5.1.0.tgz", - "integrity": "sha512-4LuWrg7EKWgQaMJfnN+wcmbAW+VSsCmqGohftWjuct47bv8uE4n/nPpq4XjJPsxgq00GGG5J8dvBczp8uxScew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.4", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.43", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.23", - "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.23.tgz", - "integrity": "sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/browserslist": { - "version": "4.27.0", - "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.27.0.tgz", - "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "baseline-browser-mapping": "^2.8.19", - "caniuse-lite": "^1.0.30001751", - "electron-to-chromium": "^1.5.238", - "node-releases": "^2.0.26", - "update-browserslist-db": "^1.1.4" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001753", - "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", - "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chart.js": { - "version": "4.5.1", - "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.5.1.tgz", - "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=8" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.244", - "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.244.tgz", - "integrity": "sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/gaxios": { - "version": "7.1.3", - "resolved": "https://registry.npmmirror.com/gaxios/-/gaxios-7.1.3.tgz", - "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2", - "rimraf": "^5.0.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/google-auth-library": { - "version": "10.5.0", - "resolved": "https://registry.npmmirror.com/google-auth-library/-/google-auth-library-10.5.0.tgz", - "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.0.0", - "gcp-metadata": "^8.0.0", - "google-logging-utils": "^1.0.0", - "gtoken": "^8.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.2", - "resolved": "https://registry.npmmirror.com/google-logging-utils/-/google-logging-utils-1.1.2.tgz", - "integrity": "sha512-YsFPGVgDFf4IzSwbwIR0iaFJQFmR5Jp7V1WuYSjuRgAm9yWqsMhKE9YPlL+wvFLnc/wMiFV4SQUD9Y/JMpxIxQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/gtoken": { - "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/gtoken/-/gtoken-8.0.0.tgz", - "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", - "license": "MIT", - "dependencies": { - "gaxios": "^7.0.0", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/openai": { - "version": "6.8.1", - "resolved": "https://registry.npmmirror.com/openai/-/openai-6.8.1.tgz", - "integrity": "sha512-ACifslrVgf+maMz9vqwMP4+v9qvx5Yzssydizks8n+YUJ6YwUoxj51sKRQ8HYMfR6wgKLSIlaI108ZwCk+8yig==", - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "license": "BlueOak-1.0.0" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/react": { - "version": "19.2.0", - "resolved": "https://registry.npmmirror.com/react/-/react-19.2.0.tgz", - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-chartjs-2": { - "version": "5.3.1", - "resolved": "https://registry.npmmirror.com/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", - "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", - "license": "MIT", - "peerDependencies": { - "chart.js": "^4.1.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.0", - "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.0.tgz", - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.0" - } - }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rollup": { - "version": "4.52.5", - "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.52.5.tgz", - "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.52.5", - "@rollup/rollup-android-arm64": "4.52.5", - "@rollup/rollup-darwin-arm64": "4.52.5", - "@rollup/rollup-darwin-x64": "4.52.5", - "@rollup/rollup-freebsd-arm64": "4.52.5", - "@rollup/rollup-freebsd-x64": "4.52.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", - "@rollup/rollup-linux-arm-musleabihf": "4.52.5", - "@rollup/rollup-linux-arm64-gnu": "4.52.5", - "@rollup/rollup-linux-arm64-musl": "4.52.5", - "@rollup/rollup-linux-loong64-gnu": "4.52.5", - "@rollup/rollup-linux-ppc64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-gnu": "4.52.5", - "@rollup/rollup-linux-riscv64-musl": "4.52.5", - "@rollup/rollup-linux-s390x-gnu": "4.52.5", - "@rollup/rollup-linux-x64-gnu": "4.52.5", - "@rollup/rollup-linux-x64-musl": "4.52.5", - "@rollup/rollup-openharmony-arm64": "4.52.5", - "@rollup/rollup-win32-arm64-msvc": "4.52.5", - "@rollup/rollup-win32-ia32-msvc": "4.52.5", - "@rollup/rollup-win32-x64-gnu": "4.52.5", - "@rollup/rollup-win32-x64-msvc": "4.52.5", - "fsevents": "~2.3.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.1.4", - "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", - "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmmirror.com/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - } - } -} diff --git a/src/springboot_demo/frontend/package.json b/src/springboot_demo/frontend/package.json deleted file mode 100644 index 54b7bc61..00000000 --- a/src/springboot_demo/frontend/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "natural-language-database-query-system", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "@google/genai": "^1.28.0", - "chart.js": "^4.5.1", - "openai": "^6.8.1", - "react": "^19.2.0", - "react-chartjs-2": "^5.3.1", - "react-dom": "^19.2.0" - }, - "devDependencies": { - "@types/node": "^22.14.0", - "@vitejs/plugin-react": "^5.0.0", - "typescript": "~5.8.2", - "vite": "^6.2.0" - } -} diff --git a/src/springboot_demo/frontend/services/api.ts b/src/springboot_demo/frontend/services/api.ts deleted file mode 100644 index 73aeccc7..00000000 --- a/src/springboot_demo/frontend/services/api.ts +++ /dev/null @@ -1,246 +0,0 @@ -// API 基础配置 -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'; - -// 从localStorage获取token -const getToken = (): string | null => { - return localStorage.getItem('token'); -}; - -// 从localStorage获取userId -const getUserId = (): string | null => { - return localStorage.getItem('userId'); -}; - -// 通用请求函数 -async function request( - endpoint: string, - options: RequestInit = {} -): Promise { - const token = getToken(); - const userId = getUserId(); - - const headers: HeadersInit = { - 'Content-Type': 'application/json', - ...options.headers, - }; - - // 添加认证token - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - - // 添加userId header(如果后端需要) - if (userId) { - headers['userId'] = userId; - } - - const response = await fetch(`${API_BASE_URL}${endpoint}`, { - ...options, - headers, - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({ message: '请求失败' })); - throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); - } - - const data = await response.json(); - - // 处理统一的Result格式 - if (data.code !== undefined) { - if (data.code === 200 || data.code === 0) { - return data.data as T; - } else { - throw new Error(data.message || '请求失败'); - } - } - - return data as T; -} - -// ==================== 认证接口 ==================== -export interface LoginRequest { - username: string; - password: string; -} - -export interface LoginResponse { - token: string; - userId: number; - username: string; - email: string; - roleId: number; - roleName: string; - avatarUrl: string; -} - -export const authApi = { - login: async (credentials: LoginRequest): Promise => { - const response = await request('/auth/login', { - method: 'POST', - body: JSON.stringify(credentials), - }); - - // 保存token和用户信息 - if (response.token) { - localStorage.setItem('token', response.token); - localStorage.setItem('userId', String(response.userId)); - localStorage.setItem('username', response.username); - localStorage.setItem('roleId', String(response.roleId)); - localStorage.setItem('roleName', response.roleName || ''); - } - - return response; - }, - - logout: () => { - localStorage.removeItem('token'); - localStorage.removeItem('userId'); - localStorage.removeItem('username'); - localStorage.removeItem('roleId'); - localStorage.removeItem('roleName'); - }, - - isAuthenticated: (): boolean => { - return !!getToken(); - }, -}; - -// ==================== 查询接口 ==================== -export interface QueryRequest { - userPrompt: string; - model: string; - database: string; - conversationId?: string; -} - -export interface QueryResponse { - id: string; - userPrompt: string; - sqlQuery: string; - conversationId: string; - queryTime: string; - executionTime: string; - database: string; - model: string; - tableData: { - headers: string[]; - rows: string[][]; - }; - chartData?: { - type: string; - labels: string[]; - datasets: Array<{ - label: string; - data: number[]; - backgroundColor?: string | string[]; - }>; - }; -} - -export const queryApi = { - execute: async (queryRequest: QueryRequest): Promise => { - return await request('/query/execute', { - method: 'POST', - body: JSON.stringify(queryRequest), - }); - }, -}; - -// ==================== 对话接口 ==================== -export interface DialogRecord { - dialogId: string; - userId: number; - topic: string; - totalRounds: number; - startTime: string; - lastTime: string; -} - -export const dialogApi = { - getList: async (): Promise => { - return await request('/dialog/list'); - }, - - getById: async (dialogId: string): Promise => { - return await request(`/dialog/${dialogId}`); - }, -}; - -// ==================== 用户接口 ==================== -export interface User { - id: number; - username: string; - email: string; - phonenumber: string; - roleId: number; - avatarUrl: string; - status: number; -} - -export const userApi = { - getById: async (id: number): Promise => { - return await request(`/user/${id}`); - }, - - getList: async (): Promise => { - return await request('/user/list'); - }, - - getByUsername: async (username: string): Promise => { - return await request(`/user/username/${username}`); - }, -}; - -// ==================== 数据库连接接口 ==================== -export interface DbConnection { - id: number; - name: string; - dbTypeId: number; - url: string; - username: string; - password?: string; - status: string; - createUserId: number; -} - -export const dbConnectionApi = { - getList: async (): Promise => { - return await request('/db-connection/list'); - }, - - getById: async (id: number): Promise => { - return await request(`/db-connection/${id}`); - }, - - test: async (id: number): Promise => { - return await request(`/db-connection/test/${id}`); - }, -}; - -// ==================== 大模型配置接口 ==================== -export interface LlmConfig { - id: number; - name: string; - version: string; - apiKey?: string; - apiUrl: string; - statusId: number; - isDisabled: number; - timeout: number; -} - -export const llmConfigApi = { - getList: async (): Promise => { - return await request('/llm-config/list'); - }, - - getAvailable: async (): Promise => { - return await request('/llm-config/list/available'); - }, - - getById: async (id: number): Promise => { - return await request(`/llm-config/${id}`); - }, -}; - diff --git a/src/springboot_demo/frontend/tsconfig.json b/src/springboot_demo/frontend/tsconfig.json deleted file mode 100644 index 531df722..00000000 --- a/src/springboot_demo/frontend/tsconfig.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "experimentalDecorators": true, - "useDefineForClassFields": false, - "module": "ESNext", - "lib": [ - "ES2022", - "DOM", - "DOM.Iterable" - ], - "skipLibCheck": true, - "types": [ - "node" - ], - "moduleResolution": "bundler", - "isolatedModules": true, - "moduleDetection": "force", - "allowJs": true, - "jsx": "react-jsx", - "paths": { - "@/*": [ - "./*" - ] - }, - "allowImportingTsExtensions": true, - "noEmit": true - }, - "include": ["*.d.ts", "**/*.ts", "**/*.tsx"] -} \ No newline at end of file diff --git a/src/springboot_demo/frontend/types.ts b/src/springboot_demo/frontend/types.ts deleted file mode 100644 index 00b7e4ca..00000000 --- a/src/springboot_demo/frontend/types.ts +++ /dev/null @@ -1,193 +0,0 @@ -// Basic types -export type UserRole = 'sys-admin' | 'data-admin' | 'normal-user'; -export type MessageRole = 'user' | 'ai'; - -// Page navigation types -export type Page = 'query' | 'history' | 'notifications' | 'account' | 'friends' | 'comparison'; -export type SysAdminPageType = 'dashboard' | 'user-management' | 'notification-management' | 'system-log' | 'llm-config' | 'account'; -export type DataAdminPageType = 'dashboard' | 'query' | 'history' | 'datasource' | 'user-permission' | 'notification-management' | 'connection-log' | 'notifications' | 'account' | 'friends' | 'comparison'; - -// Data structure types -export interface ModelOption { - name: string; - disabled: boolean; - description: string; -} - -export interface ChartData { - type: 'bar' | 'line' | 'pie'; - labels: string[]; - datasets: { - label: string; - data: number[]; - backgroundColor: string | string[]; - }[]; -} - -export interface TableData { - headers: string[]; - rows: string[][]; -} - -export interface QueryResultData { - id: string; - userPrompt: string; - sqlQuery: string; - conversationId: string; - queryTime: string; - executionTime: string; - tableData: TableData; - chartData: ChartData; - database: string; - model:string; -} - -export interface Message { - role: MessageRole; - content: string | QueryResultData; -} - -export interface Conversation { - id: string; - title: string; - messages: Message[]; - createTime: string; -} - -export interface Notification { - id: string; - type: 'system' | 'share'; - title: string; - content: string; - timestamp: string; - isRead: boolean; - isPinned: boolean; - fromUser?: { name: string, avatarUrl: string }; - relatedShareId?: string; -} - -export interface UserProfile { - id: string; - userId: string; - name: string; - email: string; - phoneNumber: string; - avatarUrl: string; - registrationDate: string; - accountStatus: 'normal' | 'disabled'; - preferences: { - defaultModel: string; - defaultDatabase: string; - }; -} - -export interface Friend { - id: string; - name: string; - avatarUrl: string; - isOnline: boolean; - email: string; - remark?: string; -} - -export interface FriendRequest { - id: string; - fromUser: { name: string; avatarUrl: string }; - timestamp: string; -} - -export interface QueryShare { - id: string; - sender: Friend; - recipientId: string; // The ID of the user receiving the share - querySnapshot: QueryResultData; - timestamp: string; - status: 'unread' | 'read'; -} - - -// Admin Panel Types -export interface AdminNotification { - id: number; - title: string; - content: string; - role: 'all' | UserRole; - priority: 'urgent' | 'important' | 'normal'; - pinned: boolean; - publisher: string; - publishTime: string; - status: 'published' | 'draft'; - dataSourceTopic?: string; -} - -export interface AdminUser { - id: number; - username: string; - role: UserRole; - email: string; - regTime: string; - status: 'active' | 'disabled'; -} - -export interface SystemLog { - id: string; - time: string; - user: string; - action: string; - model: string; - ip: string; - status: 'success' | 'failure'; - details?: string; -} - -export interface LLMConfig { - id: string; - name: string; - version: string; - apiKey: string; - endpoint: string; - status: 'available' | 'unstable' | 'unavailable' | 'testing' | 'disabled'; -} - -// Data Admin Types -export interface DataSource { - id: string; - name: string; - type: 'MySQL' | 'PostgreSQL' | 'Oracle' | 'SQL Server'; - address: string; - status: 'connected' | 'disconnected' | 'error' | 'testing' | 'disabled'; -} - -export interface DataSourcePermission { - dataSourceId: string; - dataSourceName: string; - tables: string[]; -} - -export interface UserPermissionAssignment { - id: string; - userId: string; - username: string; - permissions: DataSourcePermission[]; -} - -export interface UnassignedUser { - id: string; - username: string; - email: string; - regTime: string; -} - -export interface ConnectionLog { - id: string; - time: string; - datasource: string; - status: '成功' | '失败'; - details?: string; -} - -export interface PermissionLog { - id: string; - timestamp: string; - text: string; -} \ No newline at end of file diff --git a/src/springboot_demo/frontend/vite.config.ts b/src/springboot_demo/frontend/vite.config.ts deleted file mode 100644 index 427839e2..00000000 --- a/src/springboot_demo/frontend/vite.config.ts +++ /dev/null @@ -1,35 +0,0 @@ -import path from 'path'; -import { defineConfig, loadEnv } from 'vite'; -import react from '@vitejs/plugin-react'; - -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, '.', ''); // 加载环境变量 - return { - server: { - port: 3000, - host: '0.0.0.0', - proxy: { - // 代理通义千问请求(关键配置) - '/api/qwen': { - target: 'https://dashscope.aliyuncs.com/compatible-mode/v1', - changeOrigin: true, // 开启跨域代理 - rewrite: (path) => path.replace(/^\/api\/qwen/, ''), // 重写路径 - headers: { - // 注入API密钥(仅开发环境安全,生产需用后端代理) - Authorization: `Bearer ${env.VITE_QWEN_API_KEY}`, - }, - }, - }, - }, - plugins: [react()], - define: { - 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), - 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) - }, - resolve: { - alias: { - '@': path.resolve(__dirname, '.'), - } - } - }; -}); \ No newline at end of file diff --git a/src/springboot_demo/last.md b/src/springboot_demo/last.md deleted file mode 100644 index b71fb456..00000000 --- a/src/springboot_demo/last.md +++ /dev/null @@ -1,507 +0,0 @@ -### 一、完整表/集合清单(按MySQL + MongoDB分类) -#### (一)MySQL 8.4 数据表(结构化数据) -##### 1. 基础信息表(用户、角色、字典) -1.1 users(用户表) -1.2 roles(角色表) -1.3 db_types(数据库类型表) -1.4 notification_targets(通知目标表) -1.5 priorities(通知优先级表) -1.6 error_types(错误类型表) -1.7 llm_status(大模型状态表) - -##### 2. 数据资源表(数据库、表、字段元数据) -2.1 db_connections(数据库连接表) -2.2 table_metadata(表元数据表) -2.3 column_metadata(字段元数据表) - -##### 3. 权限与关系表(用户权限、好友关系) -3.1 user_db_permissions(用户数据权限表) -3.2 friend_relations(好友关系表) -3.3 friend_requests(好友请求表) -3.4 query_shares(查询分享记录表) - -##### 4. 系统日志与监控表 -4.1 system_health(系统健康表) -4.2 operation_logs(系统操作日志表) -4.3 llm_configs(大模型配置表) -4.4 notifications(通知表) -4.5 token_consume(token消耗表) -4.6 error_logs(错误分析表) -4.7 performance_metrics(性能趋势表) -4.8 db_connection_logs(数据库连接日志表) -4.9 query_logs(查询日志表) -4.10 user_searches(用户搜索表) - -#### (二)MongoDB 8.2 集合(非结构化数据) -1. query_collections(收藏查询表-组) -2. collection_records(收藏记录表-具体记录) -3. dialog_records(多轮对话列表) -4. dialog_details(多轮对话具体内容表) -5. sql_cache(SQL缓存集合) -6. ai_interaction_logs(AI交互日志集合) -7. friend_chats(好友聊天记录表) - ---- - -### 二、表/集合详细字段(严格遵循文档) -#### (一)MySQL 数据表字段 -##### 1. 基础信息表 -###### 1.1 users(用户表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|--------------|-------------|------------|--------------------------|--------------------------------------------|-----------------------------------------------------------| -| id | 用户ID | bigint(20) | 是(自增) | - | 唯一标识用户 | PRIMARY KEY (id) | -| username | 用户名 | varchar(50) | - | - | 登录账号,唯一 | UNIQUE (username), NOT NULL | -| password | 密码 | varchar(100)| - | - | BCrypt加密存储 | NOT NULL | -| email | 邮箱 | varchar(100)| - | - | 用于好友添加、通知 | UNIQUE (email), NOT NULL | -| phonenumber | 手机号 | varchar(50) | - | - | 用户绑定的手机号 | UNIQUE (phonenumber), NOT NULL | -| role_id | 角色ID | tinyint(2) | - | 是(关联roles.id) | 关联角色类型 | NOT NULL, FOREIGN KEY (role_id) REFERENCES roles(id) | -| avatar_url | 头像URL | varchar(255)| - | - | 头像路径,默认"/default-avatar.png" | DEFAULT '/default-avatar.png' | -| status | 账号状态 | tinyint(1) | - | - | 0-禁用,1-正常 | DEFAULT 1, NOT NULL | -| online_status | 在线状态 | tinyint(1) | - | - | 0-离线,1-在线 | DEFAULT 0, NOT NULL | -| create_time | 创建时间 | datetime | - | - | 账号创建时间(系统管理员创建) | DEFAULT CURRENT_TIMESTAMP, NOT NULL | -| update_time | 更新时间 | datetime | - | - | 信息更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | - -###### 1.2 roles(角色表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| -| id | 角色ID | tinyint(2) | 是(自增) | - | 唯一标识角色 | PRIMARY KEY (id) | -| role_name | 角色名称 | varchar(30) | - | - | 如"系统管理员"、"数据管理员"、"普通用户" | UNIQUE (role_name), NOT NULL | -| role_code | 角色编码 | varchar(20) | - | - | 如"sys_admin"、"data_admin"、"normal_user" | UNIQUE (role_code), NOT NULL | -| description | 角色描述 | varchar(500)| - | - | 角色权限范围说明 | - | - -###### 1.3 db_types(数据库类型表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| -| id | 类型ID | tinyint(2) | 是(自增) | - | 唯一标识数据库类型 | PRIMARY KEY (id) | -| type_name | 类型名称 | varchar(50) | - | - | 如"MySQL"、"MongoDB"、"SQL Server" | UNIQUE (type_name), NOT NULL | -| type_code | 类型编码 | varchar(20) | - | - | 如"mysql"、"mongodb"、"mssql" | UNIQUE (type_code), NOT NULL | -| description | 类型描述 | varchar(500)| - | - | 数据库特性说明 | - | - -###### 1.4 notification_targets(通知目标表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| -| id | 目标ID | tinyint(2) | 是(自增) | - | 唯一标识通知目标 | PRIMARY KEY (id) | -| target_name | 目标名称 | varchar(30) | - | - | 如"所有用户"、"普通用户" | UNIQUE (target_name), NOT NULL | -| target_code | 目标编码 | varchar(20) | - | - | 如"all"、"normal_user" | UNIQUE (target_code), NOT NULL | -| description | 目标描述 | varchar(200)| - | - | 接收范围说明 | - | - -###### 1.5 priorities(通知优先级表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| -| id | 优先级ID | tinyint(2) | 是(自增) | - | 唯一标识优先级 | PRIMARY KEY (id) | -| priority_name | 优先级名称 | varchar(20) | - | - | 如"紧急"、"普通"、"低" | UNIQUE (priority_name), NOT NULL | -| priority_code | 优先级编码 | varchar(20) | - | - | 如"urgent"、"normal"、"low" | UNIQUE (priority_code), NOT NULL | -| sort | 排序权重 | int(11) | - | - | 数值越小越优先展示(1-紧急,2-普通) | NOT NULL | - -###### 1.6 error_types(错误类型表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| -| id | 错误ID | tinyint(2) | 是(自增) | - | 唯一标识错误类型 | PRIMARY KEY (id) | -| error_name | 错误名称 | varchar(50) | - | - | 如"模型调用超时"、"数据库连接错误" | UNIQUE (error_name), NOT NULL | -| error_code | 错误编码 | varchar(50) | - | - | 如"llm_timeout"、"db_connection_error" | UNIQUE (error_code), NOT NULL | -| description | 错误描述 | varchar(500)| - | - | 错误原因说明 | - | - -###### 1.7 llm_status(大模型状态表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| -| id | 状态ID | tinyint(2) | 是(自增) | - | 唯一标识大模型状态 | PRIMARY KEY (id) | -| status_name | 状态名称 | varchar(20) | - | - | 如"可用"、"不可用"、"不稳定" | UNIQUE (status_name), NOT NULL | -| status_code | 状态编码 | varchar(20) | - | - | 如"available"、"unavailable"、"unstable" | UNIQUE (status_code), NOT NULL | -| description | 状态描述 | varchar(200)| - | - | 状态含义说明(如"可用:API成功率≥95%") | - | - -##### 2. 数据资源表 -###### 2.1 db_connections(数据库连接表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|--------------|-------------|------------|--------------------------|--------------------------------------------|-----------------------------------------------------------| -| id | 连接ID | bigint(20) | 是(自增) | - | 唯一标识数据库连接 | PRIMARY KEY (id) | -| name | 连接名称 | varchar(100)| - | - | 用户自定义名称(如"订单主库") | UNIQUE (name), NOT NULL | -| db_type_id | 数据库类型ID | tinyint(2) | - | 是(关联db_types.id) | 关联数据库类型 | NOT NULL, FOREIGN KEY (db_type_id) REFERENCES db_types(id) | -| url | 连接地址 | varchar(255)| - | - | 如"192.168.1.101:3306/orders_db" | NOT NULL | -| username | 数据库账号 | varchar(50) | - | - | 访问数据库的账号(加密存储) | NOT NULL | -| password | 数据库密码 | varchar(100)| - | - | 访问数据库的密码(加密存储) | NOT NULL | -| status | 连接状态 | varchar(20) | - | - | "connected"-已连接、"error"-连接错误、"disabled"-已禁用 | DEFAULT 'disconnected', NOT NULL | -| create_user_id | 创建者ID | bigint(20) | - | 是(关联users.id) | 仅数据管理员可创建 | NOT NULL, FOREIGN KEY (create_user_id) REFERENCES users(id) | -| create_time | 创建时间 | datetime | - | - | 连接创建时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | -| update_time | 更新时间 | datetime | - | - | 连接信息更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | - -###### 2.2 table_metadata(表元数据表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|------------------|-------------|------------|------------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 表ID | bigint(20) | 是(自增) | - | 唯一标识表元数据 | PRIMARY KEY (id) | -| db_connection_id | 数据库连接ID | bigint(20) | - | 是(关联db_connections.id) | 所属数据库连接 | NOT NULL, FOREIGN KEY (db_connection_id) REFERENCES db_connections(id) ON DELETE CASCADE | -| table_name | 表名 | varchar(100)| - | - | 数据库中实际表名(如"orders_2023") | NOT NULL | -| description | 表描述 | varchar(500)| - | - | 用于AI理解表含义(如"存储2023年订单数据") | - | -| create_time | 创建时间 | datetime | - | - | 元数据录入时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | -| update_time | 更新时间 | datetime | - | - | 元数据更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | - -###### 2.3 column_metadata(字段元数据表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|------------------|-------------|------------|----------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 字段ID | bigint(20) | 是(自增) | - | 唯一标识字段元数据 | PRIMARY KEY (id) | -| table_id | 表ID | bigint(20) | - | 是(关联table_metadata.id) | 所属表 | NOT NULL, FOREIGN KEY (table_id) REFERENCES table_metadata(id) ON DELETE CASCADE | -| column_name | 字段名 | varchar(100)| - | - | 表中实际字段名(如"order_amount") | NOT NULL | -| data_type | 数据类型 | varchar(50) | - | - | 字段数据类型(如"int"、"varchar(50)") | NOT NULL | -| description | 字段描述 | varchar(500)| - | - | 用于AI理解字段含义(如"订单金额,单位:元") | - | -| is_primary | 是否主键 | tinyint(1) | - | - | 0-否,1-是 | DEFAULT 0, NOT NULL | -| create_time | 创建时间 | datetime | - | - | 元数据录入时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | - -##### 3. 权限与关系表 -###### 3.1 user_db_permissions(用户数据权限表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 权限ID | bigint(20) | 是(自增) | - | 唯一标识权限记录 | PRIMARY KEY (id) | -| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 被授权用户(普通用户) | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | -| permission_details | 权限详情 | json | - | - | 可访问的表列表,格式:[{"db_connection_id":1, "table_ids":[1,2]},...] | NOT NULL | -| last_grant_user_id | 最后授权管理员ID | bigint(20) | - | 是(关联users.id) | 最后修改权限的数据管理员/系统管理员 | NOT NULL, FOREIGN KEY (last_grant_user_id) REFERENCES users(id) | -| is_assigned | 是否已分配 | tinyint(1) | - | - | 0-未分配,1-已分配(默认0) | DEFAULT 0, NOT NULL | -| last_grant_time | 最后授权时间 | datetime | - | - | 最后一次权限修改时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | -| update_time | 更新时间 | datetime | - | - | 记录更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | - -###### 3.2 friend_relations(好友关系表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 关系ID | bigint(20) | 是(自增) | - | 唯一标识好友关系 | PRIMARY KEY (id) | -| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 主动添加方 | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | -| friend_id | 好友ID | bigint(20) | - | 是(关联users.id) | 被动添加方 | NOT NULL, FOREIGN KEY (friend_id) REFERENCES users(id) ON DELETE CASCADE | -| friend_username | 好友用户名 | varchar(50) | - | - | 冗余存储,便于列表展示 | NOT NULL | -| online_status | 好友在线状态 | tinyint(1) | - | - | 0-离线,1-在线 | DEFAULT 0, NOT NULL | -| remark_name | 备注名 | varchar(50) | - | - | 用户对好友的自定义备注 | - | -| create_time | 添加时间 | datetime | - | - | 好友关系建立时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | - -###### 3.3 friend_requests(好友请求表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 请求ID | bigint(20) | 是(自增) | - | 唯一标识好友请求 | PRIMARY KEY (id) | -| applicant_id | 申请人ID | bigint(20) | - | 是(关联users.id) | 发起请求的用户 | NOT NULL, FOREIGN KEY (applicant_id) REFERENCES users(id) ON DELETE CASCADE | -| recipient_id | 接收人ID | bigint(20) | - | 是(关联users.id) | 收到请求的用户 | NOT NULL, FOREIGN KEY (recipient_id) REFERENCES users(id) ON DELETE CASCADE | -| apply_msg | 申请留言 | varchar(200)| - | - | 申请人留言(如"我是张三,加个好友") | - | -| status | 请求状态 | tinyint(1) | - | - | 0-待处理,1-已同意,2-已拒绝 | DEFAULT 0, NOT NULL | -| create_time | 申请时间 | datetime | - | - | 请求发起时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | -| handle_time | 处理时间 | datetime | - | - | 接收人处理时间(未处理为NULL) | - | - -###### 3.4 query_shares(查询分享记录表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 分享ID | bigint(20) | 是(自增) | - | 唯一标识分享记录 | PRIMARY KEY (id) | -| share_user_id | 分享人ID | bigint(20) | - | 是(关联users.id) | 发起分享的用户 | NOT NULL, FOREIGN KEY (share_user_id) REFERENCES users(id) ON DELETE CASCADE | -| receive_user_id | 接收人ID | bigint(20) | - | 是(关联users.id) | 接收分享的好友 | NOT NULL, FOREIGN KEY (receive_user_id) REFERENCES users(id) ON DELETE CASCADE | -| dialog_id | 会话ID | varchar(50) | - | - | 关联MongoDB的dialog_details集合,定位具体对话文档 | NOT NULL | -| target_rounds | 会话轮次数组 | json | - | - | 筛选指定轮次序号(roundNum)的元素 | NOT NULL | -| query_title | 查询标题 | varchar(200)| - | - | 冗余存储,便于接收人查看 | NOT NULL | -| share_time | 分享时间 | datetime | - | - | 分享发起时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | -| receive_status | 接收状态 | tinyint(1) | - | - | 0-未处理,1-已保存,2-已删除 | DEFAULT 0, NOT NULL | - -##### 4. 系统日志与监控表 -###### 4.1 system_health(系统健康表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|------|--------------------------------------------|---------------------------| -| id | 记录ID | bigint(20) | 是(自增) | - | 唯一标识健康记录 | PRIMARY KEY (id) | -| db_delay | 数据库延迟 | int(11) | - | - | 数据库服务响应延迟(ms) | NOT NULL | -| cache_delay | 缓存延迟 | int(11) | - | - | 缓存服务响应延迟(ms) | NOT NULL | -| llm_delay | 大模型延迟 | int(11) | - | - | 大模型API响应延迟(ms) | NOT NULL | -| storage_usage | 存储使用率 | decimal(5,2)| - | - | 存储服务使用率(%,如95.50) | NOT NULL | -| collect_time | 采集时间 | datetime | - | - | 数据采集时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | - -###### 4.2 operation_logs(系统操作日志表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 日志ID | bigint(20) | 是(自增) | - | 唯一标识操作日志 | PRIMARY KEY (id) | -| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 操作人ID(匿名操作为0) | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) | -| username | 用户名 | varchar(50) | - | - | 操作人用户名(冗余,便于查询) | NOT NULL | -| operation | 操作名称 | varchar(100)| - | - | 如"创建数据库连接"、"分配用户权限" | NOT NULL | -| module | 操作模块 | varchar(50) | - | - | 如"数据源管理"、"用户管理" | NOT NULL | -| related_llm | 涉及模型 | varchar(50) | - | - | 关联大模型名称(无则为NULL) | - | -| ip_address | IP地址 | varchar(50) | - | - | 操作人IP地址 | NOT NULL | -| operate_time | 操作时间 | datetime | - | - | 操作执行时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | -| result | 操作结果 | tinyint(1) | - | - | 0-失败,1-成功 | NOT NULL | -| error_msg | 错误信息 | text | - | - | 失败原因(成功为NULL) | - | - -###### 4.3 llm_configs(大模型配置表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 配置ID | bigint(20) | 是(自增) | - | 唯一标识大模型配置 | PRIMARY KEY (id) | -| name | 模型名称 | varchar(50) | - | - | 如"智谱AI"、"通义千问" | UNIQUE (name), NOT NULL | -| version | 模型版本 | varchar(20) | - | - | 如"4.0"、"3.0" | NOT NULL | -| api_key | API密钥 | varchar(200)| - | - | 大模型API Key(AES加密) | NOT NULL | -| api_url | API地址 | varchar(255)| - | - | 调用地址(如"https://api.zhipuai.com/v4/chat/completions") | NOT NULL | -| status_id | 状态ID | tinyint(2) | - | 是(关联llm_status.id) | 模型状态(可用/不可用/不稳定) | NOT NULL, FOREIGN KEY (status_id) REFERENCES llm_status(id) | -| is_disabled | 是否禁用 | tinyint(1) | - | - | 0-启用,1-禁用(强制下线) | DEFAULT 0, NOT NULL | -| timeout | 超时时间 | int(11) | - | - | API调用超时阈值(ms,如5000) | NOT NULL | -| create_user_id | 创建人ID | bigint(20) | - | 是(关联users.id) | 仅系统管理员可创建 | NOT NULL, FOREIGN KEY (create_user_id) REFERENCES users(id) | -| create_time | 创建时间 | datetime | - | - | 配置创建时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | -| update_time | 更新时间 | datetime | - | - | 配置更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | - -###### 4.4 notifications(通知表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 通知ID | bigint(20) | 是(自增) | - | 唯一标识通知 | PRIMARY KEY (id) | -| title | 通知标题 | varchar(100)| - | - | 通知标题(如"系统维护通知") | NOT NULL | -| content | 通知内容 | text | - | - | 通知详细内容(支持简单HTML) | NOT NULL | -| target_id | 目标ID | tinyint(2) | - | 是(关联notification_targets.id) | 接收目标(所有用户/指定角色) | NOT NULL, FOREIGN KEY (target_id) REFERENCES notification_targets(id) | -| priority_id | 优先级ID | tinyint(2) | - | 是(关联priorities.id) | 通知优先级(紧急/普通/低) | NOT NULL, FOREIGN KEY (priority_id) REFERENCES priorities(id) | -| publisher_id | 发布者ID | bigint(20) | - | 是(关联users.id) | 发布通知的管理员 | NOT NULL, FOREIGN KEY (publisher_id) REFERENCES users(id) | -| is_top | 是否置顶 | tinyint(1) | - | - | 0-否,1-是(置顶优先展示) | DEFAULT 0, NOT NULL | -| publish_time | 发布时间 | datetime | - | - | 发布时间(草稿为NULL) | - | -| create_time | 创建时间 | datetime | - | - | 草稿创建时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | -| Latest_updateTime | 上次更新时间 | datetime | - | - | 最近一次编辑并保存的时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | - -###### 4.5 token_consume(token消耗表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|------|--------------------------------------------|---------------------------------------------------------------| -| id | 记录ID | bigint(20) | 是(自增) | - | 唯一标识消耗记录 | PRIMARY KEY (id) | -| llm_name | 模型名称 | varchar(50) | - | - | 消耗token的大模型 | NOT NULL | -| total_tokens | 总消耗 | int(11) | - | - | 当日总消耗(输入+输出) | NOT NULL | -| prompt_tokens | 输入消耗 | int(11) | - | - | 当日输入token消耗 | NOT NULL | -| completion_tokens | 输出消耗 | int(11) | - | - | 当日输出token消耗 | NOT NULL | -| consume_date | 消耗日期 | date | - | - | 消耗日期(如"2025-11-05") | NOT NULL | -| growth_rate | 增长率 | decimal(5,2)| - | - | 较前一日增长率(%,如+12.50) | - | - -###### 4.6 error_logs(错误分析表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 记录ID | bigint(20) | 是(自增) | - | 唯一标识错误记录 | PRIMARY KEY (id) | -| error_type_id | 错误类型ID | tinyint(2) | - | 是(关联error_types.id) | 错误类型(模型超时/数据库错误等) | NOT NULL, FOREIGN KEY (error_type_id) REFERENCES error_types(id) | -| error_count | 错误次数 | int(11) | - | - | 统计周期内错误次数 | NOT NULL | -| error_rate | 错误率 | decimal(5,2)| - | - | 错误次数占总请求比例(%) | - | -| period | 统计周期 | varchar(20) | - | - | "today"-今日、"7days"-近7日 | NOT NULL | -| stat_time | 统计时间 | datetime | - | - | 统计生成时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | - -###### 4.7 performance_metrics(性能趋势表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|------|--------------------------------------------|---------------------------------------------------------------| -| id | 指标ID | bigint(20) | 是(自增) | - | 唯一标识性能指标 | PRIMARY KEY (id) | -| metric_type | 指标类型 | varchar(20) | - | - | "query_count"-查询量、"response_time"-响应时间 | NOT NULL | -| metric_value | 指标值 | decimal(10,2)| - | - | 指标数值(查询量为整数,响应时间单位ms) | NOT NULL | -| metric_time | 指标时间 | datetime | - | - | 指标采集时间(精确到小时) | NOT NULL | -| trend | 趋势标识 | tinyint(1) | - | - | 0-下降,1-上升(较上一周期) | - | - -###### 4.8 db_connection_logs(数据库连接日志表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 日志ID | bigint(20) | 是(自增) | - | 唯一标识连接日志 | PRIMARY KEY (id) | -| db_connection_id | 数据库连接ID | bigint(20) | - | 是(关联db_connections.id) | 连接的数据源 | NOT NULL, FOREIGN KEY (db_connection_id) REFERENCES db_connections(id) | -| db_name | 数据库名称 | varchar(100)| - | - | 冗余存储,便于查看 | NOT NULL | -| connect_time | 连接时间 | datetime | - | - | 尝试连接时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | -| status | 连接状态 | varchar(20) | - | - | "success"-成功、"timeout"-超时、"auth_failed"-认证失败 | NOT NULL | -| remark | 备注信息 | text | - | - | 错误代码 | - | -| handler_id | 处理人ID | bigint(20) | - | 是(关联users.id) | 触发连接的用户(NULL为系统检测) | FOREIGN KEY (handler_id) REFERENCES users(id) | - -###### 4.9 query_logs(查询日志表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 日志ID | bigint(20) | 是(自增) | - | 唯一标识查询日志 | PRIMARY KEY (id) | -| dialog_id | 对话ID | varchar(50) | - | - | 关联多轮对话ID | NOT NULL | -| data_source_id | 数据源ID | bigint(20) | - | 是(关联db_connections.id) | 查询的数据源 | NOT NULL, FOREIGN KEY (data_source_id) REFERENCES db_connections(id) | -| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 执行查询的用户 | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) | -| query_date | 查询日期 | date | - | - | 查询执行日期 | NOT NULL | -| query_time | 查询时间 | datetime | - | - | 查询执行时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | -| execute_result | 执行结果 | tinyint(1) | - | - | 0-失败,1-成功 | NOT NULL | - -###### 4.10 user_searches(用户搜索表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | -|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| -| id | 搜索ID | bigint(20) | 是(自增) | - | 唯一标识搜索记录 | PRIMARY KEY (id) | -| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 执行搜索的用户 | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | -| sql_content | SQL语句 | text | - | - | 用户执行的SQL语句 | NOT NULL | -| query_title | 查询标题 | varchar(200)| - | - | 大模型生成的标题 | NOT NULL | -| search_count | 搜索次数 | int(11) | - | - | 该搜索被执行的次数 | DEFAULT 1, NOT NULL | -| last_search_time | 最后搜索时间 | datetime | - | - | 最后一次执行该搜索的时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | - -#### (二)MongoDB 8.2 集合字段 -###### 1. query_collections(收藏查询表-组) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | -|----------|--------------|----------|--------------------|------------|--------------------------------------------|---------------------------| -| _id | 组ID | ObjectId | 是(自动生成) | - | 唯一标识收藏组 | PRIMARY KEY (_id) | -| userId | 用户ID | Long | - | users.id | 所属用户 | NOT NULL | -| groupName | 组名称 | String | - | - | 收藏组名称(由大模型生成,如"销售报表查询") | NOT NULL, 同用户内唯一 | -| createTime | 创建时间 | Date | - | - | 组创建时间 | DEFAULT new Date() | - -###### 2. collection_records(收藏记录表-具体记录) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | -|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| -| _id | 记录ID | ObjectId | 是(自动生成) | - | 唯一标识收藏记录 | PRIMARY KEY (_id) | -| queryId | 组ID | ObjectId | - | query_collections._id | 所属收藏组 | NOT NULL | -| userId | 用户ID | Long | - | users.id | 所属用户 | NOT NULL | -| sqlContent | SQL语句 | String | - | - | 收藏的SQL语句 | NOT NULL | -| queryResult | 查询结果 | Document | - | - | 结果数据(如{ "columns": [], "data": [] }) | - | -| dbConnectionId | 数据库来源 | Long | - | db_connections.id | 收藏记录来源的数据库连接 | NOT NULL | -| llmConfigId | 大模型来源 | Long | - | llm_configs.id | 收藏记录来源的大模型配置 | NOT NULL | -| createTime | 创建时间 | Date | - | - | 收藏时间 | DEFAULT new Date() | - -###### 3. dialog_records(多轮对话列表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | -|----------|--------------|----------|--------------------|------------|--------------------------------------------|---------------------------| -| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识对话列表 | PRIMARY KEY (_id) | -| dialogId | 对话ID | String | - | - | 自定义唯一标识(如"USER123_20251105") | NOT NULL, UNIQUE | -| userId | 用户ID | Long | - | users.id | 所属用户 | NOT NULL | -| topic | 对话主题 | String | - | - | 大模型生成的主题(如"2023年订单分析") | NOT NULL | -| totalRounds | 总轮数 | Int | - | - | 对话总轮次 | DEFAULT 0 | -| startTime | 开始时间 | Date | - | - | 对话开始时间 | DEFAULT new Date() | -| lastTime | 最后对话时间 | Date | - | - | 最后一轮对话时间 | DEFAULT new Date() | - -###### 4. dialog_details(多轮对话具体内容表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | -|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| -| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识对话内容 | PRIMARY KEY (_id) | -| dialogId | 对话ID | String | - | dialog_records.dialogId | 关联的对话列表 | NOT NULL | -| rounds | 对话轮次 | Array | - | - | 每轮对话详情:{ "roundNum": Int, "userInput": String, "aiResponse": String, "generatedSql": String, "roundTime": Date } | NOT NULL | - -###### 5. sql_cache(SQL缓存集合) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | -|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| -| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识缓存记录 | PRIMARY KEY (_id) | -| nlHash | 自然语言哈希 | String | - | - | 自然语言查询的MD5哈希(快速匹配) | NOT NULL | -| userId | 用户ID | Long | - | users.id | 关联用户(NULL为全局缓存) | - | -| connectionId | 数据源ID | Long | - | db_connections.id | 关联数据源 | NOT NULL | -| tableIds | 表ID列表 | Array | - | table_metadata.id | 涉及的表ID | NOT NULL | -| dbType | 数据库类型 | String | - | db_types.type_code | 如"mysql"、"mongodb" | NOT NULL | -| generatedSql | 生成的SQL | String | - | - | 缓存的SQL语句 | NOT NULL | -| hitCount | 命中次数 | Int | - | - | 缓存被复用次数 | DEFAULT 0 | -| expireTime | 过期时间 | Date | - | - | 缓存过期时间(默认7天) | NOT NULL | - -###### 6. ai_interaction_logs(AI交互日志集合) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | -|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| -| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识交互记录 | PRIMARY KEY (_id) | -| userId | 用户ID | Long | - | users.id | 发起请求的用户 | NOT NULL | -| requestType | 请求类型 | String | - | - | "nl2sql"-自然语言转SQL、"sql_optimize"-SQL优化 | NOT NULL | -| llmName | 模型名称 | String | - | llm_configs.name | 调用的大模型 | NOT NULL | -| requestParams | 请求参数 | Document | - | - | 请求参数:{ "naturalLanguage": String, "metadata": Object, "temperature": Double } | NOT NULL | -| responseResult | 响应结果 | Document | - | - | 响应结果:{ "sql": String, "confidence": Double, "suggestion": String } | - | -| tokenUsage | Token消耗 | Document | - | - | { "promptTokens": Int, "completionTokens": Int, "totalTokens": Int } | NOT NULL | -| responseTime | 响应时间 | Int | - | - | 响应耗时(ms) | NOT NULL | -| status | 交互状态 | String | - | - | "success"-成功、"fail"-失败 | NOT NULL | -| errorMsg | 错误信息 | String | - | - | 失败原因(成功为NULL) | - | -| createTime | 创建时间 | Date | - | - | 请求发起时间 | DEFAULT new Date() | - -###### 7. friend_chats(好友聊天记录表) -| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | -|----------|--------------|----------|--------------------|------------|--------------------------------------------|---------------------------| -| _id | 记录ID | ObjectId | 是(自动生成) | - | 唯一标识聊天记录 | PRIMARY KEY (_id) | -| user_id | 发送人ID | Long | - | users.id | 发送消息的用户ID(关联MySQL的users表) | NOT NULL | -| friend_id | 接收人ID | Long | - | users.id | 接收消息的好友ID(关联MySQL的users表) | NOT NULL | -| content_type | 内容类型 | String | - | - | 消息类型:text(文本)、query_share(查询分享)、image(图片)等 | NOT NULL | -| content | 消息内容 | Document | - | - | 动态内容:文本:{text: "Hello"};分享:{query_id: "xxx", title: "订单查询"};图片:{url: "xxx.png", size: 1024} | NOT NULL | -| send_time | 发送时间 | Date | - | - | 消息发送时间(精确到毫秒) | NOT NULL | -| is_read | 是否已读 | Boolean | - | - | true(已读)/false(未读),默认false | DEFAULT false | -| extra | 额外信息 | Document | - | - | 扩展字段(如quote_msg_id引用消息ID、is_recalled是否撤回等) | - | - ---- - -### 二、表/集合之间的关系(一对多/多对一/一对一) -#### (一)MySQL 表关系 -| 关联表1 | 关联表2 | 关系类型 | 说明 | -|---------------------------|---------------------------|----------|----------------------------------------| -| users(用户表) | roles(角色表) | 多对一 | 多个用户属于同一角色(如多个普通用户) | -| db_connections(数据库连接表) | db_types(数据库类型表) | 多对一 | 多个数据库连接属于同一类型(如多个MySQL连接) | -| table_metadata(表元数据表) | db_connections(数据库连接表) | 多对一 | 多个表属于同一数据库连接 | -| column_metadata(字段元数据表) | table_metadata(表元数据表) | 多对一 | 多个字段属于同一表 | -| user_db_permissions(用户数据权限表) | users(被授权用户) | 一对一 | 一个用户对应一条权限记录(每个用户一行) | -| user_db_permissions(用户数据权限表) | users(授权管理员) | 多对一 | 多个权限记录可由同一管理员修改 | -| friend_relations(好友关系表) | users(用户) | 多对一 | 一个用户可添加多个好友 | -| friend_relations(好友关系表) | users(好友) | 多对一 | 一个用户可被多个用户添加为好友 | -| friend_requests(好友请求表) | users(申请人) | 多对一 | 一个用户可发起多个好友请求 | -| friend_requests(好友请求表) | users(接收人) | 多对一 | 一个用户可接收多个好友请求 | -| query_shares(查询分享记录表) | users(分享人) | 多对一 | 一个用户可分享多个查询 | -| query_shares(查询分享记录表) | users(接收人) | 多对一 | 一个用户可接收多个分享 | -| llm_configs(大模型配置表) | llm_status(大模型状态表) | 多对一 | 多个大模型配置属于同一状态 | -| llm_configs(大模型配置表) | users(创建人) | 多对一 | 一个管理员可创建多个大模型配置 | -| notifications(通知表) | notification_targets(通知目标表) | 多对一 | 多个通知属于同一接收目标 | -| notifications(通知表) | priorities(通知优先级表) | 多对一 | 多个通知属于同一优先级 | -| notifications(通知表) | users(发布者) | 多对一 | 一个管理员可发布多个通知 | -| error_logs(错误分析表) | error_types(错误类型表) | 多对一 | 多个错误记录属于同一错误类型 | -| db_connection_logs(数据库连接日志表) | db_connections(数据库连接表) | 多对一 | 一个数据库连接有多个连接日志 | -| query_logs(查询日志表) | db_connections(数据库连接表) | 多对一 | 一个数据源有多个查询日志 | -| query_logs(查询日志表) | users(用户) | 多对一 | 一个用户有多个查询日志 | -| user_searches(用户搜索表) | users(用户) | 多对一 | 一个用户有多个搜索记录 | - -#### (二)MongoDB 集合关系 -| 关联集合1 | 关联集合2 | 关系类型 | 说明 | -|-----------------------------|-----------------------------|----------|----------------------------------------| -| collection_records(收藏记录表) | query_collections(收藏查询表) | 多对一 | 一个收藏组包含多个收藏记录 | -| collection_records(收藏记录表) | db_connections(数据库连接表) | 多对一 | 一个数据库连接有多个收藏记录 | -| collection_records(收藏记录表) | llm_configs(大模型配置表) | 多对一 | 一个大模型配置有多个收藏记录 | -| dialog_details(多轮对话具体内容表) | dialog_records(多轮对话列表) | 一对一 | 一个对话列表对应一个具体内容集合 | -| sql_cache(SQL缓存集合) | db_connections(数据库连接表) | 多对一 | 一个数据源有多个SQL缓存 | -| sql_cache(SQL缓存集合) | users(用户表) | 多对一 | 一个用户有多个个人SQL缓存 | -| ai_interaction_logs(AI交互日志集合) | users(用户表) | 多对一 | 一个用户有多个AI交互记录 | -| ai_interaction_logs(AI交互日志集合) | llm_configs(大模型配置表) | 多对一 | 一个大模型有多个交互记录 | -| friend_chats(好友聊天记录表) | users(用户表) | 多对一 | 一个用户可发送多条聊天消息 | -| friend_chats(好友聊天记录表) | users(用户表) | 多对一 | 一个用户可接收多条聊天消息 | - ---- - -### 三、完整索引设计(按表/集合分类) -#### (一)MySQL 数据表索引 -| 表名 | 索引类型 | 索引字段 | 索引说明 | -|-----------------------|----------------|-------------------------------------------|--------------------------------------------| -| users | 普通索引 | role_id | 按角色查询用户 | -| users | 普通索引 | status | 按账号状态筛选用户 | -| users | 唯一索引 | username | 用户名唯一,加速登录查询 | -| users | 唯一索引 | email | 邮箱唯一,加速好友添加查询 | -| users | 唯一索引 | phonenumber | 手机号唯一,加速账号验证 | -| roles | 主键索引 | id | 主键默认索引,加速角色关联查询 | -| db_types | 主键索引 | id | 主键默认索引,加速数据库类型关联查询 | -| notification_targets | 主键索引 | id | 主键默认索引,加速通知目标关联查询 | -| priorities | 普通索引 | sort | 按排序权重查询优先级 | -| priorities | 主键索引 | id | 主键默认索引,加速通知优先级关联查询 | -| error_types | 主键索引 | id | 主键默认索引,加速错误类型关联查询 | -| llm_status | 主键索引 | id | 主键默认索引,加速大模型状态关联查询 | -| db_connections | 普通索引 | db_type_id | 按数据库类型查询连接 | -| db_connections | 普通索引 | status | 按连接状态筛选连接 | -| db_connections | 普通索引 | create_user_id | 按创建人查询连接 | -| db_connections | 唯一索引 | name | 连接名称唯一,加速连接查询 | -| table_metadata | 唯一复合索引 | db_connection_id, table_name | 同一连接下表名唯一,加速表元数据查询 | -| table_metadata | 普通索引 | db_connection_id | 按数据库连接查询表 | -| column_metadata | 唯一复合索引 | table_id, column_name | 同一表下字段名唯一,加速字段元数据查询 | -| column_metadata | 普通索引 | table_id | 按表查询字段 | -| user_db_permissions | 唯一索引 | user_id | 每个用户一行权限,加速权限查询 | -| user_db_permissions | 普通索引 | last_grant_user_id | 按授权管理员查询权限记录 | -| user_db_permissions | 普通索引 | is_assigned | 按是否分配筛选权限 | -| friend_relations | 唯一复合索引 | user_id, friend_id | 避免重复添加好友 | -| friend_relations | 普通索引 | user_id | 按用户查询好友列表 | -| friend_relations | 普通索引 | friend_id | 按好友ID查询关联关系 | -| friend_requests | 唯一复合索引 | applicant_id, recipient_id | 避免重复发送好友请求 | -| friend_requests | 普通复合索引 | recipient_id, status | 接收人查询待处理请求 | -| query_shares | 普通复合索引 | receive_user_id, receive_status | 接收人查询未处理分享 | -| query_shares | 普通索引 | share_user_id | 按分享人查询分享记录 | -| system_health | 普通索引 | collect_time | 按时间查询系统健康趋势 | -| operation_logs | 普通索引 | operate_time | 按时间查询操作日志 | -| operation_logs | 普通索引 | user_id | 按用户查询操作日志 | -| operation_logs | 普通索引 | module | 按模块筛选操作日志 | -| llm_configs | 普通索引 | status_id | 按状态查询大模型配置 | -| llm_configs | 普通索引 | is_disabled | 按是否禁用筛选配置 | -| llm_configs | 唯一索引 | name | 模型名称唯一,加速模型查询 | -| notifications | 普通索引 | target_id | 按目标筛选通知 | -| notifications | 普通索引 | priority_id | 按优先级筛选通知 | -| notifications | 普通复合索引 | is_top DESC, publish_time DESC | 置顶通知优先,按时间排序 | -| token_consume | 唯一复合索引 | llm_name, consume_date | 同一模型每日一条记录,加速消耗查询 | -| token_consume | 普通索引 | consume_date | 按日期查询消耗趋势 | -| error_logs | 普通复合索引 | error_type_id, period | 按错误类型+周期查询错误记录 | -| error_logs | 普通索引 | stat_time | 按统计时间查询错误记录 | -| performance_metrics | 普通复合索引 | metric_type, metric_time | 按类型+时间查询性能趋势 | -| db_connection_logs | 普通索引 | db_connection_id | 按连接查询日志 | -| db_connection_logs | 普通索引 | connect_time | 按时间查询连接日志 | -| db_connection_logs | 普通索引 | status | 按状态筛选连接日志 | -| query_logs | 普通复合索引 | data_source_id, query_date | 统计数据源每日查询量 | -| query_logs | 普通索引 | user_id | 按用户查询查询日志 | -| query_logs | 普通索引 | dialog_id | 按对话ID查询查询日志 | -| user_searches | 普通复合索引 | user_id, last_search_time | 清理30天前的搜索记录 | -| user_searches | 普通复合索引 | user_id, search_count DESC | 查询用户常用搜索(按次数排序) | - -#### (二)MongoDB 集合索引 -| 集合名 | 索引类型 | 索引字段 | 索引说明 | -|-------------------------|----------------|-------------------------------------------|--------------------------------------------| -| query_collections | 复合索引 | { "userId": 1, "groupName": 1 } | 同用户内组名唯一,加速组查询 | -| collection_records | 复合索引 | { "queryId": 1, "userId": 1 } | 关联收藏组和用户,加速记录查询 | -| collection_records | 普通索引 | { "dbConnectionId": 1 } | 按数据库来源查询收藏记录 | -| collection_records | 普通索引 | { "llmConfigId": 1 } | 按大模型来源查询收藏记录 | -| dialog_records | 复合索引 | { "userId": 1, "lastTime": -1 } | 用户按时间查询对话列表(降序) | -| dialog_records | 唯一索引 | { "dialogId": 1 } | 对话ID唯一,加速对话定位 | -| dialog_details | 单字段索引 | { "dialogId": 1 } | 关联对话列表,加速内容查询 | -| sql_cache | 复合索引 | { "nlHash": 1, "connectionId": 1, "tableIds": 1 } | 精确匹配缓存,加速SQL复用 | -| sql_cache | TTL索引 | { "expireTime": 1 } | 自动删除过期缓存(默认7天) | -| ai_interaction_logs | 复合索引 | { "userId": 1, "createTime": -1 } | 用户按时间查询交互记录(降序) | -| ai_interaction_logs | 复合索引 | { "llmName": 1, "status": 1 } | 按模型+状态查询交互记录 | -| friend_chats | 复合索引 | { "user_id": 1, "friend_id": 1, "send_time": -1 } | 按用户+好友+时间查询聊天记录(最新在前) | -| friend_chats | 复合索引 | { "friend_id": 1, "is_read": 1 } | 查询好友未读消息 | \ No newline at end of file diff --git a/src/springboot_demo/mongodb_schema_from_last.js b/src/springboot_demo/mongodb_schema_from_last.js deleted file mode 100644 index 06768fe4..00000000 --- a/src/springboot_demo/mongodb_schema_from_last.js +++ /dev/null @@ -1,482 +0,0 @@ -// MongoDB数据库集合和索引创建脚本 - 严格结构定义 -// 完全按照数据库设计文档 - -db = db.getSiblingDB('natural_language_query_system'); -print("开始初始化 MongoDB 集合..."); - -try { - // 1. query_collections(收藏查询表-组) - db.createCollection("query_collections", { - validator: { - $jsonSchema: { - bsonType: "object", - required: ["userId", "groupName", "createTime"], - properties: { - userId: { - bsonType: "long", - description: "用户ID,关联users.id" - }, - groupName: { - bsonType: "string", - description: "收藏组名称(由大模型生成)" - }, - createTime: { - bsonType: "date", - description: "组创建时间" - } - } - } - } - }); - db.query_collections.createIndex({ "userId": 1, "groupName": 1 }, { unique: true }); - print("创建 query_collections 完成"); -} catch(e) { - print("query_collections 创建错误: " + e); -} - -try { - // 2. collection_records(收藏记录表-具体记录) - db.createCollection("collection_records", { - validator: { - $jsonSchema: { - bsonType: "object", - required: ["queryId", "userId", "sqlContent", "dbConnectionId", "llmConfigId", "createTime"], - properties: { - queryId: { - bsonType: "objectId", - description: "组ID,关联query_collections._id" - }, - userId: { - bsonType: "long", - description: "用户ID,关联users.id" - }, - sqlContent: { - bsonType: "string", - description: "收藏的SQL语句" - }, - queryResult: { - bsonType: "object", - description: "结果数据" - }, - dbConnectionId: { - bsonType: "long", - description: "数据库来源,关联db_connections.id" - }, - llmConfigId: { - bsonType: "long", - description: "大模型来源,关联llm_configs.id" - }, - createTime: { - bsonType: "date", - description: "收藏时间" - } - } - } - } - }); - db.collection_records.createIndex({ "queryId": 1, "userId": 1 }); - db.collection_records.createIndex({ "dbConnectionId": 1 }); - db.collection_records.createIndex({ "llmConfigId": 1 }); - print("创建 collection_records 完成"); -} catch(e) { - print("collection_records 创建错误: " + e); -} - -try { - // 3. dialog_records(多轮对话列表) - db.createCollection("dialog_records", { - validator: { - $jsonSchema: { - bsonType: "object", - required: ["dialogId", "userId", "topic", "totalRounds", "startTime", "lastTime"], - properties: { - dialogId: { - bsonType: "string", - description: "对话ID,自定义唯一标识" - }, - userId: { - bsonType: "long", - description: "用户ID,关联users.id" - }, - topic: { - bsonType: "string", - description: "对话主题(大模型生成)" - }, - totalRounds: { - bsonType: "int", - description: "对话总轮次", - minimum: 0 - }, - startTime: { - bsonType: "date", - description: "对话开始时间" - }, - lastTime: { - bsonType: "date", - description: "最后一轮对话时间" - } - } - } - } - }); - db.dialog_records.createIndex({ "userId": 1, "lastTime": -1 }); - db.dialog_records.createIndex({ "dialogId": 1 }, { unique: true }); - print("创建 dialog_records 完成"); -} catch(e) { - print("dialog_records 创建错误: " + e); -} - -try { - // 4. dialog_details(多轮对话具体内容表) - db.createCollection("dialog_details", { - validator: { - $jsonSchema: { - bsonType: "object", - required: ["dialogId", "rounds"], - properties: { - dialogId: { - bsonType: "string", - description: "对话ID,关联dialog_records.dialogId" - }, - rounds: { - bsonType: "array", - description: "对话轮次数组", - items: { - bsonType: "object", - required: ["roundNum", "userInput", "aiResponse", "generatedSql", "roundTime"], - properties: { - roundNum: { - bsonType: "int", - description: "轮次序号", - minimum: 1 - }, - userInput: { - bsonType: "string", - description: "用户输入" - }, - aiResponse: { - bsonType: "string", - description: "AI回复" - }, - generatedSql: { - bsonType: "string", - description: "生成的SQL" - }, - roundTime: { - bsonType: "date", - description: "轮次时间" - } - } - } - } - } - } - } - }); - db.dialog_details.createIndex({ "dialogId": 1 }); - print("创建 dialog_details 完成"); -} catch(e) { - print("dialog_details 创建错误: " + e); -} - -try { - // 5. sql_cache(SQL缓存集合) - db.createCollection("sql_cache", { - validator: { - $jsonSchema: { - bsonType: "object", - required: ["nlHash", "connectionId", "tableIds", "dbType", "generatedSql", "hitCount", "expireTime"], - properties: { - nlHash: { - bsonType: "string", - description: "自然语言查询的MD5哈希" - }, - userId: { - bsonType: ["long", "null"], - description: "用户ID(NULL为全局缓存)" - }, - connectionId: { - bsonType: "long", - description: "数据源ID,关联db_connections.id" - }, - tableIds: { - bsonType: "array", - description: "涉及的表ID列表", - items: { - bsonType: "long" - } - }, - dbType: { - bsonType: "string", - description: "数据库类型" - }, - generatedSql: { - bsonType: "string", - description: "缓存的SQL语句" - }, - hitCount: { - bsonType: "int", - description: "命中次数", - minimum: 0 - }, - expireTime: { - bsonType: "date", - description: "缓存过期时间" - } - } - } - } - }); - db.sql_cache.createIndex({ "nlHash": 1, "connectionId": 1, "tableIds": 1 }); - db.sql_cache.createIndex({ "expireTime": 1 }, { expireAfterSeconds: 0 }); - print("创建 sql_cache 完成"); -} catch(e) { - print("sql_cache 创建错误: " + e); -} - -try { - // 6. ai_interaction_logs(AI交互日志集合) - db.createCollection("ai_interaction_logs", { - validator: { - $jsonSchema: { - bsonType: "object", - required: ["userId", "requestType", "llmName", "requestParams", "tokenUsage", "responseTime", "status", "createTime"], - properties: { - userId: { - bsonType: "long", - description: "用户ID,关联users.id" - }, - requestType: { - bsonType: "string", - enum: ["nl2sql", "sql_optimize"], - description: "请求类型" - }, - llmName: { - bsonType: "string", - description: "模型名称,关联llm_configs.name" - }, - requestParams: { - bsonType: "object", - description: "请求参数", - properties: { - naturalLanguage: { - bsonType: "string", - description: "自然语言查询" - }, - metadata: { - bsonType: "object", - description: "元数据信息" - }, - temperature: { - bsonType: "double", - description: "温度参数", - minimum: 0, - maximum: 2 - } - } - }, - responseResult: { - bsonType: "object", - description: "响应结果", - properties: { - sql: { - bsonType: "string", - description: "生成的SQL" - }, - confidence: { - bsonType: "double", - description: "置信度", - minimum: 0, - maximum: 1 - }, - suggestion: { - bsonType: "string", - description: "优化建议" - } - } - }, - tokenUsage: { - bsonType: "object", - description: "Token消耗", - required: ["promptTokens", "completionTokens", "totalTokens"], - properties: { - promptTokens: { - bsonType: "int", - description: "输入token数", - minimum: 0 - }, - completionTokens: { - bsonType: "int", - description: "输出token数", - minimum: 0 - }, - totalTokens: { - bsonType: "int", - description: "总token数", - minimum: 0 - } - } - }, - responseTime: { - bsonType: "int", - description: "响应耗时(ms)", - minimum: 0 - }, - status: { - bsonType: "string", - enum: ["success", "fail"], - description: "交互状态" - }, - errorMsg: { - bsonType: ["string", "null"], - description: "错误信息" - }, - createTime: { - bsonType: "date", - description: "请求发起时间" - } - } - } - } - }); - db.ai_interaction_logs.createIndex({ "userId": 1, "createTime": -1 }); - db.ai_interaction_logs.createIndex({ "llmName": 1, "status": 1 }); - print("创建 ai_interaction_logs 完成"); -} catch(e) { - print("ai_interaction_logs 创建错误: " + e); -} - -try { - // 7. friend_chats(好友聊天记录表)- 完全按照设计文档 - db.createCollection("friend_chats", { - validator: { - $jsonSchema: { - bsonType: "object", - required: ["user_id", "friend_id", "content_type", "content", "send_time", "is_read"], - properties: { - user_id: { - bsonType: "long", - description: "发送人ID,关联users.id" - }, - friend_id: { - bsonType: "long", - description: "接收人ID,关联users.id" - }, - content_type: { - bsonType: "string", - enum: ["text", "query_share", "image"], - description: "消息类型" - }, - content: { - bsonType: "object", - description: "动态消息内容", - properties: { - text: { - bsonType: "string", - description: "文本内容" - }, - query_id: { - bsonType: "string", - description: "查询ID" - }, - title: { - bsonType: "string", - description: "查询标题" - }, - url: { - bsonType: "string", - description: "图片URL" - }, - size: { - bsonType: "int", - description: "文件大小", - minimum: 0 - } - } - }, - send_time: { - bsonType: "date", - description: "消息发送时间" - }, - is_read: { - bsonType: "bool", - description: "是否已读" - }, - extra: { - bsonType: "object", - description: "额外信息(扩展字段)", - properties: { - quote_msg_id: { - bsonType: "objectId", - description: "引用消息ID" - }, - is_recalled: { - bsonType: "bool", - description: "是否撤回" - } - } - } - } - } - } - }); - db.friend_chats.createIndex({ "user_id": 1, "friend_id": 1, "send_time": -1 }); - db.friend_chats.createIndex({ "friend_id": 1, "is_read": 1 }); - print("创建 friend_chats 完成"); -} catch(e) { - print("friend_chats 创建错误: " + e); -} - -// 验证创建结果 -var collections = db.getCollectionNames(); -var userCollections = collections.filter(function(name) { - return !name.startsWith('system.'); -}); - -print("\n=========================================="); -print("MongoDB 集合初始化完成"); -print("用户集合数量: " + userCollections.length); -print("集合列表: " + JSON.stringify(userCollections.sort())); -print("=========================================="); - -// 检查是否所有集合都创建成功 -var expectedCollections = [ - "query_collections", - "collection_records", - "dialog_records", - "dialog_details", - "sql_cache", - "ai_interaction_logs", - "friend_chats" -].sort(); - -var missingCollections = expectedCollections.filter(function(name) { - return userCollections.indexOf(name) === -1; -}); - -if (missingCollections.length > 0) { - print("缺失的集合: " + JSON.stringify(missingCollections)); -} else { - print("所有集合创建成功!"); - - // 显示各集合的索引信息 - print("\n📈 各集合索引详情:"); - userCollections.forEach(function(collectionName) { - var indexes = db[collectionName].getIndexes(); - var userIndexes = indexes.filter(function(index) { - return index.name !== "_id_"; - }); - print(" " + collectionName + ": " + userIndexes.length + " 个索引"); - userIndexes.forEach(function(index) { - print(" - " + index.name + ": " + JSON.stringify(index.key)); - }); - }); -} - -print("\n集合结构验证:"); -userCollections.forEach(function(collectionName) { - var stats = db[collectionName].stats(); - print(" " + collectionName + ": " + stats.count + " 个文档, " + stats.size + " 字节"); -}); - -print("\nMongoDB 严格结构定义初始化完成!"); \ No newline at end of file diff --git a/src/springboot_demo/mvnw b/src/springboot_demo/mvnw deleted file mode 100644 index bd8896bf..00000000 --- a/src/springboot_demo/mvnw +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.4 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -scriptDir="$(dirname "$0")" -scriptName="$(basename "$0")" - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -actualDistributionDir="" - -# First try the expected directory name (for regular distributions) -if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then - if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then - actualDistributionDir="$distributionUrlNameMain" - fi -fi - -# If not found, search for any directory with the Maven executable (for snapshots) -if [ -z "$actualDistributionDir" ]; then - # enable globbing to iterate over items - set +f - for dir in "$TMP_DOWNLOAD_DIR"/*; do - if [ -d "$dir" ]; then - if [ -f "$dir/bin/$MVN_CMD" ]; then - actualDistributionDir="$(basename "$dir")" - break - fi - fi - done - set -f -fi - -if [ -z "$actualDistributionDir" ]; then - verbose "Contents of $TMP_DOWNLOAD_DIR:" - verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" - die "Could not find Maven distribution directory in extracted archive" -fi - -verbose "Found extracted Maven distribution directory: $actualDistributionDir" -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/src/springboot_demo/mvnw.cmd b/src/springboot_demo/mvnw.cmd deleted file mode 100644 index 92450f93..00000000 --- a/src/springboot_demo/mvnw.cmd +++ /dev/null @@ -1,189 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/src/springboot_demo/mysql_schema_from_last.sql b/src/springboot_demo/mysql_schema_from_last.sql deleted file mode 100644 index 599d697a..00000000 --- a/src/springboot_demo/mysql_schema_from_last.sql +++ /dev/null @@ -1,341 +0,0 @@ --- MySQL数据库建表脚本 --- 基于last.md文档设计 - --- 1. 基础信息表(用户、角色、字典) - --- 1.1 roles(角色表)- 需要先创建,因为users依赖它 -CREATE TABLE `roles` ( - `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID', - `role_name` VARCHAR(30) NOT NULL UNIQUE COMMENT '角色名称', - `role_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '角色编码', - `description` VARCHAR(500) COMMENT '角色描述' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; - --- 1.2 users(用户表) -CREATE TABLE `users` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', - `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', - `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密存储)', - `email` VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱', - `phonenumber` VARCHAR(50) NOT NULL UNIQUE COMMENT '手机号', - `role_id` TINYINT(2) NOT NULL COMMENT '角色ID', - `avatar_url` VARCHAR(255) DEFAULT '/default-avatar.png' COMMENT '头像URL', - `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态:0-禁用,1-正常', - `online_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '在线状态:0-离线,1-在线', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - INDEX `idx_role_id` (`role_id`), - INDEX `idx_status` (`status`), - UNIQUE INDEX `uk_username` (`username`), - UNIQUE INDEX `uk_email` (`email`), - UNIQUE INDEX `uk_phonenumber` (`phonenumber`), - FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; - --- 1.3 db_types(数据库类型表) -CREATE TABLE `db_types` ( - `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '类型ID', - `type_name` VARCHAR(50) NOT NULL UNIQUE COMMENT '类型名称', - `type_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '类型编码', - `description` VARCHAR(500) COMMENT '类型描述' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库类型表'; - --- 1.4 notification_targets(通知目标表) -CREATE TABLE `notification_targets` ( - `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '目标ID', - `target_name` VARCHAR(30) NOT NULL UNIQUE COMMENT '目标名称', - `target_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '目标编码', - `description` VARCHAR(200) COMMENT '目标描述' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知目标表'; - --- 1.5 priorities(通知优先级表) -CREATE TABLE `priorities` ( - `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '优先级ID', - `priority_name` VARCHAR(20) NOT NULL UNIQUE COMMENT '优先级名称', - `priority_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '优先级编码', - `sort` INT(11) NOT NULL COMMENT '排序权重', - INDEX `idx_sort` (`sort`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知优先级表'; - --- 1.6 error_types(错误类型表) -CREATE TABLE `error_types` ( - `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '错误ID', - `error_name` VARCHAR(50) NOT NULL UNIQUE COMMENT '错误名称', - `error_code` VARCHAR(50) NOT NULL UNIQUE COMMENT '错误编码', - `description` VARCHAR(500) COMMENT '错误描述' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='错误类型表'; - --- 1.7 llm_status(大模型状态表) -CREATE TABLE `llm_status` ( - `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '状态ID', - `status_name` VARCHAR(20) NOT NULL UNIQUE COMMENT '状态名称', - `status_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '状态编码', - `description` VARCHAR(200) COMMENT '状态描述' -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大模型状态表'; - --- 2. 数据资源表 - --- 2.1 db_connections(数据库连接表) -CREATE TABLE `db_connections` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '连接ID', - `name` VARCHAR(100) NOT NULL UNIQUE COMMENT '连接名称', - `db_type_id` TINYINT(2) NOT NULL COMMENT '数据库类型ID', - `url` VARCHAR(255) NOT NULL COMMENT '连接地址', - `username` VARCHAR(50) NOT NULL COMMENT '数据库账号(加密存储)', - `password` VARCHAR(100) NOT NULL COMMENT '数据库密码(加密存储)', - `status` VARCHAR(20) NOT NULL DEFAULT 'disconnected' COMMENT '连接状态', - `create_user_id` BIGINT(20) NOT NULL COMMENT '创建者ID', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - INDEX `idx_db_type_id` (`db_type_id`), - INDEX `idx_status` (`status`), - INDEX `idx_create_user_id` (`create_user_id`), - UNIQUE INDEX `uk_name` (`name`), - FOREIGN KEY (`db_type_id`) REFERENCES `db_types`(`id`), - FOREIGN KEY (`create_user_id`) REFERENCES `users`(`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库连接表'; - --- 2.2 table_metadata(表元数据表) -CREATE TABLE `table_metadata` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '表ID', - `db_connection_id` BIGINT(20) NOT NULL COMMENT '数据库连接ID', - `table_name` VARCHAR(100) NOT NULL COMMENT '表名', - `description` VARCHAR(500) COMMENT '表描述', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - UNIQUE INDEX `uk_db_connection_table` (`db_connection_id`, `table_name`), - INDEX `idx_db_connection_id` (`db_connection_id`), - FOREIGN KEY (`db_connection_id`) REFERENCES `db_connections`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表元数据表'; - --- 2.3 column_metadata(字段元数据表) -CREATE TABLE `column_metadata` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '字段ID', - `table_id` BIGINT(20) NOT NULL COMMENT '表ID', - `column_name` VARCHAR(100) NOT NULL COMMENT '字段名', - `data_type` VARCHAR(50) NOT NULL COMMENT '数据类型', - `description` VARCHAR(500) COMMENT '字段描述', - `is_primary` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否主键:0-否,1-是', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - UNIQUE INDEX `uk_table_column` (`table_id`, `column_name`), - INDEX `idx_table_id` (`table_id`), - FOREIGN KEY (`table_id`) REFERENCES `table_metadata`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字段元数据表'; - --- 3. 权限与关系表 - --- 3.1 user_db_permissions(用户数据权限表) -CREATE TABLE `user_db_permissions` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID', - `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', - `permission_details` JSON NOT NULL COMMENT '权限详情', - `last_grant_user_id` BIGINT(20) NOT NULL COMMENT '最后授权管理员ID', - `is_assigned` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已分配:0-未分配,1-已分配', - `last_grant_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后授权时间', - `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - UNIQUE INDEX `uk_user_id` (`user_id`), - INDEX `idx_last_grant_user_id` (`last_grant_user_id`), - INDEX `idx_is_assigned` (`is_assigned`), - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, - FOREIGN KEY (`last_grant_user_id`) REFERENCES `users`(`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户数据权限表'; - --- 3.2 friend_relations(好友关系表) -CREATE TABLE `friend_relations` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '关系ID', - `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', - `friend_id` BIGINT(20) NOT NULL COMMENT '好友ID', - `friend_username` VARCHAR(50) NOT NULL COMMENT '好友用户名', - `online_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '好友在线状态:0-离线,1-在线', - `remark_name` VARCHAR(50) COMMENT '备注名', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间', - UNIQUE INDEX `uk_user_friend` (`user_id`, `friend_id`), - INDEX `idx_user_id` (`user_id`), - INDEX `idx_friend_id` (`friend_id`), - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, - FOREIGN KEY (`friend_id`) REFERENCES `users`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友关系表'; - --- 3.3 friend_requests(好友请求表) -CREATE TABLE `friend_requests` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '请求ID', - `applicant_id` BIGINT(20) NOT NULL COMMENT '申请人ID', - `recipient_id` BIGINT(20) NOT NULL COMMENT '接收人ID', - `apply_msg` VARCHAR(200) COMMENT '申请留言', - `status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '请求状态:0-待处理,1-已同意,2-已拒绝', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间', - `handle_time` DATETIME COMMENT '处理时间', - UNIQUE INDEX `uk_applicant_recipient` (`applicant_id`, `recipient_id`), - INDEX `idx_recipient_status` (`recipient_id`, `status`), - FOREIGN KEY (`applicant_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, - FOREIGN KEY (`recipient_id`) REFERENCES `users`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友请求表'; - --- 3.4 query_shares(查询分享记录表) -CREATE TABLE `query_shares` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '分享ID', - `share_user_id` BIGINT(20) NOT NULL COMMENT '分享人ID', - `receive_user_id` BIGINT(20) NOT NULL COMMENT '接收人ID', - `dialog_id` VARCHAR(50) NOT NULL COMMENT '会话ID', - `target_rounds` JSON NOT NULL COMMENT '会话轮次数组', - `query_title` VARCHAR(200) NOT NULL COMMENT '查询标题', - `share_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '分享时间', - `receive_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '接收状态:0-未处理,1-已保存,2-已删除', - INDEX `idx_receive_user_status` (`receive_user_id`, `receive_status`), - INDEX `idx_share_user_id` (`share_user_id`), - FOREIGN KEY (`share_user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, - FOREIGN KEY (`receive_user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='查询分享记录表'; - --- 4. 系统日志与监控表 - --- 4.1 system_health(系统健康表) -CREATE TABLE `system_health` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', - `db_delay` INT(11) NOT NULL COMMENT '数据库延迟(ms)', - `cache_delay` INT(11) NOT NULL COMMENT '缓存延迟(ms)', - `llm_delay` INT(11) NOT NULL COMMENT '大模型延迟(ms)', - `storage_usage` DECIMAL(5,2) NOT NULL COMMENT '存储使用率(%)', - `collect_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '采集时间', - INDEX `idx_collect_time` (`collect_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统健康表'; - --- 4.2 operation_logs(系统操作日志表) -CREATE TABLE `operation_logs` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', - `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', - `username` VARCHAR(50) NOT NULL COMMENT '用户名', - `operation` VARCHAR(100) NOT NULL COMMENT '操作名称', - `module` VARCHAR(50) NOT NULL COMMENT '操作模块', - `related_llm` VARCHAR(50) COMMENT '涉及模型', - `ip_address` VARCHAR(50) NOT NULL COMMENT 'IP地址', - `operate_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', - `result` TINYINT(1) NOT NULL COMMENT '操作结果:0-失败,1-成功', - `error_msg` TEXT COMMENT '错误信息', - INDEX `idx_operate_time` (`operate_time`), - INDEX `idx_user_id` (`user_id`), - INDEX `idx_module` (`module`), - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表'; - --- 4.3 llm_configs(大模型配置表) -CREATE TABLE `llm_configs` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '配置ID', - `name` VARCHAR(50) NOT NULL UNIQUE COMMENT '模型名称', - `version` VARCHAR(20) NOT NULL COMMENT '模型版本', - `api_key` VARCHAR(200) NOT NULL COMMENT 'API密钥(AES加密)', - `api_url` VARCHAR(255) NOT NULL COMMENT 'API地址', - `status_id` TINYINT(2) NOT NULL COMMENT '状态ID', - `is_disabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否禁用:0-启用,1-禁用', - `timeout` INT(11) NOT NULL COMMENT '超时时间(ms)', - `create_user_id` BIGINT(20) NOT NULL COMMENT '创建人ID', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', - INDEX `idx_status_id` (`status_id`), - INDEX `idx_is_disabled` (`is_disabled`), - UNIQUE INDEX `uk_name` (`name`), - FOREIGN KEY (`status_id`) REFERENCES `llm_status`(`id`), - FOREIGN KEY (`create_user_id`) REFERENCES `users`(`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大模型配置表'; - --- 4.4 notifications(通知表) -CREATE TABLE `notifications` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '通知ID', - `title` VARCHAR(100) NOT NULL COMMENT '通知标题', - `content` TEXT NOT NULL COMMENT '通知内容', - `target_id` TINYINT(2) NOT NULL COMMENT '目标ID', - `priority_id` TINYINT(2) NOT NULL COMMENT '优先级ID', - `publisher_id` BIGINT(20) NOT NULL COMMENT '发布者ID', - `is_top` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶:0-否,1-是', - `publish_time` DATETIME COMMENT '发布时间', - `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `Latest_updateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上次更新时间', - INDEX `idx_target_id` (`target_id`), - INDEX `idx_priority_id` (`priority_id`), - INDEX `idx_is_top_publish_time` (`is_top` DESC, `publish_time` DESC), - FOREIGN KEY (`target_id`) REFERENCES `notification_targets`(`id`), - FOREIGN KEY (`priority_id`) REFERENCES `priorities`(`id`), - FOREIGN KEY (`publisher_id`) REFERENCES `users`(`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知表'; - --- 4.5 token_consume(token消耗表) -CREATE TABLE `token_consume` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', - `llm_name` VARCHAR(50) NOT NULL COMMENT '模型名称', - `total_tokens` BIGINT(11) NOT NULL COMMENT '总消耗', - `prompt_tokens` BIGINT(11) NOT NULL COMMENT '输入消耗', - `completion_tokens` BIGINT(11) NOT NULL COMMENT '输出消耗', - `consume_date` DATE NOT NULL COMMENT '消耗日期', - `growth_rate` DECIMAL(5,2) COMMENT '增长率(%)', - UNIQUE INDEX `uk_llm_date` (`llm_name`, `consume_date`), - INDEX `idx_consume_date` (`consume_date`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='token消耗表'; - --- 4.6 error_logs(错误分析表) -CREATE TABLE `error_logs` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', - `error_type_id` TINYINT(2) NOT NULL COMMENT '错误类型ID', - `error_count` INT(11) NOT NULL COMMENT '错误次数', - `error_rate` DECIMAL(5,2) COMMENT '错误率(%)', - `period` VARCHAR(20) NOT NULL COMMENT '统计周期', - `stat_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '统计时间', - INDEX `idx_error_type_period` (`error_type_id`, `period`), - INDEX `idx_stat_time` (`stat_time`), - FOREIGN KEY (`error_type_id`) REFERENCES `error_types`(`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='错误分析表'; - --- 4.7 performance_metrics(性能趋势表) -CREATE TABLE `performance_metrics` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '指标ID', - `metric_type` VARCHAR(20) NOT NULL COMMENT '指标类型', - `metric_value` DECIMAL(10,2) NOT NULL COMMENT '指标值', - `metric_time` DATETIME NOT NULL COMMENT '指标时间', - `trend` TINYINT(1) COMMENT '趋势标识:0-下降,1-上升', - INDEX `idx_metric_type_time` (`metric_type`, `metric_time`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='性能趋势表'; - --- 4.8 db_connection_logs(数据库连接日志表) -CREATE TABLE `db_connection_logs` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', - `db_connection_id` BIGINT(20) NOT NULL COMMENT '数据库连接ID', - `db_name` VARCHAR(100) NOT NULL COMMENT '数据库名称', - `connect_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '连接时间', - `status` VARCHAR(20) NOT NULL COMMENT '连接状态', - `remark` TEXT COMMENT '备注信息', - `handler_id` BIGINT(20) COMMENT '处理人ID', - INDEX `idx_db_connection_id` (`db_connection_id`), - INDEX `idx_connect_time` (`connect_time`), - INDEX `idx_status` (`status`), - FOREIGN KEY (`db_connection_id`) REFERENCES `db_connections`(`id`), - FOREIGN KEY (`handler_id`) REFERENCES `users`(`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库连接日志表'; - --- 4.9 query_logs(查询日志表) -CREATE TABLE `query_logs` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', - `dialog_id` VARCHAR(50) NOT NULL COMMENT '对话ID', - `data_source_id` BIGINT(20) NOT NULL COMMENT '数据源ID', - `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', - `query_date` DATE NOT NULL COMMENT '查询日期', - `query_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '查询时间', - `execute_result` TINYINT(1) NOT NULL COMMENT '执行结果:0-失败,1-成功', - INDEX `idx_data_source_date` (`data_source_id`, `query_date`), - INDEX `idx_user_id` (`user_id`), - INDEX `idx_dialog_id` (`dialog_id`), - FOREIGN KEY (`data_source_id`) REFERENCES `db_connections`(`id`), - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='查询日志表'; - --- 4.10 user_searches(用户搜索表) -CREATE TABLE `user_searches` ( - `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '搜索ID', - `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', - `sql_content` TEXT NOT NULL COMMENT 'SQL语句', - `query_title` VARCHAR(200) NOT NULL COMMENT '查询标题', - `search_count` INT(11) NOT NULL DEFAULT 1 COMMENT '搜索次数', - `last_search_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后搜索时间', - INDEX `idx_user_last_search` (`user_id`, `last_search_time`), - INDEX `idx_user_search_count` (`user_id`, `search_count` DESC), - FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户搜索表'; - diff --git a/src/springboot_demo/package-lock.json b/src/springboot_demo/package-lock.json deleted file mode 100644 index 536952d1..00000000 --- a/src/springboot_demo/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "springboot_demo", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/src/springboot_demo/pom.xml b/src/springboot_demo/pom.xml deleted file mode 100644 index ef3b9334..00000000 --- a/src/springboot_demo/pom.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.5.7 - - - com.example - springboot_demo - 0.0.1-SNAPSHOT - springboot_demo - springboot_demo - - - - - - - - - - - - - - - 21 - - - - - org.springframework.boot - spring-boot-starter-web - - - - - com.mysql - mysql-connector-j - runtime - - - - - com.baomidou - mybatis-plus-spring-boot3-starter - 3.5.9 - - - - - org.springframework.boot - spring-boot-starter-data-mongodb - - - - - org.springframework.boot - spring-boot-starter-data-redis - - - - - org.projectlombok - lombok - true - - - - - com.alibaba.fastjson2 - fastjson2 - 2.0.43 - - - - - org.springframework.boot - spring-boot-starter-validation - - - - - org.springframework.boot - spring-boot-starter-security - - - - - io.jsonwebtoken - jjwt-api - 0.11.5 - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - runtime - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java deleted file mode 100644 index 60a1168e..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.springboot_demo; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class SpringbootDemoApplication { - - public static void main(String[] args) { - SpringApplication.run(SpringbootDemoApplication.class, args); - } - -} diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/common/Result.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/common/Result.java deleted file mode 100644 index ecac05d4..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/common/Result.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.springboot_demo.common; - -import lombok.Data; - -@Data -public class Result { - private Integer code; - private String message; - private T data; - - public static Result success() { - return success(null); - } - - public static Result success(T data) { - Result result = new Result<>(); - result.setCode(200); - result.setMessage("success"); - result.setData(data); - return result; - } - - public static Result error(String message) { - Result result = new Result<>(); - result.setCode(500); - result.setMessage(message); - return result; - } - - public static Result error(Integer code, String message) { - Result result = new Result<>(); - result.setCode(code); - result.setMessage(message); - return result; - } -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/CorsConfig.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/CorsConfig.java deleted file mode 100644 index e6c93e4d..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/CorsConfig.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.springboot_demo.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; - -@Configuration -public class CorsConfig { - - @Bean - public CorsFilter corsFilter() { - CorsConfiguration config = new CorsConfiguration(); - config.addAllowedOriginPattern("*"); - config.setAllowCredentials(true); - config.addAllowedMethod("*"); - config.addAllowedHeader("*"); - config.setMaxAge(3600L); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", config); - return new CorsFilter(source); - } -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java deleted file mode 100644 index e1a6df5a..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.springboot_demo.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import org.springframework.web.servlet.HandlerInterceptor; - -import com.example.springboot_demo.utils.JwtUtil; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Component -public class JwtInterceptor implements HandlerInterceptor { - - @Autowired - private JwtUtil jwtUtil; - - @Override - public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { - // Allow OPTIONS requests (CORS preflight) - if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { - return true; - } - - String authHeader = request.getHeader("Authorization"); - if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) { - String token = authHeader.substring(7); - if (jwtUtil.validateToken(token)) { - // Optionally set user info in request attributes - request.setAttribute("userId", jwtUtil.getUserIdFromToken(token)); - request.setAttribute("username", jwtUtil.getUsernameFromToken(token)); - return true; - } - } - - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return false; - } -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/SecurityConfig.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/SecurityConfig.java deleted file mode 100644 index 2b306532..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/SecurityConfig.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.springboot_demo.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; - -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .csrf(AbstractHttpConfigurer::disable) // Disable CSRF for REST APIs - .authorizeHttpRequests(auth -> auth - .requestMatchers("/**").permitAll() // Allow all requests for now, we'll handle auth with Interceptor for specific paths or refine this later. - // Note: The plan mentioned "Initially disable Spring Security's default login page". - // Permitting all here effectively bypasses Spring Security's default auth flow, relying on our custom JWT flow. - ); - return http.build(); - } -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java deleted file mode 100644 index 7dc940d8..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.springboot_demo.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.lang.NonNull; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class WebMvcConfig implements WebMvcConfigurer { - - @Autowired - private JwtInterceptor jwtInterceptor; - - @Override - public void addInterceptors(@NonNull InterceptorRegistry registry) { - registry.addInterceptor(jwtInterceptor) - .addPathPatterns("/**") - .excludePathPatterns( - "/auth/**", - "/role", // 仅 POST /role 免认证 - "/user", // 用户注册免认证 - "/error" - ); - } -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/AuthController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/AuthController.java deleted file mode 100644 index ef3ab1fa..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/AuthController.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.dto.LoginDTO; -import com.example.springboot_demo.service.AuthService; -import com.example.springboot_demo.vo.LoginVO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/auth") -public class AuthController { - - @Autowired - private AuthService authService; - - @PostMapping("/login") - public Result login(@RequestBody LoginDTO loginDTO) { - try { - LoginVO loginVO = authService.login(loginDTO); - return Result.success(loginVO); - } catch (Exception e) { - return Result.error(e.getMessage()); - } - } -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java deleted file mode 100644 index 13ba8982..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.ColumnMetadata; -import com.example.springboot_demo.service.ColumnMetadataService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/column-metadata") -public class ColumnMetadataController { - - @Autowired - private ColumnMetadataService columnMetadataService; - - /** - * 查询所有字段元数据 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(columnMetadataService.list()); - } - - /** - * 根据表ID查询字段元数据 - */ - @GetMapping("/list/{tableId}") - public Result> listByTable(@PathVariable Long tableId) { - return Result.success(columnMetadataService.listByTableId(tableId)); - } - - /** - * 根据ID查询字段元数据 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Long id) { - return Result.success(columnMetadataService.getById(id)); - } - - /** - * 添加字段元数据 - */ - @PostMapping - public Result save(@RequestBody ColumnMetadata columnMetadata) { - columnMetadata.setCreateTime(LocalDateTime.now()); - if (columnMetadata.getIsPrimary() == null) { - columnMetadata.setIsPrimary(0); - } - columnMetadataService.save(columnMetadata); - return Result.success(columnMetadata); - } - - /** - * 更新字段元数据 - */ - @PutMapping - public Result update(@RequestBody ColumnMetadata columnMetadata) { - columnMetadataService.updateById(columnMetadata); - return Result.success(columnMetadata); - } - - /** - * 删除字段元数据 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Long id) { - columnMetadataService.removeById(id); - return Result.success(); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java deleted file mode 100644 index bcb3282f..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.DbConnection; -import com.example.springboot_demo.service.DbConnectionService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/db-connection") -public class DbConnectionController { - - @Autowired - private DbConnectionService dbConnectionService; - - /** - * 查询所有数据库连接 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(dbConnectionService.list()); - } - - /** - * 根据创建者ID查询数据库连接 - */ - @GetMapping("/list/{createUserId}") - public Result> listByUser(@PathVariable Long createUserId) { - return Result.success(dbConnectionService.listByCreateUserId(createUserId)); - } - - /** - * 根据ID查询数据库连接 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Long id) { - return Result.success(dbConnectionService.getById(id)); - } - - /** - * 添加数据库连接 - */ - @PostMapping - public Result save(@RequestBody DbConnection dbConnection) { - dbConnection.setCreateTime(LocalDateTime.now()); - dbConnection.setUpdateTime(LocalDateTime.now()); - if (dbConnection.getStatus() == null) { - dbConnection.setStatus("disconnected"); - } - dbConnectionService.save(dbConnection); - return Result.success(dbConnection); - } - - /** - * 更新数据库连接 - */ - @PutMapping - public Result update(@RequestBody DbConnection dbConnection) { - dbConnection.setUpdateTime(LocalDateTime.now()); - dbConnectionService.updateById(dbConnection); - return Result.success(dbConnection); - } - - /** - * 删除数据库连接 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Long id) { - dbConnectionService.removeById(id); - return Result.success(); - } - - /** - * 测试数据库连接 - */ - @GetMapping("/test/{id}") - public Result testConnection(@PathVariable Long id) { - boolean result = dbConnectionService.testConnection(id); - return Result.success(result); - } -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbTypeController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbTypeController.java deleted file mode 100644 index 6dc048f5..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DbTypeController.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.DbType; -import com.example.springboot_demo.service.DbTypeService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/db-type") -public class DbTypeController { - - @Autowired - private DbTypeService dbTypeService; - - /** - * 查询所有数据库类型 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(dbTypeService.list()); - } - - /** - * 根据ID查询 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Integer id) { - return Result.success(dbTypeService.getById(id)); - } - - /** - * 根据类型编码查询 - */ - @GetMapping("/code/{typeCode}") - public Result getByTypeCode(@PathVariable String typeCode) { - return Result.success(dbTypeService.getByTypeCode(typeCode)); - } - - /** - * 添加数据库类型 - */ - @PostMapping - public Result save(@RequestBody DbType dbType) { - dbTypeService.save(dbType); - return Result.success(dbType); - } - - /** - * 更新数据库类型 - */ - @PutMapping - public Result update(@RequestBody DbType dbType) { - dbTypeService.updateById(dbType); - return Result.success(dbType); - } - - /** - * 删除数据库类型 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Integer id) { - dbTypeService.removeById(id); - return Result.success(); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DialogController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DialogController.java deleted file mode 100644 index 9010bb95..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/DialogController.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mongodb.DialogRecord; -import com.example.springboot_demo.service.DialogService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/dialog") -public class DialogController { - - @Autowired - private DialogService dialogService; - - @GetMapping("/list") - public Result> getUserDialogs(@RequestHeader(value = "userId", defaultValue = "1") Long userId) { - List dialogs = dialogService.getUserDialogs(userId); - return Result.success(dialogs); - } - - @GetMapping("/{dialogId}") - public Result getDialogById(@PathVariable String dialogId) { - DialogRecord dialog = dialogService.getDialogById(dialogId); - if (dialog != null) { - return Result.success(dialog); - } - return Result.error("对话不存在"); - } -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java deleted file mode 100644 index 294b93c4..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.ErrorLog; -import com.example.springboot_demo.service.ErrorLogService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/error-log") -public class ErrorLogController { - - @Autowired - private ErrorLogService errorLogService; - - /** - * 查询所有错误日志 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(errorLogService.list()); - } - - /** - * 根据错误类型查询 - */ - @GetMapping("/list/type/{errorTypeId}") - public Result> listByErrorType(@PathVariable Integer errorTypeId) { - return Result.success(errorLogService.listByErrorTypeId(errorTypeId)); - } - - /** - * 根据统计周期查询 - */ - @GetMapping("/list/period/{period}") - public Result> listByPeriod(@PathVariable String period) { - return Result.success(errorLogService.listByPeriod(period)); - } - - /** - * 根据ID查询 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Long id) { - return Result.success(errorLogService.getById(id)); - } - - /** - * 添加错误日志 - */ - @PostMapping - public Result save(@RequestBody ErrorLog errorLog) { - if (errorLog.getStatTime() == null) { - errorLog.setStatTime(LocalDateTime.now()); - } - errorLogService.save(errorLog); - return Result.success(errorLog); - } - - /** - * 更新错误日志 - */ - @PutMapping - public Result update(@RequestBody ErrorLog errorLog) { - errorLogService.updateById(errorLog); - return Result.success(errorLog); - } - - /** - * 删除错误日志 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Long id) { - errorLogService.removeById(id); - return Result.success(); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java deleted file mode 100644 index 979d7938..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.ErrorType; -import com.example.springboot_demo.service.ErrorTypeService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/error-type") -public class ErrorTypeController { - - @Autowired - private ErrorTypeService errorTypeService; - - /** - * 查询所有错误类型 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(errorTypeService.list()); - } - - /** - * 根据ID查询 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Integer id) { - return Result.success(errorTypeService.getById(id)); - } - - /** - * 根据错误编码查询 - */ - @GetMapping("/code/{errorCode}") - public Result getByErrorCode(@PathVariable String errorCode) { - return Result.success(errorTypeService.getByErrorCode(errorCode)); - } - - /** - * 添加错误类型 - */ - @PostMapping - public Result save(@RequestBody ErrorType errorType) { - errorTypeService.save(errorType); - return Result.success(errorType); - } - - /** - * 更新错误类型 - */ - @PutMapping - public Result update(@RequestBody ErrorType errorType) { - errorTypeService.updateById(errorType); - return Result.success(errorType); - } - - /** - * 删除错误类型 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Integer id) { - errorTypeService.removeById(id); - return Result.success(); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java deleted file mode 100644 index b8be3cf0..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.LlmConfig; -import com.example.springboot_demo.service.LlmConfigService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/llm-config") -public class LlmConfigController { - - @Autowired - private LlmConfigService llmConfigService; - - /** - * 查询所有大模型配置 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(llmConfigService.list()); - } - - /** - * 查询所有可用的大模型配置 - */ - @GetMapping("/list/available") - public Result> listAvailable() { - return Result.success(llmConfigService.listAvailable()); - } - - /** - * 根据ID查询大模型配置 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Long id) { - return Result.success(llmConfigService.getById(id)); - } - - /** - * 添加大模型配置 - */ - @PostMapping - public Result save(@RequestBody LlmConfig llmConfig) { - llmConfig.setCreateTime(LocalDateTime.now()); - llmConfig.setUpdateTime(LocalDateTime.now()); - if (llmConfig.getIsDisabled() == null) { - llmConfig.setIsDisabled(0); - } - llmConfigService.save(llmConfig); - return Result.success(llmConfig); - } - - /** - * 更新大模型配置 - */ - @PutMapping - public Result update(@RequestBody LlmConfig llmConfig) { - llmConfig.setUpdateTime(LocalDateTime.now()); - llmConfigService.updateById(llmConfig); - return Result.success(llmConfig); - } - - /** - * 删除大模型配置 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Long id) { - llmConfigService.removeById(id); - return Result.success(); - } - - /** - * 禁用/启用大模型配置 - */ - @PutMapping("/{id}/toggle") - public Result toggle(@PathVariable Long id) { - LlmConfig config = llmConfigService.getById(id); - if (config != null) { - config.setIsDisabled(config.getIsDisabled() == 0 ? 1 : 0); - config.setUpdateTime(LocalDateTime.now()); - llmConfigService.updateById(config); - } - return Result.success(); - } -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java deleted file mode 100644 index e7437954..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.LlmStatus; -import com.example.springboot_demo.service.LlmStatusService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/llm-status") -public class LlmStatusController { - - @Autowired - private LlmStatusService llmStatusService; - - /** - * 查询所有大模型状态 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(llmStatusService.list()); - } - - /** - * 根据ID查询 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Integer id) { - return Result.success(llmStatusService.getById(id)); - } - - /** - * 根据状态编码查询 - */ - @GetMapping("/code/{statusCode}") - public Result getByStatusCode(@PathVariable String statusCode) { - return Result.success(llmStatusService.getByStatusCode(statusCode)); - } - - /** - * 添加大模型状态 - */ - @PostMapping - public Result save(@RequestBody LlmStatus llmStatus) { - llmStatusService.save(llmStatus); - return Result.success(llmStatus); - } - - /** - * 更新大模型状态 - */ - @PutMapping - public Result update(@RequestBody LlmStatus llmStatus) { - llmStatusService.updateById(llmStatus); - return Result.success(llmStatus); - } - - /** - * 删除大模型状态 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Integer id) { - llmStatusService.removeById(id); - return Result.success(); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationController.java deleted file mode 100644 index 71f65e69..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationController.java +++ /dev/null @@ -1,125 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.Notification; -import com.example.springboot_demo.service.NotificationService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/notification") -public class NotificationController { - - @Autowired - private NotificationService notificationService; - - /** - * 查询所有通知 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(notificationService.list()); - } - - /** - * 查询已发布的通知 - */ - @GetMapping("/list/published") - public Result> listPublished() { - return Result.success(notificationService.listPublished()); - } - - /** - * 查询草稿 - */ - @GetMapping("/list/drafts") - public Result> listDrafts() { - return Result.success(notificationService.listDrafts()); - } - - /** - * 根据目标ID查询通知 - */ - @GetMapping("/list/target/{targetId}") - public Result> listByTarget(@PathVariable Integer targetId) { - return Result.success(notificationService.listByTargetId(targetId)); - } - - /** - * 根据ID查询 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Long id) { - return Result.success(notificationService.getById(id)); - } - - /** - * 添加通知(草稿) - */ - @PostMapping - public Result save(@RequestBody Notification notification) { - notification.setCreateTime(LocalDateTime.now()); - notification.setLatestUpdateTime(LocalDateTime.now()); - if (notification.getIsTop() == null) { - notification.setIsTop(0); - } - notificationService.save(notification); - return Result.success(notification); - } - - /** - * 更新通知 - */ - @PutMapping - public Result update(@RequestBody Notification notification) { - notification.setLatestUpdateTime(LocalDateTime.now()); - notificationService.updateById(notification); - return Result.success(notification); - } - - /** - * 发布通知 - */ - @PutMapping("/{id}/publish") - public Result publish(@PathVariable Long id) { - Notification notification = notificationService.getById(id); - if (notification != null) { - notification.setPublishTime(LocalDateTime.now()); - notification.setLatestUpdateTime(LocalDateTime.now()); - notificationService.updateById(notification); - } - return Result.success(); - } - - /** - * 置顶/取消置顶 - */ - @PutMapping("/{id}/toggle-top") - public Result toggleTop(@PathVariable Long id) { - Notification notification = notificationService.getById(id); - if (notification != null) { - notification.setIsTop(notification.getIsTop() == 0 ? 1 : 0); - notification.setLatestUpdateTime(LocalDateTime.now()); - notificationService.updateById(notification); - } - return Result.success(); - } - - /** - * 删除通知 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Long id) { - notificationService.removeById(id); - return Result.success(); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java deleted file mode 100644 index 4053b71a..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.NotificationTarget; -import com.example.springboot_demo.service.NotificationTargetService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/notification-target") -public class NotificationTargetController { - - @Autowired - private NotificationTargetService notificationTargetService; - - /** - * 查询所有通知目标 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(notificationTargetService.list()); - } - - /** - * 根据ID查询 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Integer id) { - return Result.success(notificationTargetService.getById(id)); - } - - /** - * 根据目标编码查询 - */ - @GetMapping("/code/{targetCode}") - public Result getByTargetCode(@PathVariable String targetCode) { - return Result.success(notificationTargetService.getByTargetCode(targetCode)); - } - - /** - * 添加通知目标 - */ - @PostMapping - public Result save(@RequestBody NotificationTarget notificationTarget) { - notificationTargetService.save(notificationTarget); - return Result.success(notificationTarget); - } - - /** - * 更新通知目标 - */ - @PutMapping - public Result update(@RequestBody NotificationTarget notificationTarget) { - notificationTargetService.updateById(notificationTarget); - return Result.success(notificationTarget); - } - - /** - * 删除通知目标 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Integer id) { - notificationTargetService.removeById(id); - return Result.success(); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/OperationLogController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/OperationLogController.java deleted file mode 100644 index 7046f985..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/OperationLogController.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.OperationLog; -import com.example.springboot_demo.service.OperationLogService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/operation-log") -public class OperationLogController { - - @Autowired - private OperationLogService operationLogService; - - /** - * 查询所有操作日志 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(operationLogService.list()); - } - - /** - * 根据用户ID查询操作日志 - */ - @GetMapping("/list/user/{userId}") - public Result> listByUser(@PathVariable Long userId) { - return Result.success(operationLogService.listByUserId(userId)); - } - - /** - * 根据模块查询操作日志 - */ - @GetMapping("/list/module/{module}") - public Result> listByModule(@PathVariable String module) { - return Result.success(operationLogService.listByModule(module)); - } - - /** - * 查询失败的操作日志 - */ - @GetMapping("/list/failed") - public Result> listFailed() { - return Result.success(operationLogService.listFailed()); - } - - /** - * 根据ID查询 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Long id) { - return Result.success(operationLogService.getById(id)); - } - - /** - * 添加操作日志 - */ - @PostMapping - public Result save(@RequestBody OperationLog operationLog) { - if (operationLog.getOperateTime() == null) { - operationLog.setOperateTime(LocalDateTime.now()); - } - operationLogService.save(operationLog); - return Result.success(operationLog); - } - - /** - * 删除操作日志 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Long id) { - operationLogService.removeById(id); - return Result.success(); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/PriorityController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/PriorityController.java deleted file mode 100644 index 7912c334..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/PriorityController.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.Priority; -import com.example.springboot_demo.service.PriorityService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/priority") -public class PriorityController { - - @Autowired - private PriorityService priorityService; - - /** - * 查询所有优先级(按排序) - */ - @GetMapping("/list") - public Result> list() { - return Result.success(priorityService.listOrderBySort()); - } - - /** - * 根据ID查询 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Integer id) { - return Result.success(priorityService.getById(id)); - } - - /** - * 根据优先级编码查询 - */ - @GetMapping("/code/{priorityCode}") - public Result getByPriorityCode(@PathVariable String priorityCode) { - return Result.success(priorityService.getByPriorityCode(priorityCode)); - } - - /** - * 添加优先级 - */ - @PostMapping - public Result save(@RequestBody Priority priority) { - priorityService.save(priority); - return Result.success(priority); - } - - /** - * 更新优先级 - */ - @PutMapping - public Result update(@RequestBody Priority priority) { - priorityService.updateById(priority); - return Result.success(priority); - } - - /** - * 删除优先级 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Integer id) { - priorityService.removeById(id); - return Result.success(); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryController.java deleted file mode 100644 index 78d2f7a3..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryController.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.dto.QueryRequestDTO; -import com.example.springboot_demo.service.QueryService; -import com.example.springboot_demo.vo.QueryResponseVO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -@RestController -@RequestMapping("/query") -public class QueryController { - - @Autowired - private QueryService queryService; - - @PostMapping("/execute") - public Result executeQuery(@RequestBody QueryRequestDTO request, - @RequestHeader(value = "userId", defaultValue = "1") Long userId) { - try { - QueryResponseVO response = queryService.executeQuery(request, userId); - return Result.success(response); - } catch (Exception e) { - return Result.error(e.getMessage()); - } - } -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryLogController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryLogController.java deleted file mode 100644 index 22f789c3..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/QueryLogController.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.QueryLog; -import com.example.springboot_demo.service.QueryLogService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/query-log") -public class QueryLogController { - - @Autowired - private QueryLogService queryLogService; - - /** - * 查询所有查询日志 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(queryLogService.list()); - } - - /** - * 根据用户ID查询查询日志 - */ - @GetMapping("/list/user/{userId}") - public Result> listByUser(@PathVariable Long userId) { - return Result.success(queryLogService.listByUserId(userId)); - } - - /** - * 根据对话ID查询查询日志 - */ - @GetMapping("/list/dialog/{dialogId}") - public Result> listByDialog(@PathVariable String dialogId) { - return Result.success(queryLogService.listByDialogId(dialogId)); - } - - /** - * 根据ID查询查询日志 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Long id) { - return Result.success(queryLogService.getById(id)); - } - - /** - * 添加查询日志 - */ - @PostMapping - public Result save(@RequestBody QueryLog queryLog) { - if (queryLog.getQueryDate() == null) { - queryLog.setQueryDate(LocalDate.now()); - } - if (queryLog.getQueryTime() == null) { - queryLog.setQueryTime(LocalDateTime.now()); - } - queryLogService.save(queryLog); - return Result.success(queryLog); - } - - /** - * 删除查询日志 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Long id) { - queryLogService.removeById(id); - return Result.success(); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/RoleController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/RoleController.java deleted file mode 100644 index b6756740..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/RoleController.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.Role; -import com.example.springboot_demo.service.RoleService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@RestController -@RequestMapping("/role") -public class RoleController { - - @Autowired - private RoleService roleService; - - @PostMapping - public Result add(@RequestBody Role role) { - roleService.save(role); - return Result.success(role); - } - - @GetMapping("/list") - public Result> list() { - return Result.success(roleService.list()); - } -} diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java deleted file mode 100644 index 0c7429bd..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.SystemHealth; -import com.example.springboot_demo.service.SystemHealthService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/system-health") -public class SystemHealthController { - - @Autowired - private SystemHealthService systemHealthService; - - /** - * 查询所有健康记录 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(systemHealthService.list()); - } - - /** - * 查询最新的健康记录 - */ - @GetMapping("/latest") - public Result getLatest() { - return Result.success(systemHealthService.getLatest()); - } - - /** - * 查询最近N条健康记录 - */ - @GetMapping("/recent/{limit}") - public Result> listRecent(@PathVariable int limit) { - return Result.success(systemHealthService.listRecent(limit)); - } - - /** - * 根据ID查询 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Long id) { - return Result.success(systemHealthService.getById(id)); - } - - /** - * 添加健康记录 - */ - @PostMapping - public Result save(@RequestBody SystemHealth systemHealth) { - if (systemHealth.getCollectTime() == null) { - systemHealth.setCollectTime(LocalDateTime.now()); - } - systemHealthService.save(systemHealth); - return Result.success(systemHealth); - } - - /** - * 删除健康记录 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Long id) { - systemHealthService.removeById(id); - return Result.success(); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java deleted file mode 100644 index f6f77f72..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.TableMetadata; -import com.example.springboot_demo.service.TableMetadataService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/table-metadata") -public class TableMetadataController { - - @Autowired - private TableMetadataService tableMetadataService; - - /** - * 查询所有表元数据 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(tableMetadataService.list()); - } - - /** - * 根据数据库连接ID查询表元数据 - */ - @GetMapping("/list/{dbConnectionId}") - public Result> listByDbConnection(@PathVariable Long dbConnectionId) { - return Result.success(tableMetadataService.listByDbConnectionId(dbConnectionId)); - } - - /** - * 根据ID查询表元数据 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Long id) { - return Result.success(tableMetadataService.getById(id)); - } - - /** - * 添加表元数据 - */ - @PostMapping - public Result save(@RequestBody TableMetadata tableMetadata) { - tableMetadata.setCreateTime(LocalDateTime.now()); - tableMetadata.setUpdateTime(LocalDateTime.now()); - tableMetadataService.save(tableMetadata); - return Result.success(tableMetadata); - } - - /** - * 更新表元数据 - */ - @PutMapping - public Result update(@RequestBody TableMetadata tableMetadata) { - tableMetadata.setUpdateTime(LocalDateTime.now()); - tableMetadataService.updateById(tableMetadata); - return Result.success(tableMetadata); - } - - /** - * 删除表元数据 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Long id) { - tableMetadataService.removeById(id); - return Result.success(); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TestController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TestController.java deleted file mode 100644 index 7e9efbd8..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/TestController.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.example.springboot_demo.controller; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/test") -public class TestController { - - @Autowired - private JdbcTemplate jdbcTemplate; - - @Autowired - private MongoTemplate mongoTemplate; - - @Autowired - private StringRedisTemplate redisTemplate; - - @GetMapping("/hello") - public String hello() { - return "Hello, Spring Boot!"; - } - - @GetMapping("/mysql") - public Map testMySQL() { - Map result = new HashMap<>(); - try { - // 查询 MySQL 版本 - String version = jdbcTemplate.queryForObject("SELECT VERSION()", String.class); - // 查询数据库名 - String database = jdbcTemplate.queryForObject("SELECT DATABASE()", String.class); - // 查询表数量 - Integer tableCount = jdbcTemplate.queryForObject( - "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ?", - Integer.class, - database - ); - - result.put("status", "success"); - result.put("version", version); - result.put("database", database); - result.put("tableCount", tableCount); - } catch (Exception e) { - result.put("status", "error"); - result.put("message", e.getMessage()); - } - return result; - } - - @GetMapping("/mongodb") - public Map testMongoDB() { - Map result = new HashMap<>(); - try { - // 获取数据库名 - String dbName = mongoTemplate.getDb().getName(); - // 获取集合数量 - int collectionCount = mongoTemplate.getDb().listCollectionNames().into(new java.util.ArrayList<>()).size(); - - result.put("status", "success"); - result.put("database", dbName); - result.put("collectionCount", collectionCount); - } catch (Exception e) { - result.put("status", "error"); - result.put("message", e.getMessage()); - } - return result; - } - - @GetMapping("/redis") - public Map testRedis() { - Map result = new HashMap<>(); - try { - // 测试写入 - redisTemplate.opsForValue().set("test:key", "test_value"); - // 测试读取 - String value = redisTemplate.opsForValue().get("test:key"); - // 删除测试数据 - redisTemplate.delete("test:key"); - - result.put("status", "success"); - result.put("message", "Redis 连接正常"); - result.put("testValue", value); - } catch (Exception e) { - result.put("status", "error"); - result.put("message", e.getMessage()); - } - return result; - } - - @GetMapping("/all") - public Map testAll() { - Map result = new HashMap<>(); - result.put("mysql", testMySQL()); - result.put("mongodb", testMongoDB()); - result.put("redis", testRedis()); - return result; - } -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserController.java deleted file mode 100644 index 77e982fb..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserController.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.example.springboot_demo.controller; - -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.User; -import com.example.springboot_demo.service.UserService; - -@RestController -@RequestMapping("/user") -public class UserController { - - @Autowired - private UserService userService; - - @GetMapping("/{id}") - public Result getById(@PathVariable Long id) { - User user = userService.getById(id); - if (user != null) { - return Result.success(user); - } - return Result.error("用户不存在"); - } - - @GetMapping("/list") - public Result> list() { - List users = userService.list(); - return Result.success(users); - } - - @GetMapping("/page") - public Result> page( - @RequestParam(defaultValue = "1") int current, - @RequestParam(defaultValue = "10") int size) { - Page page = userService.page(current, size); - return Result.success(page); - } - - @PostMapping - public Result save(@RequestBody User user) { - boolean success = userService.save(user); - if (success) { - return Result.success("添加成功"); - } - return Result.error("添加失败"); - } - - @PutMapping - public Result update(@RequestBody User user) { - boolean success = userService.updateById(user); - if (success) { - return Result.success("更新成功"); - } - return Result.error("更新失败"); - } - - @DeleteMapping("/{id}") - public Result delete(@PathVariable Long id) { - boolean success = userService.removeById(id); - if (success) { - return Result.success("删除成功"); - } - return Result.error("删除失败"); - } - - @GetMapping("/username/{username}") - public Result getByUsername(@PathVariable String username) { - User user = userService.getByUsername(username); - if (user != null) { - return Result.success(user); - } - return Result.error("用户不存在"); - } -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java deleted file mode 100644 index c91565ab..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.example.springboot_demo.controller; - -import com.example.springboot_demo.common.Result; -import com.example.springboot_demo.entity.mysql.UserDbPermission; -import com.example.springboot_demo.service.UserDbPermissionService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.time.LocalDateTime; -import java.util.List; - -@RestController -@RequestMapping("/user-db-permission") -public class UserDbPermissionController { - - @Autowired - private UserDbPermissionService userDbPermissionService; - - /** - * 查询所有权限 - */ - @GetMapping("/list") - public Result> list() { - return Result.success(userDbPermissionService.list()); - } - - /** - * 查询已分配权限的用户 - */ - @GetMapping("/list/assigned") - public Result> listAssigned() { - return Result.success(userDbPermissionService.listAssigned()); - } - - /** - * 查询未分配权限的用户 - */ - @GetMapping("/list/unassigned") - public Result> listUnassigned() { - return Result.success(userDbPermissionService.listUnassigned()); - } - - /** - * 根据ID查询 - */ - @GetMapping("/{id}") - public Result getById(@PathVariable Long id) { - return Result.success(userDbPermissionService.getById(id)); - } - - /** - * 根据用户ID查询权限 - */ - @GetMapping("/user/{userId}") - public Result getByUserId(@PathVariable Long userId) { - return Result.success(userDbPermissionService.getByUserId(userId)); - } - - /** - * 添加或更新权限 - */ - @PostMapping - public Result save(@RequestBody UserDbPermission permission) { - permission.setLastGrantTime(LocalDateTime.now()); - permission.setUpdateTime(LocalDateTime.now()); - if (permission.getIsAssigned() == null) { - permission.setIsAssigned(1); - } - userDbPermissionService.save(permission); - return Result.success(permission); - } - - /** - * 更新权限 - */ - @PutMapping - public Result update(@RequestBody UserDbPermission permission) { - permission.setLastGrantTime(LocalDateTime.now()); - permission.setUpdateTime(LocalDateTime.now()); - userDbPermissionService.updateById(permission); - return Result.success(permission); - } - - /** - * 删除权限 - */ - @DeleteMapping("/{id}") - public Result delete(@PathVariable Long id) { - userDbPermissionService.removeById(id); - return Result.success(); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/LoginDTO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/LoginDTO.java deleted file mode 100644 index bc48a109..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/LoginDTO.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.springboot_demo.dto; - -import lombok.Data; - -@Data -public class LoginDTO { - private String username; - private String password; -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java deleted file mode 100644 index dff2b8c9..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.springboot_demo.dto; - -import lombok.Data; - -@Data -public class QueryRequestDTO { - private String userPrompt; // 用户的自然语言查询 - private String model; // 使用的大模型 - private String database; // 使用的数据库 - private String conversationId; // 对话ID(用于多轮对话) -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java deleted file mode 100644 index e72179ef..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.springboot_demo.entity.mongodb; - -import lombok.Data; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.mapping.Document; - -import java.time.LocalDateTime; - -@Data -@Document(collection = "dialog_records") -public class DialogRecord { - - @Id - private String id; - - private String dialogId; - - private Long userId; - - private String topic; - - private Integer totalRounds; - - private LocalDateTime startTime; - - private LocalDateTime lastTime; -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java deleted file mode 100644 index 32f43878..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@TableName("column_metadata") -public class ColumnMetadata { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private Long tableId; - - private String columnName; - - private String dataType; - - private String description; - - private Integer isPrimary; - - private LocalDateTime createTime; -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java deleted file mode 100644 index 6b3274ca..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@TableName("db_connections") -public class DbConnection { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private String name; - - private Integer dbTypeId; - - private String url; - - private String username; - - private String password; - - private String status; - - private Long createUserId; - - private LocalDateTime createTime; - - private LocalDateTime updateTime; -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java deleted file mode 100644 index bcbff5ef..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -@Data -@TableName("db_types") -public class DbType { - @TableId(value = "id", type = IdType.AUTO) - private Integer id; - - private String typeName; - - private String typeCode; - - private String description; -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java deleted file mode 100644 index 6a708bf0..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -@Data -@TableName("error_logs") -public class ErrorLog { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private Integer errorTypeId; - - private Integer errorCount; - - private BigDecimal errorRate; - - private String period; - - private LocalDateTime statTime; -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java deleted file mode 100644 index 548e619e..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -@Data -@TableName("error_types") -public class ErrorType { - @TableId(value = "id", type = IdType.AUTO) - private Integer id; - - private String errorName; - - private String errorCode; - - private String description; -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java deleted file mode 100644 index 7fba5c66..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@TableName("llm_configs") -public class LlmConfig { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private String name; - - private String version; - - private String apiKey; - - private String apiUrl; - - private Integer statusId; - - private Integer isDisabled; - - private Integer timeout; - - private Long createUserId; - - private LocalDateTime createTime; - - private LocalDateTime updateTime; -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java deleted file mode 100644 index 834d787f..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -@Data -@TableName("llm_status") -public class LlmStatus { - @TableId(value = "id", type = IdType.AUTO) - private Integer id; - - private String statusName; - - private String statusCode; - - private String description; -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java deleted file mode 100644 index d9dac720..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@TableName("notifications") -public class Notification { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private String title; - - private String content; - - private Integer targetId; - - private Integer priorityId; - - private Long publisherId; - - private Integer isTop; - - private LocalDateTime publishTime; - - private LocalDateTime createTime; - - private LocalDateTime latestUpdateTime; -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java deleted file mode 100644 index 47f0d16f..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -@Data -@TableName("notification_targets") -public class NotificationTarget { - @TableId(value = "id", type = IdType.AUTO) - private Integer id; - - private String targetName; - - private String targetCode; - - private String description; -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java deleted file mode 100644 index 449a8f10..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@TableName("operation_logs") -public class OperationLog { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private Long userId; - - private String username; - - private String operation; - - private String module; - - private String relatedLlm; - - private String ipAddress; - - private LocalDateTime operateTime; - - private Integer result; - - private String errorMsg; -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java deleted file mode 100644 index 1800e3f5..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -@Data -@TableName("priorities") -public class Priority { - @TableId(value = "id", type = IdType.AUTO) - private Integer id; - - private String priorityName; - - private String priorityCode; - - private Integer sort; -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java deleted file mode 100644 index b192dbfe..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDate; -import java.time.LocalDateTime; - -@Data -@TableName("query_logs") -public class QueryLog { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private String dialogId; - - private Long dataSourceId; - - private Long userId; - - private LocalDate queryDate; - - private LocalDateTime queryTime; - - private Integer executeResult; -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Role.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Role.java deleted file mode 100644 index 2685fbef..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/Role.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -@Data -@TableName("roles") -public class Role { - - @TableId(type = IdType.AUTO) - private Integer id; - - private String roleName; - - private String roleCode; - - private String description; -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java deleted file mode 100644 index 949bfff0..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.math.BigDecimal; -import java.time.LocalDateTime; - -@Data -@TableName("system_health") -public class SystemHealth { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private Integer dbDelay; - - private Integer cacheDelay; - - private Integer llmDelay; - - private BigDecimal storageUsage; - - private LocalDateTime collectTime; -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java deleted file mode 100644 index 537e2607..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@TableName("table_metadata") -public class TableMetadata { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private Long dbConnectionId; - - private String tableName; - - private String description; - - private LocalDateTime createTime; - - private LocalDateTime updateTime; -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/User.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/User.java deleted file mode 100644 index 6bf4b10f..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/User.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import java.time.LocalDateTime; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; - -import lombok.Data; - -@Data -@TableName("users") -public class User { - - @TableId(type = IdType.AUTO) - private Long id; - - private String username; - - private String password; - - private String email; - - private String phonenumber; - - private Integer roleId; - - private String avatarUrl; - - private Integer status; - - private Integer onlineStatus; - - private LocalDateTime createTime; - - private LocalDateTime updateTime; -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java deleted file mode 100644 index 77395ad1..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.springboot_demo.entity.mysql; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -@Data -@TableName("user_db_permissions") -public class UserDbPermission { - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - private Long userId; - - private String permissionDetails; // JSON格式字符串 - - private Long lastGrantUserId; - - private Integer isAssigned; - - private LocalDateTime lastGrantTime; - - private LocalDateTime updateTime; -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java deleted file mode 100644 index 1dc43695..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.ColumnMetadata; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface ColumnMetadataMapper extends BaseMapper { -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java deleted file mode 100644 index 2fe5045d..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.DbConnection; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface DbConnectionMapper extends BaseMapper { -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java deleted file mode 100644 index 09d97b6e..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.DbType; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface DbTypeMapper extends BaseMapper { -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java deleted file mode 100644 index df160331..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.ErrorLog; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface ErrorLogMapper extends BaseMapper { -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java deleted file mode 100644 index b9148bca..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.ErrorType; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface ErrorTypeMapper extends BaseMapper { -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java deleted file mode 100644 index 53198bf5..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.LlmConfig; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface LlmConfigMapper extends BaseMapper { -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java deleted file mode 100644 index b03abe84..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.LlmStatus; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface LlmStatusMapper extends BaseMapper { -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java deleted file mode 100644 index 30259ac6..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.Notification; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface NotificationMapper extends BaseMapper { -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java deleted file mode 100644 index f228f075..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.NotificationTarget; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface NotificationTargetMapper extends BaseMapper { -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java deleted file mode 100644 index 0c7c52c6..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.OperationLog; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface OperationLogMapper extends BaseMapper { -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java deleted file mode 100644 index a3a8149f..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.Priority; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface PriorityMapper extends BaseMapper { -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java deleted file mode 100644 index 7f335438..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.QueryLog; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface QueryLogMapper extends BaseMapper { -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java deleted file mode 100644 index 4ea0159a..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.Role; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface RoleMapper extends BaseMapper { -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java deleted file mode 100644 index 30b80f6e..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.SystemHealth; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface SystemHealthMapper extends BaseMapper { -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java deleted file mode 100644 index 1796fd36..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.TableMetadata; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface TableMetadataMapper extends BaseMapper { -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java deleted file mode 100644 index 83b35f82..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.springboot_demo.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.UserDbPermission; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface UserDbPermissionMapper extends BaseMapper { -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserMapper.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserMapper.java deleted file mode 100644 index cf537ad9..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/mapper/UserMapper.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.springboot_demo.mapper; - -import org.apache.ibatis.annotations.Mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.example.springboot_demo.entity.mysql.User; - -@Mapper -public interface UserMapper extends BaseMapper { -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java deleted file mode 100644 index 10a74819..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.springboot_demo.repository; - -import com.example.springboot_demo.entity.mongodb.DialogRecord; -import org.springframework.data.mongodb.repository.MongoRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public interface DialogRecordRepository extends MongoRepository { - List findByUserIdOrderByLastTimeDesc(Long userId); - DialogRecord findByDialogId(String dialogId); -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/AuthService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/AuthService.java deleted file mode 100644 index 2b09304f..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/AuthService.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.springboot_demo.service; - -import com.example.springboot_demo.dto.LoginDTO; -import com.example.springboot_demo.vo.LoginVO; - -public interface AuthService { - LoginVO login(LoginDTO loginDTO); -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java deleted file mode 100644 index fb20e175..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.ColumnMetadata; - -import java.util.List; - -public interface ColumnMetadataService extends IService { - /** - * 根据表ID查询字段元数据列表 - */ - List listByTableId(Long tableId); -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbConnectionService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbConnectionService.java deleted file mode 100644 index cc488b7a..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbConnectionService.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.DbConnection; - -import java.util.List; - -public interface DbConnectionService extends IService { - /** - * 根据创建者ID查询数据库连接列表 - */ - List listByCreateUserId(Long createUserId); - - /** - * 测试数据库连接 - */ - boolean testConnection(Long id); -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbTypeService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbTypeService.java deleted file mode 100644 index bf73ad08..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DbTypeService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.DbType; - -public interface DbTypeService extends IService { - /** - * 根据类型编码查询 - */ - DbType getByTypeCode(String typeCode); -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DialogService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DialogService.java deleted file mode 100644 index 1820ac01..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/DialogService.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.springboot_demo.service; - -import com.example.springboot_demo.entity.mongodb.DialogRecord; - -import java.util.List; - -public interface DialogService { - List getUserDialogs(Long userId); - DialogRecord getDialogById(String dialogId); -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorLogService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorLogService.java deleted file mode 100644 index 7e52e227..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorLogService.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.ErrorLog; - -import java.util.List; - -public interface ErrorLogService extends IService { - /** - * 根据错误类型查询 - */ - List listByErrorTypeId(Integer errorTypeId); - - /** - * 根据统计周期查询 - */ - List listByPeriod(String period); -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java deleted file mode 100644 index 7d333874..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.ErrorType; - -public interface ErrorTypeService extends IService { - /** - * 根据错误编码查询 - */ - ErrorType getByErrorCode(String errorCode); -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmConfigService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmConfigService.java deleted file mode 100644 index ee679470..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmConfigService.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.LlmConfig; - -import java.util.List; - -public interface LlmConfigService extends IService { - /** - * 获取所有可用的大模型配置 - */ - List listAvailable(); -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmService.java deleted file mode 100644 index 6823b868..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmService.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.springboot_demo.service; - -import java.util.Map; - -/** - * 大模型调用服务接口 - */ -public interface LlmService { - /** - * 调用大模型生成SQL和结果 - * @param prompt 用户提示词 - * @param modelName 模型名称 - * @param databaseName 数据库名称 - * @return 包含SQL、表格数据和图表数据的Map - */ - Map generateQuery(String prompt, String modelName, String databaseName); -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmStatusService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmStatusService.java deleted file mode 100644 index ab52b85d..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/LlmStatusService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.LlmStatus; - -public interface LlmStatusService extends IService { - /** - * 根据状态编码查询 - */ - LlmStatus getByStatusCode(String statusCode); -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationService.java deleted file mode 100644 index 6cbea2f0..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.Notification; - -import java.util.List; - -public interface NotificationService extends IService { - /** - * 查询所有已发布的通知(置顶优先,按时间排序) - */ - List listPublished(); - - /** - * 查询所有草稿 - */ - List listDrafts(); - - /** - * 根据目标ID查询通知 - */ - List listByTargetId(Integer targetId); -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java deleted file mode 100644 index 4cc954cf..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.NotificationTarget; - -public interface NotificationTargetService extends IService { - /** - * 根据目标编码查询 - */ - NotificationTarget getByTargetCode(String targetCode); -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/OperationLogService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/OperationLogService.java deleted file mode 100644 index b15542ff..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/OperationLogService.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.OperationLog; - -import java.util.List; - -public interface OperationLogService extends IService { - /** - * 根据用户ID查询操作日志 - */ - List listByUserId(Long userId); - - /** - * 根据模块查询操作日志 - */ - List listByModule(String module); - - /** - * 查询失败的操作日志 - */ - List listFailed(); -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/PriorityService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/PriorityService.java deleted file mode 100644 index fadedad1..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/PriorityService.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.Priority; - -import java.util.List; - -public interface PriorityService extends IService { - /** - * 根据优先级编码查询 - */ - Priority getByPriorityCode(String priorityCode); - - /** - * 按排序权重查询所有优先级 - */ - List listOrderBySort(); -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryLogService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryLogService.java deleted file mode 100644 index 7aba0403..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryLogService.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.QueryLog; - -import java.util.List; - -public interface QueryLogService extends IService { - /** - * 根据用户ID查询查询日志 - */ - List listByUserId(Long userId); - - /** - * 根据对话ID查询查询日志 - */ - List listByDialogId(String dialogId); -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryService.java deleted file mode 100644 index c70897c1..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/QueryService.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example.springboot_demo.service; - -import com.example.springboot_demo.dto.QueryRequestDTO; -import com.example.springboot_demo.vo.QueryResponseVO; - -public interface QueryService { - QueryResponseVO executeQuery(QueryRequestDTO request, Long userId); -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/RoleService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/RoleService.java deleted file mode 100644 index f9a5d4ae..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/RoleService.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.Role; - -public interface RoleService extends IService { -} diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/SystemHealthService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/SystemHealthService.java deleted file mode 100644 index 5377752b..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/SystemHealthService.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.SystemHealth; - -import java.util.List; - -public interface SystemHealthService extends IService { - /** - * 查询最新的健康记录 - */ - SystemHealth getLatest(); - - /** - * 查询最近N条健康记录 - */ - List listRecent(int limit); -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/TableMetadataService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/TableMetadataService.java deleted file mode 100644 index 7d66bc37..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/TableMetadataService.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.springboot_demo.service; - -import java.util.List; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.TableMetadata; - -public interface TableMetadataService extends IService { - /** - * 根据数据库连接ID查询表元数据列表 - */ - List listByDbConnectionId(Long dbConnectionId); -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java deleted file mode 100644 index e776639d..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.springboot_demo.service; - -import com.baomidou.mybatisplus.extension.service.IService; -import com.example.springboot_demo.entity.mysql.UserDbPermission; - -import java.util.List; - -public interface UserDbPermissionService extends IService { - /** - * 根据用户ID查询权限 - */ - UserDbPermission getByUserId(Long userId); - - /** - * 查询所有已分配权限的用户 - */ - List listAssigned(); - - /** - * 查询所有未分配权限的用户 - */ - List listUnassigned(); -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserService.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserService.java deleted file mode 100644 index dae70f44..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/UserService.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.springboot_demo.service; - -import java.util.List; - -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.example.springboot_demo.entity.mysql.User; - -public interface UserService { - - User getById(Long id); - - List list(); - - Page page(int current, int size); - - boolean save(User user); - - boolean updateById(User user); - - boolean removeById(Long id); - - User getByUsername(String username); -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java deleted file mode 100644 index 8f57905d..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.example.springboot_demo.dto.LoginDTO; -import com.example.springboot_demo.entity.mysql.Role; -import com.example.springboot_demo.entity.mysql.User; -import com.example.springboot_demo.mapper.RoleMapper; -import com.example.springboot_demo.mapper.UserMapper; -import com.example.springboot_demo.service.AuthService; -import com.example.springboot_demo.utils.JwtUtil; -import com.example.springboot_demo.vo.LoginVO; - -@Service -public class AuthServiceImpl implements AuthService { - - @Autowired - private UserMapper userMapper; - - @Autowired - private RoleMapper roleMapper; - - @Autowired - private JwtUtil jwtUtil; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Override - public LoginVO login(LoginDTO loginDTO) { - // 查询用户 - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(User::getUsername, loginDTO.getUsername()); - User user = userMapper.selectOne(wrapper); - - if (user == null) { - throw new RuntimeException("用户不存在"); - } - - // 验证密码 - if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) { - throw new RuntimeException("密码错误"); - } - - // 检查账号状态 - if (user.getStatus() == 0) { - throw new RuntimeException("账号已被禁用"); - } - - // 查询角色信息 - Role role = roleMapper.selectById(user.getRoleId()); - - // 构造返回数据 - LoginVO loginVO = new LoginVO(); - loginVO.setToken(jwtUtil.generateToken(user.getId(), user.getUsername())); - loginVO.setUserId(user.getId()); - loginVO.setUsername(user.getUsername()); - loginVO.setEmail(user.getEmail()); - loginVO.setRoleId(user.getRoleId()); - loginVO.setRoleName(role != null ? role.getRoleName() : null); - loginVO.setAvatarUrl(user.getAvatarUrl()); - - return loginVO; - } -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java deleted file mode 100644 index 7cee6d47..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.ColumnMetadata; -import com.example.springboot_demo.mapper.ColumnMetadataMapper; -import com.example.springboot_demo.service.ColumnMetadataService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class ColumnMetadataServiceImpl extends ServiceImpl implements ColumnMetadataService { - - @Override - public List listByTableId(Long tableId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(ColumnMetadata::getTableId, tableId); - // 主键字段优先 - wrapper.orderByDesc(ColumnMetadata::getIsPrimary); - wrapper.orderByAsc(ColumnMetadata::getColumnName); - return list(wrapper); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java deleted file mode 100644 index b6782dfa..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.DbConnection; -import com.example.springboot_demo.mapper.DbConnectionMapper; -import com.example.springboot_demo.service.DbConnectionService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class DbConnectionServiceImpl extends ServiceImpl implements DbConnectionService { - - @Override - public List listByCreateUserId(Long createUserId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(DbConnection::getCreateUserId, createUserId); - wrapper.orderByDesc(DbConnection::getCreateTime); - return list(wrapper); - } - - @Override - public boolean testConnection(Long id) { - // TODO: 实现真实的数据库连接测试逻辑 - // 暂时返回 Mock 结果 - DbConnection connection = getById(id); - if (connection == null) { - return false; - } - // 这里应该根据 db_type_id 和连接信息实际测试连接 - return true; - } -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java deleted file mode 100644 index a4dea719..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.DbType; -import com.example.springboot_demo.mapper.DbTypeMapper; -import com.example.springboot_demo.service.DbTypeService; -import org.springframework.stereotype.Service; - -@Service -public class DbTypeServiceImpl extends ServiceImpl implements DbTypeService { - - @Override - public DbType getByTypeCode(String typeCode) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(DbType::getTypeCode, typeCode); - return getOne(wrapper); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java deleted file mode 100644 index 655f4303..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.example.springboot_demo.entity.mongodb.DialogRecord; -import com.example.springboot_demo.repository.DialogRecordRepository; -import com.example.springboot_demo.service.DialogService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class DialogServiceImpl implements DialogService { - - @Autowired - private DialogRecordRepository dialogRecordRepository; - - @Override - public List getUserDialogs(Long userId) { - return dialogRecordRepository.findByUserIdOrderByLastTimeDesc(userId); - } - - @Override - public DialogRecord getDialogById(String dialogId) { - return dialogRecordRepository.findByDialogId(dialogId); - } -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java deleted file mode 100644 index 97779cac..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.ErrorLog; -import com.example.springboot_demo.mapper.ErrorLogMapper; -import com.example.springboot_demo.service.ErrorLogService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class ErrorLogServiceImpl extends ServiceImpl implements ErrorLogService { - - @Override - public List listByErrorTypeId(Integer errorTypeId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(ErrorLog::getErrorTypeId, errorTypeId); - wrapper.orderByDesc(ErrorLog::getStatTime); - return list(wrapper); - } - - @Override - public List listByPeriod(String period) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(ErrorLog::getPeriod, period); - wrapper.orderByDesc(ErrorLog::getStatTime); - return list(wrapper); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java deleted file mode 100644 index 40971a86..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.ErrorType; -import com.example.springboot_demo.mapper.ErrorTypeMapper; -import com.example.springboot_demo.service.ErrorTypeService; -import org.springframework.stereotype.Service; - -@Service -public class ErrorTypeServiceImpl extends ServiceImpl implements ErrorTypeService { - - @Override - public ErrorType getByErrorCode(String errorCode) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(ErrorType::getErrorCode, errorCode); - return getOne(wrapper); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java deleted file mode 100644 index 36257976..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.LlmConfig; -import com.example.springboot_demo.mapper.LlmConfigMapper; -import com.example.springboot_demo.service.LlmConfigService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class LlmConfigServiceImpl extends ServiceImpl implements LlmConfigService { - - @Override - public List listAvailable() { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(LlmConfig::getIsDisabled, 0); - wrapper.orderByDesc(LlmConfig::getCreateTime); - return list(wrapper); - } -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java deleted file mode 100644 index 345e87ab..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java +++ /dev/null @@ -1,380 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONObject; -import com.example.springboot_demo.service.LlmService; -import org.springframework.stereotype.Service; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; -import java.util.*; - -/** - * 大模型调用服务实现 - */ -@Service -public class LlmServiceImpl implements LlmService { - - private final HttpClient httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(30)) - .build(); - - @Override - public Map generateQuery(String prompt, String modelName, String databaseName) { - String lowerModelName = modelName.toLowerCase(); - - try { - if (lowerModelName.contains("gemini")) { - return callGemini(prompt, modelName, databaseName); - } else if (lowerModelName.contains("gpt")) { - return callOpenAI(prompt, modelName, databaseName); - } else if (lowerModelName.contains("glm")) { - return callGLM(prompt, modelName, databaseName); - } else if (lowerModelName.contains("qwen")) { - return callQwen(prompt, modelName, databaseName); - } else if (lowerModelName.contains("kimi")) { - return callKimi(prompt, modelName, databaseName); - } else { - throw new RuntimeException("不支持的模型: " + modelName); - } - } catch (Exception e) { - throw new RuntimeException("模型调用失败: " + e.getMessage(), e); - } - } - - /** - * 生成统一的Prompt - */ - private String generatePrompt(String prompt, String databaseName) { - return String.format( - "你是数据查询助手,需将用户请求转换为指定JSON格式。\n" + - "连接的数据库为\"%s\",仅生成该数据库的SQL。\n" + - "响应必须是单个有效的JSON对象,不包含任何额外文本或格式(如```json)。\n\n" + - "用户请求:\"%s\"\n\n" + - "规则:\n" + - "- 数据查询(可SQL回答):success=true,生成SQL、表格数据和图表数据\n" + - "- 非数据查询:success=false,表格数据用[\"Message\"]和[\"抱歉,仅支持数据查询\"]\n\n" + - "返回JSON格式:\n" + - "{\n" + - " \"success\": true/false,\n" + - " \"sqlQuery\": \"SQL语句\",\n" + - " \"tableData\": {\n" + - " \"headers\": [\"列1\", \"列2\"],\n" + - " \"rows\": [[\"值1\", \"值2\"]]\n" + - " },\n" + - " \"chartData\": {\n" + - " \"type\": \"bar/line/pie\",\n" + - " \"labels\": [\"标签1\"],\n" + - " \"datasets\": [{\n" + - " \"label\": \"数据标签\",\n" + - " \"data\": [1, 2, 3],\n" + - " \"backgroundColor\": \"rgba(22, 93, 255, 0.6)\"\n" + - " }]\n" + - " }\n" + - "}", - databaseName, prompt - ); - } - - /** - * 调用Gemini模型 - */ - private Map callGemini(String prompt, String modelName, String databaseName) throws Exception { - // 从配置中获取API Key(这里简化处理,实际应该从数据库读取) - String apiKey = System.getenv("GEMINI_API_KEY"); - if (apiKey == null || apiKey.isEmpty()) { - throw new RuntimeException("Gemini API密钥未配置"); - } - - String url = "https://generativelanguage.googleapis.com/v1beta/models/" + modelName + ":generateContent?key=" + apiKey; - - JSONObject requestBody = new JSONObject(); - requestBody.put("contents", Arrays.asList(Map.of( - "parts", Arrays.asList(Map.of("text", generatePrompt(prompt, databaseName))) - ))); - requestBody.put("generationConfig", Map.of( - "responseMimeType", "application/json" - )); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) - .timeout(Duration.ofSeconds(60)) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - throw new RuntimeException("Gemini API调用失败: " + response.statusCode()); - } - - JSONObject jsonResponse = JSON.parseObject(response.body()); - - // 检查响应结构 - if (!jsonResponse.containsKey("candidates") || jsonResponse.getJSONArray("candidates").isEmpty()) { - throw new RuntimeException("Gemini API响应格式错误:缺少candidates"); - } - - JSONObject candidate = jsonResponse.getJSONArray("candidates").getJSONObject(0); - if (!candidate.containsKey("content")) { - throw new RuntimeException("Gemini API响应格式错误:缺少content"); - } - - JSONObject contentObj = candidate.getJSONObject("content"); - if (!contentObj.containsKey("parts") || contentObj.getJSONArray("parts").isEmpty()) { - throw new RuntimeException("Gemini API响应格式错误:缺少parts"); - } - - String content = contentObj.getJSONArray("parts") - .getJSONObject(0) - .getString("text"); - - if (content == null || content.isEmpty()) { - throw new RuntimeException("Gemini API返回内容为空"); - } - - return parseJsonResponse(content); - } - - /** - * 调用OpenAI模型 - */ - private Map callOpenAI(String prompt, String modelName, String databaseName) throws Exception { - String apiKey = System.getenv("OPENAI_API_KEY"); - if (apiKey == null || apiKey.isEmpty()) { - throw new RuntimeException("OpenAI API密钥未配置"); - } - - String url = "https://api.openai.com/v1/chat/completions"; - - JSONObject requestBody = new JSONObject(); - requestBody.put("model", modelName); - requestBody.put("messages", Arrays.asList(Map.of( - "role", "user", - "content", generatePrompt(prompt, databaseName) - ))); - requestBody.put("response_format", Map.of("type", "json_object")); - requestBody.put("temperature", 0.0); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + apiKey) - .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) - .timeout(Duration.ofSeconds(60)) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - throw new RuntimeException("OpenAI API调用失败: " + response.statusCode()); - } - - JSONObject jsonResponse = JSON.parseObject(response.body()); - - // 检查响应结构 - if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { - throw new RuntimeException("OpenAI API响应格式错误:缺少choices"); - } - - JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); - if (!choice.containsKey("message")) { - throw new RuntimeException("OpenAI API响应格式错误:缺少message"); - } - - String content = choice.getJSONObject("message").getString("content"); - if (content == null || content.isEmpty()) { - throw new RuntimeException("OpenAI API返回内容为空"); - } - - String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); - return parseJsonResponse(cleanedContent); - } - - /** - * 调用GLM模型 - */ - private Map callGLM(String prompt, String modelName, String databaseName) throws Exception { - String apiKey = System.getenv("GLM_API_KEY"); - if (apiKey == null || apiKey.isEmpty()) { - throw new RuntimeException("GLM API密钥未配置"); - } - - String url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"; - - JSONObject requestBody = new JSONObject(); - requestBody.put("model", modelName); - requestBody.put("messages", Arrays.asList(Map.of( - "role", "user", - "content", generatePrompt(prompt, databaseName) - ))); - requestBody.put("response_format", Map.of("type", "json_object")); - requestBody.put("temperature", 0.0); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + apiKey) - .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) - .timeout(Duration.ofSeconds(60)) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - throw new RuntimeException("GLM API调用失败: " + response.statusCode()); - } - - JSONObject jsonResponse = JSON.parseObject(response.body()); - - // 检查响应结构 - if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { - throw new RuntimeException("GLM API响应格式错误:缺少choices"); - } - - JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); - if (!choice.containsKey("message")) { - throw new RuntimeException("GLM API响应格式错误:缺少message"); - } - - String content = choice.getJSONObject("message").getString("content"); - if (content == null || content.isEmpty()) { - throw new RuntimeException("GLM API返回内容为空"); - } - - String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); - return parseJsonResponse(cleanedContent); - } - - /** - * 调用Qwen模型 - */ - private Map callQwen(String prompt, String modelName, String databaseName) throws Exception { - String apiKey = System.getenv("QWEN_API_KEY"); - if (apiKey == null || apiKey.isEmpty()) { - throw new RuntimeException("Qwen API密钥未配置"); - } - - String url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; - - JSONObject requestBody = new JSONObject(); - requestBody.put("model", modelName); - requestBody.put("messages", Arrays.asList(Map.of( - "role", "user", - "content", generatePrompt(prompt, databaseName) - ))); - requestBody.put("response_format", Map.of("type", "json_object")); - requestBody.put("temperature", 0.0); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + apiKey) - .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) - .timeout(Duration.ofSeconds(60)) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - throw new RuntimeException("Qwen API调用失败: " + response.statusCode()); - } - - JSONObject jsonResponse = JSON.parseObject(response.body()); - - // 检查响应结构 - if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { - throw new RuntimeException("Qwen API响应格式错误:缺少choices"); - } - - JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); - if (!choice.containsKey("message")) { - throw new RuntimeException("Qwen API响应格式错误:缺少message"); - } - - String content = choice.getJSONObject("message").getString("content"); - if (content == null || content.isEmpty()) { - throw new RuntimeException("Qwen API返回内容为空"); - } - - String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); - return parseJsonResponse(cleanedContent); - } - - /** - * 调用Kimi模型 - */ - private Map callKimi(String prompt, String modelName, String databaseName) throws Exception { - String apiKey = System.getenv("KIMI_API_KEY"); - if (apiKey == null || apiKey.isEmpty()) { - throw new RuntimeException("Kimi API密钥未配置"); - } - - String url = "https://api.moonshot.cn/v1/chat/completions"; - - JSONObject requestBody = new JSONObject(); - requestBody.put("model", modelName); - requestBody.put("messages", Arrays.asList(Map.of( - "role", "user", - "content", generatePrompt(prompt, databaseName) - ))); - requestBody.put("response_format", Map.of("type", "json_object")); - requestBody.put("temperature", 0.0); - - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + apiKey) - .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) - .timeout(Duration.ofSeconds(60)) - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - throw new RuntimeException("Kimi API调用失败: " + response.statusCode()); - } - - JSONObject jsonResponse = JSON.parseObject(response.body()); - - // 检查响应结构 - if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { - throw new RuntimeException("Kimi API响应格式错误:缺少choices"); - } - - JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); - if (!choice.containsKey("message")) { - throw new RuntimeException("Kimi API响应格式错误:缺少message"); - } - - String content = choice.getJSONObject("message").getString("content"); - if (content == null || content.isEmpty()) { - throw new RuntimeException("Kimi API返回内容为空"); - } - - String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); - return parseJsonResponse(cleanedContent); - } - - /** - * 解析JSON响应 - */ - private Map parseJsonResponse(String jsonContent) { - try { - JSONObject json = JSON.parseObject(jsonContent); - Map result = new HashMap<>(); - result.put("success", json.getBooleanValue("success")); - result.put("sqlQuery", json.getString("sqlQuery")); - result.put("tableData", json.getJSONObject("tableData")); - result.put("chartData", json.getJSONObject("chartData")); - return result; - } catch (Exception e) { - throw new RuntimeException("解析模型响应失败: " + e.getMessage(), e); - } - } -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java deleted file mode 100644 index c7744390..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.LlmStatus; -import com.example.springboot_demo.mapper.LlmStatusMapper; -import com.example.springboot_demo.service.LlmStatusService; -import org.springframework.stereotype.Service; - -@Service -public class LlmStatusServiceImpl extends ServiceImpl implements LlmStatusService { - - @Override - public LlmStatus getByStatusCode(String statusCode) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(LlmStatus::getStatusCode, statusCode); - return getOne(wrapper); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java deleted file mode 100644 index a086a63f..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.Notification; -import com.example.springboot_demo.mapper.NotificationMapper; -import com.example.springboot_demo.service.NotificationService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class NotificationServiceImpl extends ServiceImpl implements NotificationService { - - @Override - public List listPublished() { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.isNotNull(Notification::getPublishTime); - // 置顶优先,然后按发布时间降序 - wrapper.orderByDesc(Notification::getIsTop); - wrapper.orderByDesc(Notification::getPublishTime); - return list(wrapper); - } - - @Override - public List listDrafts() { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.isNull(Notification::getPublishTime); - wrapper.orderByDesc(Notification::getLatestUpdateTime); - return list(wrapper); - } - - @Override - public List listByTargetId(Integer targetId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(Notification::getTargetId, targetId); - wrapper.isNotNull(Notification::getPublishTime); - wrapper.orderByDesc(Notification::getIsTop); - wrapper.orderByDesc(Notification::getPublishTime); - return list(wrapper); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java deleted file mode 100644 index 78b1a76a..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.NotificationTarget; -import com.example.springboot_demo.mapper.NotificationTargetMapper; -import com.example.springboot_demo.service.NotificationTargetService; -import org.springframework.stereotype.Service; - -@Service -public class NotificationTargetServiceImpl extends ServiceImpl implements NotificationTargetService { - - @Override - public NotificationTarget getByTargetCode(String targetCode) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(NotificationTarget::getTargetCode, targetCode); - return getOne(wrapper); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java deleted file mode 100644 index 71011c81..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.OperationLog; -import com.example.springboot_demo.mapper.OperationLogMapper; -import com.example.springboot_demo.service.OperationLogService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class OperationLogServiceImpl extends ServiceImpl implements OperationLogService { - - @Override - public List listByUserId(Long userId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(OperationLog::getUserId, userId); - wrapper.orderByDesc(OperationLog::getOperateTime); - return list(wrapper); - } - - @Override - public List listByModule(String module) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(OperationLog::getModule, module); - wrapper.orderByDesc(OperationLog::getOperateTime); - return list(wrapper); - } - - @Override - public List listFailed() { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(OperationLog::getResult, 0); - wrapper.orderByDesc(OperationLog::getOperateTime); - return list(wrapper); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java deleted file mode 100644 index 1a67b1ab..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.Priority; -import com.example.springboot_demo.mapper.PriorityMapper; -import com.example.springboot_demo.service.PriorityService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class PriorityServiceImpl extends ServiceImpl implements PriorityService { - - @Override - public Priority getByPriorityCode(String priorityCode) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(Priority::getPriorityCode, priorityCode); - return getOne(wrapper); - } - - @Override - public List listOrderBySort() { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.orderByAsc(Priority::getSort); - return list(wrapper); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java deleted file mode 100644 index 1ea9557b..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.QueryLog; -import com.example.springboot_demo.mapper.QueryLogMapper; -import com.example.springboot_demo.service.QueryLogService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class QueryLogServiceImpl extends ServiceImpl implements QueryLogService { - - @Override - public List listByUserId(Long userId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(QueryLog::getUserId, userId); - wrapper.orderByDesc(QueryLog::getQueryTime); - return list(wrapper); - } - - @Override - public List listByDialogId(String dialogId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(QueryLog::getDialogId, dialogId); - wrapper.orderByAsc(QueryLog::getQueryTime); - return list(wrapper); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java deleted file mode 100644 index 08e538a2..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java +++ /dev/null @@ -1,247 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.alibaba.fastjson2.JSONArray; -import com.alibaba.fastjson2.JSONObject; -import com.example.springboot_demo.dto.QueryRequestDTO; -import com.example.springboot_demo.entity.mongodb.DialogRecord; -import com.example.springboot_demo.repository.DialogRecordRepository; -import com.example.springboot_demo.service.LlmService; -import com.example.springboot_demo.service.QueryService; -import com.example.springboot_demo.vo.QueryResponseVO; -import com.example.springboot_demo.vo.TableDataVO; -import com.example.springboot_demo.vo.ChartDataVO; -import com.example.springboot_demo.vo.DatasetVO; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.*; -import java.util.stream.Collectors; - -@Service -public class QueryServiceImpl implements QueryService { - - @Autowired - private DialogRecordRepository dialogRecordRepository; - - @Autowired - private LlmService llmService; - - @Override - public QueryResponseVO executeQuery(QueryRequestDTO request, Long userId) { - long startTime = System.currentTimeMillis(); - - // 生成或获取对话ID - String conversationId = request.getConversationId(); - if (conversationId == null || conversationId.isEmpty()) { - conversationId = "conv_" + UUID.randomUUID().toString().substring(0, 8); - // 创建新对话记录 - DialogRecord dialogRecord = new DialogRecord(); - dialogRecord.setDialogId(conversationId); - dialogRecord.setUserId(userId); - dialogRecord.setTopic(request.getUserPrompt().substring(0, Math.min(20, request.getUserPrompt().length()))); - dialogRecord.setTotalRounds(1); - dialogRecord.setStartTime(LocalDateTime.now()); - dialogRecord.setLastTime(LocalDateTime.now()); - dialogRecordRepository.save(dialogRecord); - } else { - // 更新对话记录 - DialogRecord dialogRecord = dialogRecordRepository.findByDialogId(conversationId); - if (dialogRecord != null) { - dialogRecord.setTotalRounds(dialogRecord.getTotalRounds() + 1); - dialogRecord.setLastTime(LocalDateTime.now()); - dialogRecordRepository.save(dialogRecord); - } - } - - // 调用大模型API生成SQL和结果 - Map llmResult = llmService.generateQuery( - request.getUserPrompt(), - request.getModel(), - request.getDatabase() - ); - - // 计算执行时间 - long endTime = System.currentTimeMillis(); - String executionTime = String.format("%.1f秒", (endTime - startTime) / 1000.0); - - // 构建响应 - QueryResponseVO response = new QueryResponseVO(); - response.setId("query_" + UUID.randomUUID().toString().substring(0, 8)); - response.setUserPrompt(request.getUserPrompt()); - response.setSqlQuery((String) llmResult.getOrDefault("sqlQuery", "")); - response.setConversationId(conversationId); - response.setQueryTime(LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)); - response.setExecutionTime(executionTime); - response.setDatabase(request.getDatabase()); - response.setModel(request.getModel()); - - // 解析表格数据 - Object tableDataObj = llmResult.get("tableData"); - if (tableDataObj != null) { - TableDataVO tableData = parseTableData(tableDataObj); - response.setTableData(tableData); - } - - // 解析图表数据 - Object chartDataObj = llmResult.get("chartData"); - if (chartDataObj != null) { - ChartDataVO chartData = parseChartData(chartDataObj); - response.setChartData(chartData); - } - - return response; - } - - /** - * 解析表格数据 - */ - @SuppressWarnings("unchecked") - private TableDataVO parseTableData(Object tableDataObj) { - TableDataVO tableData = new TableDataVO(); - - if (tableDataObj instanceof Map) { - Map map = (Map) tableDataObj; - - // 解析headers - Object headersObj = map.get("headers"); - if (headersObj instanceof List) { - List headers = ((List) headersObj).stream() - .map(String::valueOf) - .collect(Collectors.toList()); - tableData.setHeaders(headers); - } - - // 解析rows - Object rowsObj = map.get("rows"); - if (rowsObj instanceof List) { - List> rows = new ArrayList<>(); - for (Object row : (List) rowsObj) { - if (row instanceof List) { - List rowList = ((List) row).stream() - .map(String::valueOf) - .collect(Collectors.toList()); - rows.add(rowList); - } else { - rows.add(Collections.emptyList()); - } - } - tableData.setRows(rows); - } - } else if (tableDataObj instanceof JSONObject) { - JSONObject json = (JSONObject) tableDataObj; - JSONArray headersArray = json.getJSONArray("headers"); - JSONArray rowsArray = json.getJSONArray("rows"); - - List headers = headersArray != null ? - headersArray.toJavaList(String.class) : Collections.emptyList(); - - List> rows = new ArrayList<>(); - if (rowsArray != null) { - for (Object row : rowsArray) { - if (row instanceof JSONArray) { - rows.add(((JSONArray) row).toJavaList(String.class)); - } else { - rows.add(Collections.emptyList()); - } - } - } - - tableData.setHeaders(headers); - tableData.setRows(rows); - } - - return tableData; - } - - /** - * 解析图表数据 - */ - @SuppressWarnings("unchecked") - private ChartDataVO parseChartData(Object chartDataObj) { - ChartDataVO chartData = new ChartDataVO(); - - if (chartDataObj instanceof Map) { - Map map = (Map) chartDataObj; - chartData.setType((String) map.getOrDefault("type", "bar")); - - Object labelsObj = map.get("labels"); - if (labelsObj instanceof List) { - List labels = ((List) labelsObj).stream() - .map(String::valueOf) - .collect(Collectors.toList()); - chartData.setLabels(labels); - } - - Object datasetsObj = map.get("datasets"); - if (datasetsObj instanceof List) { - List datasets = ((List) datasetsObj).stream() - .map(datasetObj -> { - DatasetVO dataset = new DatasetVO(); - if (datasetObj instanceof Map) { - Map datasetMap = (Map) datasetObj; - dataset.setLabel((String) datasetMap.get("label")); - - Object dataObj = datasetMap.get("data"); - if (dataObj instanceof List) { - List data = ((List) dataObj).stream() - .map(item -> { - if (item instanceof Number) { - return ((Number) item).doubleValue(); - } - return 0.0; - }) - .collect(Collectors.toList()); - dataset.setData(data); - } - - dataset.setBackgroundColor((String) datasetMap.get("backgroundColor")); - } - return dataset; - }) - .collect(Collectors.toList()); - chartData.setDatasets(datasets); - } - } else if (chartDataObj instanceof JSONObject) { - JSONObject json = (JSONObject) chartDataObj; - chartData.setType(json.getString("type")); - - JSONArray labelsArray = json.getJSONArray("labels"); - if (labelsArray != null) { - chartData.setLabels(labelsArray.toJavaList(String.class)); - } - - JSONArray datasetsArray = json.getJSONArray("datasets"); - if (datasetsArray != null) { - List datasets = datasetsArray.stream() - .map(item -> { - JSONObject datasetJson = (JSONObject) item; - DatasetVO dataset = new DatasetVO(); - dataset.setLabel(datasetJson.getString("label")); - - JSONArray dataArray = datasetJson.getJSONArray("data"); - if (dataArray != null) { - List data = dataArray.stream() - .map(obj -> { - if (obj instanceof Number) { - return ((Number) obj).doubleValue(); - } - return 0.0; - }) - .collect(Collectors.toList()); - dataset.setData(data); - } - - dataset.setBackgroundColor(datasetJson.getString("backgroundColor")); - return dataset; - }) - .collect(Collectors.toList()); - chartData.setDatasets(datasets); - } - } - - return chartData; - } -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java deleted file mode 100644 index 5873277e..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import org.springframework.stereotype.Service; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.Role; -import com.example.springboot_demo.mapper.RoleMapper; -import com.example.springboot_demo.service.RoleService; - -@Service -public class RoleServiceImpl extends ServiceImpl implements RoleService { -} diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java deleted file mode 100644 index 6c245698..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.SystemHealth; -import com.example.springboot_demo.mapper.SystemHealthMapper; -import com.example.springboot_demo.service.SystemHealthService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class SystemHealthServiceImpl extends ServiceImpl implements SystemHealthService { - - @Override - public SystemHealth getLatest() { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.orderByDesc(SystemHealth::getCollectTime); - wrapper.last("LIMIT 1"); - return getOne(wrapper); - } - - @Override - public List listRecent(int limit) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.orderByDesc(SystemHealth::getCollectTime); - wrapper.last("LIMIT " + limit); - return list(wrapper); - } -} - - - - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java deleted file mode 100644 index 66eb9c73..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.TableMetadata; -import com.example.springboot_demo.mapper.TableMetadataMapper; -import com.example.springboot_demo.service.TableMetadataService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class TableMetadataServiceImpl extends ServiceImpl implements TableMetadataService { - - @Override - public List listByDbConnectionId(Long dbConnectionId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(TableMetadata::getDbConnectionId, dbConnectionId); - wrapper.orderByAsc(TableMetadata::getTableName); - return list(wrapper); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java deleted file mode 100644 index 7a7548de..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.example.springboot_demo.entity.mysql.UserDbPermission; -import com.example.springboot_demo.mapper.UserDbPermissionMapper; -import com.example.springboot_demo.service.UserDbPermissionService; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Service -public class UserDbPermissionServiceImpl extends ServiceImpl implements UserDbPermissionService { - - @Override - public UserDbPermission getByUserId(Long userId) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(UserDbPermission::getUserId, userId); - return getOne(wrapper); - } - - @Override - public List listAssigned() { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(UserDbPermission::getIsAssigned, 1); - wrapper.orderByDesc(UserDbPermission::getLastGrantTime); - return list(wrapper); - } - - @Override - public List listUnassigned() { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(UserDbPermission::getIsAssigned, 0); - return list(wrapper); - } -} - - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java deleted file mode 100644 index 38148be3..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.example.springboot_demo.service.impl; - -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.example.springboot_demo.entity.mysql.User; -import com.example.springboot_demo.mapper.UserMapper; -import com.example.springboot_demo.service.UserService; - -@Service -public class UserServiceImpl implements UserService { - - @Autowired - private UserMapper userMapper; - - @Autowired - private PasswordEncoder passwordEncoder; - - @Override - public User getById(Long id) { - return userMapper.selectById(id); - } - - @Override - public List list() { - return userMapper.selectList(null); - } - - @Override - public Page page(int current, int size) { - Page page = new Page<>(current, size); - return userMapper.selectPage(page, null); - } - - @Override - public boolean save(User user) { - if (StringUtils.hasText(user.getPassword())) { - user.setPassword(passwordEncoder.encode(user.getPassword())); - } - return userMapper.insert(user) > 0; - } - - @Override - public boolean updateById(User user) { - if (StringUtils.hasText(user.getPassword())) { - user.setPassword(passwordEncoder.encode(user.getPassword())); - } - return userMapper.updateById(user) > 0; - } - - @Override - public boolean removeById(Long id) { - return userMapper.deleteById(id) > 0; - } - - @Override - public User getByUsername(String username) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(User::getUsername, username); - return userMapper.selectOne(wrapper); - } -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/utils/JwtUtil.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/utils/JwtUtil.java deleted file mode 100644 index 13206046..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/utils/JwtUtil.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.springboot_demo.utils; - -import java.security.Key; -import java.util.Date; - -import org.springframework.stereotype.Component; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.Keys; - -@Component -public class JwtUtil { - - // Use a secure key for HS256 - private static final Key KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256); - private static final long EXPIRATION_TIME = 86400000; // 24 hours - - public String generateToken(Long userId, String username) { - return Jwts.builder() - .setSubject(username) - .claim("userId", userId) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) - .signWith(KEY) - .compact(); - } - - public Claims getClaimsFromToken(String token) { - return Jwts.parserBuilder() - .setSigningKey(KEY) - .build() - .parseClaimsJws(token) - .getBody(); - } - - public boolean validateToken(String token) { - try { - getClaimsFromToken(token); - return true; - } catch (JwtException | IllegalArgumentException e) { - return false; - } - } - - public Long getUserIdFromToken(String token) { - Claims claims = getClaimsFromToken(token); - return claims.get("userId", Long.class); - } - - public String getUsernameFromToken(String token) { - return getClaimsFromToken(token).getSubject(); - } -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java deleted file mode 100644 index f4f687e0..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.springboot_demo.vo; - -import lombok.Data; -import java.util.List; - -@Data -public class ChartDataVO { - private String type; // bar, line, pie - private List labels; - private List datasets; -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/DatasetVO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/DatasetVO.java deleted file mode 100644 index 135458dc..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/DatasetVO.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.springboot_demo.vo; - -import lombok.Data; -import java.util.List; - -@Data -public class DatasetVO { - private String label; - private List data; - private Object backgroundColor; // 可以是字符串或数组 -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/LoginVO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/LoginVO.java deleted file mode 100644 index ba21ed6c..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/LoginVO.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.springboot_demo.vo; - -import lombok.Data; - -@Data -public class LoginVO { - private String token; - private Long userId; - private String username; - private String email; - private Integer roleId; - private String roleName; - private String avatarUrl; -} - - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java deleted file mode 100644 index 7ca36218..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.example.springboot_demo.vo; - -import lombok.Data; - -@Data -public class QueryResponseVO { - private String id; - private String userPrompt; - private String sqlQuery; - private String conversationId; - private String queryTime; - private String executionTime; - private TableDataVO tableData; - private ChartDataVO chartData; - private String database; - private String model; -} - diff --git a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/TableDataVO.java b/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/TableDataVO.java deleted file mode 100644 index 446dd465..00000000 --- a/src/springboot_demo/src/main/java/com/example/springboot_demo/vo/TableDataVO.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.springboot_demo.vo; - -import lombok.Data; -import java.util.List; - -@Data -public class TableDataVO { - private List headers; - private List> rows; -} - - diff --git a/src/springboot_demo/src/main/resources/application.yml b/src/springboot_demo/src/main/resources/application.yml deleted file mode 100644 index 0df01fb4..00000000 --- a/src/springboot_demo/src/main/resources/application.yml +++ /dev/null @@ -1,75 +0,0 @@ -server: - port: 8080 - -spring: - application: - name: springboot_demo - - # MySQL 数据源配置 - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/natural_language_query_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true - username: root - password: root123456 - hikari: - minimum-idle: 5 - maximum-pool-size: 20 - idle-timeout: 600000 - max-lifetime: 1800000 - connection-timeout: 30000 - - # MongoDB 配置 - data: - mongodb: - host: 127.0.0.1 - port: 27017 - database: natural_language_query_system - username: admin - password: admin123456 - authentication-database: admin - - # Redis 配置(可选) - redis: - host: localhost - port: 6379 - password: - database: 0 - timeout: 3000ms - lettuce: - pool: - max-active: 8 - max-wait: -1ms - max-idle: 8 - min-idle: 0 - -# MyBatis Plus 配置 -mybatis-plus: - # Mapper XML 文件位置 - mapper-locations: classpath:mapper/**/*.xml - # 实体类包路径 - type-aliases-package: com.example.springboot_demo.entity.mysql - configuration: - # 日志输出 - log-impl: org.apache.ibatis.logging.stdout.StdOutImpl - # 驼峰命名转换 - map-underscore-to-camel-case: true - # 缓存 - cache-enabled: true - global-config: - db-config: - # 主键策略:自增 - id-type: auto - # 逻辑删除字段 - logic-delete-field: deleted - logic-delete-value: 1 - logic-not-delete-value: 0 - -# 日志配置 -logging: - level: - root: INFO - com.example.springboot_demo: DEBUG - com.example.springboot_demo.mapper: DEBUG - pattern: - console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" - diff --git a/src/springboot_demo/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java b/src/springboot_demo/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java deleted file mode 100644 index 4ba0d8fa..00000000 --- a/src/springboot_demo/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.springboot_demo; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class SpringbootDemoApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test b/src/test new file mode 160000 index 00000000..5124d5e1 --- /dev/null +++ b/src/test @@ -0,0 +1 @@ +Subproject commit 5124d5e1eed1e3654a7036a2712813cca6dbb288 -- 2.34.1 From 050997f2b98c79ecbd971a9f163e1f48d0489c45 Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Mon, 1 Dec 2025 12:28:29 +0800 Subject: [PATCH 06/14] fix: properly add test directory --- src/test | 1 - src/test/.gitattributes | 2 + src/test/.gitignore | 33 + .../.mvn/wrapper/maven-wrapper.properties | 3 + src/test/DATABASE_SETUP.md | 269 ++ src/test/QUICK_START.md | 206 ++ src/test/TEST_ACCOUNTS.md | 292 ++ src/test/api-test.http | 636 ++++ src/test/docker-compose.yml | 123 + src/test/frontend/.gitignore | 24 + src/test/frontend/App.tsx | 732 +++++ src/test/frontend/README.md | 20 + src/test/frontend/components/AccountPage.tsx | 222 ++ src/test/frontend/components/ChatMessage.tsx | 40 + src/test/frontend/components/ChatModal.tsx | 384 +++ .../frontend/components/ComparisonModal.tsx | 232 ++ .../frontend/components/DataAdminPage.tsx | 123 + .../frontend/components/DataAdminSidebar.tsx | 93 + src/test/frontend/components/Dropdown.tsx | 63 + src/test/frontend/components/FriendsPage.tsx | 728 +++++ src/test/frontend/components/HistoryPage.tsx | 455 +++ .../frontend/components/HistorySidebar.tsx | 130 + src/test/frontend/components/LoginPage.tsx | 193 ++ src/test/frontend/components/Modal.tsx | 64 + .../frontend/components/NotificationsPage.tsx | 156 + .../frontend/components/PlaceholderPage.tsx | 18 + src/test/frontend/components/QueryPage.tsx | 292 ++ src/test/frontend/components/QueryResult.tsx | 264 ++ src/test/frontend/components/RightSidebar.tsx | 83 + src/test/frontend/components/Sidebar.tsx | 94 + src/test/frontend/components/SysAdminPage.tsx | 43 + .../frontend/components/SysAdminSidebar.tsx | 90 + src/test/frontend/components/TopHeader.tsx | 267 ++ .../components/admin/AdminAccountPage.tsx | 92 + .../frontend/components/admin/AdminModal.tsx | 28 + .../components/admin/DashboardPage.tsx | 243 ++ .../components/admin/LLMConfigPage.tsx | 397 +++ .../admin/NotificationManagementPage.tsx | 129 + .../components/admin/SystemLogPage.tsx | 130 + .../components/admin/UserManagementPage.tsx | 353 +++ .../data-admin/ConnectionLogPage.tsx | 169 ++ .../data-admin/DataAdminDashboardPage.tsx | 126 + .../data-admin/DataAdminNotificationPage.tsx | 107 + .../data-admin/DataSourceManagementPage.tsx | 306 ++ .../data-admin/UserPermissionPage.tsx | 434 +++ src/test/frontend/constants.ts | 270 ++ src/test/frontend/env.d.ts | 14 + src/test/frontend/index.html | 66 + src/test/frontend/index.tsx | 16 + src/test/frontend/metadata.json | 5 + src/test/frontend/package-lock.json | 2630 +++++++++++++++++ src/test/frontend/package.json | 25 + src/test/frontend/services/api.ts | 364 +++ src/test/frontend/tsconfig.json | 30 + src/test/frontend/types.ts | 193 ++ src/test/frontend/vite.config.ts | 35 + src/test/last.md | 507 ++++ src/test/mongodb_schema_from_last.js | 493 +++ src/test/mvnw | 295 ++ src/test/mvnw.cmd | 189 ++ src/test/mysql_schema_from_last.sql | 397 +++ src/test/pom.xml | 133 + src/test/scripts/export-data.bat | 25 + src/test/scripts/export-data.sh | 37 + src/test/scripts/import-data.bat | 30 + src/test/scripts/import-data.sh | 36 + .../SpringbootDemoApplication.java | 13 + .../springboot_demo/common/Result.java | 37 + .../springboot_demo/config/CorsConfig.java | 26 + .../config/JwtInterceptor.java | 42 + .../config/SecurityConfig.java | 33 + .../springboot_demo/config/WebMvcConfig.java | 30 + .../controller/AuthController.java | 31 + .../controller/ColumnMetadataController.java | 76 + .../controller/DbConnectionController.java | 109 + .../controller/DbTypeController.java | 71 + .../controller/DialogController.java | 34 + .../controller/ErrorLogController.java | 86 + .../controller/ErrorTypeController.java | 74 + .../controller/LlmConfigController.java | 117 + .../controller/LlmStatusController.java | 71 + .../controller/NotificationController.java | 125 + .../NotificationTargetController.java | 74 + .../controller/OperationLogController.java | 82 + .../controller/PriorityController.java | 74 + .../controller/QueryController.java | 29 + .../controller/QueryLogController.java | 78 + .../controller/RoleController.java | 28 + .../controller/SystemHealthController.java | 77 + .../controller/TableMetadataController.java | 75 + .../controller/TestController.java | 107 + .../controller/UserController.java | 87 + .../UserDbPermissionController.java | 95 + .../example/springboot_demo/dto/LoginDTO.java | 11 + .../springboot_demo/dto/QueryRequestDTO.java | 13 + .../entity/mongodb/DialogRecord.java | 29 + .../entity/mysql/ColumnMetadata.java | 30 + .../entity/mysql/DbConnection.java | 35 + .../springboot_demo/entity/mysql/DbType.java | 22 + .../entity/mysql/ErrorLog.java | 29 + .../entity/mysql/ErrorType.java | 25 + .../entity/mysql/LlmConfig.java | 37 + .../entity/mysql/LlmStatus.java | 22 + .../entity/mysql/Notification.java | 39 + .../entity/mysql/NotificationTarget.java | 25 + .../entity/mysql/OperationLog.java | 36 + .../entity/mysql/Priority.java | 25 + .../entity/mysql/QueryLog.java | 31 + .../springboot_demo/entity/mysql/Role.java | 22 + .../entity/mysql/SystemHealth.java | 32 + .../entity/mysql/TableMetadata.java | 27 + .../springboot_demo/entity/mysql/User.java | 38 + .../entity/mysql/UserDbPermission.java | 30 + .../exception/BusinessException.java | 42 + .../mapper/ColumnMetadataMapper.java | 12 + .../mapper/DbConnectionMapper.java | 11 + .../springboot_demo/mapper/DbTypeMapper.java | 12 + .../mapper/ErrorLogMapper.java | 15 + .../mapper/ErrorTypeMapper.java | 15 + .../mapper/LlmConfigMapper.java | 11 + .../mapper/LlmStatusMapper.java | 12 + .../mapper/NotificationMapper.java | 15 + .../mapper/NotificationTargetMapper.java | 15 + .../mapper/OperationLogMapper.java | 12 + .../mapper/PriorityMapper.java | 15 + .../mapper/QueryLogMapper.java | 12 + .../springboot_demo/mapper/RoleMapper.java | 11 + .../mapper/SystemHealthMapper.java | 15 + .../mapper/TableMetadataMapper.java | 11 + .../mapper/UserDbPermissionMapper.java | 12 + .../springboot_demo/mapper/UserMapper.java | 11 + .../repository/DialogRecordRepository.java | 15 + .../springboot_demo/service/AuthService.java | 10 + .../service/ColumnMetadataService.java | 16 + .../service/DbConnectionService.java | 20 + .../service/DbTypeService.java | 14 + .../service/DialogService.java | 12 + .../service/ErrorLogService.java | 24 + .../service/ErrorTypeService.java | 17 + .../service/LlmConfigService.java | 15 + .../springboot_demo/service/LlmService.java | 23 + .../service/LlmStatusService.java | 14 + .../service/NotificationService.java | 29 + .../service/NotificationTargetService.java | 17 + .../service/OperationLogService.java | 26 + .../service/PriorityService.java | 24 + .../service/QueryLogService.java | 21 + .../springboot_demo/service/QueryService.java | 10 + .../springboot_demo/service/RoleService.java | 7 + .../service/SystemHealthService.java | 24 + .../service/TableMetadataService.java | 14 + .../service/UserDbPermissionService.java | 26 + .../springboot_demo/service/UserService.java | 24 + .../service/impl/AuthServiceImpl.java | 70 + .../impl/ColumnMetadataServiceImpl.java | 27 + .../service/impl/DbConnectionServiceImpl.java | 36 + .../service/impl/DbTypeServiceImpl.java | 22 + .../service/impl/DialogServiceImpl.java | 28 + .../service/impl/ErrorLogServiceImpl.java | 36 + .../service/impl/ErrorTypeServiceImpl.java | 25 + .../service/impl/LlmConfigServiceImpl.java | 24 + .../service/impl/LlmServiceImpl.java | 380 +++ .../service/impl/LlmStatusServiceImpl.java | 22 + .../service/impl/NotificationServiceImpl.java | 48 + .../impl/NotificationTargetServiceImpl.java | 25 + .../service/impl/OperationLogServiceImpl.java | 41 + .../service/impl/PriorityServiceImpl.java | 34 + .../service/impl/QueryLogServiceImpl.java | 33 + .../service/impl/QueryServiceImpl.java | 247 ++ .../service/impl/RoleServiceImpl.java | 12 + .../service/impl/SystemHealthServiceImpl.java | 36 + .../impl/TableMetadataServiceImpl.java | 25 + .../impl/UserDbPermissionServiceImpl.java | 39 + .../service/impl/UserServiceImpl.java | 69 + .../springboot_demo/utils/JwtUtil.java | 57 + .../springboot_demo/vo/ChartDataVO.java | 13 + .../example/springboot_demo/vo/DatasetVO.java | 13 + .../example/springboot_demo/vo/LoginVO.java | 16 + .../springboot_demo/vo/QueryResponseVO.java | 18 + .../springboot_demo/vo/TableDataVO.java | 12 + src/test/src/main/resources/application.yml | 80 + .../SpringbootDemoApplicationTests.java | 13 + 182 files changed, 19179 insertions(+), 1 deletion(-) delete mode 160000 src/test create mode 100644 src/test/.gitattributes create mode 100644 src/test/.gitignore create mode 100644 src/test/.mvn/wrapper/maven-wrapper.properties create mode 100644 src/test/DATABASE_SETUP.md create mode 100644 src/test/QUICK_START.md create mode 100644 src/test/TEST_ACCOUNTS.md create mode 100644 src/test/api-test.http create mode 100644 src/test/docker-compose.yml create mode 100644 src/test/frontend/.gitignore create mode 100644 src/test/frontend/App.tsx create mode 100644 src/test/frontend/README.md create mode 100644 src/test/frontend/components/AccountPage.tsx create mode 100644 src/test/frontend/components/ChatMessage.tsx create mode 100644 src/test/frontend/components/ChatModal.tsx create mode 100644 src/test/frontend/components/ComparisonModal.tsx create mode 100644 src/test/frontend/components/DataAdminPage.tsx create mode 100644 src/test/frontend/components/DataAdminSidebar.tsx create mode 100644 src/test/frontend/components/Dropdown.tsx create mode 100644 src/test/frontend/components/FriendsPage.tsx create mode 100644 src/test/frontend/components/HistoryPage.tsx create mode 100644 src/test/frontend/components/HistorySidebar.tsx create mode 100644 src/test/frontend/components/LoginPage.tsx create mode 100644 src/test/frontend/components/Modal.tsx create mode 100644 src/test/frontend/components/NotificationsPage.tsx create mode 100644 src/test/frontend/components/PlaceholderPage.tsx create mode 100644 src/test/frontend/components/QueryPage.tsx create mode 100644 src/test/frontend/components/QueryResult.tsx create mode 100644 src/test/frontend/components/RightSidebar.tsx create mode 100644 src/test/frontend/components/Sidebar.tsx create mode 100644 src/test/frontend/components/SysAdminPage.tsx create mode 100644 src/test/frontend/components/SysAdminSidebar.tsx create mode 100644 src/test/frontend/components/TopHeader.tsx create mode 100644 src/test/frontend/components/admin/AdminAccountPage.tsx create mode 100644 src/test/frontend/components/admin/AdminModal.tsx create mode 100644 src/test/frontend/components/admin/DashboardPage.tsx create mode 100644 src/test/frontend/components/admin/LLMConfigPage.tsx create mode 100644 src/test/frontend/components/admin/NotificationManagementPage.tsx create mode 100644 src/test/frontend/components/admin/SystemLogPage.tsx create mode 100644 src/test/frontend/components/admin/UserManagementPage.tsx create mode 100644 src/test/frontend/components/data-admin/ConnectionLogPage.tsx create mode 100644 src/test/frontend/components/data-admin/DataAdminDashboardPage.tsx create mode 100644 src/test/frontend/components/data-admin/DataAdminNotificationPage.tsx create mode 100644 src/test/frontend/components/data-admin/DataSourceManagementPage.tsx create mode 100644 src/test/frontend/components/data-admin/UserPermissionPage.tsx create mode 100644 src/test/frontend/constants.ts create mode 100644 src/test/frontend/env.d.ts create mode 100644 src/test/frontend/index.html create mode 100644 src/test/frontend/index.tsx create mode 100644 src/test/frontend/metadata.json create mode 100644 src/test/frontend/package-lock.json create mode 100644 src/test/frontend/package.json create mode 100644 src/test/frontend/services/api.ts create mode 100644 src/test/frontend/tsconfig.json create mode 100644 src/test/frontend/types.ts create mode 100644 src/test/frontend/vite.config.ts create mode 100644 src/test/last.md create mode 100644 src/test/mongodb_schema_from_last.js create mode 100644 src/test/mvnw create mode 100644 src/test/mvnw.cmd create mode 100644 src/test/mysql_schema_from_last.sql create mode 100644 src/test/pom.xml create mode 100644 src/test/scripts/export-data.bat create mode 100644 src/test/scripts/export-data.sh create mode 100644 src/test/scripts/import-data.bat create mode 100644 src/test/scripts/import-data.sh create mode 100644 src/test/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/common/Result.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/config/CorsConfig.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/config/SecurityConfig.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/AuthController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/DbTypeController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/DialogController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/NotificationController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/OperationLogController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/PriorityController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/QueryController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/QueryLogController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/RoleController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/TestController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/UserController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/dto/LoginDTO.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/Role.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/User.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/exception/BusinessException.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/UserMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/AuthService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/DbConnectionService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/DbTypeService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/DialogService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/ErrorLogService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/LlmConfigService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/LlmService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/LlmStatusService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/NotificationService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/OperationLogService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/PriorityService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/QueryLogService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/QueryService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/RoleService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/SystemHealthService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/TableMetadataService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/UserService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/utils/JwtUtil.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/vo/DatasetVO.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/vo/LoginVO.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/vo/TableDataVO.java create mode 100644 src/test/src/main/resources/application.yml create mode 100644 src/test/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java diff --git a/src/test b/src/test deleted file mode 160000 index 5124d5e1..00000000 --- a/src/test +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5124d5e1eed1e3654a7036a2712813cca6dbb288 diff --git a/src/test/.gitattributes b/src/test/.gitattributes new file mode 100644 index 00000000..3b41682a --- /dev/null +++ b/src/test/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/src/test/.gitignore b/src/test/.gitignore new file mode 100644 index 00000000..667aaef0 --- /dev/null +++ b/src/test/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/src/test/.mvn/wrapper/maven-wrapper.properties b/src/test/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..c0bcafe9 --- /dev/null +++ b/src/test/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/src/test/DATABASE_SETUP.md b/src/test/DATABASE_SETUP.md new file mode 100644 index 00000000..fe09c0fa --- /dev/null +++ b/src/test/DATABASE_SETUP.md @@ -0,0 +1,269 @@ +# 数据库环境搭建指南 + +本项目使用 Docker Compose 管理数据库环境,确保团队成员之间的开发环境一致。 + +## 一、首次启动(全新环境) + +### 1. 启动所有服务 + +```bash +# Windows PowerShell / CMD +docker compose up -d + +# 或者只启动必需的服务 +docker compose up -d mysql mongodb redis +``` + +### 2. 验证服务状态 + +```bash +docker ps +``` + +应该看到以下容器都是 `healthy` 状态: +- `nlq_mysql` (端口 3306) +- `nlq_mongodb` (端口 27017) +- `nlq_redis` (端口 6379) + +### 3. 初始数据说明 + +容器首次启动时会自动执行初始化脚本: + +**MySQL (`mysql_schema_from_last.sql`)**: +- ✅ 创建所有表结构 +- ✅ 插入基础数据: + - 3 个角色(系统管理员、数据管理员、普通用户) + - 4 种数据库类型(MySQL、MongoDB、PostgreSQL、SQL Server) + - 3 种大模型状态(可用、不可用、不稳定) + - 3 个内置用户账号(密码统一为 `123456`): + - `sys_admin` - 系统管理员 + - `data_admin` - 数据管理员 + - `normal_user` - 普通用户 + +**MongoDB (`mongodb_schema_from_last.js`)**: +- ✅ 创建 7 个集合(dialog_records、sql_cache 等) +- ✅ 配置索引和数据验证规则 + +### 4. 登录测试 + +**MySQL 管理界面**:http://localhost:8082 +- 系统:MySQL +- 服务器:mysql +- 用户名:root +- 密码:root123456 +- 数据库:natural_language_query_system + +**MongoDB 管理界面**:http://localhost:8081 +- 用户名:admin +- 密码:admin + +--- + +## 二、数据持久化说明 + +### Docker 数据卷 + +项目使用 Docker 命名卷存储数据,**即使删除容器,数据也不会丢失**: + +```bash +# 查看数据卷 +docker volume ls | findstr nlq + +# 输出示例: +# local springboot_demo_mysql_data +# local springboot_demo_mongodb_data +# local springboot_demo_redis_data +``` + +### 常见操作 + +**重启容器(数据不丢失)**: +```bash +docker compose restart +``` + +**停止容器(数据不丢失)**: +```bash +docker compose stop +``` + +**删除容器但保留数据**: +```bash +docker compose down +``` + +**完全清理(包括数据卷)**: +```bash +# ⚠️ 警告:这会删除所有数据! +docker compose down -v +``` + +--- + +## 三、团队协作:数据共享 + +### 场景 1:你配置好了数据,想分享给团队 + +1. **导出数据**: + ```bash + # Windows + cd D:\test + scripts\export-data.bat + + # Linux/Mac + chmod +x scripts/export-data.sh + ./scripts/export-data.sh + ``` + +2. **分享文件**: + 将生成的 `data-backup` 文件夹打包,通过网盘/Git LFS 分享给团队成员。 + +### 场景 2:团队成员导入你的数据 + +1. **获取备份文件**: + 将收到的 `data-backup` 文件夹放到项目根目录。 + +2. **启动容器**: + ```bash + docker compose up -d + ``` + +3. **导入数据**: + ```bash + # Windows + scripts\import-data.bat + + # Linux/Mac + chmod +x scripts/import-data.sh + ./scripts/import-data.sh + ``` + +--- + +## 四、常见问题 + +### Q1: 修改了初始化脚本,但重启后没生效? + +**原因**:初始化脚本只在数据卷为空时执行一次。 + +**解决**: +```bash +# 1. 停止并删除容器和数据卷 +docker compose down -v + +# 2. 重新启动(会重新执行初始化脚本) +docker compose up -d +``` + +### Q2: 端口被占用怎么办? + +**错误示例**: +``` +Error: bind: address already in use +``` + +**解决**:修改 `docker-compose.yml` 中的端口映射: +```yaml +ports: + - "3307:3306" # 将 MySQL 映射到本机 3307 端口 +``` + +### Q3: 如何查看容器日志? + +```bash +# 查看 MySQL 日志 +docker logs -f nlq_mysql + +# 查看 MongoDB 日志 +docker logs -f nlq_mongodb +``` + +### Q4: 如何进入容器内部? + +```bash +# 进入 MySQL 容器 +docker exec -it nlq_mysql bash +mysql -uroot -proot123456 + +# 进入 MongoDB 容器 +docker exec -it nlq_mongodb bash +mongosh -u admin -p admin123456 --authenticationDatabase admin +``` + +--- + +## 五、开发建议 + +### 本地开发流程 + +1. **首次启动**: + ```bash + docker compose up -d + # 等待 30 秒让初始化脚本执行完成 + ``` + +2. **启动后端**: + ```bash + # IDEA 中直接运行 SpringbootDemoApplication + # 或命令行: + mvn spring-boot:run + ``` + +3. **启动前端**: + ```bash + cd frontend + npm install + npm run dev + ``` + +### 数据备份习惯 + +建议每周执行一次数据导出,避免重要测试数据丢失: + +```bash +# 每周五下班前 +scripts\export-data.bat +git add data-backup/ +git commit -m "chore: 备份测试数据 $(date)" +``` + +--- + +## 六、生产环境部署 + +⚠️ **注意**:`docker-compose.yml` 中的密码仅用于开发环境! + +生产环境部署时请: +1. 修改所有默认密码 +2. 使用环境变量管理敏感信息 +3. 配置防火墙规则 +4. 启用 SSL/TLS 加密连接 +5. 定期备份数据 + +--- + +## 附录:默认账号信息 + +### 数据库账号 + +| 服务 | 用户名 | 密码 | 说明 | +|------|--------|------|------| +| MySQL (root) | root | root123456 | 数据库管理员 | +| MySQL (应用) | nlq_user | nlq_pass123 | 应用连接账号 | +| MongoDB | admin | admin123456 | 数据库管理员 | +| Adminer | - | - | 无需登录 | +| Mongo Express | admin | admin | Web 界面登录 | + +### 应用内置账号(密码统一为 123456) + +| 用户名 | 角色 | 权限说明 | +|--------|------|----------| +| sys_admin | 系统管理员 | 拥有系统所有权限,可管理用户、配置大模型 | +| data_admin | 数据管理员 | 可管理数据源连接、分配用户数据权限 | +| normal_user | 普通用户 | 可执行自然语言查询、查看历史记录 | + +--- + +**最后更新**:2025-12-01 +**维护者**:项目组 + diff --git a/src/test/QUICK_START.md b/src/test/QUICK_START.md new file mode 100644 index 00000000..fb66786d --- /dev/null +++ b/src/test/QUICK_START.md @@ -0,0 +1,206 @@ +# 快速启动指南 + +## 一、启动后端 + +1. **在 IDE 中重启 Spring Boot** + - 停止当前运行的 `SpringbootDemoApplication` + - 重新运行(让新的配置生效) + +2. **验证后端启动成功** + ```bash + # 浏览器访问或命令行执行 + curl http://localhost:8080/actuator/health + ``` + + 应返回: + ```json + {"status":"UP"} + ``` + +--- + +## 二、启动前端 + +```bash +cd frontend +npm install # 首次运行需要安装依赖 +npm run dev +``` + +浏览器会自动打开 `http://localhost:5173` + +--- + +## 三、登录系统 + +### 方式 1:在前端界面登录(推荐) + +1. 打开 `http://localhost:5173` +2. 在登录页面输入: + - 用户名:`sys_admin` + - 密码:`123456` +3. 点击登录 + +### 方式 2:用 API 工具测试 + +```http +POST http://localhost:8080/auth/login +Content-Type: application/json + +{ + "username": "sys_admin", + "password": "123456" +} +``` + +成功后会返回: +```json +{ + "code": 200, + "data": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "userId": 1, + "username": "sys_admin", + "roleId": 1, + "roleName": "系统管理员" + } +} +``` + +--- + +## 四、测试功能 + +### 1. 配置大模型(系统管理员) + +登录后,在前端界面: +1. 进入"系统管理" → "大模型配置" +2. 点击"添加模型" +3. 填写信息: + - 模型名称:DeepSeek + - 版本:chat + - API Key:你的密钥 + - API 地址:https://api.deepseek.com/chat/completions + - 超时时间:30000 + +或用 API 测试: + +```http +POST http://localhost:8080/llm-config +Authorization: Bearer {{你的token}} +Content-Type: application/json + +{ + "name": "DeepSeek", + "version": "chat", + "apiKey": "sk-xxx", + "apiUrl": "https://api.deepseek.com/chat/completions", + "statusId": 1, + "isDisabled": 0, + "timeout": 30000, + "createUserId": 1 +} +``` + +### 2. 添加数据源(数据管理员) + +用 `data_admin / 123456` 登录后: +1. 进入"数据管理" → "数据源管理" +2. 添加 MySQL 连接 + +### 3. 执行查询(普通用户) + +用 `normal_user / 123456` 登录后: +1. 在主页输入自然语言:"查询所有用户" +2. 选择模型和数据库 +3. 点击发送 + +--- + +## 五、常见问题 + +### Q1: 前端显示 "Failed to fetch" + +**原因**:后端未启动或未登录 + +**解决**: +1. 确认后端在 8080 端口运行:`netstat -ano | findstr :8080` +2. 先登录获取 token +3. 检查浏览器 DevTools → Network 查看具体错误 + +### Q2: 接口返回 401 Unauthorized + +**原因**:Token 无效或未携带 + +**解决**: +1. 重新登录获取新 token +2. 确认前端 localStorage 中有 token: + - 打开 DevTools → Application → Local Storage + - 检查是否有 `token` 和 `userId` + +### Q3: MongoDB 认证失败 + +**原因**:`application.yml` 中密码配置错误 + +**解决**: +确认 `src/main/resources/application.yml` 第 28 行: +```yaml +password: admin123456 # 不是 admin +``` + +--- + +## 六、安全说明 + +### 当前配置(开发环境) + +**免认证的接口**: +- `/auth/**` - 登录注册 +- `/actuator/**` - 健康检查 +- `/user` - 用户注册 +- `/role` - 角色查询 + +**需要认证的接口**: +- `/llm-config/**` - 大模型配置 +- `/db-connection/**` - 数据源管理 +- `/query/**` - 查询执行 +- 其他所有业务接口 + +### 生产环境建议 + +部署到生产环境前,务必: + +1. **移除 `/actuator/**` 的免认证** + ```java + // WebMvcConfig.java 删除这一行 + "/actuator/**", + ``` + +2. **修改默认密码** + - 三个内置账号的密码改为强密码 + - 数据库密码改为强密码 + +3. **配置 HTTPS** + - 使用 SSL 证书 + - 强制 HTTPS 访问 + +4. **启用 CORS 白名单** + ```java + // CorsConfig.java + .allowedOriginPatterns("https://yourdomain.com") + ``` + +--- + +## 附录:三个内置账号 + +| 用户名 | 密码 | 角色 | 权限 | +|--------|------|------|------| +| sys_admin | 123456 | 系统管理员 | 全部权限 | +| data_admin | 123456 | 数据管理员 | 数据源、权限管理 | +| normal_user | 123456 | 普通用户 | 查询、历史记录 | + +--- + +**最后更新**:2025-12-01 + diff --git a/src/test/TEST_ACCOUNTS.md b/src/test/TEST_ACCOUNTS.md new file mode 100644 index 00000000..ea25564e --- /dev/null +++ b/src/test/TEST_ACCOUNTS.md @@ -0,0 +1,292 @@ +# 内置测试账号说明 + +## 账号列表 + +系统已预置 3 个测试账号,对应三种角色: + +| 用户名 | 密码 | 角色 | 角色ID | 权限范围 | +|--------|------|------|--------|----------| +| `sys_admin` | `123456` | 系统管理员 | 1 | 全部权限 | +| `data_admin` | `123456` | 数据管理员 | 2 | 数据源管理、权限分配 | +| `normal_user` | `123456` | 普通用户 | 3 | 查询、历史记录 | + +--- + +## 快速测试 + +### 1. 登录测试 + +```http +### 测试系统管理员登录 +POST http://localhost:8080/auth/login +Content-Type: application/json + +{ + "username": "sys_admin", + "password": "123456" +} + +### + +### 测试数据管理员登录 +POST http://localhost:8080/auth/login +Content-Type: application/json + +{ + "username": "data_admin", + "password": "123456" +} + +### + +### 测试普通用户登录 +POST http://localhost:8080/auth/login +Content-Type: application/json + +{ + "username": "normal_user", + "password": "123456" +} +``` + +### 2. 预期返回结果 + +成功登录后应返回: + +```json +{ + "code": 200, + "data": { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "userId": 1, + "username": "sys_admin", + "email": "sys_admin@example.com", + "roleId": 1, + "roleName": "系统管理员", + "avatarUrl": "/default-avatar.png" + }, + "message": "success" +} +``` + +--- + +## 角色权限说明 + +### 系统管理员 (sys_admin) + +**可访问的功能模块**: +- ✅ 用户管理(增删改查用户) +- ✅ 角色管理 +- ✅ 大模型配置管理 +- ✅ 系统日志查看 +- ✅ 通知管理 +- ✅ 数据源管理(查看) +- ✅ 自然语言查询 + +**典型操作**: +```http +### 创建新用户 +POST http://localhost:8080/user +Authorization: Bearer {{sys_admin_token}} +Content-Type: application/json + +{ + "username": "test_user", + "password": "123456", + "email": "test@example.com", + "phonenumber": "13900000000", + "roleId": 3 +} + +### + +### 配置大模型 +POST http://localhost:8080/llm-config +Authorization: Bearer {{sys_admin_token}} +Content-Type: application/json + +{ + "name": "Gemini", + "version": "2.0", + "apiKey": "your-api-key", + "apiUrl": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent", + "statusId": 1, + "isDisabled": 0, + "timeout": 5000, + "createUserId": 1 +} +``` + +--- + +### 数据管理员 (data_admin) + +**可访问的功能模块**: +- ✅ 数据源连接管理(增删改查) +- ✅ 用户数据权限分配 +- ✅ 数据库连接日志查看 +- ✅ 表/字段元数据管理 +- ✅ 自然语言查询 + +**典型操作**: +```http +### 添加数据库连接 +POST http://localhost:8080/db-connection +Authorization: Bearer {{data_admin_token}} +Content-Type: application/json + +{ + "name": "测试MySQL", + "dbTypeId": 1, + "url": "localhost:3306/test_db", + "username": "test_user", + "password": "test_pass", + "createUserId": 2 +} + +### + +### 为用户分配数据权限 +PUT http://localhost:8080/user-db-permission +Authorization: Bearer {{data_admin_token}} +Content-Type: application/json + +{ + "userId": 3, + "permissionDetails": "[{\"db_connection_id\":1,\"table_ids\":[1,2,3]}]", + "isAssigned": 1, + "lastGrantUserId": 2 +} +``` + +--- + +### 普通用户 (normal_user) + +**可访问的功能模块**: +- ✅ 自然语言查询(仅限已授权的数据源) +- ✅ 查询历史记录查看 +- ✅ 对话管理 +- ✅ 查询收藏 +- ✅ 好友功能(如果实现) + +**典型操作**: +```http +### 执行自然语言查询 +POST http://localhost:8080/query/execute +Authorization: Bearer {{normal_user_token}} +userId: 3 +Content-Type: application/json + +{ + "userPrompt": "查询所有用户", + "model": "Gemini", + "database": "测试MySQL" +} + +### + +### 查看历史对话 +GET http://localhost:8080/dialog/list +Authorization: Bearer {{normal_user_token}} +userId: 3 +``` + +--- + +## 权限验证测试 + +### 测试 1:普通用户尝试创建用户(应失败) + +```http +POST http://localhost:8080/user +Authorization: Bearer {{normal_user_token}} +Content-Type: application/json + +{ + "username": "hacker", + "password": "123456", + "email": "hacker@example.com", + "phonenumber": "13900000001", + "roleId": 1 +} +``` + +**预期结果**:403 Forbidden 或权限不足错误 + +### 测试 2:数据管理员尝试配置大模型(应失败) + +```http +POST http://localhost:8080/llm-config +Authorization: Bearer {{data_admin_token}} +Content-Type: application/json + +{ + "name": "Test Model", + "version": "1.0", + "apiKey": "test-key", + "apiUrl": "https://test.com", + "statusId": 1, + "createUserId": 2 +} +``` + +**预期结果**:403 Forbidden 或权限不足错误 + +--- + +## 密码修改建议 + +⚠️ **安全提示**: +- 默认密码 `123456` 仅用于开发测试 +- 生产环境部署前必须修改所有默认密码 +- 建议使用强密码策略(8位以上,包含大小写字母、数字、特殊字符) + +**修改密码示例**: +```http +PUT http://localhost:8080/user +Authorization: Bearer {{token}} +Content-Type: application/json + +{ + "id": 1, + "password": "NewSecurePassword@2025" +} +``` + +--- + +## 数据库直接查询 + +如果需要在数据库中直接查看用户信息: + +```sql +-- 查看所有内置用户 +SELECT + u.id, + u.username, + u.email, + r.role_name, + u.status, + u.create_time +FROM users u +LEFT JOIN roles r ON u.role_id = r.id +WHERE u.id IN (1, 2, 3); + +-- 查看用户权限分配情况 +SELECT + u.username, + p.permission_details, + p.is_assigned, + p.last_grant_time +FROM user_db_permissions p +LEFT JOIN users u ON p.user_id = u.id +WHERE p.user_id IN (1, 2, 3); +``` + +--- + +**最后更新**:2025-12-01 +**密码策略**:统一使用 `123456`(BCrypt 加密存储) + diff --git a/src/test/api-test.http b/src/test/api-test.http new file mode 100644 index 00000000..80801dfb --- /dev/null +++ b/src/test/api-test.http @@ -0,0 +1,636 @@ +### 测试 Hello +GET http://localhost:8080/test/hello + +### 测试所有数据库 +GET http://localhost:8080/test/all + +### ==================== 认证接口 ==================== + +### 登录 +POST http://localhost:8080/auth/login +Content-Type: application/json + +{ + "username": "admin", + "password": "123456" +} + +### ==================== 查询执行接口 ==================== + +### 执行查询(新对话) +POST http://localhost:8080/query/execute +Content-Type: application/json +userId: 1 + +{ + "userPrompt": "查询所有用户信息", + "model": "gemini-2.5-pro", + "database": "销售数据库" +} + +### 执行查询(继续对话) +POST http://localhost:8080/query/execute +Content-Type: application/json +userId: 1 + +{ + "userPrompt": "按订单量排序", + "model": "gemini-2.5-pro", + "database": "销售数据库", + "conversationId": "conv_12345678" +} + +### ==================== 对话历史接口 ==================== + +### 获取用户对话列表 +GET http://localhost:8080/dialog/list +userId: 1 + +### 获取对话详情 +GET http://localhost:8080/dialog/conv_12345678 + +### ==================== 用户管理接口 ==================== + +### 查询所有用户 +GET http://localhost:8080/user/list + +### 添加用户 +POST http://localhost:8080/user +Content-Type: application/json + +{ + "username": "admin", + "password": "123456", + "email": "admin@example.com", + "phonenumber": "13800138000", + "roleId": 1, + "status": 1 +} + +### 分页查询 +GET http://localhost:8080/user/page?current=1&size=10 + +### 根据ID查询 +GET http://localhost:8080/user/3 + +### 更新用户 +PUT http://localhost:8080/user +Content-Type: application/json + +{ + "id": 3, + "username": "admin", + "email": "newemail@example.com", + "status": 1 +} + +### 删除用户 +DELETE http://localhost:8080/user/3 + +### 根据用户名查询 +GET http://localhost:8080/user/username/admin + +### ==================== 数据库连接管理接口 ==================== + +### 查询所有数据库连接 +GET http://localhost:8080/db-connection/list + +### 根据创建者查询数据库连接 +GET http://localhost:8080/db-connection/list/4 + +### 根据ID查询数据库连接 +GET http://localhost:8080/db-connection/1 + +### 添加数据库连接 +POST http://localhost:8080/db-connection +Content-Type: application/json + +{ + "name": "测试MySQL连接", + "dbTypeId": 1, + "url": "127.0.0.1:3306/test_db", + "username": "root", + "password": "password", + "status": "disconnected", + "createUserId": 4 +} + +### 更新数据库连接 +PUT http://localhost:8080/db-connection +Content-Type: application/json + +{ + "id": 1, + "name": "更新后的连接名称", + "status": "connected" +} + +### 测试数据库连接 +GET http://localhost:8080/db-connection/test/1 + +### 删除数据库连接 +DELETE http://localhost:8080/db-connection/1 + +### ==================== 大模型配置接口 ==================== + +### 查询所有大模型配置 +GET http://localhost:8080/llm-config/list + +### 查询可用的大模型配置 +GET http://localhost:8080/llm-config/list/available + +### 根据ID查询大模型配置 +GET http://localhost:8080/llm-config/1 + +### 添加大模型配置 +POST http://localhost:8080/llm-config +Content-Type: application/json + +{ + "name": "智谱AI", + "version": "4.0", + "apiKey": "your-api-key-here", + "apiUrl": "https://api.zhipuai.com/v4/chat/completions", + "statusId": 1, + "isDisabled": 0, + "timeout": 5000, + "createUserId": 4 +} + +### 更新大模型配置 +PUT http://localhost:8080/llm-config +Content-Type: application/json + +{ + "id": 1, + "name": "智谱AI", + "version": "4.5", + "timeout": 8000 +} + +### 禁用/启用大模型配置 +PUT http://localhost:8080/llm-config/1/toggle + +### 删除大模型配置 +DELETE http://localhost:8080/llm-config/1 + +### ==================== 数据表元数据接口 ==================== + +### 查询所有表元数据 +GET http://localhost:8080/table-metadata/list + +### 根据数据库连接ID查询表元数据 +GET http://localhost:8080/table-metadata/list/1 + +### 根据ID查询表元数据 +GET http://localhost:8080/table-metadata/1 + +### 添加表元数据 +POST http://localhost:8080/table-metadata +Content-Type: application/json + +{ + "dbConnectionId": 1, + "tableName": "orders", + "description": "订单表,存储所有订单信息" +} + +### 更新表元数据 +PUT http://localhost:8080/table-metadata +Content-Type: application/json + +{ + "id": 1, + "tableName": "orders", + "description": "订单表(已更新)" +} + +### 删除表元数据 +DELETE http://localhost:8080/table-metadata/1 + +### ==================== 字段元数据接口 ==================== + +### 查询所有字段元数据 +GET http://localhost:8080/column-metadata/list + +### 根据表ID查询字段元数据 +GET http://localhost:8080/column-metadata/list/1 + +### 根据ID查询字段元数据 +GET http://localhost:8080/column-metadata/1 + +### 添加字段元数据 +POST http://localhost:8080/column-metadata +Content-Type: application/json + +{ + "tableId": 1, + "columnName": "order_id", + "dataType": "bigint(20)", + "description": "订单唯一标识", + "isPrimary": 1 +} + +### 更新字段元数据 +PUT http://localhost:8080/column-metadata +Content-Type: application/json + +{ + "id": 1, + "columnName": "order_id", + "description": "订单ID(主键)" +} + +### 删除字段元数据 +DELETE http://localhost:8080/column-metadata/1 + +### ==================== 查询日志接口 ==================== + +### 查询所有查询日志 +GET http://localhost:8080/query-log/list + +### 根据用户ID查询查询日志 +GET http://localhost:8080/query-log/list/user/4 + +### 根据对话ID查询查询日志 +GET http://localhost:8080/query-log/list/dialog/conv_12345678 + +### 根据ID查询查询日志 +GET http://localhost:8080/query-log/1 + +### 添加查询日志 +POST http://localhost:8080/query-log +Content-Type: application/json + +{ + "dialogId": "conv_12345678", + "dataSourceId": 1, + "userId": 4, + "executeResult": 1 +} + +### 删除查询日志 +DELETE http://localhost:8080/query-log/1 + +### ==================== 数据库类型字典接口 ==================== + +### 查询所有数据库类型 +GET http://localhost:8080/db-type/list + +### 根据ID查询数据库类型 +GET http://localhost:8080/db-type/1 + +### 根据类型编码查询 +GET http://localhost:8080/db-type/code/mysql + +### 添加数据库类型 +POST http://localhost:8080/db-type +Content-Type: application/json + +{ + "typeName": "MySQL", + "typeCode": "mysql", + "description": "关系型数据库" +} + +### 更新数据库类型 +PUT http://localhost:8080/db-type +Content-Type: application/json + +{ + "id": 1, + "typeName": "MySQL", + "description": "关系型数据库(已更新)" +} + +### 删除数据库类型 +DELETE http://localhost:8080/db-type/1 + +### ==================== 大模型状态字典接口 ==================== + +### 查询所有大模型状态 +GET http://localhost:8080/llm-status/list + +### 根据ID查询大模型状态 +GET http://localhost:8080/llm-status/1 + +### 根据状态编码查询 +GET http://localhost:8080/llm-status/code/available + +### 添加大模型状态 +POST http://localhost:8080/llm-status +Content-Type: application/json + +{ + "statusName": "可用", + "statusCode": "available", + "description": "API成功率≥95%" +} + +### 更新大模型状态 +PUT http://localhost:8080/llm-status +Content-Type: application/json + +{ + "id": 1, + "statusName": "可用", + "description": "API成功率≥98%" +} + +### 删除大模型状态 +DELETE http://localhost:8080/llm-status/1 + +### ==================== 用户数据权限接口 ==================== + +### 查询所有权限 +GET http://localhost:8080/user-db-permission/list + +### 查询已分配权限的用户 +GET http://localhost:8080/user-db-permission/list/assigned + +### 查询未分配权限的用户 +GET http://localhost:8080/user-db-permission/list/unassigned + +### 根据ID查询权限 +GET http://localhost:8080/user-db-permission/1 + +### 根据用户ID查询权限 +GET http://localhost:8080/user-db-permission/user/4 + +### 添加用户权限 +POST http://localhost:8080/user-db-permission +Content-Type: application/json + +{ + "userId": 4, + "permissionDetails": "[{\"db_connection_id\":1,\"table_ids\":[1,2,3]}]", + "lastGrantUserId": 4, + "isAssigned": 1 +} + +### 更新用户权限 +PUT http://localhost:8080/user-db-permission +Content-Type: application/json + +{ + "id": 1, + "userId": 4, + "permissionDetails": "[{\"db_connection_id\":1,\"table_ids\":[1,2,3,4,5]}]", + "lastGrantUserId": 4 +} + +### 删除用户权限 +DELETE http://localhost:8080/user-db-permission/1 + +### ==================== 系统操作日志接口 ==================== + +### 查询所有操作日志 +GET http://localhost:8080/operation-log/list + +### 根据用户ID查询操作日志 +GET http://localhost:8080/operation-log/list/user/4 + +### 根据模块查询操作日志 +GET http://localhost:8080/operation-log/list/module/数据源管理 + +### 查询失败的操作日志 +GET http://localhost:8080/operation-log/list/failed + +### 根据ID查询操作日志 +GET http://localhost:8080/operation-log/1 + +### 添加操作日志 +POST http://localhost:8080/operation-log +Content-Type: application/json + +{ + "userId": 4, + "username": "admin", + "operation": "创建数据库连接", + "module": "数据源管理", + "relatedLlm": null, + "ipAddress": "127.0.0.1", + "result": 1, + "errorMsg": null +} + +### 删除操作日志 +DELETE http://localhost:8080/operation-log/1 + +### ==================== 错误日志接口 ==================== + +### 查询所有错误日志 +GET http://localhost:8080/error-log/list + +### 根据错误类型查询 +GET http://localhost:8080/error-log/list/type/1 + +### 根据统计周期查询 +GET http://localhost:8080/error-log/list/period/today + +### 根据ID查询错误日志 +GET http://localhost:8080/error-log/1 + +### 添加错误日志 +POST http://localhost:8080/error-log +Content-Type: application/json + +{ + "errorTypeId": 1, + "errorCount": 5, + "errorRate": 2.5, + "period": "today" +} + +### 更新错误日志 +PUT http://localhost:8080/error-log +Content-Type: application/json + +{ + "id": 1, + "errorCount": 10, + "errorRate": 5.0 +} + +### 删除错误日志 +DELETE http://localhost:8080/error-log/1 + +### ==================== 错误类型字典接口 ==================== + +### 查询所有错误类型 +GET http://localhost:8080/error-type/list + +### 根据ID查询错误类型 +GET http://localhost:8080/error-type/1 + +### 根据错误编码查询 +GET http://localhost:8080/error-type/code/llm_timeout + +### 添加错误类型 +POST http://localhost:8080/error-type +Content-Type: application/json + +{ + "errorName": "模型调用超时", + "errorCode": "llm_timeout", + "description": "大模型API响应时间超过设定阈值" +} + +### 更新错误类型 +PUT http://localhost:8080/error-type +Content-Type: application/json + +{ + "id": 1, + "errorName": "模型调用超时", + "description": "大模型API响应时间超过5秒" +} + +### 删除错误类型 +DELETE http://localhost:8080/error-type/1 + +### ==================== 通知目标字典接口 ==================== + +### 查询所有通知目标 +GET http://localhost:8080/notification-target/list + +### 根据ID查询通知目标 +GET http://localhost:8080/notification-target/1 + +### 根据目标编码查询 +GET http://localhost:8080/notification-target/code/all + +### 添加通知目标 +POST http://localhost:8080/notification-target +Content-Type: application/json + +{ + "targetName": "所有用户", + "targetCode": "all", + "description": "发送给系统所有用户" +} + +### 更新通知目标 +PUT http://localhost:8080/notification-target +Content-Type: application/json + +{ + "id": 1, + "targetName": "所有用户", + "description": "发送给系统所有注册用户" +} + +### 删除通知目标 +DELETE http://localhost:8080/notification-target/1 + +### ==================== 优先级字典接口 ==================== + +### 查询所有优先级(按排序) +GET http://localhost:8080/priority/list + +### 根据ID查询优先级 +GET http://localhost:8080/priority/1 + +### 根据优先级编码查询 +GET http://localhost:8080/priority/code/urgent + +### 添加优先级 +POST http://localhost:8080/priority +Content-Type: application/json + +{ + "priorityName": "紧急", + "priorityCode": "urgent", + "sort": 1 +} + +### 更新优先级 +PUT http://localhost:8080/priority +Content-Type: application/json + +{ + "id": 1, + "priorityName": "紧急", + "sort": 0 +} + +### 删除优先级 +DELETE http://localhost:8080/priority/1 + +### ==================== 通知管理接口 ==================== + +### 查询所有通知 +GET http://localhost:8080/notification/list + +### 查询已发布的通知 +GET http://localhost:8080/notification/list/published + +### 查询草稿 +GET http://localhost:8080/notification/list/drafts + +### 根据目标ID查询通知 +GET http://localhost:8080/notification/list/target/1 + +### 根据ID查询通知 +GET http://localhost:8080/notification/1 + +### 添加通知(草稿) +POST http://localhost:8080/notification +Content-Type: application/json + +{ + "title": "系统维护通知", + "content": "系统将于今晚22:00-24:00进行维护,请提前保存数据", + "targetId": 1, + "priorityId": 1, + "publisherId": 4, + "isTop": 0 +} + +### 更新通知 +PUT http://localhost:8080/notification +Content-Type: application/json + +{ + "id": 1, + "title": "系统维护通知(已更新)", + "content": "系统将于今晚22:00-次日00:30进行维护,请提前保存数据" +} + +### 发布通知 +PUT http://localhost:8080/notification/1/publish + +### 置顶/取消置顶 +PUT http://localhost:8080/notification/1/toggle-top + +### 删除通知 +DELETE http://localhost:8080/notification/1 + +### ==================== 系统健康监控接口 ==================== + +### 查询所有健康记录 +GET http://localhost:8080/system-health/list + +### 查询最新的健康记录 +GET http://localhost:8080/system-health/latest + +### 查询最近10条健康记录 +GET http://localhost:8080/system-health/recent/10 + +### 根据ID查询健康记录 +GET http://localhost:8080/system-health/1 + +### 添加健康记录 +POST http://localhost:8080/system-health +Content-Type: application/json + +{ + "dbDelay": 10, + "cacheDelay": 2, + "llmDelay": 150, + "storageUsage": 75.5 +} + +### 删除健康记录 +DELETE http://localhost:8080/system-health/1 + diff --git a/src/test/docker-compose.yml b/src/test/docker-compose.yml new file mode 100644 index 00000000..d4a846fb --- /dev/null +++ b/src/test/docker-compose.yml @@ -0,0 +1,123 @@ +services: + # MySQL 8.4 服务 + mysql: + image: mysql:8.4 + container_name: nlq_mysql + restart: always + environment: + MYSQL_ROOT_PASSWORD: root123456 + MYSQL_DATABASE: natural_language_query_system + MYSQL_USER: nlq_user + MYSQL_PASSWORD: nlq_pass123 + TZ: Asia/Shanghai + ports: + - "3306:3306" + volumes: + # 挂载初始化脚本(MySQL会自动执行/docker-entrypoint-initdb.d/目录下的.sql文件) + - ./mysql_schema_from_last.sql:/docker-entrypoint-initdb.d/init.sql + # 数据持久化 + - mysql_data:/var/lib/mysql + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + - --mysql-native-password=ON + networks: + - nlq_network + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123456"] + interval: 10s + timeout: 5s + retries: 5 + + # MongoDB 8.2 服务 + mongodb: + image: mongo:8.2 + container_name: nlq_mongodb + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: admin + MONGO_INITDB_ROOT_PASSWORD: admin123456 + MONGO_INITDB_DATABASE: natural_language_query_system + TZ: Asia/Shanghai + ports: + - "27017:27017" + volumes: + # 挂载初始化脚本 + - ./mongodb_schema_from_last.js:/docker-entrypoint-initdb.d/init.js + # 数据持久化 + - mongodb_data:/data/db + - mongodb_config:/data/configdb + networks: + - nlq_network + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + + # 可选:Mongo Express (MongoDB 的 Web 管理界面) + mongo-express: + image: mongo-express:latest + container_name: nlq_mongo_express + restart: always + ports: + - "8081:8081" + environment: + ME_CONFIG_MONGODB_ADMINUSERNAME: admin + ME_CONFIG_MONGODB_ADMINPASSWORD: admin123456 + ME_CONFIG_MONGODB_URL: mongodb://admin:admin123456@mongodb:27017/ + ME_CONFIG_BASICAUTH_USERNAME: admin + ME_CONFIG_BASICAUTH_PASSWORD: admin + depends_on: + - mongodb + networks: + - nlq_network + + # 可选:Adminer (MySQL 的 Web 管理界面) + adminer: + image: adminer:latest + container_name: nlq_adminer + restart: always + ports: + - "8082:8080" + environment: + ADMINER_DEFAULT_SERVER: mysql + depends_on: + - mysql + networks: + - nlq_network + + # Redis 缓存服务 + redis: + image: redis:7-alpine + container_name: nlq_redis + restart: always + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + networks: + - nlq_network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 5 + +# 数据卷(数据持久化) +volumes: + mysql_data: + driver: local + mongodb_data: + driver: local + mongodb_config: + driver: local + redis_data: + driver: local + +# 网络配置 +networks: + nlq_network: + driver: bridge + diff --git a/src/test/frontend/.gitignore b/src/test/frontend/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/src/test/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/src/test/frontend/App.tsx b/src/test/frontend/App.tsx new file mode 100644 index 00000000..2310b52f --- /dev/null +++ b/src/test/frontend/App.tsx @@ -0,0 +1,732 @@ +import React, { useState, useEffect } from 'react'; +// 页面组件导入 +import { LoginPage } from './components/LoginPage'; +import { Sidebar } from './components/Sidebar'; +import { QueryPage } from './components/QueryPage'; +import { HistoryPage } from './components/HistoryPage'; +import { NotificationsPage } from './components/NotificationsPage'; +import { AccountPage } from './components/AccountPage'; +import { FriendsPage } from './components/FriendsPage'; +import { SysAdminSidebar } from './components/SysAdminSidebar'; +import { DataAdminSidebar } from './components/DataAdminSidebar'; +import { SysAdminPage } from './components/SysAdminPage'; +import { DataAdminPage } from './components/DataAdminPage'; +import { TopHeader } from './components/TopHeader'; +import { ComparisonModal } from './components/ComparisonModal'; +import { Modal } from './components/Modal'; + +// 模拟数据导入 +import { + MOCK_INITIAL_CONVERSATION, + MOCK_SAVED_QUERIES, + MOCK_USER_PROFILE, + MOCK_NOTIFICATIONS, + MOCK_QUERY_SHARES, + MOCK_FRIENDS_LIST +} from './constants'; + +// 类型定义导入 +import { + Conversation, + MessageRole, + Page, + QueryResultData, + UserRole, + SysAdminPageType, + DataAdminPageType, + QueryShare, + Notification +} from './types'; + +/** + * 各角色侧边栏配置项 + * 按角色划分功能菜单,与对应Sidebar组件菜单结构保持一致 + */ +// 普通用户侧边栏项目 - 基础数据查询与个人中心功能 +const normalUserSidebarItems = [ + { href: 'query', label: '数据查询' }, + { href: 'history', label: '收藏夹' }, + { href: 'notifications', label: '通知中心' }, + { href: 'friends', label: '好友管理' }, + { href: 'account', label: '账户管理' }, +] as const; + +// 数据管理员侧边栏项目 - 包含数据管理与基础功能 +const dataAdminSidebarItems = [ + { href: 'query', label: '数据查询' }, + { href: 'history', label: '收藏夹' }, + { href: 'datasource', label: '数据源管理' }, + { href: 'dashboard', label: '数据源概览' }, + { href: 'user-permission', label: '用户权限管理' }, + { href: 'notification-management', label: '通知管理(数据员)' }, + { href: 'connection-log', label: '数据源连接日志' }, + { href: 'notifications', label: '通知中心' }, + { href: 'account', label: '我的账户' }, + { href: 'friends', label: '好友管理' }, +] as const; + +// 系统管理员侧边栏项目 - 系统配置与管理功能 +const sysAdminSidebarItems = [ + { href: 'dashboard', label: '系统概览' }, + { href: 'user-management', label: '用户管理' }, + { href: 'account', label: '我的账户' }, + { href: 'system-log', label: '系统日志' }, + { href: 'llm-config', label: '大模型配置' }, + { href: 'notification-management', label: '通知管理' }, +] as const; + +/** + * 应用根组件 - 全局状态管理与角色路由控制 + * 负责用户身份验证、页面切换、数据状态维护 + */ +const App: React.FC = () => { + // ===== 全局核心状态 ===== + // 当前用户角色(null表示未登录) + const [userRole, setUserRole] = useState(null); + // 所有对话列表 + const [conversations, setConversations] = useState([MOCK_INITIAL_CONVERSATION]); + // 当前激活的对话ID + const [currentConversationId, setCurrentConversationId] = useState(MOCK_INITIAL_CONVERSATION.id); + // 历史对话面板显示状态(仅query页面生效) + const [isHistoryOpen, setIsHistoryOpen] = useState(false); + // 普通用户当前激活页面 + const [activePage, setActivePage] = useState('query'); + // 已保存的查询结果列表 + const [savedQueries, setSavedQueries] = useState(MOCK_SAVED_QUERIES); + // 新对话初始提示语(用于重新运行查询场景) + const [initialPrompt, setInitialPrompt] = useState(undefined); + // 控制弹窗显示/隐藏 + const [isCompareModalOpen, setIsCompareModalOpen] = useState(false); + // 存储要对比的查询 + const [compareQueries, setCompareQueries] = useState<{ oldQuery: QueryResultData; newQuery: QueryResultData } | null>(null); + // 待对比的查询ID组合(旧查询ID, 新查询ID) + const [comparisonQueryIds, setComparisonQueryIds] = useState<[string, string] | null>(null); + // 对话不存在提示弹窗显示状态 + const [notFoundModalOpen, setNotFoundModalOpen] = useState(false); + // 查询分享记录列表 + const [queryShares, setQueryShares] = useState(MOCK_QUERY_SHARES); + // 通知消息列表 + const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS); + + // ===== 管理员专属状态 ===== + // 系统管理员当前激活页面 + const [activeSysAdminPage, setActiveSysAdminPage] = useState('dashboard'); + // 数据管理员当前激活页面 + const [activeDataAdminPage, setActiveDataAdminPage] = useState('dashboard'); + + // 当前激活的对话详情(通过currentConversationId从列表中匹配) + const currentConversation = conversations.find(c => c.id === currentConversationId); + + // ===== 辅助函数 ===== + /** + * 获取侧边栏当前页面名称 + * 根据用户角色和激活页面,匹配对应的菜单名称 + * @returns 当前页面的显示名称(空字符串表示无需显示) + */ + const getSidebarPageName = () => { + if (userRole === 'normal-user') { + // 普通用户query页面不显示侧边栏名称 + if (activePage === 'query') return ''; + return normalUserSidebarItems.find(item => item.href === activePage)?.label || ''; + } + + if (userRole === 'data-admin') { + // 数据管理员query页面不显示侧边栏名称 + if (activeDataAdminPage === 'query') return ''; + return dataAdminSidebarItems.find(item => item.href === activeDataAdminPage)?.label || ''; + } + + if (userRole === 'sys-admin') { + return sysAdminSidebarItems.find(item => item.href === activeSysAdminPage)?.label || ''; + } + + return ''; + }; + + /** + * 获取顶部导航栏标题 + * 规则:query页面显示对话标题,其他页面显示侧边栏菜单名称 + * @returns 顶部导航栏显示标题 + */ + const getHeaderTitle = () => { + // 判定是否为query页面(跨所有角色) + const isQueryPage = ( + (userRole === 'normal-user' && activePage === 'query') || + (userRole === 'data-admin' && activeDataAdminPage === 'query') || + (userRole === 'sys-admin' && activeSysAdminPage === 'query') + ); + + // query页面优先显示对话标题,无则显示默认文本 + if (isQueryPage) { + return currentConversation?.title || '新对话'; + } + + // 非query页面显示侧边栏菜单名称 + return getSidebarPageName(); + }; + + // 顶部导航栏最终显示标题 + const headerTitle = getHeaderTitle(); + + // ===== 事件处理函数 ===== + /** + * 登录处理函数 + * 优先使用传入的角色,无则从sessionStorage读取保存的角色 + * @param role - 登录选择的用户角色 + */ + const handleLogin = (role: UserRole) => { + const savedRole = sessionStorage.getItem('userRole') as UserRole; + const roleToSet = role || savedRole; + if (roleToSet) { + setUserRole(roleToSet); + } + }; + + /** + * 组件挂载时初始化登录状态 + * 从sessionStorage读取保存的角色,自动登录 + */ + useEffect(() => { + handleLogin(userRole); + }, []); + + /** + * 退出登录处理函数 + * 清除sessionStorage中的角色信息,重置所有状态到初始值 + */ + const handleLogout = () => { + sessionStorage.removeItem('userRole'); + setUserRole(null); + setActivePage('query'); + setActiveSysAdminPage('dashboard'); + setActiveDataAdminPage('dashboard'); + }; + + /** + * 添加对话消息 + * 向当前激活的对话中追加新消息,首次用户消息自动生成对话标题 + * @param role - 消息发送者角色(user/ai) + * @param content - 消息内容(文本或查询结果数据) + */ + const handleAddMessage = (role: MessageRole, content: string | QueryResultData) => { + // 优先使用传入的目标对话ID,无则使用当前激活对话ID + const convId = currentConversationId; + if (!convId) return; + + setConversations(prev => prev.map(conv => { + // 只更新当前激活的对话 + if (conv.id === currentConversationId) { + const newMessages = [...conv.messages, { role, content }]; + let newTitle = conv.title; + + // 首次用户消息(对话初始状态),截取前20字作为对话标题 + if (conv.messages.length === 1 && role === 'user' && typeof content === 'string') { + newTitle = content.substring(0, 20); + } + + return { ...conv, title: newTitle, messages: newMessages }; + } + return conv; + })); + }; + + /** + * 创建新对话 + * 优先复用现有空对话,无则创建全新对话,自动切换到query页面 + */ + const handleNewConversation = () => { + // 判断对话是否为空(仅包含AI初始问候语) + const isEmptyConv = (conv: Conversation) => { + return conv.messages.length === 1 && + conv.messages[0].role === 'ai' && + typeof conv.messages[0].content === 'string' && + conv.messages[0].content.includes('您好!我是数据查询助手'); + }; + + // 查找现有空对话 + const existingEmptyConv = conversations.find(isEmptyConv); + + if (existingEmptyConv) { + // 复用空对话 + setCurrentConversationId(existingEmptyConv.id); + } else { + // 创建新对话 + const newConv: Conversation = { + id: 'conv-' + Date.now(), // 时间戳生成唯一ID + title: '新对话', + messages: [{ + role: 'ai', + content: '您好!我是数据查询助手,您可以通过自然语言描述您的查询需求(例如:"展示2023年各季度的订单量"),我会为您生成相应的结果。' + }], + createTime: new Date().toISOString(), + }; + // 添加到对话列表头部 + setConversations(prev => [newConv, ...prev]); + setCurrentConversationId(newConv.id); + } + + // 关闭历史对话面板 + setIsHistoryOpen(false); + + // 切换到query页面(区分角色) + if (userRole === 'data-admin') { + setActiveDataAdminPage('query'); + } else { + setActivePage('query'); + } + }; + + /** + * 切换对话 + * 从历史对话列表中选择已有对话,自动切换到query页面 + * @param id - 目标对话ID + */ + const handleSwitchConversation = (id: string) => { + setCurrentConversationId(id); + setIsHistoryOpen(true); + setActivePage('query'); + }; + + /** + * 删除对话 + * 从对话列表中移除目标对话,若删除的是当前对话则自动切换到首个对话(无则创建新对话) + * @param deleteId - 待删除对话ID + */ + const handleDeleteConversation = (deleteId: string) => { + const updatedConversations = conversations.filter(conv => conv.id !== deleteId); + + // 若删除的是当前激活的对话 + if (currentConversationId === deleteId) { + if (updatedConversations.length > 0) { + // 切换到剩余对话的首个 + setCurrentConversationId(updatedConversations[0].id); + } else { + // 无剩余对话时创建新对话 + const newConv: Conversation = { + id: 'conv-' + Date.now(), + title: '新对话', + messages: [{ + role: 'ai', + content: '您好!我是数据查询助手,您可以通过自然语言描述您的查询需求...' + }], + createTime: new Date().toISOString(), + }; + updatedConversations.unshift(newConv); + setCurrentConversationId(newConv.id); + } + } + + setConversations(updatedConversations); + }; + + /** + * 保存查询结果 + * 将查询结果添加到收藏夹,避免重复保存 + * @param query - 待保存的查询结果数据 + */ + const handleSaveQuery = (query: QueryResultData) => { + setSavedQueries(prev => { + if (prev.some(q => q.id === query.id)) return prev; + return [query, ...prev]; + }); + }; + + /** + * 删除收藏的查询结果 + * 从收藏夹中移除目标查询 + * @param id - 待删除查询的ID + */ + const handleDeleteSavedQuery = (id: string) => { + setSavedQueries(prev => prev.filter(q => q.id !== id)); + }; + + /** + * 在对话中查看收藏的查询 + * 匹配对应的对话,存在则切换到该对话,不存在则显示提示弹窗 + * @param conversationId - 收藏查询关联的对话ID + */ + const handleViewInChat = (conversationId: string) => { + const convExists = conversations.some(c => c.id === conversationId); + if (convExists) { + setCurrentConversationId(conversationId); + // 切换到query页面(区分角色) + if (userRole === 'data-admin') { + setActiveDataAdminPage('query'); + } else { + setActivePage('query'); + } + } else { + setNotFoundModalOpen(true); + } + }; + + /** + * 重新运行查询 + * 创建新对话并带入原始查询提示语 + * @param prompt - 原始查询提示语 + */ + const handleRerunQuery = (prompt: string) => { + handleNewConversation(); + setInitialPrompt(prompt); + }; + + + /** + * 对比两个查询结果 + * 按查询时间排序(旧在前,新在后),打开对比弹窗 + * @param queryId1 - 第一个查询ID + * @param queryId2 - 第二个查询ID + */ + const handleCompareQueries = (queryId1: string, queryId2: string) => { + const query1 = savedQueries.find(q => q.id === queryId1); + const query2 = savedQueries.find(q => q.id === queryId2); + if (!query1 || !query2) return; + + // 按查询时间排序,确保旧查询在前 + const date1 = new Date(query1.queryTime); + const date2 = new Date(query2.queryTime); + const oldQuery = date1 < date2 ? query1 : query2; + const newQuery = date1 < date2 ? query2 : query1; + + //设置弹窗数据并打开弹窗 + setCompareQueries({ oldQuery, newQuery }); + setIsCompareModalOpen(true); +}; + + /** + * 分享查询结果给好友 + * 创建分享记录并生成对应通知 + * @param queryId - 待分享的查询ID + * @param friendId - 接收分享的好友ID + */ + const handleShareQuery = (queryId: string, friendId: string) => { + const queryToShare = savedQueries.find(q => q.id === queryId); + const friend = MOCK_FRIENDS_LIST.find(f => f.id === friendId); + + if (queryToShare && friend) { + // 创建分享记录 + const newShare: QueryShare = { + id: `share-${Date.now()}`, + sender: { ...MOCK_USER_PROFILE, isOnline: true }, // 发送者为当前用户 + recipientId: friend.id, // 接收者为选中的好友 + querySnapshot: queryToShare, // 分享的查询快照 + timestamp: new Date().toISOString(), + status: 'unread', // 初始状态为未读 + }; + setQueryShares(prev => [newShare, ...prev]); + + // 生成分享通知(模拟) + const newNotification: Notification = { + id: `notif-${Date.now()}`, + type: 'share', + title: `${MOCK_USER_PROFILE.name} 分享了一个查询给你`, + content: `"${queryToShare.userPrompt}"`, + timestamp: new Date().toISOString(), + isRead: false, + isPinned: false, + fromUser: { name: MOCK_USER_PROFILE.name, avatarUrl: MOCK_USER_PROFILE.avatarUrl }, + relatedShareId: newShare.id, + }; + setNotifications(prev => [newNotification, ...prev]); + } + }; + + /** + * 标记分享记录为已读 + * 更新目标分享记录的状态 + * @param shareId - 待标记的分享ID + */ + const handleMarkShareAsRead = (shareId: string) => { + setQueryShares(prev => prev.map(s => s.id === shareId ? { ...s, status: 'read' } : s)); + }; + + /** + * 删除分享记录 + * 从分享列表中移除目标记录 + * @param shareId - 待删除的分享ID + */ + const handleDeleteShare = (shareId: string) => { + setQueryShares(prev => prev.filter(s => s.id !== shareId)); + }; + + /** + * 点击通知图标处理 + * 切换到通知中心页面(区分角色) + */ + const handleNotificationClick = () => { + if (userRole === 'normal-user') { + setActivePage('notifications'); + } else if (userRole === 'data-admin') { + setActiveDataAdminPage('notifications'); + } + }; + + /** + * 点击头像处理 + * 切换到个人中心页面(区分角色) + */ + const handleAvatarClick = () => { + if (userRole === 'normal-user') { + setActivePage('account'); + } else if (userRole === 'data-admin') { + setActiveDataAdminPage('account'); + } else if (userRole === 'sys-admin') { + setActiveSysAdminPage('account'); + } + }; + + // ===== 页面渲染函数 ===== + /** + * 普通用户页面渲染 + * 根据当前激活页面,渲染对应的功能页面 + * @param page - 普通用户当前激活页面 + * @returns 对应的功能页面组件 + */ + const renderNormalUserPage = (page: Page) => { + switch (page) { + case 'query': + return ( + setIsHistoryOpen(!isHistoryOpen)} + isHistoryOpen={isHistoryOpen} + onAddMessage={handleAddMessage} + onSaveQuery={handleSaveQuery} + onShareQuery={handleShareQuery} + savedQueries={savedQueries} + initialPrompt={initialPrompt} + onClearInitialPrompt={() => setInitialPrompt(undefined)} + conversations={conversations} + currentConversationId={currentConversationId || ''} + onSwitchConversation={handleSwitchConversation} + onNewConversation={handleNewConversation} + onDeleteConversation={handleDeleteConversation} + /> + ); + case 'history': + return ( + + ); + case 'notifications': + return ; + case 'account': + return ; + case 'friends': + return ( + + ); + default: + return
    Page not found
    ; + } + }; + + // ===== 角色路由渲染 ===== + // 未登录状态 - 渲染登录页面 + if (!userRole) { + return ; + } + + // 系统管理员 - 渲染系统管理页面 + if (userRole === 'sys-admin') { + return ( +
    + +
    + !n.isRead).length} + notifications={notifications} + onNotificationClick={handleNotificationClick} + onAvatarClick={handleAvatarClick} + currentConversationName={headerTitle} + /> + +
    +
    + ); + } + + // 数据管理员 - 渲染数据管理页面 + if (userRole === 'data-admin') { + return ( + <> +
    + +
    + !n.isRead).length} + notifications={notifications} + onNotificationClick={handleNotificationClick} + showHistoryToggle={activeDataAdminPage === 'query'} // 仅query页面显示历史面板切换按钮 + onToggleHistory={() => setIsHistoryOpen(!isHistoryOpen)} + onAvatarClick={handleAvatarClick} + onNewConversation={handleNewConversation} + currentConversationName={headerTitle} + /> + setIsHistoryOpen(!isHistoryOpen)} + isHistoryOpen={isHistoryOpen} + onAddMessage={handleAddMessage} + onSaveQuery={handleSaveQuery} + onShareQuery={handleShareQuery} + savedQueries={savedQueries} + initialPrompt={initialPrompt} + onClearInitialPrompt={() => setInitialPrompt(undefined)} + conversations={conversations} + currentConversationId={currentConversationId || ''} + onSwitchConversation={handleSwitchConversation} + onNewConversation={handleNewConversation} + onDelete={handleDeleteSavedQuery} + onViewInChat={handleViewInChat} + onRerun={handleRerunQuery} + onCompare={handleCompareQueries} + shares={queryShares} + onMarkShareAsRead={handleMarkShareAsRead} + onDeleteShare={handleDeleteShare} + /> +
    +
    + + {/* 对话不存在提示弹窗 */} + setNotFoundModalOpen(false)} + title="操作提示" + > +
    +
    + +
    +

    对话记录不存在或已被删除。

    +

    这可能是由于它已被手动删除或系统自动清理。

    +
    + +
    +
    +
    + {/* 2. 对比弹窗(全局浮层,独立于其他弹窗!) */} + {compareQueries && ( + { + setIsCompareModalOpen(false); + setCompareQueries(null); + }} + oldQuery={compareQueries.oldQuery} + newQuery={compareQueries.newQuery} + /> + )} + + ); + } + + // 普通用户 - 渲染基础功能页面 + return ( + <> +
    + +
    + !n.isRead).length} + notifications={notifications} + onNotificationClick={handleNotificationClick} + showHistoryToggle={activePage === 'query'} // 仅query页面显示历史面板切换按钮 + onToggleHistory={() => setIsHistoryOpen(!isHistoryOpen)} + onAvatarClick={handleAvatarClick} + onNewConversation={handleNewConversation} + currentConversationName={headerTitle} + /> + {renderNormalUserPage(activePage)} +
    +
    + + {/* 对话不存在提示弹窗 */} + setNotFoundModalOpen(false)} + title="操作提示" + > +
    +
    + +
    +

    对话记录不存在或已被删除。

    +

    这可能是由于它已被手动删除或系统自动清理。

    +
    + +
    +
    +
    + {/* 2. 对比弹窗(全局浮层,独立于其他弹窗!) */} + {compareQueries && ( + { + setIsCompareModalOpen(false); + setCompareQueries(null); + }} + oldQuery={compareQueries.oldQuery} + newQuery={compareQueries.newQuery} + /> + )} + + ); +}; + +export default App; \ No newline at end of file diff --git a/src/test/frontend/README.md b/src/test/frontend/README.md new file mode 100644 index 00000000..94bfbe13 --- /dev/null +++ b/src/test/frontend/README.md @@ -0,0 +1,20 @@ +
    +GHBanner +
    + +# Run and deploy your AI Studio app + +This contains everything you need to run your app locally. + +View your app in AI Studio: https://ai.studio/apps/drive/1l5dsPVkxm5nl4yN-ckKSgRGs0nlklvz9 + +## Run Locally + +**Prerequisites:** Node.js + + +1. Install dependencies: + `npm install` +2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key +3. Run the app: + `npm run dev` diff --git a/src/test/frontend/components/AccountPage.tsx b/src/test/frontend/components/AccountPage.tsx new file mode 100644 index 00000000..f640d37a --- /dev/null +++ b/src/test/frontend/components/AccountPage.tsx @@ -0,0 +1,222 @@ +import React, { useState, useRef } from 'react'; +import { MOCK_USER_PROFILE } from '../constants'; +import { UserProfile } from '../types'; +import { Modal } from './Modal'; + +const InfoField: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => ( +
    +
    {label}
    +
    {value}
    +
    +); + +const maskPhoneNumber = (phone: string) => { + if (phone.length === 11) { + return `${phone.substring(0, 3)}****${phone.substring(7)}`; + } + return phone; +}; + +export const AccountPage: React.FC = () => { + const [user, setUser] = useState(MOCK_USER_PROFILE); + const [isEditModalOpen, setEditModalOpen] = useState(false); + const [isPasswordModalOpen, setPasswordModalOpen] = useState(false); + const [isPhoneModalOpen, setPhoneModalOpen] = useState(false); + const [editForm, setEditForm] = useState({ name: '', email: '', phoneNumber: '' }); + const [avatarPreview, setAvatarPreview] = useState(user.avatarUrl); + const fileInputRef = useRef(null); + + const handleOpenEditModal = () => { + setEditForm({ + name: user.name, + email: user.email, + phoneNumber: user.phoneNumber, + }); + setEditModalOpen(true); + }; + + const handleSaveProfile = () => { + setUser(prev => ({ + ...prev, + name: editForm.name, + email: editForm.email, + phoneNumber: editForm.phoneNumber, + avatarUrl: avatarPreview + })); + setEditModalOpen(false); + }; + + const handleAvatarChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setAvatarPreview(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + return ( +
    +
    +
    +
    + {/* Personal Info Card */} +
    +
    + {/* Left side: Avatar */} +
    + User Avatar + + +
    + + {/* Right side: Info */} +
    +
    +

    个人基本信息

    +
    +
    + + + + + + + {user.accountStatus === 'normal' ? '正常' : '禁用'} + + } + /> +
    +
    + +
    +
    +
    +
    + + {/* Security Settings Card */} +
    +

    安全设置

    +
    +
    +
    +

    修改密码

    +

    上次修改时间:2025-05-10

    +
    + +
    +
    +
    +

    绑定手机

    +

    已绑定:{maskPhoneNumber(user.phoneNumber)}

    +
    + +
    +
    +
    +
    + + {/* Edit Profile Modal */} + setEditModalOpen(false)} title="编辑个人信息"> +
    +
    + + setEditForm(prev => ({...prev, name: e.target.value}))} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary/30 focus:border-primary" + /> +
    +
    + + setEditForm(prev => ({...prev, email: e.target.value}))} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary/30 focus:border-primary" + /> +
    +
    + + setEditForm(prev => ({...prev, phoneNumber: e.target.value}))} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-primary/30 focus:border-primary" + /> +
    +
    +
    + + +
    +
    + + {/* Password Change Modal */} + setPasswordModalOpen(false)} title="修改密码"> +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    + + {/* Phone Change Modal */} + setPhoneModalOpen(false)} title="更换绑定手机"> +
    +
    + + +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/ChatMessage.tsx b/src/test/frontend/components/ChatMessage.tsx new file mode 100644 index 00000000..e3109870 --- /dev/null +++ b/src/test/frontend/components/ChatMessage.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Message, QueryResultData } from '../types'; +import { QueryResult } from './QueryResult'; + +interface ChatMessageProps { + message: Message; + onSaveQuery: (query: QueryResultData) => void; + onShareQuery: (queryId: string, friendId: string) => void; + savedQueries: QueryResultData[]; +} + +export const ChatMessage: React.FC = ({ message, onSaveQuery, onShareQuery, savedQueries }) => { + const { role, content } = message; + + const isUser = role === 'user'; + const wrapperClass = `flex items-start gap-3 ${isUser ? 'flex-row-reverse' : ''}`; + const avatarClass = `w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 text-lg ${isUser ? 'bg-primary text-white' : 'bg-primary/10 text-primary'}`; + const iconClass = `fa ${isUser ? 'fa-user' : 'fa-robot'}`; + const bubbleClass = `p-4 shadow-sm max-w-[85%] md:max-w-[80%] text-sm rounded-lg ${isUser ? 'bg-primary text-white rounded-tr-none' : 'bg-white border border-gray-200 rounded-tl-none'}`; + + return ( +
    +
    + +
    +
    + {typeof content === 'string' ? ( +

    {content}

    + ) : ( + + )} +
    +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/ChatModal.tsx b/src/test/frontend/components/ChatModal.tsx new file mode 100644 index 00000000..5731f9c0 --- /dev/null +++ b/src/test/frontend/components/ChatModal.tsx @@ -0,0 +1,384 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Modal } from './Modal'; +import { Friend, QueryResultData } from '../types'; + +// 聊天消息接口定义:描述单条聊天消息的结构规范 +interface Message { + id: string; // 消息唯一标识(时间戳+随机字符串生成) + content: string; // 消息文本内容 + isSent: boolean; // 发送者标识(true:当前用户发送;false:好友发送) + timestamp: Date; // 消息发送时间 + isRead: boolean; // 消息已读状态(true:已读;false:未读) +} + +// 聊天模态框属性接口:定义组件接收的外部参数 +interface ChatModalProps { + isOpen: boolean; // 控制聊天弹窗显示/隐藏 + onClose: () => void; // 弹窗关闭回调函数 + friend: Friend; // 聊天对象(好友)的基础信息 + savedQueries: QueryResultData[]; // 可分享的查询记录列表(来自收藏夹) + currentUnreadCount: number; // 当前好友的未读消息数 + updateUnreadCount: (friendId: string, count: number) => void; // 更新未读消息数的回调 + messages: Message[]; // 聊天记录列表(从父组件传入,双向绑定) + updateMessages: (newMessages: Message[]) => void; // 更新聊天记录的回调(通知父组件同步) +} + +export const ChatModal: React.FC = ({ + isOpen, onClose, friend, savedQueries, + currentUnreadCount, updateUnreadCount, + messages, + updateMessages +}) => { + // 消息输入框内容状态:绑定输入框的值 + const [inputText, setInputText] = useState(''); + // 消息展示容器的DOM引用:核心用于滚动控制(定位到消息底部) + const messagesContainerRef = useRef(null); + // 输入框的DOM引用:用于手动控制输入框聚焦 + const inputRef = useRef(null); + // 分享查询弹窗的显示状态:false=关闭,true=打开 + const [isShareModalOpen, setShareModalOpen] = useState(false); + // 选中待分享的查询记录ID:null=未选中,存储选中查询的唯一标识 + const [selectedQueryId, setSelectedQueryId] = useState(null); + + // 模拟好友回复的随机文本池:避免回复内容重复,提升交互自然度 + const randomReplies = [ + '收到,我看看~', + '好的,我研究一下', + '这个问题我得仔细瞧瞧', + '明白,我马上处理', + '哦,这个我知道怎么弄', + '稍等,我查一下资料', + ]; + + // ===== 终极修复:每次打开弹窗都滚到底(不管关闭多少次)===== + useEffect(() => { + // 只在弹窗打开时执行(isOpen为true) + if (isOpen) { + // 延迟200ms:给Modal动画+DOM重新渲染留足时间(关闭再打开时DOM要重新生成) + const scrollTimer = setTimeout(() => { + // 用requestAnimationFrame确保DOM完全更新后再滚动(解决scrollHeight计算不准确的问题) + requestAnimationFrame(() => { + if (messagesContainerRef.current) { + const container = messagesContainerRef.current; + // 强制滚到底部(和HTML的window.scrollTo逻辑完全一致) + container.scrollTop = container.scrollHeight; + // 双重保险:如果第一次没滚到(距离底部超过10px),再补一次滚动 + if (Math.abs(container.scrollHeight - (container.scrollTop + container.clientHeight)) > 10) { + container.scrollTop = container.scrollHeight; + } + } + }); + }, 200); + + // 弹窗关闭时清除定时器:避免定时器残留导致无效滚动或内存泄漏 + return () => clearTimeout(scrollTimer); + } + }, [isOpen]); // 只依赖isOpen状态:确保每次打开弹窗都触发滚动,不受其他状态影响 + + // ===== 其他初始化逻辑(不变)===== + useEffect(() => { + if (isOpen) { + // 1. 打开弹窗时,清空当前好友的未读消息数(通知父组件更新) + updateUnreadCount(friend.id, 0); + // 2. 标记好友发送的未读消息为"已读"(遍历消息列表,修改未读状态后同步给父组件) + const updatedMessages = messages.map(msg => + !msg.isSent && !msg.isRead ? { ...msg, isRead: true } : msg + ); + updateMessages(updatedMessages); + + // 延迟200ms聚焦输入框:和滚动延迟同步,确保DOM渲染完成后再聚焦 + setTimeout(() => inputRef.current?.focus(), 200); + } else { + // 关闭弹窗时,清空输入框内容(避免下次打开残留上次输入) + setInputText(''); + } + }, [isOpen, friend.id, updateUnreadCount, messages, updateMessages]); + + // ===== 发送消息(不变,保留滚动)===== + const handleSendMessage = () => { + // 1. 获取输入框内容(去首尾空格,避免发送空消息) + const currentValue = inputRef.current?.value.trim() || ''; + if (!currentValue) return; // 内容为空则不执行发送逻辑 + + // 2. 生成当前用户发送的新消息(唯一ID由时间戳+随机字符串组成,确保不重复) + const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const newMessage: Message = { + id: uniqueId, + content: currentValue, + isSent: true, + timestamp: new Date(), + isRead: false, + }; + + // 3. 更新消息列表,拼接新消息后通知父组件同步 + const updatedMessages = [...messages, newMessage]; + updateMessages(updatedMessages); + + // 4. 清空输入框(同时更新状态和DOM,适配中文输入法等特殊场景) + setInputText(''); + if (inputRef.current) inputRef.current.value = ''; + + // 发送后自动滚到底部:确保用户能看到自己发送的最新消息 + requestAnimationFrame(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } + }); + + // 模拟好友1秒后回复消息(延迟1000ms,模拟实时聊天交互) + setTimeout(() => { + // 1. 先标记用户发送的消息为"已读"(模拟好友已查看) + const messagesWithRead = updatedMessages.map(msg => + msg.isSent && !msg.isRead ? { ...msg, isRead: true } : msg + ); + // 2. 生成好友回复消息(随机选择回复文本,生成唯一ID) + const replyId = `${Date.now()}-reply-${Math.random().toString(36).slice(2, 8)}`; + const randomReply = randomReplies[Math.floor(Math.random() * randomReplies.length)]; + const replyMessage: Message = { + id: replyId, + content: randomReply, + isSent: false, + timestamp: new Date(), + isRead: false, + }; + // 3. 一次更新:同时包含"已读标记"和"好友回复",同步给父组件 + const finalMessages = [...messagesWithRead, replyMessage]; + updateMessages(finalMessages); + + // 回复后自动滚到底部:确保用户能看到好友的最新回复 + requestAnimationFrame(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } + }); + }, 1000); + }; + + // ===== 事件:打开"分享查询"弹窗 ===== + const handleOpenShareModal = () => { + setSelectedQueryId(null); // 重置选中状态(避免残留上次选中的查询ID) + setShareModalOpen(true); // 显示分享查询弹窗 + }; + + // ===== 事件:确认分享查询给好友 ===== + const handleConfirmShare = () => { + // 1. 校验:未选择查询记录时,不执行分享逻辑 + if (!selectedQueryId) return; + // 根据选中的ID找到对应的查询记录 + const selectedQuery = savedQueries.find(q => q.id === selectedQueryId); + if (!selectedQuery) return; + + // 2. 生成"分享查询"的消息(作为一条聊天消息发送) + const shareId = `${Date.now()}-share-${Math.random().toString(36).slice(2, 8)}`; + const shareMessage: Message = { + id: shareId, + content: `分享了查询:${selectedQuery.userPrompt}`, + isSent: true, + timestamp: new Date(), + isRead: false, + }; + + // 3. 更新消息列表,添加分享消息后通知父组件同步 + const updatedMessages = [...messages, shareMessage]; + updateMessages(updatedMessages); + + // 4. 关闭分享弹窗,清空输入框 + setInputText(''); + setShareModalOpen(false); + + // 分享后自动滚到底部:确保分享消息显示在最新位置 + requestAnimationFrame(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } + }); + + // 模拟好友1.5秒后回复分享内容(延迟1500ms,适配分享场景的交互节奏) + setTimeout(() => { + // 1. 先标记分享消息为"已读"(模拟好友已查看分享) + const messagesWithRead = updatedMessages.map(msg => + msg.isSent && !msg.isRead ? { ...msg, isRead: true } : msg + ); + // 2. 生成好友回复消息(替换关键词,让回复更贴合分享场景) + const replyShareId = `${Date.now()}-share-reply-${Math.random().toString(36).slice(2, 8)}`; + const randomShareReply = randomReplies[Math.floor(Math.random() * randomReplies.length)].replace('看看', '研究'); + const replyMessage: Message = { + id: replyShareId, + content: randomShareReply || '我看到了,这个查询很有用!', + isSent: false, + timestamp: new Date(), + isRead: false, + }; + // 3. 一次更新:同时包含"已读标记"和"好友回复",同步给父组件 + const finalMessages = [...messagesWithRead, replyMessage]; + updateMessages(finalMessages); + + // 回复后自动滚到底部:确保好友的回复显示在最新位置 + requestAnimationFrame(() => { + if (messagesContainerRef.current) { + messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight; + } + }); + }, 1500); + }; + + // ===== 事件:回车键发送消息 ===== + const handleKeyDown = (e: React.KeyboardEvent) => { + // 监听回车键,触发发送消息逻辑 + if (e.key === 'Enter') { + e.preventDefault(); // 阻止输入框默认的换行行为(单行输入框无需换行) + handleSendMessage(); // 调用发送消息函数 + } + }; + + // ===== 渲染(不变)===== + return ( + <> + {/* 1. 聊天主弹窗:展示聊天记录、输入框、分享按钮 */} + +
    + {/* 消息展示区域:可滚动,自动定位到底部 */} +
    + {/* 渲染所有聊天消息:遍历messages数组,每条消息对应一个DOM节点 */} + {messages.map((message) => ( +
    + {/* 头像:自己用默认头像,好友用其专属头像(无则用默认) */} + {message.isSent + + {/* 消息内容+时间戳容器:对齐方式与消息一致 */} +
    + {/* 消息气泡:区分自己和好友的样式(颜色、圆角、边框) */} +
    +

    {message.content}

    +
    + + {/* 时间戳+已读状态:小字体弱化显示,区分自己和好友的文本颜色 */} +
    + {message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} + {message.isRead ? '已读' : '未读'} +
    +
    +
    + ))} +
    + + {/* 消息输入区域:包含分享按钮、输入框、发送按钮 */} +
    +
    + {/* 分享查询按钮:点击打开分享弹窗 */} + + {/* 消息输入框:绑定输入状态,支持回车发送 */} + setInputText(e.target.value.trimStart())} + onKeyDown={handleKeyDown} + className="flex-1 px-4 py-3 border border-gray-300 rounded-full focus:ring-2 focus:ring-primary/30 outline-none" + readOnly={false} + /> + {/* 发送按钮:空消息时禁用,点击发送消息 */} + +
    +
    +
    +
    + + {/* 2. 分享查询弹窗:选择要分享的查询记录 */} + setShareModalOpen(false)} title="分享查询结果"> +
    +

    从您的历史记录中选择一个查询结果分享给 {friend.name}。

    + {/* 可分享查询列表:滚动展示,单选模式 */} +
    + {savedQueries.length > 0 ? ( + savedQueries.map(q => ( + + )) + ) : ( +

    没有收藏的查询记录

    + )} +
    +
    + {/* 分享弹窗底部按钮:取消/确认分享 */} +
    + + +
    +
    + + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/ComparisonModal.tsx b/src/test/frontend/components/ComparisonModal.tsx new file mode 100644 index 00000000..0bc5e5d3 --- /dev/null +++ b/src/test/frontend/components/ComparisonModal.tsx @@ -0,0 +1,232 @@ +import React, { useState, useMemo } from 'react'; +import { Chart as ChartJS, registerables } from 'chart.js'; +import { Chart } from 'react-chartjs-2'; +import { QueryResultData } from '../types'; +import { Modal } from './Modal'; // 引入你的Modal组件(确保路径正确) + +// 注册ChartJS所需的所有组件 +ChartJS.register(...registerables); + +// 表格行差异结果类型定义:包含新增、删除、修改和共同的行数据 +type DiffResult = { + added: string[][]; // 新增的行 + deleted: string[][]; // 删除的行 + modified: { oldRow: string[], newRow: string[] }[]; // 修改的行(包含旧行和新行) + common: string[][]; // 未变化的共同行 +}; + +// 查找两个表格数据集之间行差异的辅助函数 +// 参数:旧行数据、新行数据;返回:差异结果对象 +const findRowChanges = (oldRows: string[][], newRows: string[][]): DiffResult => { + // 以每行第一个元素为键,构建旧行和新行的映射表(便于快速查找) + const oldMap = new Map(oldRows.map(row => [row[0], row])); + const newMap = new Map(newRows.map(row => [row[0], row])); + + // 初始化差异结果对象 + const result: DiffResult = { added: [], deleted: [], modified: [], common: [] }; + + // 遍历旧行映射,检查每行在新行中的状态(删除/修改/共同) + oldMap.forEach((oldRow, key) => { + if (!newMap.has(key)) { + result.deleted.push(oldRow); // 新行中无此键:标记为删除 + } else { + const newRow = newMap.get(key)!; + // 行数据字符串化后不等:标记为修改 + if (JSON.stringify(oldRow) !== JSON.stringify(newRow)) { + result.modified.push({ oldRow, newRow }); + } else { + result.common.push(oldRow); // 行数据相同:标记为共同 + } + } + }); + + // 遍历新行映射,检查新增的行(旧行中无此键) + newMap.forEach((newRow, key) => { + if (!oldMap.has(key)) { + result.added.push(newRow); + } + }); + + return result; +}; + +// 表格对比组件:展示两个查询结果的表格差异(新增/删除/修改行) +// 参数:旧查询结果、新查询结果、差异结果 +const TableComparison: React.FC<{ oldQuery: QueryResultData; newQuery: QueryResultData, changes: DiffResult }> = ({ oldQuery, newQuery, changes }) => { + // 渲染单元格内容:若新旧单元格内容不同,高亮显示新内容并标注旧内容 + const renderCell = (oldCell: string, newCell: string) => { + if (oldCell !== newCell) { + return ( + + {newCell} {oldCell} + + ); + } + return newCell; + }; + + return ( +
    + {/* 旧数据表格 */} +
    +

    旧数据 ({new Date(oldQuery.queryTime).toLocaleString()})

    +
    + + {oldQuery.tableData.headers.map(h => )} + + {/* 显示删除的行(红色背景+删除线) */} + {changes.deleted.map((row, i) => )} + {/* 显示修改的旧行 */} + {changes.modified.map(({ oldRow }, i) => ( + + {oldRow.map((cell, j) => )} + + ))} + {/* 显示未变化的共同行 */} + {changes.common.map((row, i) => {row.map((cell, j) => )})} + +
    {h}
    {row.join(' | ')}
    {cell}
    {cell}
    +
    +
    + + {/* 新数据表格 */} +
    +

    新数据 ({new Date(newQuery.queryTime).toLocaleString()})

    +
    + + {newQuery.tableData.headers.map(h => )} + + {/* 显示新增的行(绿色背景) */} + {changes.added.map((row, i) => {row.map((cell, j) => )})} + {/* 显示修改的新行(高亮变化的单元格) */} + {changes.modified.map(({ oldRow, newRow }, i) => ( + + {newRow.map((cell, j) => )} + + ))} + {/* 显示未变化的共同行 */} + {changes.common.map((row, i) => {row.map((cell, j) => )})} + +
    {h}
    {cell}
    {renderCell(oldRow[j], cell)}
    {cell}
    +
    +
    +
    + ); +}; + +// 图表对比组件:在同一图表中展示新旧查询结果的图表数据 +// 参数:旧查询结果、新查询结果 +const ChartComparison: React.FC<{ oldQuery: QueryResultData; newQuery: QueryResultData }> = ({ oldQuery, newQuery }) => { + // 构建图表数据:合并新旧标签,分别配置新旧数据集样式 + const chartData = { + labels: [...new Set([...oldQuery.chartData.labels, ...newQuery.chartData.labels])], // 合并去重标签 + datasets: [ + { + ...oldQuery.chartData.datasets[0], + label: `${oldQuery.chartData.datasets[0].label} (旧)`, + backgroundColor: 'rgba(22, 93, 255, 0.3)', + borderColor: 'rgba(22, 93, 255, 0.5)', + borderDash: [5, 5], // 旧数据用虚线 + }, + { + ...newQuery.chartData.datasets[0], + label: `${newQuery.chartData.datasets[0].label} (新)`, + backgroundColor: 'rgba(54, 162, 235, 0.6)', + borderColor: 'rgba(54, 162, 235, 1)', // 新数据用实线 + } + ], + }; + + return ( +
    +
    + +
    +
    + ); +}; + +// 弹窗式对比组件属性接口:新增弹窗控制参数,保留原有对比参数 +interface ComparisonModalProps { + oldQuery: QueryResultData; + newQuery: QueryResultData; + isOpen: boolean; // 控制弹窗显示/隐藏 + onClose: () => void; // 弹窗关闭回调 +} + +// 弹窗式对比组件:用Modal包裹原有对比内容,适配弹窗形态 +export const ComparisonModal: React.FC = ({ oldQuery, newQuery, isOpen, onClose }) => { + // 视图切换状态:表格视图/图表视图 + const [activeView, setActiveView] = useState<'table' | 'chart'>('table'); + // 计算表格行差异(使用useMemo避免重复计算) + const changes = useMemo(() => findRowChanges(oldQuery.tableData.rows, newQuery.tableData.rows), [oldQuery, newQuery]); + + // 对比摘要统计项:新增/删除/变更行数量 + const summaryItems = [ + { icon: 'fa-plus-circle', color: 'text-green-500', value: changes.added.length, label: '新增行' }, + { icon: 'fa-minus-circle', color: 'text-red-500', value: changes.deleted.length, label: '删除行' }, + { icon: 'fa-pencil-square-o', color: 'text-yellow-500', value: changes.modified.length, label: '变更行' }, + ]; + + return ( + + {/* 弹窗内容区域:限制高度+滚动 */} +
    + {/* 对比说明 */} +
    +

    对比查询 "{oldQuery.userPrompt}" 的两个不同版本。

    +
    + + {/* 对比摘要卡片 */} +
    +

    对比摘要

    +
    + {summaryItems.map(item => ( +
    + +
    +

    {item.value}

    +

    {item.label}

    +
    +
    + ))} +
    +
    + + {/* 对比视图切换区域 */} +
    +
    +
    + + +
    +
    + {/* 根据当前视图状态渲染对应的对比组件 */} + {activeView === 'table' + ? + : + } +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/DataAdminPage.tsx b/src/test/frontend/components/DataAdminPage.tsx new file mode 100644 index 00000000..95706be8 --- /dev/null +++ b/src/test/frontend/components/DataAdminPage.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import { DataAdminPageType, Page, Conversation, QueryResultData, MessageRole, QueryShare } from '../types'; +import { DataSourceManagementPage } from './data-admin/DataSourceManagementPage'; +import { UserPermissionPage } from './data-admin/UserPermissionPage'; +import { ConnectionLogPage } from './data-admin/ConnectionLogPage'; +import { QueryPage } from './QueryPage'; +import { HistoryPage } from './HistoryPage'; +import { AccountPage } from './AccountPage'; +import { FriendsPage } from './FriendsPage'; +import { NotificationsPage } from './NotificationsPage'; +import { DataAdminNotificationPage } from './data-admin/DataAdminNotificationPage'; +import { ComparisonModal } from './ComparisonModal'; +import { DataAdminDashboardPage } from './data-admin/DataAdminDashboardPage'; + +interface DataAdminPageProps { + activePage: DataAdminPageType; + setActivePage: (page: DataAdminPageType) => void; + // Props for QueryPage and others + currentConversation: Conversation | undefined; + onToggleHistory: () => void; + isHistoryOpen: boolean; + onAddMessage: (role: MessageRole, content: string | QueryResultData) => void; + onSaveQuery: (query: QueryResultData) => void; + onShareQuery: (queryId: string, friendId: string) => void; + savedQueries: QueryResultData[]; + initialPrompt?: string; + onClearInitialPrompt: () => void; + // Props for HistoryPage + conversations: Conversation[]; + currentConversationId: string; + onSwitchConversation: (id: string) => void; + onNewConversation: () => void; + onDelete: (id: string) => void; + onViewInChat: (conversationId: string) => void; + onRerun: (prompt: string) => void; + onCompare: (queryId1: string, queryId2: string) => void; + // Props for FriendsPage + shares: QueryShare[]; + onMarkShareAsRead: (shareId: string) => void; + onDeleteShare: (shareId: string) => void; + onDeleteConversation: (id: string) => void; +} + +export const DataAdminPage: React.FC = (props) => { + const { activePage } = props; + + // Duplicated from App.tsx to render shared pages + const renderNormalUserPage = (page: Page) => { + switch (page) { + case 'query': + return ( + + ); + case 'history': + return ( + + ); + case 'notifications': + return ; + case 'account': + return ; + case 'friends': + return ( + + ); + default: + // This case handles 'comparison', which is rendered by the parent switch + return
    Page not found
    ; + } + }; + + switch (activePage) { + // Admin specific pages + case 'dashboard': + return ; + case 'datasource': + return ; + case 'user-permission': + return ; + case 'notification-management': + return ; + case 'connection-log': + return ; + case 'query': + case 'history': + case 'notifications': + case 'account': + case 'friends': + return renderNormalUserPage(activePage); + default: + const exhaustiveCheck: never = activePage; + return
    Page not found
    ; + } +}; \ No newline at end of file diff --git a/src/test/frontend/components/DataAdminSidebar.tsx b/src/test/frontend/components/DataAdminSidebar.tsx new file mode 100644 index 00000000..ea2a521d --- /dev/null +++ b/src/test/frontend/components/DataAdminSidebar.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { DataAdminPageType } from '../types'; + +interface SidebarItemProps { + href: DataAdminPageType; + icon: string; + label: string; + isActive: boolean; + onClick: (page: DataAdminPageType) => void; +} + +const SidebarItem: React.FC = ({ href, icon, label, isActive, onClick }) => { + const activeClass = 'bg-primary/10 text-primary border-l-4 border-primary'; + const inactiveClass = 'text-gray-600 hover:bg-gray-50'; + + return ( +
  • + { + e.preventDefault(); + onClick(href); + }} + > + + {label} + +
  • + ); +}; + +interface SidebarProps { + activePage: DataAdminPageType; + setActivePage: (page: DataAdminPageType) => void; + onLogout: () => void; +} + +export const DataAdminSidebar: React.FC = ({ activePage, setActivePage, onLogout }) => { + const queryItems = [ + { href: 'query', icon: 'fa-search', label: '数据查询' }, + { href: 'history', icon: 'fa-star', label: '收藏夹' }, + ] as const; + + const managementItems = [ + { href: 'dashboard', icon: 'fa-tachometer', label: '仪表盘' }, + { href: 'datasource', icon: 'fa-plug', label: '数据源管理' }, + { href: 'user-permission', icon: 'fa-key', label: '用户权限管理' }, + { href: 'notification-management', icon: 'fa-bullhorn', label: '通知管理' }, + { href: 'connection-log', icon: 'fa-link', label: '连接日志' }, + ] as const; + + const personalItems = [ + { href: 'notifications', icon: 'fa-bell', label: '通知中心' }, + { href: 'account', icon: 'fa-user', label: '账户管理' }, + { href: 'friends', icon: 'fa-users', label: '好友管理' }, + ] as const; + + return ( + + ); +}; diff --git a/src/test/frontend/components/Dropdown.tsx b/src/test/frontend/components/Dropdown.tsx new file mode 100644 index 00000000..4611cee1 --- /dev/null +++ b/src/test/frontend/components/Dropdown.tsx @@ -0,0 +1,63 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { ModelOption } from '../types'; + +interface DropdownProps { + options: ModelOption[]; + selected: string; + setSelected: (option: string) => void; + icon: string; +} + +export const Dropdown: React.FC = ({ options, selected, setSelected, icon }) => { + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleSelect = (option: ModelOption) => { + if (option.disabled) return; + setSelected(option.name); + setIsOpen(false); + }; + + return ( +
    + + {isOpen && ( +
    + {options.map(option => ( + + ))} +
    + )} +
    + ); +}; diff --git a/src/test/frontend/components/FriendsPage.tsx b/src/test/frontend/components/FriendsPage.tsx new file mode 100644 index 00000000..c77a78a0 --- /dev/null +++ b/src/test/frontend/components/FriendsPage.tsx @@ -0,0 +1,728 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { MOCK_FRIENDS_LIST, MOCK_FRIEND_REQUESTS } from '../constants'; +import { Friend, FriendRequest, QueryResultData, QueryShare } from '../types'; +import { Modal } from './Modal'; +import { ChatModal } from './ChatModal'; +import { QueryResult } from './QueryResult'; + +// 补充Message接口定义(若types文件已包含可删除) +interface Message { + id: string; + content: string; + isSent: boolean; + timestamp: Date; + isRead: boolean; +} + +// 标签类型定义 +type ActiveTab = 'friends' | 'requests' | 'shares'; + +// 1. 好友卡片组件(新增备注显示+备注/主页按钮) +const FriendCard: React.FC<{ + friend: Friend; + onChat: (friend: Friend) => void; + onDelete: (friend: Friend) => void; + onRemark: (friend: Friend) => void; // 新增:触发备注弹窗 + onViewProfile: (friend: Friend) => void; // 新增:触发查看主页 + unreadCount: number; +}> = ({ friend, onChat, onDelete, onRemark, onViewProfile, unreadCount }) => ( +
    +
    +
    + {friend.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} + /> + {/* 在线状态标记 */} + {friend.isOnline && ( + + )} +
    +
    +
    + {/* 有备注显示备注,无备注显示原名(作为主要标识) */} +

    + {friend.remark || friend.name} +

    + {/* 未读消息提示 */} + {unreadCount > 0 && ( + + {unreadCount} + + )} +
    + {/* 有备注时才显示原名(作为补充),无备注则不显示 */} + {friend.remark && ( +

    「{friend.name}」

    + )} +
    +
    +
    + {/* 聊天按钮(原有) */} + + {/* 新增:备注按钮 */} + + {/* 新增:访问主页按钮 */} + + {/* 删除好友按钮(原有) */} + +
    +
    +); + +// 2. 好友请求卡片组件(原有功能不变) +const RequestCard: React.FC<{ + request: FriendRequest; + onAccept: (request: FriendRequest) => void; + onReject: (requestId: string) => void; +}> = ({ request, onAccept, onReject }) => ( +
    +
    + {request.fromUser.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} + /> +
    +

    {request.fromUser.name}

    +

    {request.timestamp}

    +
    +
    +
    + + +
    +
    +); + +// 3. 分享记录卡片组件(原有功能不变) +const ShareRecordCard: React.FC<{ + share: QueryShare; + onView: () => void; + onRerun: () => void; + onDelete: () => void; + onSave: () => void; +}> = ({ share, onView, onRerun, onDelete, onSave }) => ( +
    +
    + {share.sender.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} + /> +
    +

    {share.sender.name}分享了一个查询:

    +

    "{share.querySnapshot.userPrompt}"

    +

    {new Date(share.timestamp).toLocaleString()}

    +
    +
    +
    +
    + + + + +
    +
    +
    +); + +// 4. 标签按钮组件(原有功能不变) +const TabButton: React.FC<{ + tab: ActiveTab; + label: string; + count?: number; + activeTab: ActiveTab; + onTabChange: (tab: ActiveTab) => void; +}> = ({ tab, label, count, activeTab, onTabChange }) => ( + +); + +// 页面Props定义(原有功能不变) +interface FriendsPageProps { + savedQueries: QueryResultData[]; + shares: QueryShare[]; + onMarkShareAsRead: (shareId: string) => void; + onDeleteShare: (shareId: string) => void; + onRerunQuery: (prompt: string) => void; + onSaveQuery: (query: QueryResultData) => void; +} + +export const FriendsPage: React.FC = ({ + savedQueries, shares, onMarkShareAsRead, + onDeleteShare, onRerunQuery, onSaveQuery +}) => { + // 页面状态 + const [friends, setFriends] = useState( + // 初始化好友数据:给原有Mock数据添加默认空备注 + MOCK_FRIENDS_LIST.map(friend => ({ ...friend, remark: '' })) + ); + const [requests, setRequests] = useState(MOCK_FRIEND_REQUESTS); + const [isAddFriendModalOpen, setAddFriendModalOpen] = useState(false); + const [chattingWith, setChattingWith] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [friendToDelete, setFriendToDelete] = useState(null); + const [activeTab, setActiveTab] = useState('friends'); + const [viewingShare, setViewingShare] = useState(null); + const [activeModal, setActiveModal] = useState(null); + + // 新增:备注相关状态 + const [isRemarkModalOpen, setIsRemarkModalOpen] = useState(false); + const [currentRemarkFriend, setCurrentRemarkFriend] = useState(null); + const [remarkInput, setRemarkInput] = useState(''); + + // 新增:好友主页相关状态 + const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); + const [currentProfileFriend, setCurrentProfileFriend] = useState(null); + + // 未读消息状态 + const [unreadMessages, setUnreadMessages] = useState>({}); + + // 聊天记录状态 + const [friendMessages, setFriendMessages] = useState>(() => ({ + 'friend-1': [ + { + id: `${Date.now()}-init-1`, + content: '你好!最近在忙什么?', + isSent: false, + timestamp: new Date(Date.now() - 3600000), + isRead: true, + }, + { + id: `${Date.now()}-init-2`, + content: '我在处理一个数据查询,挺复杂的~', + isSent: true, + timestamp: new Date(Date.now() - 3500000), + isRead: false, + } + ], + 'friend-2': [ + { + id: `${Date.now()}-init-3`, + content: '在吗?上次分享的查询结果很有用!', + isSent: false, + timestamp: new Date(Date.now() - 86400000), + isRead: true, + } + ] + })); + + // 初始化未读消息 + useEffect(() => { + setUnreadMessages({ + 'friend-1': 2, + 'friend-2': 1 + }); + }, []); + + // 新增:打开备注弹窗时初始化输入值 + useEffect(() => { + if (currentRemarkFriend) { + setRemarkInput(currentRemarkFriend.remark || ''); + } + }, [currentRemarkFriend]); + + // 更新未读消息数量 + const updateUnreadCount = (friendId: string, count: number) => { + setUnreadMessages(prev => ({ + ...prev, + [friendId]: count + })); + }; + + // 更新聊天记录 + const updateFriendMessages = (friendId: string, newMessages: Message[]) => { + setFriendMessages(prev => ({ + ...prev, + [friendId]: newMessages + })); + }; + + // 打开聊天 + const handleOpenChat = (friend: Friend) => { + setChattingWith(friend); + if (!friendMessages[friend.id]) { + setFriendMessages(prev => ({ + ...prev, + [friend.id]: [ + { + id: `${Date.now()}-init-empty`, + content: `你好!开始和${friend.name}聊天吧~`, + isSent: false, + timestamp: new Date(), + isRead: true, + } + ] + })); + } + }; + + // 模拟新消息(测试用) + const mockNewMessage = (friendId: string) => { + setUnreadMessages(prev => ({ + ...prev, + [friendId]: (prev[friendId] || 0) + 1 + })); + + const friend = friends.find(f => f.id === friendId); + if (friend) { + const randomTexts = [ + "你好!在忙吗?", + "这个查询结果我觉得很有用", + "我们下次什么时候再讨论一下?", + "你之前分享的内容帮了我大忙", + "有空可以看看我刚发的查询" + ]; + const randomText = randomTexts[Math.floor(Math.random() * randomTexts.length)]; + + const newMessage: Message = { + id: `mock-${Date.now()}`, + content: randomText, + isSent: false, + timestamp: new Date(), + isRead: false + }; + + setFriendMessages(prev => ({ + ...prev, + [friendId]: prev[friendId] + ? [...prev[friendId], newMessage] + : [newMessage] + })); + } + }; + + // 筛选好友 + const filteredFriends = useMemo(() => + friends.filter(friend => + friend.name.toLowerCase().includes(searchTerm.toLowerCase()) || + (friend.remark && friend.remark.toLowerCase().includes(searchTerm.toLowerCase())) // 支持按备注搜索 + ), + [friends, searchTerm] + ); + + // 删除好友逻辑 + const leteRequest = (friend: Friend) => setFriendToDelete(friend); + const handleConfirmDelete = () => { + if (friendToDelete) { + setFriends(prev => prev.filter(f => f.id !== friendToDelete.id)); + setUnreadMessages(prev => { + const newUnread = { ...prev }; + delete newUnread[friendToDelete.id]; + return newUnread; + }); + setFriendMessages(prev => { + const newMessages = { ...prev }; + delete newMessages[friendToDelete.id]; + return newMessages; + }); + setFriendToDelete(null); + } + }; + + // 好友请求处理 + const handleAcceptRequest = (request: FriendRequest) => { + const newFriend: Friend = { + id: `friend-${Date.now()}`, + name: request.fromUser.name, + avatarUrl: request.fromUser.avatarUrl, + isOnline: false, + remark: '', + email:'' + }; + setFriends(prev => [newFriend, ...prev]); + setRequests(prev => prev.filter(req => req.id !== request.id)); + }; + const handleRejectRequest = (requestId: string) => { + setRequests(prev => prev.filter(req => req.id !== requestId)); + }; + + // 分享记录处理 + const handleViewShare = (share: QueryShare) => { + setViewingShare(share); + onMarkShareAsRead(share.id); + }; + const handleRerunShare = (share: QueryShare) => { + onRerunQuery(share.querySnapshot.userPrompt); + }; + const handleSaveShare = (share: QueryShare) => { + onSaveQuery(share.querySnapshot); + setActiveModal('saveSuccess'); + }; + const [shareToDelete, setShareToDelete] = useState(null); + // 新增:备注功能相关函数 + const handleOpenRemarkModal = (friend: Friend) => { + setCurrentRemarkFriend(friend); + setIsRemarkModalOpen(true); + }; + const handleSaveRemark = () => { + if (currentRemarkFriend) { + setFriends(prev => prev.map(friend => + friend.id === currentRemarkFriend.id + ? { ...friend, remark: remarkInput.trim() } + : friend + )); + setIsRemarkModalOpen(false); + setCurrentRemarkFriend(null); + } + }; + + // 新增:好友主页功能相关函数 + const handleOpenProfileModal = (friend: Friend) => { + console.log('打开主页的好友数据:', friend); + setCurrentProfileFriend(friend); + setIsProfileModalOpen(true); + }; + // 新增:处理分享删除请求(触发确认弹窗) +const handleDeleteShareRequest = (share: QueryShare) => { + setShareToDelete(share); +}; + +// 新增:确认删除分享 +const handleConfirmDeleteShare = () => { + if (shareToDelete) { + onDeleteShare(shareToDelete.id); + setShareToDelete(null); + } +}; + return ( +
    + {/* 页面标题栏 */} +
    +

    + +

    +
    + + +
    +
    + + {/* 标签栏 */} +
    +
    + + + s.status === 'unread').length} + activeTab={activeTab} + onTabChange={setActiveTab} + /> +
    +
    + + {/* 内容区域 */} +
    + {/* 1. 好友列表标签 */} + {activeTab === 'friends' && ( +
    + {/* 搜索框(支持搜索名称/备注) */} +
    +
    + + setSearchTerm(e.target.value)} + className="w-full pl-9 pr-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/30" + /> +
    +
    + + {/* 好友列表 */} + {filteredFriends.length > 0 ? ( +
    + {filteredFriends.map(friend => ( + + ))} +
    + ) : ( +
    + +

    未找到匹配的好友

    +
    + )} +
    + )} + + {/* 2. 好友请求标签(原有不变) */} + {activeTab === 'requests' && ( +
    + {requests.length > 0 ? ( + requests.map(req => ( + + )) + ) : ( +
    + +

    没有新的好友请求

    +
    + )} +
    + )} + + {/* 3. 分享记录标签(原有不变) */} + {activeTab === 'shares' && ( +
    + {shares.length > 0 ? ( + shares.map(share => ( + handleViewShare(share)} + onRerun={() => handleRerunShare(share)} + onDelete={() => handleDeleteShareRequest(share)} + onSave={() => handleSaveShare(share)} + /> + )) + ) : ( +
    + +

    没有收到任何分享

    +
    + )} +
    + )} +
    + + {/* 原有模态框:添加好友 */} + setAddFriendModalOpen(false)} title="添加好友"> +
    +
    + + +
    +
    +
    + + +
    +
    + + {/* 原有模态框:删除好友确认 */} + setFriendToDelete(null)} title="删除好友"> +

    您确定要删除好友 "{friendToDelete?.name}" 吗?此操作无法撤销。

    +
    + + +
    +
    + + {/* 原有模态框:聊天窗口 */} + {chattingWith && ( + setChattingWith(null)} + savedQueries={savedQueries} + currentUnreadCount={unreadMessages[chattingWith.id] || 0} + updateUnreadCount={updateUnreadCount} + messages={friendMessages[chattingWith.id] || []} + updateMessages={(newMessages) => updateFriendMessages(chattingWith.id, newMessages)} + /> + )} + + {/* 原有模态框:查看分享结果 */} + setViewingShare(null)} title={`查看分享: "${viewingShare?.querySnapshot.userPrompt}"`}> + {viewingShare && ( +
    + +
    + )} +
    + + {/* 原有模态框:保存成功提示 */} + setActiveModal(null)} hideTitle> +
    +
    + +
    +

    保存成功

    +

    该查询结果已保存至您的历史记录中。

    + +
    +
    + + {/* 新增:备注好友模态框 */} + setIsRemarkModalOpen(false)} title="备注好友"> +
    +
    + + setRemarkInput(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg" + maxLength={20} // 限制备注长度 + /> +

    备注将显示在好友名称下方,最多20个字符

    +
    +
    +
    + + +
    +
    + + {/* 新增:好友主页模态框 */} + setIsProfileModalOpen(false)} title="好友主页" hideTitle={false}> + {currentProfileFriend && ( +
    + {/* 好友头像+基本信息 */} +
    +
    + {currentProfileFriend.name} (e.target as HTMLImageElement).src = '/default-avatar.png'} + /> +
    +

    {currentProfileFriend.name}

    + {currentProfileFriend.remark && ( +

    备注:{currentProfileFriend.remark}

    + )} +

    + + {currentProfileFriend?.email || '未设置邮箱'} +

    + + {currentProfileFriend.isOnline ? '在线' : '离线'} + + +
    + + {/* 操作按钮 */} +
    + + +
    +
    + )} +
    + {/* 新增:删除分享确认模态框 */} + setShareToDelete(null)} title="删除分享"> +

    + 您确定要删除 "{shareToDelete?.sender.name}" 分享的查询吗?此操作无法撤销。 +

    +
    + + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/HistoryPage.tsx b/src/test/frontend/components/HistoryPage.tsx new file mode 100644 index 00000000..5ba30779 --- /dev/null +++ b/src/test/frontend/components/HistoryPage.tsx @@ -0,0 +1,455 @@ +import React, { useState, useMemo, useCallback } from 'react'; +import { Conversation, QueryResultData } from '../types'; +import { Modal } from './Modal'; +import { QueryResult } from './QueryResult'; +import { MODEL_OPTIONS, DATABASE_OPTIONS } from '../constants'; // 引入真实选项数据 + +// 日期筛选类型定义:全部、今天、近7天、近30天 +type FilterType = 'all' | 'today' | '7days' | '30days'; +// 确认弹窗操作类型定义:单条删除、重新执行、批量删除 +type ConfirmAction = 'delete' | 'rerun' | 'bulkDelete'; + +// 历史页面属性接口定义 +interface HistoryPageProps { + savedQueries: QueryResultData[]; + conversations: Conversation[]; + onDelete: (id: string) => void; + onViewInChat: (conversationId: string) => void; + onRerun: (prompt: string) => void; + onCompare: (queryId1: string, queryId2: string) => void; +} + +// 校验日期是否在指定天数范围内(含起始日期) +const isDateInRage = (date: Date, days: number): boolean => { + const today = new Date(); + const pastDate = new Date(); + if (days > 0) { + pastDate.setDate(today.getDate() - (days - 1)); + } + today.setHours(23, 59, 59, 999); + pastDate.setHours(0, 0, 0, 0); + return date >= pastDate && date <= today; +}; + +// 历史查询页面组件 +export const HistoryPage: React.FC = ({ savedQueries, conversations, onDelete, onViewInChat, onRerun, onCompare }) => { + // 搜索关键词状态 + const [searchTerm, setSearchTerm] = useState(''); + // 当前激活的筛选条件状态 + const [activeFilters, setActiveFilters] = useState({ + date: 'all' as FilterType, + model: 'all', + database: 'all' + }); + // 展开的查询分组标识状态(按用户查询提示语分组) + const [expandedGroup, setExpandedGroup] = useState(null); + // 选中的查询记录ID集合(用于批量操作或对比) + const [selectedIds, setSelectedIds] = useState>(new Set()); + // 正在查看详情的查询记录状态 + const [viewingQuery, setViewingQuery] = useState(null); + + // 确认弹窗状态管理:控制弹窗显示、操作类型及目标数据 + const [confirmModalState, setConfirmModalState] = useState<{ + isOpen: boolean; + action: ConfirmAction | null; + targetId: string | null; + targetPrompt: string | null; + targetIds: string[] | null; + }>({ + isOpen: false, + action: null, + targetId: null, + targetPrompt: null, + targetIds: null, + }); + + // 根据对话ID获取对应的对话标题 + const getConversationTitle = (id: string) => { + return conversations.find(c => c.id === id)?.title || '未知对话'; + }; + + // 切换查询分组的展开/收起状态,切换时清空已选中的查询记录 + const handleToggleGroup = (prompt: string) => { + setExpandedGroup(prev => (prev === prompt ? null : prompt)); + setSelectedIds(new Set()); + }; + + // 切换单个查询记录的选中状态(最多支持100条选中) + const handleSelectSnapshot = (id: string) => { + setSelectedIds(prev => { + const newSet = new Set(prev); + if (newSet.has(id)) { + newSet.delete(id); + } else if (newSet.size < 100) { + newSet.add(id); + } + return newSet; + }); + }; + + // 执行两条选中查询记录的对比操作 + const handleCompare = () => { + if (selectedIds.size !== 2) return; + const [id1, id2] = Array.from(selectedIds); + onCompare(id1, id2); + setSelectedIds(new Set()); + }; + + // 打开单条查询记录的删除确认弹窗 + const openDeleteConfirm = useCallback((id: string) => { + setConfirmModalState({ + isOpen: true, + action: 'delete', + targetId: id, + targetPrompt: null, + targetIds: null, + }); + }, []); + + // 打开查询的重新执行确认弹窗 + const openRerunConfirm = useCallback((prompt: string) => { + setConfirmModalState({ + isOpen: true, + action: 'rerun', + targetId: null, + targetPrompt: prompt, + targetIds: null, + }); + }, []); + + // 打开批量删除确认弹窗 + const openBulkDeleteConfirm = useCallback(() => { + setConfirmModalState({ + isOpen: true, + action: 'bulkDelete', + targetId: null, + targetPrompt: null, + targetIds: Array.from(selectedIds), + }); + }, [selectedIds]); + + // 确认弹窗的操作执行逻辑(根据不同操作类型分发处理) + const handleConfirmAction = () => { + if (confirmModalState.action === 'delete' && confirmModalState.targetId) { + onDelete(confirmModalState.targetId); + } else if (confirmModalState.action === 'rerun' && confirmModalState.targetPrompt) { + onRerun(confirmModalState.targetPrompt); + } else if (confirmModalState.action === 'bulkDelete' && confirmModalState.targetIds) { + confirmModalState.targetIds.forEach(id => onDelete(id)); + setSelectedIds(new Set()); + } + setConfirmModalState({ isOpen: false, action: null, targetId: null, targetPrompt: null, targetIds: null }); + }; + + // 取消确认弹窗操作 + const handleCancelConfirm = () => { + setConfirmModalState({ isOpen: false, action: null, targetId: null, targetPrompt: null, targetIds: null }); + }; + + // 切换筛选条件 + const handleFilterChange = (type: 'date' | 'model' | 'database', value: string) => { + setActiveFilters(prev => ({ + ...prev, + [type]: value + })); + }; + + // 重置所有筛选条件 + const handleResetFilters = () => { + setActiveFilters({ + date: 'all', + model: 'all', + database: 'all' + }); + setSearchTerm(''); + }; + + // 按用户查询提示语分组查询记录,每组内按执行时间倒序排序 + const queryGroups = useMemo(() => { + const groups: Record = {}; + + savedQueries.forEach(query => { + if (!groups[query.userPrompt]) { + groups[query.userPrompt] = []; + } + groups[query.userPrompt].push(query); + }); + + Object.values(groups).forEach(snapshots => { + snapshots.sort((a, b) => new Date(b.queryTime).getTime() - new Date(a.queryTime).getTime()); + }); + return groups; + }, [savedQueries]); + + // 根据搜索关键词和所有筛选条件,过滤查询分组 + const filteredGroups = useMemo(() => { + return Object.entries(queryGroups) + .filter(([prompt, snapshots]) => { + const matchesSearch = prompt.toLowerCase().includes(searchTerm.toLowerCase()); + if (!matchesSearch) return false; + + // 过滤出符合所有筛选条件的记录 + const filteredSnapshots = snapshots.filter(query => { + // 日期筛选 + if (activeFilters.date !== 'all') { + const queryDate = new Date(query.queryTime); + const dateMatch = + activeFilters.date === 'today' ? isDateInRage(queryDate, 1) : + activeFilters.date === '7days' ? isDateInRage(queryDate, 7) : + activeFilters.date === '30days' ? isDateInRage(queryDate, 30) : + false; + if (!dateMatch) return false; + } + + // 大模型筛选(使用真实model字段) + if (activeFilters.model !== 'all' && query.model !== activeFilters.model) { + return false; + } + + // 数据库筛选(使用真实database字段) + if (activeFilters.database !== 'all' && query.database !== activeFilters.database) { + return false; + } + + return true; + }); + + return filteredSnapshots.length > 0; + }); + }, [queryGroups, searchTerm, activeFilters]); + + // 渲染下拉筛选组件(复用逻辑) + const renderFilterDropdown = ( + label: string, + type: 'date' | 'model' | 'database', + options: { value: string; label: string }[] + ) => ( +
    + +
    + ); + + + // 大模型筛选选项(从MODEL_OPTIONS提取,去重并添加"全部") + const modelOptions = useMemo(() => { + const uniqueModels = Array.from(new Set(MODEL_OPTIONS.map(option => option.name))); + return [ + { value: 'all', label: '全部大模型' }, + ...uniqueModels.map(model => ({ value: model, label: model })) + ]; + }, []); + + // 数据库筛选选项(从DATABASE_OPTIONS提取,去重并添加"全部") + const databaseOptions = useMemo(() => { + const uniqueDatabases = Array.from(new Set(DATABASE_OPTIONS.map(option => option.name))); + return [ + { value: 'all', label: '全部数据库' }, + ...uniqueDatabases.map(db => ({ value: db, label: db })) + ]; + }, []); + + + // 日期筛选选项 + const dateOptions = [ + { value: 'all', label: '全部日期' }, + { value: 'today', label: '今天' }, + { value: '7days', label: '近7天' }, + { value: '30days', label: '近30天' } + ]; + + // 根据确认弹窗的操作类型,生成对应的弹窗内容(标题、提示信息、按钮文本及样式) + const confirmModalContent = useMemo(() => { + if (confirmModalState.action === 'delete') { + return { + title: '确认删除查询记录?', + message: '此操作将永久删除该条查询快照。请确认是否继续?', + buttonText: '确认删除', + buttonClass: 'bg-red-600 hover:bg-red-700', + }; + } + if (confirmModalState.action === 'rerun') { + return { + title: '确认重新执行查询?', + message: `您确定要重新执行查询:"${confirmModalState.targetPrompt}" 吗? 这将消耗新的计算资源。`, + buttonText: '重新执行', + buttonClass: 'bg-primary hover:bg-primary/90', + }; + } + if (confirmModalState.action === 'bulkDelete' && confirmModalState.targetIds) { + return { + title: '确认批量删除?', + message: `您确定要永久删除选中的 ${confirmModalState.targetIds.length} 条查询记录吗?此操作不可恢复。`, + buttonText: '批量删除', + buttonClass: 'bg-red-600 hover:bg-red-700', + }; + } + return { title: '', message: '', buttonText: '', buttonClass: '' }; + }, [confirmModalState.action, confirmModalState.targetPrompt, confirmModalState.targetIds]); + + return ( +
    + +
    +
    + {/* 搜索框 - 左侧缩小 */} +
    + + setSearchTerm(e.target.value)} + className="w-full pl-9 pr-3 py-1.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/30" + /> +
    + + {/* 筛选区域 - 右侧横向分布 */} +
    + {renderFilterDropdown('大模型', 'model', modelOptions)} + {renderFilterDropdown('数据库', 'database', databaseOptions)} + {renderFilterDropdown('日期', 'date', dateOptions)} + + {/* 重置按钮 */} + + + {/* 批量删除按钮*/} + +
    +
    +
    + +
    + {filteredGroups.length > 0 ? ( + filteredGroups.map(([prompt, snapshots]) => ( +
    +
    handleToggleGroup(prompt)}> +
    +

    {prompt}

    +

    {snapshots.length} 次执行

    +
    +
    + {snapshots.length > 1 && expandedGroup === prompt && ( + + )} + +
    +
    + {expandedGroup === prompt && ( +
    + {snapshots.map(query => ( +
    +
    +
    + handleSelectSnapshot(query.id)} + className="mr-4 h-4 w-4 text-primary focus:ring-primary/50 border-gray-300 rounded" + /> +

    执行于: {new Date(query.queryTime).toLocaleString()}

    +
    + {/* 展示真实的查询详情信息 */} +
    + 耗时: {query.executionTime} + 大模型: {query.model} + 数据库: {query.database} + 所属对话: "{query.conversationId}" +
    +
    +
    + + + + +
    +
    + ))} +
    + )} +
    + )) + ) : ( +
    + +

    未找到匹配的查询记录

    +
    + )} +
    + + {/* 查询详情弹窗 */} + setViewingQuery(null)} + > + {viewingQuery && ( +
    + +
    + )} +
    + + {/* 通用确认弹窗(支持单删/批量删/重新执行) */} + +

    {confirmModalContent.message}

    + +
    + + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/HistorySidebar.tsx b/src/test/frontend/components/HistorySidebar.tsx new file mode 100644 index 00000000..93d8f634 --- /dev/null +++ b/src/test/frontend/components/HistorySidebar.tsx @@ -0,0 +1,130 @@ +import React, { useState } from 'react'; +import { Conversation } from '../types'; + +interface HistorySidebarProps { + isOpen: boolean; + onClose: () => void; + conversations: Conversation[]; + currentConversationId: string; + onSwitchConversation: (id: string) => void; + onNewConversation: () => void; + onDeleteConversation: (id: string) => void; +} + +export const HistorySidebar: React.FC = ({ + isOpen, + onClose, + conversations, + currentConversationId, + onSwitchConversation, + onNewConversation, + onDeleteConversation +}) => { + // 新增:删除确认弹窗状态 + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleteTargetId, setDeleteTargetId] = useState(null); + + const sidebarClasses = ` + bg-white border-l border-gray-200 h-full flex flex-col + transition-transform duration-300 ease-in-out + fixed top-0 right-0 z-40 transform + ${isOpen ? 'translate-x-0' : 'translate-x-full'} + w-80 shadow-lg + `; + + const innerContentClasses = `w-80 h-full flex flex-col overflow-hidden`; + + return ( + <> + {isOpen &&
    } + + + + {/* 新增:删除确认弹窗 */} + {showDeleteConfirm && ( +
    +
    +

    确认删除

    +

    + 确定要删除这条对话吗?删除后无法恢复。 +

    +
    + + +
    +
    +
    + )} + + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/LoginPage.tsx b/src/test/frontend/components/LoginPage.tsx new file mode 100644 index 00000000..ae1681a9 --- /dev/null +++ b/src/test/frontend/components/LoginPage.tsx @@ -0,0 +1,193 @@ +import React, { useState } from 'react'; +import { UserRole } from '../types'; +import { authApi, LoginRequest } from '../services/api'; + +interface LoginPageProps { + onLogin: (role: UserRole) => void; +} + +const roles: { id: UserRole; name: string; icon: string }[] = [ + { id: 'sys-admin', name: '系统管理员', icon: 'fa-shield' }, + { id: 'data-admin', name: '数据管理员', icon: 'fa-database' }, + { id: 'normal-user', name: '普通用户', icon: 'fa-user' }, +]; + + +export const LoginPage: React.FC = ({ onLogin }) => { + const [isForgotModalOpen, setForgotModalOpen] = useState(false); + const [resetEmailSent, setResetEmailSent] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + + const handleLogin = async (e: React.MouseEvent) => { + e.preventDefault(); + setError(null); + + if (!username.trim() || !password.trim()) { + setError('请输入用户名和密码'); + return; + } + + setIsLoading(true); + try { + const loginRequest: LoginRequest = { + username: username.trim(), + password: password, + }; + + const response = await authApi.login(loginRequest); + + // 完全根据后端返回的roleId决定角色,不再允许前端选择 + // 1=系统管理员, 2=数据管理员, 3=普通用户 + let role: UserRole = 'normal-user'; + if (response.roleId === 1) { + role = 'sys-admin'; + } else if (response.roleId === 2) { + role = 'data-admin'; + } else if (response.roleId === 3) { + role = 'normal-user'; + } else { + throw new Error('未知的用户角色'); + } + + onLogin(role); + } catch (err) { + setError(err instanceof Error ? err.message : '登录失败,请检查用户名和密码'); + } finally { + setIsLoading(false); + } + }; + + const handleRequestReset = (e: React.FormEvent) => { + e.preventDefault(); + setResetEmailSent(true); + setTimeout(() => { + handleCloseForgotModal(); + }, 3000); + }; + + const handleCloseForgotModal = () => { + setForgotModalOpen(false); + setTimeout(() => setResetEmailSent(false), 300); + }; + + const illustrationSvg = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbD0iI0VBRUlGRiIgZD0iTTQzMyAxNDFhMTQgMTQgMCAwIDAtMTQgMTQuMDQ0djE5OC45MTJBMTQgMTQgMCAwIDAgNDMzIDM2OGgxNFYxNDFoLTE0eiIvPjxwYXRoIGZpbGw9IiNDRUQ4RkYiIGQ9Ik0zMzUgMTg0YTQgNCAwIDAgMC00IDR2MTM2YTQgNCAwIDAgMCA4IDBWMTg4YTQgNCAwIDAgMC00LTR6TTI1NSA5N2E0IDQgMCAwIDAtNCA0djI1MGE0IDQgMCAwIDAgOCAwdl0yNTBhNCA0IDAgMCAwLTQtNHpNMzc1IDIyN2E0IDQgMCAwIDAtNCA0djg5YTQgNCAwIDAgMCA4IDB2LTg5YTQgNCAwIDAgMC00LTR6Ii8+PHBhdGggZmlsbD0iI0VBRUlGRiIgZD0iTTgxIDIwMWExNCAxNCAwIDAgMC0xNCAxNC4wNDR2MTM3LjkyQTE0IDE0IDAgMCAwIDgxIDM2OGgxNFYyMDFIODF6Ii8+PHBhdGggZmlsbD0iI0NFRDhGRiIgZD0iTTE5NSA2MWExNCAxNCAwIDAgMC0xNCAxNC4wNDR2MjgwLjkyQTE0IDE0IDAgMCAwIDE5NSAzNjhIMTlWMjYwaDE2NHYtNTZIMTlWOTFoMTc2VjYxaC0xNHptLTE2NCAxODVWMjEzaDE2NFYxNTVIMzF2NDh6bTE2NC05NVY5OWgtMTZWNzdhNCA0IDAgMCAwLTQtNFY2MWgtNDB2MTJhNCA0IDAgMCAwLTQgN3YxNkgzMXY0MGgxNjR6Ii8+PHBhdGggZmlsbD0iI0VBRUlGRiIgZD0iTTI5NiAyNTlhMTQgMTQgMCAwIDAtMTQgMTQuMDQ0djk0LjkyQTE0IDE0IDAgMCAwIDI5NiAzODJoMTRWMjU5aC0xNHpNMzU2IDMwOWExNCAxNCAwIDAgMC0xNCAxNC4wNDR2NTUuOTJBMTQgMTQgMCAwIDAgMzU2IDM5MWgxNFYzMDloLTM1eiIvPjxwYXRoIGZpbGw9IiNDRUQ4RkYiIGQ9Ik0xMzUgMTI0YTQgNCAwIDAgMC00IDR2MjE3YTQgNCAwIDAgMCA4IDBWMTE4YTQgNCAwIDAgMC00LTR6Ii8+PHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTQyMyAzODRINjlhMTQgMTQgMCAwIDAtMTQgMTR2NTRoMzg1VjM5OGExNCAxNCAwIDAgMC0xNC0xNHoiLz48cGF0aCBmaWxsPSIjQ0VEOEZGIiBkPSJNMzY4IDQyNEgxNDR2MTZoMjI0di0xNnpNMzYxIDQwNUg4NmExNCAxNCAwIDAgMCAwIDI4aDI3NWExNCAxNCAwIDAgMCAwLTI4eiIvPjxwYXRoIGZpbGw9IiNGNEY5RkYiIGQ9Ik01NSAzOTIuMTY0VjM5OGExNCAxNCAwIDAgMCAxNCAxNGg1NDFBMTQgMTQgMCAwIDEgNDM3IDQyNkg1OFY0MDZoMzAzdjEyaDI0di0xMmgyOHYxMmgyNHYtMTJoLTh2LTEySDU4djEyLjE2NEExMy45IDEzLjkgMCAwIDEgNTUgMzkyLjE2NHptMCAzMS44MzZWMzk4YTE0IDE0IDAgMCAxIDE0LTE0aDM1N2EzMyAzMyAwIDAgMSAzMyAzM2gtNDIydi0zMmgzMDN2LTguMTY0QTEzLjkgMTMuOSAwIDAgMSA0MjQgNDI0eiIvPjxwYXRoIGZpbGw9IiM0Q0I2RkYiIGQ9Ik0yMTEgMjg4aC0zM2wtMzUtOTBoLTI4bDQ3IDEyM2g0OGwxNi00NGg0NmwtOCA0NGg0Mmw0Ny0xMjNoLTM4bC0yMyA2M2gtNDVsMTYtNDVaIi8+PHBhdGggZmlsbD0iI0ZGRkZGRiIgZD0iTTk1IDQ1MmgyNHYyNEg5NXptMjA4IDBoMjR2MjRIMzAzem0xMjAgMGgyNHYyNGgtMjR6Ii8+PC9zdmc+"; + + return ( +
    +
    +
    +
    +
    + +

    自然语言查询系统

    +
    +

    欢迎回来

    +

    请输入您的账号和密码登录。

    +
    + +
    +
    +
    + + setUsername(e.target.value)} + className="w-full px-4 py-3 pl-10 bg-gray-50 text-dark border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary placeholder-gray-400" + /> +
    +
    +
    +
    + + setPassword(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && handleLogin(e as any)} + className="w-full px-4 py-3 pl-10 bg-gray-50 text-dark border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary/50 focus:border-primary placeholder-gray-400" + /> +
    +
    + {error && ( +
    + {error} +
    + )} +
    + + +
    +
    +
    + © 2024 Your Company. All Rights Reserved. +
    +
    + +
    +
    +

    智能数据,一语洞穿

    +

    + 无需复杂的SQL,只需用您最熟悉的自然语言提问,即可获得精准的数据洞察、直观的可视化图表,并轻松与团队分享。 +

    +
      +
    • 自然语言查询
    • +
    • 智能图表生成
    • +
    • 结果轻松分享
    • +
    +
    +
    + Data Analysis Illustration +
    +
    +
    + + {/* 忘记密码模态框 */} + {isForgotModalOpen && ( +
    +
    e.stopPropagation()}> + {!resetEmailSent ? ( + <> +

    重置密码

    +
    +

    请输入您注册时使用的邮箱地址,我们将向您发送一封密码重置邮件。

    +
    + + +
    +
    + + +
    +
    + + ) : ( +
    +
    + +
    +

    已发送

    +

    如果该邮箱地址已注册,一封包含密码重置链接的邮件已经发送到您的邮箱。

    +
    + )} +
    +
    + )} +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/Modal.tsx b/src/test/frontend/components/Modal.tsx new file mode 100644 index 00000000..16850394 --- /dev/null +++ b/src/test/frontend/components/Modal.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useState } from 'react'; + +interface ModalProps { + isOpen: boolean; + onClose: () => void; + title?: string; + hideTitle?: boolean; + children: React.ReactNode; + contentClassName?: string; // 外部自定义样式(会覆盖默认) +} + +export const Modal: React.FC = ({ + isOpen, + onClose, + title, + hideTitle = false, + children, + contentClassName = "" +}) => { + const [isRendered, setIsRendered] = useState(isOpen); + + useEffect(() => { + if (isOpen) { + setIsRendered(true); + } else { + const timer = setTimeout(() => setIsRendered(false), 300); + return () => clearTimeout(timer); + } + }, [isOpen]); + + if (!isRendered) { + return null; + } + + // 容器样式(保持居中逻辑不变) + const modalContainerClass = `fixed inset-0 bg-black/50 flex flex-col items-center justify-center z-50 transition-opacity duration-300 ${ + isOpen ? 'opacity-100' : 'opacity-0' + }`; + + // 核心修改:添加默认宽度 max-w-md(和你原来的一样),同时让 contentClassName 能覆盖它 + // 注意类的顺序:默认样式在前,自定义样式在后(后定义的会覆盖前定义的) + const modalContentClass = ` + bg-white rounded-xl p-6 w-full mx-4 my-auto transition-all duration-300 + max-w-md // 默认宽度 + max-h-[90vh] // 弹窗最大高度(限制上限) + height: 100% // 让内容容器高度充满可用空间 + ${isOpen ? 'scale-100 opacity-100' : 'scale-95 opacity-0'} + ${contentClassName} + `; + + return ( +
    +
    e.stopPropagation()}> + {!hideTitle && ( +
    +

    {title}

    + +
    + )} + {children} +
    +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/NotificationsPage.tsx b/src/test/frontend/components/NotificationsPage.tsx new file mode 100644 index 00000000..fee2ee8f --- /dev/null +++ b/src/test/frontend/components/NotificationsPage.tsx @@ -0,0 +1,156 @@ +import React, { useState } from 'react'; +import { MOCK_NOTIFICATIONS } from '../constants'; +import { Notification } from '../types'; +import { Modal } from './Modal'; // 引入Modal组件 + +const NotificationIcon: React.FC<{ type: 'share' | 'system' }> = ({ type }) => { + switch (type) { + case 'share': + return ; + case 'system': + return ; + default: + return null; + } +}; + +const NotificationItem: React.FC<{ notification: Notification; onToggleRead: (id: string) => void; onDelete: (id: string) => void; }> = ({ notification, onToggleRead, onDelete }) => { + const { id, type, title, content, timestamp, isRead, isPinned } = notification; + + return ( +
  • +
    + + {isPinned && } +
    +
    +

    {title}

    +

    {content}

    + {new Date(timestamp).toLocaleString()} +
    +
    + + {!isPinned && ( + + )} +
    +
  • + ); +}; + + +export const NotificationsPage: React.FC = () => { + const [notifications, setNotifications] = useState(MOCK_NOTIFICATIONS); + // 新增:删除确认弹窗状态 + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [deleteTargetId, setDeleteTargetId] = useState(null); + + const pinnedNotifications = notifications.filter(n => n.isPinned); + const regularNotifications = notifications.filter(n => !n.isPinned); + + const handleToggleRead = (id: string) => { + setNotifications( + notifications.map(n => n.id === id ? { ...n, isRead: !n.isRead } : n) + ); + }; + + const handleMarkAllRead = () => { + setNotifications(notifications.map(n => ({ ...n, isRead: true }))); + }; + + // 修改:点击删除按钮时显示弹窗 + const handleDelete = (id: string) => { + setDeleteTargetId(id); + setShowDeleteConfirm(true); + }; + + // 新增:确认删除后执行的逻辑 + const handleConfirmDelete = () => { + if (deleteTargetId) { + setNotifications(notifications.filter(n => n.id !== deleteTargetId)); + } + setShowDeleteConfirm(false); + setDeleteTargetId(null); + }; + + const handleClearAll = () => { + setNotifications(notifications.filter(n => n.isPinned)); + }; + + return ( +
    +
    +
    + + +
    +
    + + {pinnedNotifications.length > 0 && ( +
    +

    置顶通知

    +
    +
      + {pinnedNotifications.map(n => ( + + ))} +
    +
    +
    + )} + +
    +

    + {pinnedNotifications.length > 0 ? '普通通知' : ''} +

    +
    +
      + {regularNotifications.length > 0 ? regularNotifications.map(n => ( + + )) : ( +
      + +

      没有新的通知

      +
      + )} +
    +
    +
    + + {/* 新增:删除确认弹窗 */} + { + setShowDeleteConfirm(false); + setDeleteTargetId(null); + }} + title="确认删除" + > +
    +

    确定要删除这条通知吗?删除后无法恢复。

    +
    + + +
    +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/PlaceholderPage.tsx b/src/test/frontend/components/PlaceholderPage.tsx new file mode 100644 index 00000000..11c299f7 --- /dev/null +++ b/src/test/frontend/components/PlaceholderPage.tsx @@ -0,0 +1,18 @@ + +import React from 'react'; + +interface PlaceholderPageProps { + title: string; +} + +export const PlaceholderPage: React.FC = ({ title }) => { + return ( +
    +
    + +

    {title}

    +

    此页面正在建设中。

    +
    +
    + ); +}; diff --git a/src/test/frontend/components/QueryPage.tsx b/src/test/frontend/components/QueryPage.tsx new file mode 100644 index 00000000..ff32a752 --- /dev/null +++ b/src/test/frontend/components/QueryPage.tsx @@ -0,0 +1,292 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Conversation, MessageRole, QueryResultData } from '../types'; +import { ChatMessage } from './ChatMessage'; +import { Dropdown } from './Dropdown'; +import { DATABASE_OPTIONS } from '../constants'; +import { HistorySidebar } from './HistorySidebar'; +import { RightSidebar } from './RightSidebar'; +import { queryApi, QueryResponse, llmConfigApi } from '../services/api'; + +interface QueryPageProps { + currentConversation: Conversation | undefined; + onToggleHistory: () => void; + isHistoryOpen: boolean; + onAddMessage: (role: MessageRole, content: string | QueryResultData) => void; + onSaveQuery: (query: QueryResultData) => void; + onShareQuery: (queryId: string, friendId: string) => void; + savedQueries: QueryResultData[]; + initialPrompt?: string; + onClearInitialPrompt: () => void; + conversations: Conversation[]; + currentConversationId: string; // 当前激活的对话ID(关键) + onSwitchConversation: (id: string) => void; + onNewConversation: () => void; + onDeleteConversation: (id: string) => void; +} + +export const QueryPage: React.FC = ({ + currentConversation, + onToggleHistory, + isHistoryOpen, + onAddMessage, + onSaveQuery, + onShareQuery, + savedQueries, + initialPrompt, + onClearInitialPrompt, + conversations, + currentConversationId, // 接收当前对话ID + onSwitchConversation, + onNewConversation, + onDeleteConversation, +}) => { + const [prompt, setPrompt] = useState(''); + const [modelOptions, setModelOptions] = useState>([]); + const [selectedModel, setSelectedModel] = useState(''); + const [selectedDatabase, setSelectedDatabase] = useState(DATABASE_OPTIONS[0].name); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [abortController, setAbortController] = useState(null); + // 新增:记录当前正在处理的请求对应的对话ID + const [pendingConversationId, setPendingConversationId] = useState(null); + const chatContainerRef = useRef(null); + + // 从后端加载可用的大模型配置 + useEffect(() => { + loadAvailableModels(); + }, []); + + const loadAvailableModels = async () => { + try { + const configs = await llmConfigApi.getAvailable(); + const options = configs.map(config => ({ + name: `${config.name}-${config.version}`, + disabled: false, + description: `${config.name} ${config.version}`, + })); + setModelOptions(options); + if (options.length > 0) { + setSelectedModel(options[0].name); + } + } catch (error) { + console.error('加载大模型配置失败:', error); + // 如果加载失败,使用默认选项 + setModelOptions([ + { name: 'gemini-2.5-pro', disabled: false, description: '最强大的模型,用于复杂查询' }, + ]); + setSelectedModel('gemini-2.5-pro'); + } + }; + + // 自动滚动到底部 + useEffect(() => { + if (chatContainerRef.current) { + chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; + } + }, [currentConversation?.messages]); + + // 处理初始提示 + useEffect(() => { + if (initialPrompt) { + setPrompt(initialPrompt); + const syntheticEvent = { preventDefault: () => {} } as React.FormEvent; + handleSubmit(syntheticEvent, initialPrompt); + onClearInitialPrompt(); + } + }, [initialPrompt, onClearInitialPrompt]); + + // 关键修改1:当对话ID变化时,自动中断当前请求 + useEffect(() => { + // 如果正在加载中,且当前对话ID与请求时的ID不一致,中断请求 + if (isLoading && pendingConversationId && pendingConversationId !== currentConversationId) { + handleStop(); + } + }, [currentConversationId, isLoading, pendingConversationId]); + + const handleSubmit = async (e: React.FormEvent, customPrompt?: string) => { + e.preventDefault(); + const finalPrompt = customPrompt || prompt; + if (!finalPrompt.trim() || isLoading) return; + + // 1. 记录当前请求对应的对话ID(关键) + const requestConversationId = currentConversationId; + setPendingConversationId(requestConversationId); + + // 2. 创建中断控制器 + const controller = new AbortController(); + setAbortController(controller); + + onAddMessage('user', finalPrompt); + setPrompt(''); + setIsLoading(true); + setError(null); + + try { + if (!currentConversation) throw new Error("No active conversation."); + + // 3. 调用后端API + const response: QueryResponse = await queryApi.execute({ + userPrompt: finalPrompt, + model: selectedModel, + database: selectedDatabase, + conversationId: currentConversation.id !== 'conv-1' ? currentConversation.id : undefined, + }); + + // 4. 将后端响应转换为前端格式 + const result: QueryResultData = { + id: response.id, + userPrompt: response.userPrompt, + sqlQuery: response.sqlQuery, + conversationId: response.conversationId, + queryTime: response.queryTime, + executionTime: response.executionTime, + database: response.database, + model: response.model, + tableData: response.tableData, + chartData: response.chartData ? { + type: (response.chartData.type || 'bar') as 'bar' | 'line' | 'pie', + labels: response.chartData.labels || [], + datasets: (response.chartData.datasets || []).map(dataset => ({ + label: dataset.label, + data: dataset.data, + backgroundColor: dataset.backgroundColor || '#3b82f6', // 默认颜色 + })), + } : undefined, + }; + + // 5. 关键校验:只有当前对话仍为请求时的对话,才添加AI回复 + if (currentConversationId === requestConversationId) { + onAddMessage('ai', result); + } else { + // 对话已切换,丢弃回复(可选:添加日志便于调试) + console.log(`AI回复已丢弃(目标对话已切换):原对话ID=${requestConversationId},新对话ID=${currentConversationId}`); + } + } catch (err) { + // 6. 错误处理也需校验对话ID + if (err instanceof Error && err.name === 'AbortError') { + // 仅在原对话中显示"已停止"提示 + if (currentConversationId === requestConversationId) { + onAddMessage('ai', '查询已被手动停止'); + } + } else { + const errorMessage = err instanceof Error ? err.message : '查询失败,请稍后重试'; + // 仅在原对话中显示错误 + if (currentConversationId === requestConversationId) { + setError(errorMessage); + onAddMessage('ai', errorMessage); + } + } + } finally { + // 7. 仅在原对话中重置状态 + if (currentConversationId === requestConversationId) { + setIsLoading(false); + setAbortController(null); + setPendingConversationId(null); + } + } + }; + + // 停止请求 + const handleStop = () => { + if (abortController) { + abortController.abort(); // 触发中断 + setIsLoading(false); + setAbortController(null); + setPendingConversationId(null); + } + }; + + const handleRecommendationClick = (recommendation: string) => { + const fakeEvent = { preventDefault: () => {} } as React.FormEvent; + handleSubmit(fakeEvent, recommendation); + }; + + return ( +
    +
    +
    + {/* Chat Area */} +
    + {currentConversation?.messages.map((msg, index) => ( + + ))} + {isLoading && ( +
    +
    + +
    +
    +
    +
    + 正在生成结果... +
    +
    +
    + )} +
    + + {/* Input Area */} +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + + + setModal(null)} title="确认删除通知"> +

    您确定要删除通知 "{currentItem?.title}" 吗?

    +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/admin/SystemLogPage.tsx b/src/test/frontend/components/admin/SystemLogPage.tsx new file mode 100644 index 00000000..b2fbc6f6 --- /dev/null +++ b/src/test/frontend/components/admin/SystemLogPage.tsx @@ -0,0 +1,130 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { AdminModal } from './AdminModal'; +import { SystemLog } from '../../types'; +import { MOCK_SYSTEM_LOGS } from '../../constants'; + +interface SystemLogPageProps { + initialStatusFilter: string; + clearInitialFilter: () => void; +} + +export const SystemLogPage: React.FC = ({ initialStatusFilter, clearInitialFilter }) => { + const [logs, setLogs] = useState(MOCK_SYSTEM_LOGS); + const [filters, setFilters] = useState({ startDate: '', endDate: '', user: '', action: '', status: initialStatusFilter }); + const [isExportModalOpen, setExportModalOpen] = useState(false); + const [viewingLog, setViewingLog] = useState(null); + + useEffect(() => { + // Clear the initial filter from the parent so it's not reapplied on re-renders + if (initialStatusFilter) { + clearInitialFilter(); + } + }, [initialStatusFilter, clearInitialFilter]); + + const filteredLogs = useMemo(() => { + return logs.filter(log => + (!filters.startDate || log.time >= filters.startDate) && + (!filters.endDate || log.time <= `${filters.endDate} 23:59:59`) && + (!filters.user || log.user.toLowerCase().includes(filters.user.toLowerCase())) && + (!filters.action || log.action.toLowerCase().includes(filters.action.toLowerCase())) && + (!filters.status || log.status === filters.status) + ); + }, [logs, filters]); + + const handleFilterChange = (e: React.ChangeEvent) => { + setFilters(prev => ({ ...prev, [e.target.name]: e.target.value })); + }; + + const resetFilters = () => { + setFilters({ startDate: '', endDate: '', user: '', action: '', status: '' }); + }; + + const handleExport = () => { + alert('日志已开始导出...'); + setExportModalOpen(false); + } + + return ( +
    +
    + +
    +
    +
    +
    + +
    -
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    + + + + {filteredLogs.map(log => ( + + + + + + + + + + + ))} + +
    ID操作时间操作用户操作内容涉及模型IP地址状态操作
    {log.id}{log.time}{log.user}{log.action}{log.model}{log.ip}{log.status === 'success' ? '成功' : '失败'} + {log.status === 'failure' && log.details && ( + + )} +
    +
    +

    显示 {filteredLogs.length} 条,共 {logs.length} 条

    +
    + + setExportModalOpen(false)} title="导出系统日志"> +
    { e.preventDefault(); handleExport(); }} className="space-y-4"> +
    + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + setViewingLog(null)} title={`日志详情 (ID: ${viewingLog?.id})`}> +
    +

    错误信息

    +
    +                        {viewingLog?.details}
    +                    
    +
    +
    + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/admin/UserManagementPage.tsx b/src/test/frontend/components/admin/UserManagementPage.tsx new file mode 100644 index 00000000..91242112 --- /dev/null +++ b/src/test/frontend/components/admin/UserManagementPage.tsx @@ -0,0 +1,353 @@ +import React, { useState, useMemo, useCallback, useEffect } from 'react'; +import { AdminModal } from './AdminModal'; +import { AdminUser, UserRole } from '../../types'; +import { userApi, User } from '../../services/api'; + +export const UserManagementPage: React.FC = () => { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [filters, setFilters] = useState({ search: '', role: '', status: '' }); + const [selectedUserIds, setSelectedUserIds] = useState>(new Set()); + const [modal, setModal] = useState<'add' | 'edit' | 'confirmDelete' | 'confirmBatch' | 'confirmResetPassword' | 'confirmToggleStatus' | null>(null); + const [userToProcess, setUserToProcess] = useState(null); + const [batchAction, setBatchAction] = useState<'enable' | 'disable' | 'delete' | ''>(''); + + // 加载用户列表 + useEffect(() => { + loadUsers(); + }, []); + + const loadUsers = async () => { + try { + setLoading(true); + const userList = await userApi.getList(); + // 转换后端User格式到前端AdminUser格式 + const adminUsers: AdminUser[] = userList.map(user => ({ + id: user.id, + username: user.username, + role: mapRoleIdToRole(user.roleId), + email: user.email, + regTime: new Date().toISOString().split('T')[0], // 后端没有返回createTime,暂时用当前日期 + status: user.status === 1 ? 'active' : 'disabled', + })); + setUsers(adminUsers); + } catch (error) { + console.error('加载用户列表失败:', error); + alert('加载用户列表失败: ' + (error instanceof Error ? error.message : '未知错误')); + } finally { + setLoading(false); + } + }; + + // 映射角色ID到前端角色类型 + const mapRoleIdToRole = (roleId: number): UserRole => { + if (roleId === 1) return 'sys-admin'; + if (roleId === 2) return 'data-admin'; + return 'normal-user'; + }; + + // 映射前端角色到后端角色ID + const mapRoleToRoleId = (role: UserRole): number => { + if (role === 'sys-admin') return 1; + if (role === 'data-admin') return 2; + return 3; + }; + + const filteredUsers = useMemo(() => { + return users.filter(user => + (user.username.toLowerCase().includes(filters.search.toLowerCase()) || user.email.toLowerCase().includes(filters.search.toLowerCase())) && + (filters.role ? user.role === filters.role : true) && + (filters.status ? user.status === filters.status : true) + ); + }, [users, filters]); + + const handleFilterChange = (e: React.ChangeEvent) => { + setFilters(prev => ({ ...prev, [e.target.name]: e.target.value })); + }; + + const handleSelectAll = (e: React.ChangeEvent) => { + if (e.target.checked) { + setSelectedUserIds(new Set(filteredUsers.map(u => u.id))); + } else { + setSelectedUserIds(new Set()); + } + }; + + const handleSelectUser = (id: number, checked: boolean) => { + const newSet = new Set(selectedUserIds); + if (checked) { + newSet.add(id); + } else { + newSet.delete(id); + } + setSelectedUserIds(newSet); + }; + + const requestDeleteUser = useCallback((user: AdminUser) => { + setUserToProcess(user); + setModal('confirmDelete'); + }, []); + + const confirmDeleteUser = async () => { + if (userToProcess) { + try { + await userApi.delete(userToProcess.id); + setUsers(prev => prev.filter(u => u.id !== userToProcess.id)); + alert('删除成功'); + } catch (error) { + console.error('删除用户失败:', error); + alert('删除失败: ' + (error instanceof Error ? error.message : '未知错误')); + } + } + setModal(null); + }; + + const requestToggleUserStatus = useCallback((user: AdminUser) => { + setUserToProcess(user); + setModal('confirmToggleStatus'); + }, []); + + const confirmToggleUserStatus = async () => { + if(userToProcess) { + try { + const newStatus = userToProcess.status === 'active' ? 0 : 1; + await userApi.update({ + id: userToProcess.id, + status: newStatus, + }); + setUsers(prev => prev.map(u => u.id === userToProcess.id ? { ...u, status: u.status === 'active' ? 'disabled' : 'active' } : u)); + alert('操作成功'); + } catch (error) { + console.error('更新用户状态失败:', error); + alert('操作失败: ' + (error instanceof Error ? error.message : '未知错误')); + } + } + setModal(null); + }; + + const handleApplyBatchAction = () => { + if (!batchAction || selectedUserIds.size === 0) return; + setModal('confirmBatch'); + }; + + const confirmBatchAction = async () => { + try { + if (batchAction === 'delete') { + // 批量删除 + const deletePromises = Array.from(selectedUserIds).map(id => userApi.delete(id)); + await Promise.all(deletePromises); + setUsers(prev => prev.filter(u => !selectedUserIds.has(u.id))); + } else { + // 批量启用/禁用 + const newStatus = batchAction === 'enable' ? 1 : 0; + const updatePromises = Array.from(selectedUserIds).map(id => + userApi.update({ id, status: newStatus }) + ); + await Promise.all(updatePromises); + setUsers(prev => prev.map(u => selectedUserIds.has(u.id) ? { ...u, status: batchAction === 'enable' ? 'active' : 'disabled' } : u)); + } + alert('批量操作成功'); + } catch (error) { + console.error('批量操作失败:', error); + alert('批量操作失败: ' + (error instanceof Error ? error.message : '未知错误')); + } + setSelectedUserIds(new Set()); + setModal(null); + }; + + const requestEditUser = useCallback((user: AdminUser) => { + setUserToProcess(user); + setModal('edit'); + }, []); + + const requestAddUser = () => { + setUserToProcess(null); + setModal('add'); + }; + + const handleSaveUser = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + try { + if (modal === 'add') { + const password = formData.get('password') as string; + if (password.length < 6) { + alert('密码至少需要6位!'); + return; + } + const role = formData.get('role') as UserRole; + await userApi.create({ + username: formData.get('username') as string, + email: formData.get('email') as string, + password: password, + roleId: mapRoleToRoleId(role), + status: 1, + phonenumber: '', + }); + alert('添加成功'); + await loadUsers(); // 重新加载列表 + + } else if (modal === 'edit' && userToProcess) { + await userApi.update({ + id: userToProcess.id, + username: formData.get('username') as string, + email: formData.get('email') as string, + roleId: mapRoleToRoleId(formData.get('role') as UserRole), + }); + alert('更新成功'); + await loadUsers(); // 重新加载列表 + } + } catch (error) { + console.error('保存用户失败:', error); + alert('保存失败: ' + (error instanceof Error ? error.message : '未知错误')); + return; + } + + setModal(null); + }, [modal, userToProcess]); + + const requestResetPassword = useCallback((user: AdminUser) => { + setUserToProcess(user); + setModal('confirmResetPassword'); + }, []); + + const confirmResetPassword = () => { + if (userToProcess) { + alert(`已向用户 ${userToProcess.username} (${userToProcess.email}) 发送密码重置邮件。`); + } + setModal(null); + }; + + const getRoleName = (role: UserRole) => ({ 'sys-admin': '系统管理员', 'data-admin': '数据管理员', 'normal-user': '普通用户' }[role]); + const getRoleClass = (role: UserRole) => ({ 'sys-admin': 'bg-primary/10 text-primary', 'data-admin': 'bg-success/10 text-success', 'normal-user': 'bg-secondary/10 text-secondary' }[role]); + + if (loading) { + return ( +
    +
    +
    + +

    加载中...

    +
    +
    +
    + ); + } + + return ( +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + + +
    +
    + +
    +
    + + + + + + + + + + + + + + {filteredUsers.map(user => ( + + + + + + + + + + ))} + +
    0 && selectedUserIds.size === filteredUsers.length} className="w-4 h-4 text-primary focus:ring-primary/30" />用户名角色邮箱注册时间状态操作
    handleSelectUser(user.id, e.target.checked)} className="w-4 h-4 text-primary focus:ring-primary/30" />{user.username}{getRoleName(user.role)}{user.email}{user.regTime}{user.status === 'active' ? '正常' : '禁用'} +
    + + + + +
    +
    +
    +

    显示 {filteredUsers.length} 条,共 {users.length} 条

    +
    + + setModal(null)} title={modal === 'add' ? '添加新用户' : '编辑用户信息'}> +
    +
    +
    + {modal === 'add' && <> +
    + } +
    + + +
    +
    + + +
    +
    +
    + + setModal(null)} title="确认删除用户"> +

    您确定要删除用户 "{userToProcess?.username}" 吗?此操作不可撤销。

    +
    + + +
    +
    + + setModal(null)} title="确认重置密码"> +

    您确定要为用户 "{userToProcess?.username}" 重置密码吗?系统将向其邮箱发送一封密码重置邮件。

    +
    + + +
    +
    + + setModal(null)} title={`确认${userToProcess?.status === 'active' ? '禁用' : '启用'}用户`}> +

    您确定要{userToProcess?.status === 'active' ? '禁用' : '启用'}用户 "{userToProcess?.username}" 吗?

    +
    + + +
    +
    + + setModal(null)} title="确认批量操作"> +

    您确定要对选中的 {selectedUserIds.size} 个用户执行 "{ {enable: '启用', disable: '禁用', delete: '删除'}[batchAction] }" 操作吗?

    +
    + + +
    +
    +
    + ); +}; diff --git a/src/test/frontend/components/data-admin/ConnectionLogPage.tsx b/src/test/frontend/components/data-admin/ConnectionLogPage.tsx new file mode 100644 index 00000000..ae3f16ca --- /dev/null +++ b/src/test/frontend/components/data-admin/ConnectionLogPage.tsx @@ -0,0 +1,169 @@ +import React, { useState, useMemo } from 'react'; +import { AdminModal } from '../admin/AdminModal'; +import { ConnectionLog } from '../../types'; +import { MOCK_CONNECTION_LOGS } from '../../constants'; + +export const ConnectionLogPage: React.FC = () => { + const [logs, setLogs] = useState(MOCK_CONNECTION_LOGS); + const [isExportModalOpen, setExportModalOpen] = useState(false); + const [viewingLog, setViewingLog] = useState(null); + // 新增搜索相关状态 + const [searchTerm, setSearchTerm] = useState(''); + const [searchType, setSearchType] = useState<'all' | 'time' | 'datasource' | 'status'>('all'); + + const getStatusClass = (status: '成功' | '失败') => { + return status === '成功' ? 'text-success' : 'text-danger'; + }; + + const handleExport = () => { + alert('日志已开始导出...'); + setExportModalOpen(false); + }; + + // 搜索筛选逻辑 + const filteredLogs = useMemo(() => { + if (!searchTerm.trim()) return logs; + + const term = searchTerm.toLowerCase(); + return logs.filter(log => { + switch (searchType) { + case 'time': + return new Date(log.time).toLocaleString().toLowerCase().includes(term); + case 'datasource': + return log.datasource.toLowerCase().includes(term); + case 'status': + return log.status.toLowerCase().includes(term); + default: // 'all' + return ( + new Date(log.time).toLocaleString().toLowerCase().includes(term) || + log.datasource.toLowerCase().includes(term) || + log.status.toLowerCase().includes(term) + ); + } + }); + }, [logs, searchTerm, searchType]); + + return ( +
    + {/* 修改顶部区域,添加搜索功能 */} +
    +
    + setSearchTerm(e.target.value)} + className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50" + /> + +
    + {/* 导出按钮靠右放置 */} + +
    + +
    +
    + + + + + + + + + + + {/* 使用筛选后的日志列表 */} + {filteredLogs.map(log => ( + + + + + + + ))} + {/* 无搜索结果时显示 */} + {filteredLogs.length === 0 && ( + + + + )} + +
    时间数据源状态操作
    {new Date(log.time).toLocaleString()}{log.datasource}{log.status} + {log.status === '失败' && log.details && ( + + )} +
    + 没有找到匹配的日志记录 +
    +
    +
    + + setExportModalOpen(false)} title="导出连接日志"> +
    { e.preventDefault(); handleExport(); }} className="space-y-4"> +
    + +
    + + +
    +
    +
    + +
    + + +
    +
    +
    + + +
    +
    +
    + + setViewingLog(null)} title={`连接失败详情 (${viewingLog?.datasource})`}> +
    +

    错误信息

    +
    +                        {viewingLog?.details}
    +                    
    +
    +
    + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/data-admin/DataAdminDashboardPage.tsx b/src/test/frontend/components/data-admin/DataAdminDashboardPage.tsx new file mode 100644 index 00000000..3d168b60 --- /dev/null +++ b/src/test/frontend/components/data-admin/DataAdminDashboardPage.tsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { DataAdminPageType } from '../../types'; +import { MOCK_DATASOURCES, MOCK_CONNECTION_LOGS, MOCK_PERMISSION_LOGS, MOCK_QUERY_LOAD } from '../../constants'; +import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement } from 'chart.js'; +import { Pie, Bar } from 'react-chartjs-2'; + +ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement); + +// Reusable StatCard component specific for this dashboard +const StatCard: React.FC<{ title: string; value: string; icon: string; color: string; onClick: () => void; }> = ({ title, value, icon, color, onClick }) => ( +
    +
    +
    +

    {title}

    +

    {value}

    +
    +
    + +
    +
    +
    +); + +// Helper to format timestamp into a relative string +const formatTimeAgo = (timestamp: string): string => { + const now = new Date(); + const then = new Date(timestamp); + const diffInSeconds = Math.round((now.getTime() - then.getTime()) / 1000); + + if (diffInSeconds < 60) return `${diffInSeconds}秒前`; + const diffInMinutes = Math.round(diffInSeconds / 60); + if (diffInMinutes < 60) return `${diffInMinutes}分钟前`; + const diffInHours = Math.round(diffInMinutes / 60); + if (diffInHours < 24) return `${diffInHours}小时前`; + const diffInDays = Math.round(diffInHours / 24); + return `${diffInDays}天前`; +}; + +export const DataAdminDashboardPage: React.FC<{ setActivePage: (page: DataAdminPageType) => void; }> = ({ setActivePage }) => { + // Data processing for cards and charts + const connectedCount = MOCK_DATASOURCES.filter(ds => ds.status === 'connected').length; + const errorCount = MOCK_CONNECTION_LOGS.filter(log => log.status === '失败').length; + const pendingRequests = 3; // Mock value + + const healthStatusCounts = MOCK_DATASOURCES.reduce((acc, ds) => { + const statusMap = { connected: '已连接', disconnected: '未连接', error: '错误', testing: '测试中', disabled: '已禁用' }; + const status = statusMap[ds.status]; + acc[status] = (acc[status] || 0) + 1; + return acc; + }, {} as Record); + + const healthStatusData = { + labels: Object.keys(healthStatusCounts), + datasets: [{ + data: Object.values(healthStatusCounts), + backgroundColor: ['#00B42A', '#86909C', '#F53F3F', '#36BFFA', '#C9CDD4'], + borderWidth: 0, + }], + }; + + const queryLoadData = { + labels: MOCK_QUERY_LOAD.labels, + datasets: [{ + label: '查询量', + data: MOCK_QUERY_LOAD.data, + backgroundColor: '#165DFF', + borderRadius: 4, + }], + }; + + const recentFailures = MOCK_CONNECTION_LOGS.filter(log => log.status === '失败').slice(0, 5); + + return ( +
    + +
    + setActivePage('datasource')} /> + setActivePage('datasource')} /> + setActivePage('connection-log')} /> + setActivePage('user-permission')} /> +
    + +
    +
    +

    数据源健康状态

    +
    +
    +
    +

    数据源查询量 Top 5

    +
    +
    +
    + +
    +
    +

    近期连接失败日志

    +
    + {recentFailures.length > 0 ? recentFailures.map(log => ( +
    +
    +

    {log.datasource}: {log.note}

    +

    {log.time}

    +
    + +
    + )) :

    无失败记录

    } +
    +
    +
    +

    近期权限变更动态

    +
    + {MOCK_PERMISSION_LOGS.slice(0, 4).map(log => ( +
    + +
    +

    +

    {formatTimeAgo(log.timestamp)}

    +
    +
    + ))} +
    +
    +
    +
    + ); +}; diff --git a/src/test/frontend/components/data-admin/DataAdminNotificationPage.tsx b/src/test/frontend/components/data-admin/DataAdminNotificationPage.tsx new file mode 100644 index 00000000..f32c01e9 --- /dev/null +++ b/src/test/frontend/components/data-admin/DataAdminNotificationPage.tsx @@ -0,0 +1,107 @@ +import React, { useState, useMemo } from 'react'; +import { AdminModal } from '../admin/AdminModal'; +import { AdminNotification } from '../../types'; +import { MOCK_DATA_ADMIN_NOTIFICATIONS } from '../../constants'; + +export const DataAdminNotificationPage: React.FC = () => { + const [notifications, setNotifications] = useState(MOCK_DATA_ADMIN_NOTIFICATIONS); + const [modal, setModal] = useState<'add' | 'edit' | 'delete' | null>(null); + const [currentItem, setCurrentItem] = useState(null); + + const sortedNotifications = useMemo(() => { + return [...notifications].sort((a, b) => (b.pinned ? 1 : -1) - (a.pinned ? 1 : -1) || new Date(b.publishTime).getTime() - new Date(a.publishTime).getTime()); + }, [notifications]); + + const openModal = (type: 'add' | 'edit' | 'delete', item?: AdminNotification) => { + setCurrentItem(item || null); + setModal(type); + }; + + const handleTogglePin = (item: AdminNotification) => { + setNotifications(prev => prev.map(n => n.id === item.id ? { ...n, pinned: !n.pinned } : n)); + }; + + const handleSave = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + + if (modal === 'add') { + const newNotif: AdminNotification = { + id: Date.now(), + title: formData.get('title') as string, + content: formData.get('content') as string, + dataSourceTopic: formData.get('dataSourceTopic') as string, + role: 'all', // Data admins typically notify all or data-related roles + priority: 'normal', + pinned: formData.get('pinned') === 'on', + publisher: '数据管理员', + publishTime: new Date().toISOString().split('T')[0], + status: 'published', + }; + setNotifications(prev => [newNotif, ...prev]); + } else if (modal === 'edit' && currentItem) { + const updated = { + ...currentItem, + title: formData.get('title') as string, + content: formData.get('content') as string, + dataSourceTopic: formData.get('dataSourceTopic') as string, + pinned: formData.get('pinned') === 'on', + }; + setNotifications(prev => prev.map(n => n.id === currentItem.id ? updated : n)); + } + setModal(null); + }; + + const confirmDelete = () => { + if (currentItem) { + setNotifications(prev => prev.filter(n => n.id !== currentItem.id)); + } + setModal(null); + }; + + return ( +
    +
    + +
    + +
    +
    + + + + {sortedNotifications.map(n => ( + + + + + + + + ))} + +
    标题数据源主题发布者发布时间操作
    {n.title} {n.pinned && }{n.dataSourceTopic}{n.publisher}{n.publishTime} + + + +
    +
    +
    + + setModal(null)} title={modal === 'add' ? '发布新通知' : '编辑通知'}> +
    +
    +
    +
    +
    +
    +
    +
    + + setModal(null)} title="确认删除通知"> +

    您确定要删除通知 "{currentItem?.title}" 吗?

    +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/data-admin/DataSourceManagementPage.tsx b/src/test/frontend/components/data-admin/DataSourceManagementPage.tsx new file mode 100644 index 00000000..a9486c04 --- /dev/null +++ b/src/test/frontend/components/data-admin/DataSourceManagementPage.tsx @@ -0,0 +1,306 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { AdminModal } from '../admin/AdminModal'; +import { DataSource } from '../../types'; +import { dbConnectionApi, DbConnection } from '../../services/api'; + +export const DataSourceManagementPage: React.FC = () => { + const [dataSources, setDataSources] = useState([]); + const [loading, setLoading] = useState(true); + const [filters, setFilters] = useState({ search: '', type: '', status: '' }); + const [modal, setModal] = useState<'add' | 'edit' | 'delete' | 'confirmDisable' | null>(null); + const [currentItem, setCurrentItem] = useState(null); + + // 加载数据源列表 + useEffect(() => { + loadDataSources(); + }, []); + + const loadDataSources = async () => { + try { + setLoading(true); + const connections = await dbConnectionApi.getList(); + // 转换后端DbConnection格式到前端DataSource格式 + const frontendDataSources: DataSource[] = connections.map(conn => ({ + id: String(conn.id), + name: conn.name, + type: mapDbTypeIdToType(conn.dbTypeId), // 需要根据dbTypeId映射 + address: conn.url, + status: mapBackendStatusToFrontend(conn.status), + })); + setDataSources(frontendDataSources); + } catch (error) { + console.error('加载数据源列表失败:', error); + alert('加载数据源列表失败: ' + (error instanceof Error ? error.message : '未知错误')); + } finally { + setLoading(false); + } + }; + + // 映射数据库类型ID到前端类型(需要从后端获取类型列表,这里先简化处理) + const mapDbTypeIdToType = (dbTypeId: number): DataSource['type'] => { + // 简化映射,实际应该从后端获取类型列表 + if (dbTypeId === 1) return 'MySQL'; + if (dbTypeId === 2) return 'PostgreSQL'; + if (dbTypeId === 3) return 'Oracle'; + if (dbTypeId === 4) return 'SQL Server'; + return 'MySQL'; + }; + + // 映射前端类型到数据库类型ID + const mapTypeToDbTypeId = (type: DataSource['type']): number => { + if (type === 'MySQL') return 1; + if (type === 'PostgreSQL') return 2; + if (type === 'Oracle') return 3; + if (type === 'SQL Server') return 4; + return 1; + }; + + // 映射后端状态到前端状态 + const mapBackendStatusToFrontend = (status: string): DataSource['status'] => { + if (status === 'connected') return 'connected'; + if (status === 'disconnected') return 'disconnected'; + if (status === 'error') return 'error'; + if (status === 'disabled') return 'disabled'; + return 'disconnected'; + }; + + // 映射前端状态到后端状态 + const mapFrontendStatusToBackend = (status: DataSource['status']): string => { + if (status === 'connected') return 'connected'; + if (status === 'disconnected') return 'disconnected'; + if (status === 'error') return 'error'; + if (status === 'disabled') return 'disabled'; + return 'disconnected'; + }; + + const filteredDataSources = useMemo(() => { + return dataSources.filter(ds => + ds.name.toLowerCase().includes(filters.search.toLowerCase()) && + (filters.type ? ds.type === filters.type : true) && + (filters.status ? ds.status === filters.status : true) + ); + }, [dataSources, filters]); + + const handleFilterChange = (e: React.ChangeEvent) => { + setFilters(prev => ({ ...prev, [e.target.name]: e.target.value })); + }; + + const resetFilters = () => setFilters({ search: '', type: '', status: '' }); + + const openModal = (type: 'add' | 'edit' | 'delete', item?: DataSource) => { + setCurrentItem(item || null); + setModal(type); + }; + + const handleSave = async (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const data = Object.fromEntries(formData.entries()); + + // 验证必填字段 + if (!data.name || !(data.name as string).trim()) { + alert('连接名称不能为空'); + return; + } + if (!data.host || !(data.host as string).trim()) { + alert('数据库主机地址不能为空'); + return; + } + if (!data.port || !(data.port as string).trim()) { + alert('数据库端口不能为空'); + return; + } + if (!data.username || !(data.username as string).trim()) { + alert('数据库账号不能为空'); + return; + } + if (!data.password || !(data.password as string).trim()) { + alert('数据库密码不能为空'); + return; + } + + try { + if (modal === 'add') { + const backendConnection: Partial = { + name: (data.name as string).trim(), + dbTypeId: mapTypeToDbTypeId(data.type as DataSource['type']), + url: `${(data.host as string).trim()}:${(data.port as string).trim()}`, + username: (data.username as string).trim(), + password: (data.password as string).trim(), + status: 'disconnected', + createUserId: Number(localStorage.getItem('userId') || '1'), + }; + await dbConnectionApi.create(backendConnection); + alert('添加成功'); + await loadDataSources(); + } else if (modal === 'edit' && currentItem) { + const backendConnection: Partial = { + id: Number(currentItem.id), + name: data.name as string, + dbTypeId: mapTypeToDbTypeId(data.type as DataSource['type']), + url: `${data.host}:${data.port}`, + }; + await dbConnectionApi.update(backendConnection); + alert('更新成功'); + await loadDataSources(); + } + } catch (error) { + console.error('保存数据源失败:', error); + alert('保存失败: ' + (error instanceof Error ? error.message : '未知错误')); + return; + } + setModal(null); + }; + + const confirmDelete = async () => { + if (currentItem) { + try { + await dbConnectionApi.delete(Number(currentItem.id)); + alert('删除成功'); + await loadDataSources(); + } catch (error) { + console.error('删除数据源失败:', error); + alert('删除失败: ' + (error instanceof Error ? error.message : '未知错误')); + } + } + setModal(null); + }; + + const handleTestConnection = async (id: string) => { + setDataSources(prev => prev.map(ds => ds.id === id ? { ...ds, status: 'testing' } : ds)); + try { + const result = await dbConnectionApi.test(Number(id)); + setDataSources(prev => prev.map(ds => { + if (ds.id === id) { + return { ...ds, status: result ? 'connected' : 'error' }; + } + return ds; + })); + if (result) { + alert('连接测试成功'); + } else { + alert('连接测试失败'); + } + } catch (error) { + console.error('测试连接失败:', error); + setDataSources(prev => prev.map(ds => ds.id === id ? { ...ds, status: 'error' } : ds)); + alert('测试连接失败: ' + (error instanceof Error ? error.message : '未知错误')); + } + }; + + const requestToggleDisable = (dataSource: DataSource) => { + setCurrentItem(dataSource); + setModal('confirmDisable'); + }; + + const confirmToggleDisable = async () => { + if (currentItem) { + try { + const newStatus = currentItem.status === 'disabled' ? 'disconnected' : 'disabled'; + await dbConnectionApi.update({ + id: Number(currentItem.id), + status: newStatus, + }); + alert('操作成功'); + await loadDataSources(); + } catch (error) { + console.error('切换数据源状态失败:', error); + alert('操作失败: ' + (error instanceof Error ? error.message : '未知错误')); + } + } + setModal(null); + }; + + const getStatusChip = (status: DataSource['status']) => { + const styles = { + connected: 'bg-success/10 text-success', + disconnected: 'bg-gray-100 text-gray-600', + error: 'bg-danger/10 text-danger', + testing: 'bg-blue-100 text-blue-600 animate-pulse', + disabled: 'bg-gray-200 text-gray-700' + }; + const text = { + connected: '已连接', + disconnected: '未连接', + error: '连接错误', + testing: '测试中...', + disabled: '已禁用' + }; + return {text[status]} + }; + + if (loading) { + return ( +
    +
    +
    + +

    加载中...

    +
    +
    +
    + ); + } + + return ( +
    +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + + + + {filteredDataSources.map(ds => ( + + + + + ))} + +
    数据源名称数据库类型连接地址状态操作
    {ds.name}{ds.type}{ds.address}{getStatusChip(ds.status)} + + + + +
    +
    +
    + + setModal(null)} title={modal === 'add' ? '添加数据源' : '编辑数据源'}> +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + + setModal(null)} title="确认删除数据源"> +

    您确定要删除数据源 "{currentItem?.name}" 吗?此操作不可撤销。

    +
    +
    + + setModal(null)} title={`确认${currentItem?.status === 'disabled' ? '启用' : '禁用'}数据源`}> +

    您确定要{currentItem?.status === 'disabled' ? '启用' : '禁用'}数据源 "{currentItem?.name}" 吗?

    +
    + + +
    +
    +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/components/data-admin/UserPermissionPage.tsx b/src/test/frontend/components/data-admin/UserPermissionPage.tsx new file mode 100644 index 00000000..724aacb9 --- /dev/null +++ b/src/test/frontend/components/data-admin/UserPermissionPage.tsx @@ -0,0 +1,434 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { AdminModal } from '../admin/AdminModal'; +import { UserPermissionAssignment, UnassignedUser, DataSourcePermission } from '../../types'; +import { userDbPermissionApi, UserDbPermission, userApi, User, dbConnectionApi, DbConnection } from '../../services/api'; + +export const UserPermissionPage: React.FC = () => { + const [unassignedUsers, setUnassignedUsers] = useState([]); + const [assignedPermissions, setAssignedPermissions] = useState([]); + const [dataSources, setDataSources] = useState([]); + const [loading, setLoading] = useState(true); + +// 定义支持的搜索类别 +type SearchCategory = 'all' | 'username' | 'email' | 'datasource' | 'table'; + + // 加载数据 + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + try { + setLoading(true); + // 加载所有用户 + const allUsers = await userApi.getList(); + // 加载已分配权限 + const assignedPerms = await userDbPermissionApi.getAssigned(); + // 加载数据源 + const connections = await dbConnectionApi.getList(); + setDataSources(connections); + + // 解析已分配权限 + const parsedAssigned: UserPermissionAssignment[] = assignedPerms.map(perm => { + const user = allUsers.find(u => u.id === perm.userId); + let permissions: DataSourcePermission[] = []; + try { + const details = JSON.parse(perm.permissionDetails || '[]'); + permissions = details.map((detail: any) => { + const conn = connections.find(c => c.id === detail.db_connection_id); + return { + dataSourceId: String(detail.db_connection_id), + dataSourceName: conn?.name || '未知数据源', + tables: detail.table_ids?.map((tid: number) => `table_${tid}`) || [], + }; + }); + } catch (e) { + console.error('解析权限详情失败:', e); + } + return { + id: String(perm.id), + userId: String(perm.userId), + username: user?.username || '未知用户', + permissions, + }; + }); + + // 找出未分配权限的用户(所有用户中不在已分配列表中的) + const assignedUserIds = new Set(assignedPerms.map(p => p.userId)); + const unassigned: UnassignedUser[] = allUsers + .filter(u => !assignedUserIds.has(u.id)) + .map(u => ({ + id: String(u.id), + username: u.username, + email: u.email, + regTime: new Date().toISOString().split('T')[0], + })); + + setAssignedPermissions(parsedAssigned); + setUnassignedUsers(unassigned); + } catch (error) { + console.error('加载权限数据失败:', error); + alert('加载权限数据失败: ' + (error instanceof Error ? error.message : '未知错误')); + } finally { + setLoading(false); + } + }; + + const [selectedUserIds, setSelectedUserIds] = useState>(new Set()); + const [modal, setModal] = useState<'assign' | 'manage' | null>(null); + const [currentItem, setCurrentItem] = useState(null); + const [usersToAssign, setUsersToAssign] = useState([]); + const [searchKeyword, setSearchKeyword] = useState(''); + const [searchCategory, setSearchCategory] = useState('all'); + + // 2. 优化:按「搜索类别+关键词」过滤待分配用户 + const filteredUnassignedUsers = useMemo(() => { + const keyword = searchKeyword.toLowerCase().trim(); + if (!keyword) return unassignedUsers; + + return unassignedUsers.filter(user => { + switch (searchCategory) { + case 'username': + return user.username.toLowerCase().includes(keyword); + case 'email': + return user.email.toLowerCase().includes(keyword); + case 'all': + return user.username.toLowerCase().includes(keyword) || user.email.toLowerCase().includes(keyword); + default: + return false; + } + }); + }, [unassignedUsers, searchKeyword, searchCategory]); + + // 3. 优化:按「搜索类别+关键词」过滤已分配用户 + const filteredAssignedPermissions = useMemo(() => { + const keyword = searchKeyword.toLowerCase().trim(); + if (!keyword) return assignedPermissions; + + return assignedPermissions.filter(assignment => { + switch (searchCategory) { + case 'username': + return assignment.username.toLowerCase().includes(keyword); + case 'email': // 已分配用户无邮箱字段,不匹配 + return false; + case 'datasource': + return assignment.permissions.some(perm => perm.dataSourceName.toLowerCase().includes(keyword)); + case 'table': + return assignment.permissions.some(perm => perm.tables.some(table => table.toLowerCase().includes(keyword))); + case 'all': // 全部:匹配用户名/数据源/表名 + return assignment.username.toLowerCase().includes(keyword) || + assignment.permissions.some(perm => perm.dataSourceName.toLowerCase().includes(keyword)) || + assignment.permissions.some(perm => perm.tables.some(table => table.toLowerCase().includes(keyword))); + default: + return false; + } + }); + }, [assignedPermissions, searchKeyword, searchCategory]); + + const handleSelectAll = (e: React.ChangeEvent) => { + if (e.target.checked) { + setSelectedUserIds(new Set(filteredUnassignedUsers.map(u => u.id))); + } else { + setSelectedUserIds(new Set()); + } + }; + + const handleSelectUser = (id: string, checked: boolean) => { + const newSet = new Set(selectedUserIds); + if (checked) newSet.add(id); + else newSet.delete(id); + setSelectedUserIds(newSet); + }; + + const openAssignModal = (users: UnassignedUser[]) => { + if (users.length === 0) return; + setUsersToAssign(users); + setCurrentItem(null); + setModal('assign'); + }; + + const openManageModal = (permission: UserPermissionAssignment) => { + setCurrentItem(permission); + setModal('manage'); + }; + + const handleSavePermissions = async (userIds: string[], permissions: DataSourcePermission[]) => { + try { + const filteredPerms = permissions.filter(p => p.tables.length > 0); + + if (modal === 'assign') { + // 为多个用户分配权限 + const currentUserId = Number(localStorage.getItem('userId') || '1'); + const permissionDetails = filteredPerms.map(p => ({ + db_connection_id: Number(p.dataSourceId), + table_ids: p.tables.map(t => Number(t.replace('table_', ''))), + })); + + for (const userId of userIds) { + await userDbPermissionApi.create({ + userId: Number(userId), + permissionDetails: JSON.stringify(permissionDetails), + isAssigned: 1, + lastGrantUserId: currentUserId, + }); + } + alert('分配权限成功'); + await loadData(); + } else if (modal === 'manage' && currentItem) { + // 更新权限 + const permissionDetails = filteredPerms.map(p => ({ + db_connection_id: Number(p.dataSourceId), + table_ids: p.tables.map(t => Number(t.replace('table_', ''))), + })); + await userDbPermissionApi.update({ + id: Number(currentItem.id), + permissionDetails: JSON.stringify(permissionDetails), + lastGrantUserId: Number(localStorage.getItem('userId') || '1'), + }); + alert('更新权限成功'); + await loadData(); + } + } catch (error) { + console.error('保存权限失败:', error); + alert('保存权限失败: ' + (error instanceof Error ? error.message : '未知错误')); + return; + } + setModal(null); + }; + + const PermissionModal: React.FC<{ + users: { id: string, username: string }[]; + existingPermissions?: DataSourcePermission[]; + onSave: (userIds: string[], permissions: DataSourcePermission[]) => void; + onClose: () => void; + }> = ({ users, existingPermissions = [], onSave, onClose }) => { + const [perms, setPerms] = useState( + dataSources.map(ds => { + const existing = existingPermissions.find(p => p.dataSourceId === String(ds.id)); + return { + dataSourceId: String(ds.id), + dataSourceName: ds.name, + tables: existing ? [...existing.tables] : [] + }; + }) + ); + + const handleTableToggle = (dsId: string, table: string, checked: boolean) => { + setPerms(prev => prev.map(p => { + if (p.dataSourceId === dsId) { + const newTables = new Set(p.tables); + if (checked) newTables.add(table); + else newTables.delete(table); + return { ...p, tables: Array.from(newTables) }; + } + return p; + })); + }; + + const handleSelectAllTables = (dsId: string, checked: boolean) => { + // 简化处理:假设每个数据源有默认的表列表 + // 实际应该从后端获取表列表 + const defaultTables = ['table_1', 'table_2', 'table_3']; // 临时处理 + setPerms(prev => prev.map(p => p.dataSourceId === dsId ? { ...p, tables: checked ? defaultTables : [] } : p)); + }; + + const title = users.length > 1 ? `为 ${users.length} 位用户分配权限` : `为 ${users[0].username} 分配权限`; + const isEditing = existingPermissions.length > 0; + + return ( + +
    + {dataSources.map(ds => { + const currentPerm = perms.find(p => p.dataSourceId === String(ds.id)); + // 简化处理:使用默认表列表,实际应该从后端获取 + const allTablesForDs = ['table_1', 'table_2', 'table_3']; + const allSelected = currentPerm ? currentPerm.tables.length === allTablesForDs.length : false; + + return ( +
    +

    {ds.name}

    +
    + +
    + {allTablesForDs.map(table => ( + + ))} +
    +
    +
    + ) + })} +
    +
    + + +
    +
    + ); + }; + + if (loading) { + return ( +
    +
    +
    + +

    加载中...

    +
    +
    +
    + ); + } + + return ( +
    +
    + + setSearchKeyword(e.target.value)} + className="flex-1 px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/50" + /> +
    + +
    +
    +

    待分配权限用户 ({filteredUnassignedUsers.length})

    + +
    +
    + + + + + + + + + + + + {filteredUnassignedUsers.map(user => ( + + + + + + + + ))} + {filteredUnassignedUsers.length === 0 && ( + + + + )} + +
    + 0 && selectedUserIds.size === filteredUnassignedUsers.length} + /> + 用户名邮箱注册时间操作
    + handleSelectUser(user.id, e.target.checked)} + /> + {user.username}{user.email}{user.regTime} + +
    未找到匹配的待分配用户
    +
    +
    + +
    +

    已分配权限用户 ({filteredAssignedPermissions.length})

    +
    + + + + + + + + + + {filteredAssignedPermissions.map(p => ( + + + + + + ))} + {filteredAssignedPermissions.length === 0 && ( + + + + )} + +
    用户名数据源权限操作
    {p.username} + {p.permissions.map(perm => ( +
    + {perm.dataSourceName}: + {perm.tables.join(', ')} +
    + ))} +
    + +
    未找到匹配的已分配权限用户
    +
    +
    + + {(modal === 'assign' && usersToAssign.length > 0) && ( + setModal(null)} + /> + )} + + {(modal === 'manage' && currentItem) && ( + setModal(null)} + /> + )} +
    + ); +}; \ No newline at end of file diff --git a/src/test/frontend/constants.ts b/src/test/frontend/constants.ts new file mode 100644 index 00000000..560457e5 --- /dev/null +++ b/src/test/frontend/constants.ts @@ -0,0 +1,270 @@ +import { Conversation, QueryResultData, Notification, UserProfile, Friend, FriendRequest, ModelOption, AdminNotification, SystemLog, DataSource, ConnectionLog, PermissionLog, QueryShare } from './types'; + +// Used in QueryPage.tsx +export const MODEL_OPTIONS: ModelOption[] = [ + { name: 'gemini-2.5-pro', disabled: false, description: '最强大的模型,用于复杂查询' }, + { name: 'gemini-2.5-flash', disabled: false, description: '速度最快的模型,用于快速响应' }, + { name: 'GPT-4', disabled: false, description: 'OpenAI 的先进模型' }, + { name: 'GLM-4.6', disabled: false, description: '智谱AI GLM-4 (即将支持)' }, + { name: 'qwen3-max', disabled: false, description: '阿里通义千问 (即将支持)' }, + { name: 'kimi-k2-0905-preview', disabled: false, description: 'kimi-k2-0905-preview K2 (即将支持)' }, +]; + +export const DATABASE_OPTIONS: ModelOption[] = [ + { name: '销售数据库', disabled: false, description: '包含订单、客户和销售数据' }, + { name: '用户数据库', disabled: false, description: '包含用户信息和活动日志' }, + { name: '产品数据库', disabled: false, description: '包含产品目录和库存信息' }, +]; + + +// Mocks for normal user view +export const MOCK_INITIAL_CONVERSATION: Conversation = { + id: 'conv-1', + title: '', + messages: [{ + role: 'ai', + content: '您好!我是数据查询助手,您可以通过自然语言描述您的查询需求(例如:"展示2023年各季度的订单量"),我会为您生成相应的结果。' + }], + createTime: new Date().toISOString(), +}; + +const MOCK_QUERY_RESULT_1: QueryResultData = { + id: 'query-1', + userPrompt: '展示2023年各季度的订单量', + sqlQuery: "SELECT strftime('%Y-Q', order_date) as quarter, COUNT(order_id) as order_count FROM orders WHERE strftime('%Y', order_date) = '2023' GROUP BY quarter ORDER BY quarter;", + conversationId: 'conv-1', + queryTime: new Date('2023-11-20T10:30:00Z').toISOString(), + executionTime: '0.8秒', + tableData: { + headers: ['季度', '订单量', '同比增长'], + rows: [ + ['2023-Q1', '1,200', '+15%'], + ['2023-Q2', '1,550', '+18%'], + ['2023-Q3', '1,400', '+12%'], + ['2023-Q4', '1,850', '+25%'] + ] + }, + chartData: { + type: 'bar', + labels: ['2023-Q1', '2023-Q2', '2023-Q3', '2023-Q4'], + datasets: [{ + label: '订单量', + data: [1200, 1550, 1400, 1850], + backgroundColor: 'rgba(22, 93, 255, 0.6)', + }] + }, + database:"销售数据库", + model:"gemini-2.5-pro", +}; + +const MOCK_QUERY_RESULT_2: QueryResultData = { + id: 'query-2', + userPrompt: '展示2023年各季度的订单量', // Same prompt + sqlQuery: "SELECT strftime('%Y-Q', order_date) as quarter, COUNT(order_id) as order_count FROM orders WHERE strftime('%Y', order_date) = '2023' GROUP BY quarter ORDER BY quarter;", + conversationId: 'conv-2', // Different conversation + queryTime: new Date('2023-11-21T11:00:00Z').toISOString(), // Later time + executionTime: '0.9秒', + tableData: { + headers: ['季度', '订单量', '同比增长'], + rows: [ + // ['2023-Q1', '1,200', '+15%'] is now deleted + ['2023-Q2', '1,600', '+20%'], // Changed value + ['2023-Q3', '1,400', '+12%'], // Same value + ['2023-Q4', '1,850', '+25%'], // Same value + ['2023-Q5 (预测)', '2,100', '+30%'] // Added row + ] + }, + chartData: { + type: 'bar', + labels: ['2023-Q2', '2023-Q3', '2023-Q4', '2023-Q5 (预测)'], // Changed labels + datasets: [{ + label: '订单量', + data: [1600, 1400, 1850, 2100], // Changed data + backgroundColor: 'rgba(54, 162, 235, 0.6)', + }] + }, + database:"销售数据库", + model:"qwen3-max", +}; + +const MOCK_QUERY_RESULT_3: QueryResultData = { + id: 'query-3', + userPrompt: '统计每月新增用户数', // A different prompt + sqlQuery: "SELECT strftime('%Y-%m', registration_date) as month, COUNT(user_id) as new_users FROM users GROUP BY month;", + conversationId: 'conv-3', + queryTime: new Date('2023-11-22T09:00:00Z').toISOString(), + executionTime: '1.1秒', + tableData: { + headers: ['月份', '新增用户数'], + rows: [ + ['2023-09', '5,200'], + ['2023-10', '6,100'], + ] + }, + chartData: { + type: 'line', + labels: ['2023-09', '2023-10'], + datasets: [{ + label: '新增用户数', + data: [5200, 6100], + backgroundColor: 'rgba(75, 192, 192, 0.6)', + }] + }, + database:"产品数据库", + model:"qwen3-max", +} + +const MOCK_QUERY_RESULT_4: QueryResultData = { + id: 'query-4', + userPrompt: '各产品线销售额占比', + sqlQuery: "SELECT product_line, SUM(sales) as total_sales FROM sales_by_product_line GROUP BY product_line;", + conversationId: 'conv-4', + queryTime: new Date('2023-11-23T14:00:00Z').toISOString(), + executionTime: '0.7秒', + tableData: { + headers: ['产品线', '销售额', '占比'], + rows: [ + ['电子产品', '550,000', '55%'], + ['家居用品', '250,000', '25%'], + ['服装配饰', '200,000', '20%'] + ] + }, + chartData: { + type: 'pie', + labels: ['电子产品', '家居用品', '服装配饰'], + datasets: [{ + label: '销售额', + data: [550000, 250000, 200000], + backgroundColor: [ + 'rgba(22, 93, 255, 0.7)', + 'rgba(54, 162, 235, 0.7)', + 'rgba(255, 206, 86, 0.7)', + ], + }] + }, + database:"用户数据库", + model:"qwen3-max", + +}; + + +export const MOCK_SAVED_QUERIES: QueryResultData[] = [MOCK_QUERY_RESULT_1, MOCK_QUERY_RESULT_2, MOCK_QUERY_RESULT_3, MOCK_QUERY_RESULT_4]; + + +export const MOCK_NOTIFICATIONS: Notification[] = [ + { id: '3', type: 'system', title: '系统将在今晚2点进行维护。', content: '系统将在今晚2点进行维护。', timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), isRead: false, isPinned: true }, + { id: '1', type: 'system', title: '新用户 王小明 已注册。', content: '新用户 王小明 已注册。', timestamp: new Date(Date.now() - 5 * 60 * 1000).toISOString(), isRead: false, isPinned: false }, + { id: '2', type: 'system', title: '模型 Gemini 连接失败。', content: '模型 Gemini 连接失败。', timestamp: new Date(Date.now() - 60 * 60 * 1000).toISOString(), isRead: false, isPinned: false }, + { id: 'share-1', type: 'share', title: '李琪雯 分享了一个查询给你', content: '"展示2023年各季度的订单量"', timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), isRead: false, isPinned: false, fromUser: { name: '李琪雯', avatarUrl: 'https://i.pravatar.cc/150?u=li-si' }, relatedShareId: 'share-1' }, +]; + +export const MOCK_USER_PROFILE: UserProfile = { + id: 'user-001', + userId: 'zhangsan', + name: '李瑜清', + email: 'zhangsan@example.com', + phoneNumber: '13812345678', + avatarUrl: 'https://i.pravatar.cc/150?u=zhang-san', + registrationDate: '2024-03-22', + accountStatus: 'normal', + preferences: { + defaultModel: 'gemini-2.5-pro', + defaultDatabase: '销售数据库', + }, +}; + +export const MOCK_FRIENDS_LIST: Friend[] = [{ id: 'friend-1', name: '李琪雯', avatarUrl: 'https://i.pravatar.cc/150?u=li-si', isOnline: true,email:'Maem12129@gmail.com'} + , + { id: 'friend-2', name: '马芳琼', avatarUrl: 'https://i.pravatar.cc/150?u=wang-wu', isOnline: false,email:'DonQuixote@gmail.com' }, +]; + +export const MOCK_FRIEND_REQUESTS: FriendRequest[] = [ + { id: 'req-1', fromUser: { name: '赵文琪', avatarUrl: 'https://i.pravatar.cc/150?u=zhao-liu' }, timestamp: '2小时前' } +]; + +export const MOCK_QUERY_SHARES: QueryShare[] = [ + { + id: 'share-1', + sender: { id: 'friend-1', name: '李琪雯', avatarUrl: 'https://i.pravatar.cc/150?u=li-si', isOnline: true ,email:'Maem12129@gmail.com'}, + recipientId: 'user-001', + querySnapshot: MOCK_QUERY_RESULT_1, + timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), + status: 'unread', + }, + { + id: 'share-2', + sender: { id: 'friend-2', name: '马芳琼', avatarUrl: 'https://i.pravatar.cc/150?u=wang-wu', isOnline: false ,email:'DonQuixote@gmail.com'}, + recipientId: 'user-001', + querySnapshot: MOCK_QUERY_RESULT_3, + timestamp: new Date(Date.now() - 28 * 60 * 60 * 1000).toISOString(), + status: 'read', + } +]; + +// Used in RightSidebar.tsx +export const COMMON_RECOMMENDATIONS = [ + '近7天用户增长趋势', + '上个季度各产品线销售额对比', + '查询华东地区销量最高的产品', + '统计每月新增用户数' +]; + +export const MOCK_SUCCESS_SUGGESTIONS = [ + '按地区细分订单量', + '与去年同期数据进行对比', + '分析各季度订单的平均金额', +]; + +export const MOCK_FAILURE_SUGGESTIONS = [ + '换一种更简单的问法', + '检查是否选择了正确的数据源', + '尝试询问“你能做什么?”', +]; + +// Mocks for Admin Panel +export const MOCK_ADMIN_NOTIFICATIONS: AdminNotification[] = [ + { id: 1, title: '系统将于今晚23:00进行升级维护', content: '...', role: 'all', priority: 'urgent', pinned: true, publisher: '系统管理员', publishTime: '2025-10-28 18:00', status: 'published' }, + { id: 2, title: '【草稿】新功能发布预告', content: '...', role: 'normal-user', priority: 'normal', pinned: false, publisher: '系统管理员', publishTime: '2025-10-27 09:00', status: 'draft' }, +]; + +export const MOCK_DATA_ADMIN_NOTIFICATIONS: AdminNotification[] = [ + { id: 1, title: '销售数据库表结构变更', content: '`orders` 表新增 `discount_rate` 字段。', role: 'data-admin', priority: 'important', pinned: false, publisher: '李琪雯', publishTime: '2025-10-28 10:00', status: 'published', dataSourceTopic: '销售数据库' }, + { id: 2, title: '用户数据库计划下线旧表', content: '`users_old` 表将于11月30日下线,请及时迁移。', role: 'all', priority: 'normal', pinned: false, publisher: '李琪雯', publishTime: '2025-10-26 15:00', status: 'published', dataSourceTopic: '用户数据库' }, +]; + +export const MOCK_SYSTEM_LOGS: SystemLog[] = [ + { id: '#LOG001', time: '2025-10-29 14:32:18', user: '李瑜清', action: '执行自然语言查询', model: 'gemini-2.5-pro', ip: '192.168.1.102', status: 'success' }, + { id: '#LOG002', time: '2025-10-29 14:28:45', user: '李琪雯', action: '添加MySQL数据源', model: '-', ip: '192.168.1.105', status: 'success' }, + { id: '#LOG003', time: '2025-10-29 14:25:10', user: '李瑜清', action: '执行自然语言查询', model: 'GPT-4', ip: '192.168.1.102', status: 'failure', details: 'Error: API call to OpenAI failed with status 429 - Too Many Requests. Please check your plan and billing details.' }, + { id: '#LOG004', time: '2025-10-29 13:50:21', user: '未知用户', action: '尝试登录系统', model: '-', ip: '203.0.113.45', status: 'failure', details: 'Authentication failed: Invalid credentials provided for user "unknown".' }, + { id: '#LOG005', time: '2025-10-29 12:15:05', user: 'admin', action: '更新用户角色', model: '-', ip: '127.0.0.1', status: 'success' }, +]; + +// Mocks for Data Admin Panel +export const MOCK_DATASOURCES: DataSource[] = [ + { id: 'ds-1', name: '销售数据库', type: 'MySQL', address: '192.168.1.101:3306', status: 'connected' }, + { id: 'ds-2', name: '用户数据库', type: 'PostgreSQL', address: '192.168.1.102:5432', status: 'connected' }, + { id: 'ds-3', name: '产品数据库', type: 'MySQL', address: '192.168.1.103:3306', status: 'error' }, + { id: 'ds-4', name: '日志数据库', type: 'SQL Server', address: '192.168.1.104:1433', status: 'disconnected' }, + { id: 'ds-5', name: '库存数据库', type: 'Oracle', address: '192.168.1.105:1521', status: 'disabled' }, +]; + +export const MOCK_CONNECTION_LOGS: ConnectionLog[] = [ + { id: 'log-1', time: '2023-06-15 16:42:30', datasource: '销售数据库', status: '成功' }, + { id: 'log-2', time: '2023-06-15 14:20:15', datasource: '产品数据库', status: '失败', details: "Error: Connection timed out after 15000ms. Could not connect to 192.168.1.103:3306. Please check network connectivity and firewall rules." }, + { id: 'log-3', time: '2023-06-14 11:05:42', datasource: '用户数据库', status: '成功' }, + { id: 'log-4', time: '2023-06-13 09:30:18', datasource: '日志数据库', status: '失败', details: "SQLSTATE[28000]: [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]Login failed for user 'log_reader'." }, + { id: 'log-5', time: new Date(Date.now() - 2 * 60 * 1000).toISOString(), datasource: '产品数据库', status: '失败', details: "Error: Access denied for user 'prod_user'@'localhost' (using password: YES)" } +]; + +export const MOCK_PERMISSION_LOGS: PermissionLog[] = [ + { id: 'plog-1', timestamp: new Date(Date.now() - 5 * 60 * 1000).toISOString(), text: '管理员 李琪雯 授予 李瑜清 "销售数据库" 的访问权限。' }, + { id: 'plog-2', timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), text: '管理员 李琪雯 撤销了 马芳琼 对 "产品数据库" 的所有权限。' }, + { id: 'plog-3', timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), text: '管理员 李琪雯 修改了 李瑜清 对 "销售数据库" 的 orders 表权限。' }, + { id: 'plog-4', timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(), text: '系统自动为新用户 赵文琪 分配了默认权限。' }, +]; + +export const MOCK_QUERY_LOAD = { + labels: ['销售数据库', '用户数据库', '产品数据库', '日志数据库', '库存数据库'], + data: [1250, 890, 650, 320, 150] +}; \ No newline at end of file diff --git a/src/test/frontend/env.d.ts b/src/test/frontend/env.d.ts new file mode 100644 index 00000000..391b89c9 --- /dev/null +++ b/src/test/frontend/env.d.ts @@ -0,0 +1,14 @@ +/// + +interface ImportMetaEnv { + // 与 .env.local 中的变量一一对应 + readonly VITE_GEMINI_API_KEY: string; + readonly VITE_OPENAI_API_KEY: string; + readonly VITE_GLM_API_KEY: string; + readonly VITE_QWEN_API_KEY: string; + readonly VITE_KIMI_API_KEY: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} \ No newline at end of file diff --git a/src/test/frontend/index.html b/src/test/frontend/index.html new file mode 100644 index 00000000..749fd6d4 --- /dev/null +++ b/src/test/frontend/index.html @@ -0,0 +1,66 @@ + + + + + + 自然语言数据库查询系统 + + + + + + + + +
    + + + \ No newline at end of file diff --git a/src/test/frontend/index.tsx b/src/test/frontend/index.tsx new file mode 100644 index 00000000..aaa0c6e4 --- /dev/null +++ b/src/test/frontend/index.tsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +const rootElement = document.getElementById('root'); +if (!rootElement) { + throw new Error("Could not find root element to mount to"); +} + +const root = ReactDOM.createRoot(rootElement); +root.render( + + + +); diff --git a/src/test/frontend/metadata.json b/src/test/frontend/metadata.json new file mode 100644 index 00000000..c73209e9 --- /dev/null +++ b/src/test/frontend/metadata.json @@ -0,0 +1,5 @@ +{ + "name": "Natural Language Database Query System", + "description": "A web application that allows users to query databases using natural language. It provides a chat-based interface, displays results in tables and charts, and manages conversation history.", + "requestFramePermissions": [] +} \ No newline at end of file diff --git a/src/test/frontend/package-lock.json b/src/test/frontend/package-lock.json new file mode 100644 index 00000000..ee222760 --- /dev/null +++ b/src/test/frontend/package-lock.json @@ -0,0 +1,2630 @@ +{ + "name": "natural-language-database-query-system", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "natural-language-database-query-system", + "version": "0.0.0", + "dependencies": { + "@google/genai": "^1.28.0", + "chart.js": "^4.5.1", + "openai": "^6.8.1", + "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.30.0.tgz", + "integrity": "sha512-3MRcgczBFbUat1wIlZoLJ0vCCfXgm7Qxjh59cZi2X08RgWLtm9hKOspzp7TOg1TV2e26/MLxR2GR5yD5GmBV2w==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.20.1" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.32.tgz", + "integrity": "sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.262", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.262.tgz", + "integrity": "sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gaxios": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz", + "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2", + "rimraf": "^5.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz", + "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^8.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/openai": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.9.1.tgz", + "integrity": "sha512-vQ5Rlt0ZgB3/BNmTa7bIijYFhz3YBceAA3Z4JuoMSBftBF9YqFHIEhZakSs+O/Ad7EaoEimZvHxD5ylRjN11Lg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.0" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "license": "ISC", + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/src/test/frontend/package.json b/src/test/frontend/package.json new file mode 100644 index 00000000..54b7bc61 --- /dev/null +++ b/src/test/frontend/package.json @@ -0,0 +1,25 @@ +{ + "name": "natural-language-database-query-system", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@google/genai": "^1.28.0", + "chart.js": "^4.5.1", + "openai": "^6.8.1", + "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@types/node": "^22.14.0", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.2", + "vite": "^6.2.0" + } +} diff --git a/src/test/frontend/services/api.ts b/src/test/frontend/services/api.ts new file mode 100644 index 00000000..1177a375 --- /dev/null +++ b/src/test/frontend/services/api.ts @@ -0,0 +1,364 @@ +// API 基础配置 +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'; + +// 从localStorage获取token +const getToken = (): string | null => { + return localStorage.getItem('token'); +}; + +// 从localStorage获取userId +const getUserId = (): string | null => { + return localStorage.getItem('userId'); +}; + +// 通用请求函数 +async function request( + endpoint: string, + options: RequestInit = {} +): Promise { + const token = getToken(); + const userId = getUserId(); + + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + // 添加认证token + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + // 添加userId header(如果后端需要) + if (userId) { + headers['userId'] = userId; + } + + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + ...options, + headers, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ message: '请求失败' })); + throw new Error(errorData.message || `HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + // 处理统一的Result格式 + if (data.code !== undefined) { + if (data.code === 200 || data.code === 0) { + return data.data as T; + } else { + throw new Error(data.message || '请求失败'); + } + } + + return data as T; +} + +// ==================== 认证接口 ==================== +export interface LoginRequest { + username: string; + password: string; +} + +export interface LoginResponse { + token: string; + userId: number; + username: string; + email: string; + roleId: number; + roleName: string; + avatarUrl: string; +} + +export const authApi = { + login: async (credentials: LoginRequest): Promise => { + const response = await request('/auth/login', { + method: 'POST', + body: JSON.stringify(credentials), + }); + + // 保存token和用户信息 + if (response.token) { + localStorage.setItem('token', response.token); + localStorage.setItem('userId', String(response.userId)); + localStorage.setItem('username', response.username); + localStorage.setItem('roleId', String(response.roleId)); + localStorage.setItem('roleName', response.roleName || ''); + } + + return response; + }, + + logout: () => { + localStorage.removeItem('token'); + localStorage.removeItem('userId'); + localStorage.removeItem('username'); + localStorage.removeItem('roleId'); + localStorage.removeItem('roleName'); + }, + + isAuthenticated: (): boolean => { + return !!getToken(); + }, +}; + +// ==================== 查询接口 ==================== +export interface QueryRequest { + userPrompt: string; + model: string; + database: string; + conversationId?: string; +} + +export interface QueryResponse { + id: string; + userPrompt: string; + sqlQuery: string; + conversationId: string; + queryTime: string; + executionTime: string; + database: string; + model: string; + tableData: { + headers: string[]; + rows: string[][]; + }; + chartData?: { + type: string; + labels: string[]; + datasets: Array<{ + label: string; + data: number[]; + backgroundColor?: string | string[]; + }>; + }; +} + +export const queryApi = { + execute: async (queryRequest: QueryRequest): Promise => { + return await request('/query/execute', { + method: 'POST', + body: JSON.stringify(queryRequest), + }); + }, +}; + +// ==================== 对话接口 ==================== +export interface DialogRecord { + dialogId: string; + userId: number; + topic: string; + totalRounds: number; + startTime: string; + lastTime: string; +} + +export const dialogApi = { + getList: async (): Promise => { + return await request('/dialog/list'); + }, + + getById: async (dialogId: string): Promise => { + return await request(`/dialog/${dialogId}`); + }, +}; + +// ==================== 用户接口 ==================== +export interface User { + id: number; + username: string; + email: string; + phonenumber: string; + roleId: number; + avatarUrl: string; + status: number; +} + +export const userApi = { + getById: async (id: number): Promise => { + return await request(`/user/${id}`); + }, + + getList: async (): Promise => { + return await request('/user/list'); + }, + + getByUsername: async (username: string): Promise => { + return await request(`/user/username/${username}`); + }, + + create: async (user: Partial): Promise => { + return await request('/user', { + method: 'POST', + body: JSON.stringify(user), + }); + }, + + update: async (user: Partial): Promise => { + return await request('/user', { + method: 'PUT', + body: JSON.stringify(user), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/user/${id}`, { + method: 'DELETE', + }); + }, +}; + +// ==================== 数据库连接接口 ==================== +export interface DbConnection { + id: number; + name: string; + dbTypeId: number; + url: string; + username: string; + password?: string; + status: string; + createUserId: number; +} + +export const dbConnectionApi = { + getList: async (): Promise => { + return await request('/db-connection/list'); + }, + + getById: async (id: number): Promise => { + return await request(`/db-connection/${id}`); + }, + + getListByUser: async (createUserId: number): Promise => { + return await request(`/db-connection/list/${createUserId}`); + }, + + create: async (dbConnection: Partial): Promise => { + return await request('/db-connection', { + method: 'POST', + body: JSON.stringify(dbConnection), + }); + }, + + update: async (dbConnection: Partial): Promise => { + return await request('/db-connection', { + method: 'PUT', + body: JSON.stringify(dbConnection), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/db-connection/${id}`, { + method: 'DELETE', + }); + }, + + test: async (id: number): Promise => { + return await request(`/db-connection/test/${id}`); + }, +}; + +// ==================== 大模型配置接口 ==================== +export interface LlmConfig { + id: number; + name: string; + version: string; + apiKey?: string; + apiUrl: string; + statusId: number; + isDisabled: number; + timeout: number; +} + +export const llmConfigApi = { + getList: async (): Promise => { + return await request('/llm-config/list'); + }, + + getAvailable: async (): Promise => { + return await request('/llm-config/list/available'); + }, + + getById: async (id: number): Promise => { + return await request(`/llm-config/${id}`); + }, + + create: async (llmConfig: Partial): Promise => { + return await request('/llm-config', { + method: 'POST', + body: JSON.stringify(llmConfig), + }); + }, + + update: async (llmConfig: Partial): Promise => { + return await request('/llm-config', { + method: 'PUT', + body: JSON.stringify(llmConfig), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/llm-config/${id}`, { + method: 'DELETE', + }); + }, + + toggle: async (id: number): Promise => { + return await request(`/llm-config/${id}/toggle`, { + method: 'PUT', + }); + }, +}; + +// ==================== 用户数据权限接口 ==================== +export interface UserDbPermission { + id: number; + userId: number; + permissionDetails: string; // JSON字符串 + isAssigned: number; + lastGrantUserId: number; + lastGrantTime?: string; +} + +export const userDbPermissionApi = { + getList: async (): Promise => { + return await request('/user-db-permission/list'); + }, + + getAssigned: async (): Promise => { + return await request('/user-db-permission/list/assigned'); + }, + + getUnassigned: async (): Promise => { + return await request('/user-db-permission/list/unassigned'); + }, + + getByUserId: async (userId: number): Promise => { + return await request(`/user-db-permission/user/${userId}`); + }, + + create: async (permission: Partial): Promise => { + return await request('/user-db-permission', { + method: 'POST', + body: JSON.stringify(permission), + }); + }, + + update: async (permission: Partial): Promise => { + return await request('/user-db-permission', { + method: 'PUT', + body: JSON.stringify(permission), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/user-db-permission/${id}`, { + method: 'DELETE', + }); + }, +}; + diff --git a/src/test/frontend/tsconfig.json b/src/test/frontend/tsconfig.json new file mode 100644 index 00000000..531df722 --- /dev/null +++ b/src/test/frontend/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2022", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + "types": [ + "node" + ], + "moduleResolution": "bundler", + "isolatedModules": true, + "moduleDetection": "force", + "allowJs": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "./*" + ] + }, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": ["*.d.ts", "**/*.ts", "**/*.tsx"] +} \ No newline at end of file diff --git a/src/test/frontend/types.ts b/src/test/frontend/types.ts new file mode 100644 index 00000000..00b7e4ca --- /dev/null +++ b/src/test/frontend/types.ts @@ -0,0 +1,193 @@ +// Basic types +export type UserRole = 'sys-admin' | 'data-admin' | 'normal-user'; +export type MessageRole = 'user' | 'ai'; + +// Page navigation types +export type Page = 'query' | 'history' | 'notifications' | 'account' | 'friends' | 'comparison'; +export type SysAdminPageType = 'dashboard' | 'user-management' | 'notification-management' | 'system-log' | 'llm-config' | 'account'; +export type DataAdminPageType = 'dashboard' | 'query' | 'history' | 'datasource' | 'user-permission' | 'notification-management' | 'connection-log' | 'notifications' | 'account' | 'friends' | 'comparison'; + +// Data structure types +export interface ModelOption { + name: string; + disabled: boolean; + description: string; +} + +export interface ChartData { + type: 'bar' | 'line' | 'pie'; + labels: string[]; + datasets: { + label: string; + data: number[]; + backgroundColor: string | string[]; + }[]; +} + +export interface TableData { + headers: string[]; + rows: string[][]; +} + +export interface QueryResultData { + id: string; + userPrompt: string; + sqlQuery: string; + conversationId: string; + queryTime: string; + executionTime: string; + tableData: TableData; + chartData: ChartData; + database: string; + model:string; +} + +export interface Message { + role: MessageRole; + content: string | QueryResultData; +} + +export interface Conversation { + id: string; + title: string; + messages: Message[]; + createTime: string; +} + +export interface Notification { + id: string; + type: 'system' | 'share'; + title: string; + content: string; + timestamp: string; + isRead: boolean; + isPinned: boolean; + fromUser?: { name: string, avatarUrl: string }; + relatedShareId?: string; +} + +export interface UserProfile { + id: string; + userId: string; + name: string; + email: string; + phoneNumber: string; + avatarUrl: string; + registrationDate: string; + accountStatus: 'normal' | 'disabled'; + preferences: { + defaultModel: string; + defaultDatabase: string; + }; +} + +export interface Friend { + id: string; + name: string; + avatarUrl: string; + isOnline: boolean; + email: string; + remark?: string; +} + +export interface FriendRequest { + id: string; + fromUser: { name: string; avatarUrl: string }; + timestamp: string; +} + +export interface QueryShare { + id: string; + sender: Friend; + recipientId: string; // The ID of the user receiving the share + querySnapshot: QueryResultData; + timestamp: string; + status: 'unread' | 'read'; +} + + +// Admin Panel Types +export interface AdminNotification { + id: number; + title: string; + content: string; + role: 'all' | UserRole; + priority: 'urgent' | 'important' | 'normal'; + pinned: boolean; + publisher: string; + publishTime: string; + status: 'published' | 'draft'; + dataSourceTopic?: string; +} + +export interface AdminUser { + id: number; + username: string; + role: UserRole; + email: string; + regTime: string; + status: 'active' | 'disabled'; +} + +export interface SystemLog { + id: string; + time: string; + user: string; + action: string; + model: string; + ip: string; + status: 'success' | 'failure'; + details?: string; +} + +export interface LLMConfig { + id: string; + name: string; + version: string; + apiKey: string; + endpoint: string; + status: 'available' | 'unstable' | 'unavailable' | 'testing' | 'disabled'; +} + +// Data Admin Types +export interface DataSource { + id: string; + name: string; + type: 'MySQL' | 'PostgreSQL' | 'Oracle' | 'SQL Server'; + address: string; + status: 'connected' | 'disconnected' | 'error' | 'testing' | 'disabled'; +} + +export interface DataSourcePermission { + dataSourceId: string; + dataSourceName: string; + tables: string[]; +} + +export interface UserPermissionAssignment { + id: string; + userId: string; + username: string; + permissions: DataSourcePermission[]; +} + +export interface UnassignedUser { + id: string; + username: string; + email: string; + regTime: string; +} + +export interface ConnectionLog { + id: string; + time: string; + datasource: string; + status: '成功' | '失败'; + details?: string; +} + +export interface PermissionLog { + id: string; + timestamp: string; + text: string; +} \ No newline at end of file diff --git a/src/test/frontend/vite.config.ts b/src/test/frontend/vite.config.ts new file mode 100644 index 00000000..427839e2 --- /dev/null +++ b/src/test/frontend/vite.config.ts @@ -0,0 +1,35 @@ +import path from 'path'; +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, '.', ''); // 加载环境变量 + return { + server: { + port: 3000, + host: '0.0.0.0', + proxy: { + // 代理通义千问请求(关键配置) + '/api/qwen': { + target: 'https://dashscope.aliyuncs.com/compatible-mode/v1', + changeOrigin: true, // 开启跨域代理 + rewrite: (path) => path.replace(/^\/api\/qwen/, ''), // 重写路径 + headers: { + // 注入API密钥(仅开发环境安全,生产需用后端代理) + Authorization: `Bearer ${env.VITE_QWEN_API_KEY}`, + }, + }, + }, + }, + plugins: [react()], + define: { + 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), + 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) + }, + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + } + } + }; +}); \ No newline at end of file diff --git a/src/test/last.md b/src/test/last.md new file mode 100644 index 00000000..b71fb456 --- /dev/null +++ b/src/test/last.md @@ -0,0 +1,507 @@ +### 一、完整表/集合清单(按MySQL + MongoDB分类) +#### (一)MySQL 8.4 数据表(结构化数据) +##### 1. 基础信息表(用户、角色、字典) +1.1 users(用户表) +1.2 roles(角色表) +1.3 db_types(数据库类型表) +1.4 notification_targets(通知目标表) +1.5 priorities(通知优先级表) +1.6 error_types(错误类型表) +1.7 llm_status(大模型状态表) + +##### 2. 数据资源表(数据库、表、字段元数据) +2.1 db_connections(数据库连接表) +2.2 table_metadata(表元数据表) +2.3 column_metadata(字段元数据表) + +##### 3. 权限与关系表(用户权限、好友关系) +3.1 user_db_permissions(用户数据权限表) +3.2 friend_relations(好友关系表) +3.3 friend_requests(好友请求表) +3.4 query_shares(查询分享记录表) + +##### 4. 系统日志与监控表 +4.1 system_health(系统健康表) +4.2 operation_logs(系统操作日志表) +4.3 llm_configs(大模型配置表) +4.4 notifications(通知表) +4.5 token_consume(token消耗表) +4.6 error_logs(错误分析表) +4.7 performance_metrics(性能趋势表) +4.8 db_connection_logs(数据库连接日志表) +4.9 query_logs(查询日志表) +4.10 user_searches(用户搜索表) + +#### (二)MongoDB 8.2 集合(非结构化数据) +1. query_collections(收藏查询表-组) +2. collection_records(收藏记录表-具体记录) +3. dialog_records(多轮对话列表) +4. dialog_details(多轮对话具体内容表) +5. sql_cache(SQL缓存集合) +6. ai_interaction_logs(AI交互日志集合) +7. friend_chats(好友聊天记录表) + +--- + +### 二、表/集合详细字段(严格遵循文档) +#### (一)MySQL 数据表字段 +##### 1. 基础信息表 +###### 1.1 users(用户表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|--------------------------|--------------------------------------------|-----------------------------------------------------------| +| id | 用户ID | bigint(20) | 是(自增) | - | 唯一标识用户 | PRIMARY KEY (id) | +| username | 用户名 | varchar(50) | - | - | 登录账号,唯一 | UNIQUE (username), NOT NULL | +| password | 密码 | varchar(100)| - | - | BCrypt加密存储 | NOT NULL | +| email | 邮箱 | varchar(100)| - | - | 用于好友添加、通知 | UNIQUE (email), NOT NULL | +| phonenumber | 手机号 | varchar(50) | - | - | 用户绑定的手机号 | UNIQUE (phonenumber), NOT NULL | +| role_id | 角色ID | tinyint(2) | - | 是(关联roles.id) | 关联角色类型 | NOT NULL, FOREIGN KEY (role_id) REFERENCES roles(id) | +| avatar_url | 头像URL | varchar(255)| - | - | 头像路径,默认"/default-avatar.png" | DEFAULT '/default-avatar.png' | +| status | 账号状态 | tinyint(1) | - | - | 0-禁用,1-正常 | DEFAULT 1, NOT NULL | +| online_status | 在线状态 | tinyint(1) | - | - | 0-离线,1-在线 | DEFAULT 0, NOT NULL | +| create_time | 创建时间 | datetime | - | - | 账号创建时间(系统管理员创建) | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| update_time | 更新时间 | datetime | - | - | 信息更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | + +###### 1.2 roles(角色表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 角色ID | tinyint(2) | 是(自增) | - | 唯一标识角色 | PRIMARY KEY (id) | +| role_name | 角色名称 | varchar(30) | - | - | 如"系统管理员"、"数据管理员"、"普通用户" | UNIQUE (role_name), NOT NULL | +| role_code | 角色编码 | varchar(20) | - | - | 如"sys_admin"、"data_admin"、"normal_user" | UNIQUE (role_code), NOT NULL | +| description | 角色描述 | varchar(500)| - | - | 角色权限范围说明 | - | + +###### 1.3 db_types(数据库类型表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 类型ID | tinyint(2) | 是(自增) | - | 唯一标识数据库类型 | PRIMARY KEY (id) | +| type_name | 类型名称 | varchar(50) | - | - | 如"MySQL"、"MongoDB"、"SQL Server" | UNIQUE (type_name), NOT NULL | +| type_code | 类型编码 | varchar(20) | - | - | 如"mysql"、"mongodb"、"mssql" | UNIQUE (type_code), NOT NULL | +| description | 类型描述 | varchar(500)| - | - | 数据库特性说明 | - | + +###### 1.4 notification_targets(通知目标表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 目标ID | tinyint(2) | 是(自增) | - | 唯一标识通知目标 | PRIMARY KEY (id) | +| target_name | 目标名称 | varchar(30) | - | - | 如"所有用户"、"普通用户" | UNIQUE (target_name), NOT NULL | +| target_code | 目标编码 | varchar(20) | - | - | 如"all"、"normal_user" | UNIQUE (target_code), NOT NULL | +| description | 目标描述 | varchar(200)| - | - | 接收范围说明 | - | + +###### 1.5 priorities(通知优先级表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 优先级ID | tinyint(2) | 是(自增) | - | 唯一标识优先级 | PRIMARY KEY (id) | +| priority_name | 优先级名称 | varchar(20) | - | - | 如"紧急"、"普通"、"低" | UNIQUE (priority_name), NOT NULL | +| priority_code | 优先级编码 | varchar(20) | - | - | 如"urgent"、"normal"、"low" | UNIQUE (priority_code), NOT NULL | +| sort | 排序权重 | int(11) | - | - | 数值越小越优先展示(1-紧急,2-普通) | NOT NULL | + +###### 1.6 error_types(错误类型表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 错误ID | tinyint(2) | 是(自增) | - | 唯一标识错误类型 | PRIMARY KEY (id) | +| error_name | 错误名称 | varchar(50) | - | - | 如"模型调用超时"、"数据库连接错误" | UNIQUE (error_name), NOT NULL | +| error_code | 错误编码 | varchar(50) | - | - | 如"llm_timeout"、"db_connection_error" | UNIQUE (error_code), NOT NULL | +| description | 错误描述 | varchar(500)| - | - | 错误原因说明 | - | + +###### 1.7 llm_status(大模型状态表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 状态ID | tinyint(2) | 是(自增) | - | 唯一标识大模型状态 | PRIMARY KEY (id) | +| status_name | 状态名称 | varchar(20) | - | - | 如"可用"、"不可用"、"不稳定" | UNIQUE (status_name), NOT NULL | +| status_code | 状态编码 | varchar(20) | - | - | 如"available"、"unavailable"、"unstable" | UNIQUE (status_code), NOT NULL | +| description | 状态描述 | varchar(200)| - | - | 状态含义说明(如"可用:API成功率≥95%") | - | + +##### 2. 数据资源表 +###### 2.1 db_connections(数据库连接表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|--------------|-------------|------------|--------------------------|--------------------------------------------|-----------------------------------------------------------| +| id | 连接ID | bigint(20) | 是(自增) | - | 唯一标识数据库连接 | PRIMARY KEY (id) | +| name | 连接名称 | varchar(100)| - | - | 用户自定义名称(如"订单主库") | UNIQUE (name), NOT NULL | +| db_type_id | 数据库类型ID | tinyint(2) | - | 是(关联db_types.id) | 关联数据库类型 | NOT NULL, FOREIGN KEY (db_type_id) REFERENCES db_types(id) | +| url | 连接地址 | varchar(255)| - | - | 如"192.168.1.101:3306/orders_db" | NOT NULL | +| username | 数据库账号 | varchar(50) | - | - | 访问数据库的账号(加密存储) | NOT NULL | +| password | 数据库密码 | varchar(100)| - | - | 访问数据库的密码(加密存储) | NOT NULL | +| status | 连接状态 | varchar(20) | - | - | "connected"-已连接、"error"-连接错误、"disabled"-已禁用 | DEFAULT 'disconnected', NOT NULL | +| create_user_id | 创建者ID | bigint(20) | - | 是(关联users.id) | 仅数据管理员可创建 | NOT NULL, FOREIGN KEY (create_user_id) REFERENCES users(id) | +| create_time | 创建时间 | datetime | - | - | 连接创建时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| update_time | 更新时间 | datetime | - | - | 连接信息更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | + +###### 2.2 table_metadata(表元数据表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|------------------|-------------|------------|------------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 表ID | bigint(20) | 是(自增) | - | 唯一标识表元数据 | PRIMARY KEY (id) | +| db_connection_id | 数据库连接ID | bigint(20) | - | 是(关联db_connections.id) | 所属数据库连接 | NOT NULL, FOREIGN KEY (db_connection_id) REFERENCES db_connections(id) ON DELETE CASCADE | +| table_name | 表名 | varchar(100)| - | - | 数据库中实际表名(如"orders_2023") | NOT NULL | +| description | 表描述 | varchar(500)| - | - | 用于AI理解表含义(如"存储2023年订单数据") | - | +| create_time | 创建时间 | datetime | - | - | 元数据录入时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| update_time | 更新时间 | datetime | - | - | 元数据更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | + +###### 2.3 column_metadata(字段元数据表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|------------------|-------------|------------|----------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 字段ID | bigint(20) | 是(自增) | - | 唯一标识字段元数据 | PRIMARY KEY (id) | +| table_id | 表ID | bigint(20) | - | 是(关联table_metadata.id) | 所属表 | NOT NULL, FOREIGN KEY (table_id) REFERENCES table_metadata(id) ON DELETE CASCADE | +| column_name | 字段名 | varchar(100)| - | - | 表中实际字段名(如"order_amount") | NOT NULL | +| data_type | 数据类型 | varchar(50) | - | - | 字段数据类型(如"int"、"varchar(50)") | NOT NULL | +| description | 字段描述 | varchar(500)| - | - | 用于AI理解字段含义(如"订单金额,单位:元") | - | +| is_primary | 是否主键 | tinyint(1) | - | - | 0-否,1-是 | DEFAULT 0, NOT NULL | +| create_time | 创建时间 | datetime | - | - | 元数据录入时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +##### 3. 权限与关系表 +###### 3.1 user_db_permissions(用户数据权限表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 权限ID | bigint(20) | 是(自增) | - | 唯一标识权限记录 | PRIMARY KEY (id) | +| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 被授权用户(普通用户) | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | +| permission_details | 权限详情 | json | - | - | 可访问的表列表,格式:[{"db_connection_id":1, "table_ids":[1,2]},...] | NOT NULL | +| last_grant_user_id | 最后授权管理员ID | bigint(20) | - | 是(关联users.id) | 最后修改权限的数据管理员/系统管理员 | NOT NULL, FOREIGN KEY (last_grant_user_id) REFERENCES users(id) | +| is_assigned | 是否已分配 | tinyint(1) | - | - | 0-未分配,1-已分配(默认0) | DEFAULT 0, NOT NULL | +| last_grant_time | 最后授权时间 | datetime | - | - | 最后一次权限修改时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| update_time | 更新时间 | datetime | - | - | 记录更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | + +###### 3.2 friend_relations(好友关系表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 关系ID | bigint(20) | 是(自增) | - | 唯一标识好友关系 | PRIMARY KEY (id) | +| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 主动添加方 | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | +| friend_id | 好友ID | bigint(20) | - | 是(关联users.id) | 被动添加方 | NOT NULL, FOREIGN KEY (friend_id) REFERENCES users(id) ON DELETE CASCADE | +| friend_username | 好友用户名 | varchar(50) | - | - | 冗余存储,便于列表展示 | NOT NULL | +| online_status | 好友在线状态 | tinyint(1) | - | - | 0-离线,1-在线 | DEFAULT 0, NOT NULL | +| remark_name | 备注名 | varchar(50) | - | - | 用户对好友的自定义备注 | - | +| create_time | 添加时间 | datetime | - | - | 好友关系建立时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +###### 3.3 friend_requests(好友请求表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 请求ID | bigint(20) | 是(自增) | - | 唯一标识好友请求 | PRIMARY KEY (id) | +| applicant_id | 申请人ID | bigint(20) | - | 是(关联users.id) | 发起请求的用户 | NOT NULL, FOREIGN KEY (applicant_id) REFERENCES users(id) ON DELETE CASCADE | +| recipient_id | 接收人ID | bigint(20) | - | 是(关联users.id) | 收到请求的用户 | NOT NULL, FOREIGN KEY (recipient_id) REFERENCES users(id) ON DELETE CASCADE | +| apply_msg | 申请留言 | varchar(200)| - | - | 申请人留言(如"我是张三,加个好友") | - | +| status | 请求状态 | tinyint(1) | - | - | 0-待处理,1-已同意,2-已拒绝 | DEFAULT 0, NOT NULL | +| create_time | 申请时间 | datetime | - | - | 请求发起时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| handle_time | 处理时间 | datetime | - | - | 接收人处理时间(未处理为NULL) | - | + +###### 3.4 query_shares(查询分享记录表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 分享ID | bigint(20) | 是(自增) | - | 唯一标识分享记录 | PRIMARY KEY (id) | +| share_user_id | 分享人ID | bigint(20) | - | 是(关联users.id) | 发起分享的用户 | NOT NULL, FOREIGN KEY (share_user_id) REFERENCES users(id) ON DELETE CASCADE | +| receive_user_id | 接收人ID | bigint(20) | - | 是(关联users.id) | 接收分享的好友 | NOT NULL, FOREIGN KEY (receive_user_id) REFERENCES users(id) ON DELETE CASCADE | +| dialog_id | 会话ID | varchar(50) | - | - | 关联MongoDB的dialog_details集合,定位具体对话文档 | NOT NULL | +| target_rounds | 会话轮次数组 | json | - | - | 筛选指定轮次序号(roundNum)的元素 | NOT NULL | +| query_title | 查询标题 | varchar(200)| - | - | 冗余存储,便于接收人查看 | NOT NULL | +| share_time | 分享时间 | datetime | - | - | 分享发起时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| receive_status | 接收状态 | tinyint(1) | - | - | 0-未处理,1-已保存,2-已删除 | DEFAULT 0, NOT NULL | + +##### 4. 系统日志与监控表 +###### 4.1 system_health(系统健康表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|------|--------------------------------------------|---------------------------| +| id | 记录ID | bigint(20) | 是(自增) | - | 唯一标识健康记录 | PRIMARY KEY (id) | +| db_delay | 数据库延迟 | int(11) | - | - | 数据库服务响应延迟(ms) | NOT NULL | +| cache_delay | 缓存延迟 | int(11) | - | - | 缓存服务响应延迟(ms) | NOT NULL | +| llm_delay | 大模型延迟 | int(11) | - | - | 大模型API响应延迟(ms) | NOT NULL | +| storage_usage | 存储使用率 | decimal(5,2)| - | - | 存储服务使用率(%,如95.50) | NOT NULL | +| collect_time | 采集时间 | datetime | - | - | 数据采集时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +###### 4.2 operation_logs(系统操作日志表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 日志ID | bigint(20) | 是(自增) | - | 唯一标识操作日志 | PRIMARY KEY (id) | +| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 操作人ID(匿名操作为0) | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) | +| username | 用户名 | varchar(50) | - | - | 操作人用户名(冗余,便于查询) | NOT NULL | +| operation | 操作名称 | varchar(100)| - | - | 如"创建数据库连接"、"分配用户权限" | NOT NULL | +| module | 操作模块 | varchar(50) | - | - | 如"数据源管理"、"用户管理" | NOT NULL | +| related_llm | 涉及模型 | varchar(50) | - | - | 关联大模型名称(无则为NULL) | - | +| ip_address | IP地址 | varchar(50) | - | - | 操作人IP地址 | NOT NULL | +| operate_time | 操作时间 | datetime | - | - | 操作执行时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| result | 操作结果 | tinyint(1) | - | - | 0-失败,1-成功 | NOT NULL | +| error_msg | 错误信息 | text | - | - | 失败原因(成功为NULL) | - | + +###### 4.3 llm_configs(大模型配置表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 配置ID | bigint(20) | 是(自增) | - | 唯一标识大模型配置 | PRIMARY KEY (id) | +| name | 模型名称 | varchar(50) | - | - | 如"智谱AI"、"通义千问" | UNIQUE (name), NOT NULL | +| version | 模型版本 | varchar(20) | - | - | 如"4.0"、"3.0" | NOT NULL | +| api_key | API密钥 | varchar(200)| - | - | 大模型API Key(AES加密) | NOT NULL | +| api_url | API地址 | varchar(255)| - | - | 调用地址(如"https://api.zhipuai.com/v4/chat/completions") | NOT NULL | +| status_id | 状态ID | tinyint(2) | - | 是(关联llm_status.id) | 模型状态(可用/不可用/不稳定) | NOT NULL, FOREIGN KEY (status_id) REFERENCES llm_status(id) | +| is_disabled | 是否禁用 | tinyint(1) | - | - | 0-启用,1-禁用(强制下线) | DEFAULT 0, NOT NULL | +| timeout | 超时时间 | int(11) | - | - | API调用超时阈值(ms,如5000) | NOT NULL | +| create_user_id | 创建人ID | bigint(20) | - | 是(关联users.id) | 仅系统管理员可创建 | NOT NULL, FOREIGN KEY (create_user_id) REFERENCES users(id) | +| create_time | 创建时间 | datetime | - | - | 配置创建时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| update_time | 更新时间 | datetime | - | - | 配置更新时间 | DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL | + +###### 4.4 notifications(通知表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 通知ID | bigint(20) | 是(自增) | - | 唯一标识通知 | PRIMARY KEY (id) | +| title | 通知标题 | varchar(100)| - | - | 通知标题(如"系统维护通知") | NOT NULL | +| content | 通知内容 | text | - | - | 通知详细内容(支持简单HTML) | NOT NULL | +| target_id | 目标ID | tinyint(2) | - | 是(关联notification_targets.id) | 接收目标(所有用户/指定角色) | NOT NULL, FOREIGN KEY (target_id) REFERENCES notification_targets(id) | +| priority_id | 优先级ID | tinyint(2) | - | 是(关联priorities.id) | 通知优先级(紧急/普通/低) | NOT NULL, FOREIGN KEY (priority_id) REFERENCES priorities(id) | +| publisher_id | 发布者ID | bigint(20) | - | 是(关联users.id) | 发布通知的管理员 | NOT NULL, FOREIGN KEY (publisher_id) REFERENCES users(id) | +| is_top | 是否置顶 | tinyint(1) | - | - | 0-否,1-是(置顶优先展示) | DEFAULT 0, NOT NULL | +| publish_time | 发布时间 | datetime | - | - | 发布时间(草稿为NULL) | - | +| create_time | 创建时间 | datetime | - | - | 草稿创建时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| Latest_updateTime | 上次更新时间 | datetime | - | - | 最近一次编辑并保存的时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +###### 4.5 token_consume(token消耗表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|------|--------------------------------------------|---------------------------------------------------------------| +| id | 记录ID | bigint(20) | 是(自增) | - | 唯一标识消耗记录 | PRIMARY KEY (id) | +| llm_name | 模型名称 | varchar(50) | - | - | 消耗token的大模型 | NOT NULL | +| total_tokens | 总消耗 | int(11) | - | - | 当日总消耗(输入+输出) | NOT NULL | +| prompt_tokens | 输入消耗 | int(11) | - | - | 当日输入token消耗 | NOT NULL | +| completion_tokens | 输出消耗 | int(11) | - | - | 当日输出token消耗 | NOT NULL | +| consume_date | 消耗日期 | date | - | - | 消耗日期(如"2025-11-05") | NOT NULL | +| growth_rate | 增长率 | decimal(5,2)| - | - | 较前一日增长率(%,如+12.50) | - | + +###### 4.6 error_logs(错误分析表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 记录ID | bigint(20) | 是(自增) | - | 唯一标识错误记录 | PRIMARY KEY (id) | +| error_type_id | 错误类型ID | tinyint(2) | - | 是(关联error_types.id) | 错误类型(模型超时/数据库错误等) | NOT NULL, FOREIGN KEY (error_type_id) REFERENCES error_types(id) | +| error_count | 错误次数 | int(11) | - | - | 统计周期内错误次数 | NOT NULL | +| error_rate | 错误率 | decimal(5,2)| - | - | 错误次数占总请求比例(%) | - | +| period | 统计周期 | varchar(20) | - | - | "today"-今日、"7days"-近7日 | NOT NULL | +| stat_time | 统计时间 | datetime | - | - | 统计生成时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +###### 4.7 performance_metrics(性能趋势表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|------|--------------------------------------------|---------------------------------------------------------------| +| id | 指标ID | bigint(20) | 是(自增) | - | 唯一标识性能指标 | PRIMARY KEY (id) | +| metric_type | 指标类型 | varchar(20) | - | - | "query_count"-查询量、"response_time"-响应时间 | NOT NULL | +| metric_value | 指标值 | decimal(10,2)| - | - | 指标数值(查询量为整数,响应时间单位ms) | NOT NULL | +| metric_time | 指标时间 | datetime | - | - | 指标采集时间(精确到小时) | NOT NULL | +| trend | 趋势标识 | tinyint(1) | - | - | 0-下降,1-上升(较上一周期) | - | + +###### 4.8 db_connection_logs(数据库连接日志表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 日志ID | bigint(20) | 是(自增) | - | 唯一标识连接日志 | PRIMARY KEY (id) | +| db_connection_id | 数据库连接ID | bigint(20) | - | 是(关联db_connections.id) | 连接的数据源 | NOT NULL, FOREIGN KEY (db_connection_id) REFERENCES db_connections(id) | +| db_name | 数据库名称 | varchar(100)| - | - | 冗余存储,便于查看 | NOT NULL | +| connect_time | 连接时间 | datetime | - | - | 尝试连接时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| status | 连接状态 | varchar(20) | - | - | "success"-成功、"timeout"-超时、"auth_failed"-认证失败 | NOT NULL | +| remark | 备注信息 | text | - | - | 错误代码 | - | +| handler_id | 处理人ID | bigint(20) | - | 是(关联users.id) | 触发连接的用户(NULL为系统检测) | FOREIGN KEY (handler_id) REFERENCES users(id) | + +###### 4.9 query_logs(查询日志表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 日志ID | bigint(20) | 是(自增) | - | 唯一标识查询日志 | PRIMARY KEY (id) | +| dialog_id | 对话ID | varchar(50) | - | - | 关联多轮对话ID | NOT NULL | +| data_source_id | 数据源ID | bigint(20) | - | 是(关联db_connections.id) | 查询的数据源 | NOT NULL, FOREIGN KEY (data_source_id) REFERENCES db_connections(id) | +| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 执行查询的用户 | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) | +| query_date | 查询日期 | date | - | - | 查询执行日期 | NOT NULL | +| query_time | 查询时间 | datetime | - | - | 查询执行时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | +| execute_result | 执行结果 | tinyint(1) | - | - | 0-失败,1-成功 | NOT NULL | + +###### 4.10 user_searches(用户搜索表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键 | 字段描述 | 约束条件 | +|----------|----------------------|-------------|------------|--------------------------|--------------------------------------------|---------------------------------------------------------------| +| id | 搜索ID | bigint(20) | 是(自增) | - | 唯一标识搜索记录 | PRIMARY KEY (id) | +| user_id | 用户ID | bigint(20) | - | 是(关联users.id) | 执行搜索的用户 | NOT NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE | +| sql_content | SQL语句 | text | - | - | 用户执行的SQL语句 | NOT NULL | +| query_title | 查询标题 | varchar(200)| - | - | 大模型生成的标题 | NOT NULL | +| search_count | 搜索次数 | int(11) | - | - | 该搜索被执行的次数 | DEFAULT 1, NOT NULL | +| last_search_time | 最后搜索时间 | datetime | - | - | 最后一次执行该搜索的时间 | DEFAULT CURRENT_TIMESTAMP, NOT NULL | + +#### (二)MongoDB 8.2 集合字段 +###### 1. query_collections(收藏查询表-组) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------|--------------------------------------------|---------------------------| +| _id | 组ID | ObjectId | 是(自动生成) | - | 唯一标识收藏组 | PRIMARY KEY (_id) | +| userId | 用户ID | Long | - | users.id | 所属用户 | NOT NULL | +| groupName | 组名称 | String | - | - | 收藏组名称(由大模型生成,如"销售报表查询") | NOT NULL, 同用户内唯一 | +| createTime | 创建时间 | Date | - | - | 组创建时间 | DEFAULT new Date() | + +###### 2. collection_records(收藏记录表-具体记录) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| +| _id | 记录ID | ObjectId | 是(自动生成) | - | 唯一标识收藏记录 | PRIMARY KEY (_id) | +| queryId | 组ID | ObjectId | - | query_collections._id | 所属收藏组 | NOT NULL | +| userId | 用户ID | Long | - | users.id | 所属用户 | NOT NULL | +| sqlContent | SQL语句 | String | - | - | 收藏的SQL语句 | NOT NULL | +| queryResult | 查询结果 | Document | - | - | 结果数据(如{ "columns": [], "data": [] }) | - | +| dbConnectionId | 数据库来源 | Long | - | db_connections.id | 收藏记录来源的数据库连接 | NOT NULL | +| llmConfigId | 大模型来源 | Long | - | llm_configs.id | 收藏记录来源的大模型配置 | NOT NULL | +| createTime | 创建时间 | Date | - | - | 收藏时间 | DEFAULT new Date() | + +###### 3. dialog_records(多轮对话列表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------|--------------------------------------------|---------------------------| +| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识对话列表 | PRIMARY KEY (_id) | +| dialogId | 对话ID | String | - | - | 自定义唯一标识(如"USER123_20251105") | NOT NULL, UNIQUE | +| userId | 用户ID | Long | - | users.id | 所属用户 | NOT NULL | +| topic | 对话主题 | String | - | - | 大模型生成的主题(如"2023年订单分析") | NOT NULL | +| totalRounds | 总轮数 | Int | - | - | 对话总轮次 | DEFAULT 0 | +| startTime | 开始时间 | Date | - | - | 对话开始时间 | DEFAULT new Date() | +| lastTime | 最后对话时间 | Date | - | - | 最后一轮对话时间 | DEFAULT new Date() | + +###### 4. dialog_details(多轮对话具体内容表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| +| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识对话内容 | PRIMARY KEY (_id) | +| dialogId | 对话ID | String | - | dialog_records.dialogId | 关联的对话列表 | NOT NULL | +| rounds | 对话轮次 | Array | - | - | 每轮对话详情:{ "roundNum": Int, "userInput": String, "aiResponse": String, "generatedSql": String, "roundTime": Date } | NOT NULL | + +###### 5. sql_cache(SQL缓存集合) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| +| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识缓存记录 | PRIMARY KEY (_id) | +| nlHash | 自然语言哈希 | String | - | - | 自然语言查询的MD5哈希(快速匹配) | NOT NULL | +| userId | 用户ID | Long | - | users.id | 关联用户(NULL为全局缓存) | - | +| connectionId | 数据源ID | Long | - | db_connections.id | 关联数据源 | NOT NULL | +| tableIds | 表ID列表 | Array | - | table_metadata.id | 涉及的表ID | NOT NULL | +| dbType | 数据库类型 | String | - | db_types.type_code | 如"mysql"、"mongodb" | NOT NULL | +| generatedSql | 生成的SQL | String | - | - | 缓存的SQL语句 | NOT NULL | +| hitCount | 命中次数 | Int | - | - | 缓存被复用次数 | DEFAULT 0 | +| expireTime | 过期时间 | Date | - | - | 缓存过期时间(默认7天) | NOT NULL | + +###### 6. ai_interaction_logs(AI交互日志集合) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------------------|--------------------------------------------|---------------------------| +| _id | 文档ID | ObjectId | 是(自动生成) | - | 唯一标识交互记录 | PRIMARY KEY (_id) | +| userId | 用户ID | Long | - | users.id | 发起请求的用户 | NOT NULL | +| requestType | 请求类型 | String | - | - | "nl2sql"-自然语言转SQL、"sql_optimize"-SQL优化 | NOT NULL | +| llmName | 模型名称 | String | - | llm_configs.name | 调用的大模型 | NOT NULL | +| requestParams | 请求参数 | Document | - | - | 请求参数:{ "naturalLanguage": String, "metadata": Object, "temperature": Double } | NOT NULL | +| responseResult | 响应结果 | Document | - | - | 响应结果:{ "sql": String, "confidence": Double, "suggestion": String } | - | +| tokenUsage | Token消耗 | Document | - | - | { "promptTokens": Int, "completionTokens": Int, "totalTokens": Int } | NOT NULL | +| responseTime | 响应时间 | Int | - | - | 响应耗时(ms) | NOT NULL | +| status | 交互状态 | String | - | - | "success"-成功、"fail"-失败 | NOT NULL | +| errorMsg | 错误信息 | String | - | - | 失败原因(成功为NULL) | - | +| createTime | 创建时间 | Date | - | - | 请求发起时间 | DEFAULT new Date() | + +###### 7. friend_chats(好友聊天记录表) +| 字段代码 | 字段名 | 数据类型 | 主键 | 外键关联 | 字段描述 | 约束条件 | +|----------|--------------|----------|--------------------|------------|--------------------------------------------|---------------------------| +| _id | 记录ID | ObjectId | 是(自动生成) | - | 唯一标识聊天记录 | PRIMARY KEY (_id) | +| user_id | 发送人ID | Long | - | users.id | 发送消息的用户ID(关联MySQL的users表) | NOT NULL | +| friend_id | 接收人ID | Long | - | users.id | 接收消息的好友ID(关联MySQL的users表) | NOT NULL | +| content_type | 内容类型 | String | - | - | 消息类型:text(文本)、query_share(查询分享)、image(图片)等 | NOT NULL | +| content | 消息内容 | Document | - | - | 动态内容:文本:{text: "Hello"};分享:{query_id: "xxx", title: "订单查询"};图片:{url: "xxx.png", size: 1024} | NOT NULL | +| send_time | 发送时间 | Date | - | - | 消息发送时间(精确到毫秒) | NOT NULL | +| is_read | 是否已读 | Boolean | - | - | true(已读)/false(未读),默认false | DEFAULT false | +| extra | 额外信息 | Document | - | - | 扩展字段(如quote_msg_id引用消息ID、is_recalled是否撤回等) | - | + +--- + +### 二、表/集合之间的关系(一对多/多对一/一对一) +#### (一)MySQL 表关系 +| 关联表1 | 关联表2 | 关系类型 | 说明 | +|---------------------------|---------------------------|----------|----------------------------------------| +| users(用户表) | roles(角色表) | 多对一 | 多个用户属于同一角色(如多个普通用户) | +| db_connections(数据库连接表) | db_types(数据库类型表) | 多对一 | 多个数据库连接属于同一类型(如多个MySQL连接) | +| table_metadata(表元数据表) | db_connections(数据库连接表) | 多对一 | 多个表属于同一数据库连接 | +| column_metadata(字段元数据表) | table_metadata(表元数据表) | 多对一 | 多个字段属于同一表 | +| user_db_permissions(用户数据权限表) | users(被授权用户) | 一对一 | 一个用户对应一条权限记录(每个用户一行) | +| user_db_permissions(用户数据权限表) | users(授权管理员) | 多对一 | 多个权限记录可由同一管理员修改 | +| friend_relations(好友关系表) | users(用户) | 多对一 | 一个用户可添加多个好友 | +| friend_relations(好友关系表) | users(好友) | 多对一 | 一个用户可被多个用户添加为好友 | +| friend_requests(好友请求表) | users(申请人) | 多对一 | 一个用户可发起多个好友请求 | +| friend_requests(好友请求表) | users(接收人) | 多对一 | 一个用户可接收多个好友请求 | +| query_shares(查询分享记录表) | users(分享人) | 多对一 | 一个用户可分享多个查询 | +| query_shares(查询分享记录表) | users(接收人) | 多对一 | 一个用户可接收多个分享 | +| llm_configs(大模型配置表) | llm_status(大模型状态表) | 多对一 | 多个大模型配置属于同一状态 | +| llm_configs(大模型配置表) | users(创建人) | 多对一 | 一个管理员可创建多个大模型配置 | +| notifications(通知表) | notification_targets(通知目标表) | 多对一 | 多个通知属于同一接收目标 | +| notifications(通知表) | priorities(通知优先级表) | 多对一 | 多个通知属于同一优先级 | +| notifications(通知表) | users(发布者) | 多对一 | 一个管理员可发布多个通知 | +| error_logs(错误分析表) | error_types(错误类型表) | 多对一 | 多个错误记录属于同一错误类型 | +| db_connection_logs(数据库连接日志表) | db_connections(数据库连接表) | 多对一 | 一个数据库连接有多个连接日志 | +| query_logs(查询日志表) | db_connections(数据库连接表) | 多对一 | 一个数据源有多个查询日志 | +| query_logs(查询日志表) | users(用户) | 多对一 | 一个用户有多个查询日志 | +| user_searches(用户搜索表) | users(用户) | 多对一 | 一个用户有多个搜索记录 | + +#### (二)MongoDB 集合关系 +| 关联集合1 | 关联集合2 | 关系类型 | 说明 | +|-----------------------------|-----------------------------|----------|----------------------------------------| +| collection_records(收藏记录表) | query_collections(收藏查询表) | 多对一 | 一个收藏组包含多个收藏记录 | +| collection_records(收藏记录表) | db_connections(数据库连接表) | 多对一 | 一个数据库连接有多个收藏记录 | +| collection_records(收藏记录表) | llm_configs(大模型配置表) | 多对一 | 一个大模型配置有多个收藏记录 | +| dialog_details(多轮对话具体内容表) | dialog_records(多轮对话列表) | 一对一 | 一个对话列表对应一个具体内容集合 | +| sql_cache(SQL缓存集合) | db_connections(数据库连接表) | 多对一 | 一个数据源有多个SQL缓存 | +| sql_cache(SQL缓存集合) | users(用户表) | 多对一 | 一个用户有多个个人SQL缓存 | +| ai_interaction_logs(AI交互日志集合) | users(用户表) | 多对一 | 一个用户有多个AI交互记录 | +| ai_interaction_logs(AI交互日志集合) | llm_configs(大模型配置表) | 多对一 | 一个大模型有多个交互记录 | +| friend_chats(好友聊天记录表) | users(用户表) | 多对一 | 一个用户可发送多条聊天消息 | +| friend_chats(好友聊天记录表) | users(用户表) | 多对一 | 一个用户可接收多条聊天消息 | + +--- + +### 三、完整索引设计(按表/集合分类) +#### (一)MySQL 数据表索引 +| 表名 | 索引类型 | 索引字段 | 索引说明 | +|-----------------------|----------------|-------------------------------------------|--------------------------------------------| +| users | 普通索引 | role_id | 按角色查询用户 | +| users | 普通索引 | status | 按账号状态筛选用户 | +| users | 唯一索引 | username | 用户名唯一,加速登录查询 | +| users | 唯一索引 | email | 邮箱唯一,加速好友添加查询 | +| users | 唯一索引 | phonenumber | 手机号唯一,加速账号验证 | +| roles | 主键索引 | id | 主键默认索引,加速角色关联查询 | +| db_types | 主键索引 | id | 主键默认索引,加速数据库类型关联查询 | +| notification_targets | 主键索引 | id | 主键默认索引,加速通知目标关联查询 | +| priorities | 普通索引 | sort | 按排序权重查询优先级 | +| priorities | 主键索引 | id | 主键默认索引,加速通知优先级关联查询 | +| error_types | 主键索引 | id | 主键默认索引,加速错误类型关联查询 | +| llm_status | 主键索引 | id | 主键默认索引,加速大模型状态关联查询 | +| db_connections | 普通索引 | db_type_id | 按数据库类型查询连接 | +| db_connections | 普通索引 | status | 按连接状态筛选连接 | +| db_connections | 普通索引 | create_user_id | 按创建人查询连接 | +| db_connections | 唯一索引 | name | 连接名称唯一,加速连接查询 | +| table_metadata | 唯一复合索引 | db_connection_id, table_name | 同一连接下表名唯一,加速表元数据查询 | +| table_metadata | 普通索引 | db_connection_id | 按数据库连接查询表 | +| column_metadata | 唯一复合索引 | table_id, column_name | 同一表下字段名唯一,加速字段元数据查询 | +| column_metadata | 普通索引 | table_id | 按表查询字段 | +| user_db_permissions | 唯一索引 | user_id | 每个用户一行权限,加速权限查询 | +| user_db_permissions | 普通索引 | last_grant_user_id | 按授权管理员查询权限记录 | +| user_db_permissions | 普通索引 | is_assigned | 按是否分配筛选权限 | +| friend_relations | 唯一复合索引 | user_id, friend_id | 避免重复添加好友 | +| friend_relations | 普通索引 | user_id | 按用户查询好友列表 | +| friend_relations | 普通索引 | friend_id | 按好友ID查询关联关系 | +| friend_requests | 唯一复合索引 | applicant_id, recipient_id | 避免重复发送好友请求 | +| friend_requests | 普通复合索引 | recipient_id, status | 接收人查询待处理请求 | +| query_shares | 普通复合索引 | receive_user_id, receive_status | 接收人查询未处理分享 | +| query_shares | 普通索引 | share_user_id | 按分享人查询分享记录 | +| system_health | 普通索引 | collect_time | 按时间查询系统健康趋势 | +| operation_logs | 普通索引 | operate_time | 按时间查询操作日志 | +| operation_logs | 普通索引 | user_id | 按用户查询操作日志 | +| operation_logs | 普通索引 | module | 按模块筛选操作日志 | +| llm_configs | 普通索引 | status_id | 按状态查询大模型配置 | +| llm_configs | 普通索引 | is_disabled | 按是否禁用筛选配置 | +| llm_configs | 唯一索引 | name | 模型名称唯一,加速模型查询 | +| notifications | 普通索引 | target_id | 按目标筛选通知 | +| notifications | 普通索引 | priority_id | 按优先级筛选通知 | +| notifications | 普通复合索引 | is_top DESC, publish_time DESC | 置顶通知优先,按时间排序 | +| token_consume | 唯一复合索引 | llm_name, consume_date | 同一模型每日一条记录,加速消耗查询 | +| token_consume | 普通索引 | consume_date | 按日期查询消耗趋势 | +| error_logs | 普通复合索引 | error_type_id, period | 按错误类型+周期查询错误记录 | +| error_logs | 普通索引 | stat_time | 按统计时间查询错误记录 | +| performance_metrics | 普通复合索引 | metric_type, metric_time | 按类型+时间查询性能趋势 | +| db_connection_logs | 普通索引 | db_connection_id | 按连接查询日志 | +| db_connection_logs | 普通索引 | connect_time | 按时间查询连接日志 | +| db_connection_logs | 普通索引 | status | 按状态筛选连接日志 | +| query_logs | 普通复合索引 | data_source_id, query_date | 统计数据源每日查询量 | +| query_logs | 普通索引 | user_id | 按用户查询查询日志 | +| query_logs | 普通索引 | dialog_id | 按对话ID查询查询日志 | +| user_searches | 普通复合索引 | user_id, last_search_time | 清理30天前的搜索记录 | +| user_searches | 普通复合索引 | user_id, search_count DESC | 查询用户常用搜索(按次数排序) | + +#### (二)MongoDB 集合索引 +| 集合名 | 索引类型 | 索引字段 | 索引说明 | +|-------------------------|----------------|-------------------------------------------|--------------------------------------------| +| query_collections | 复合索引 | { "userId": 1, "groupName": 1 } | 同用户内组名唯一,加速组查询 | +| collection_records | 复合索引 | { "queryId": 1, "userId": 1 } | 关联收藏组和用户,加速记录查询 | +| collection_records | 普通索引 | { "dbConnectionId": 1 } | 按数据库来源查询收藏记录 | +| collection_records | 普通索引 | { "llmConfigId": 1 } | 按大模型来源查询收藏记录 | +| dialog_records | 复合索引 | { "userId": 1, "lastTime": -1 } | 用户按时间查询对话列表(降序) | +| dialog_records | 唯一索引 | { "dialogId": 1 } | 对话ID唯一,加速对话定位 | +| dialog_details | 单字段索引 | { "dialogId": 1 } | 关联对话列表,加速内容查询 | +| sql_cache | 复合索引 | { "nlHash": 1, "connectionId": 1, "tableIds": 1 } | 精确匹配缓存,加速SQL复用 | +| sql_cache | TTL索引 | { "expireTime": 1 } | 自动删除过期缓存(默认7天) | +| ai_interaction_logs | 复合索引 | { "userId": 1, "createTime": -1 } | 用户按时间查询交互记录(降序) | +| ai_interaction_logs | 复合索引 | { "llmName": 1, "status": 1 } | 按模型+状态查询交互记录 | +| friend_chats | 复合索引 | { "user_id": 1, "friend_id": 1, "send_time": -1 } | 按用户+好友+时间查询聊天记录(最新在前) | +| friend_chats | 复合索引 | { "friend_id": 1, "is_read": 1 } | 查询好友未读消息 | \ No newline at end of file diff --git a/src/test/mongodb_schema_from_last.js b/src/test/mongodb_schema_from_last.js new file mode 100644 index 00000000..ccebabb1 --- /dev/null +++ b/src/test/mongodb_schema_from_last.js @@ -0,0 +1,493 @@ +// MongoDB数据库集合和索引创建脚本 - 严格结构定义 +// 完全按照数据库设计文档 + +db = db.getSiblingDB('natural_language_query_system'); +print("开始初始化 MongoDB 集合..."); + +try { + // 1. query_collections(收藏查询表-组) + db.createCollection("query_collections", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["userId", "groupName", "createTime"], + properties: { + userId: { + bsonType: "long", + description: "用户ID,关联users.id" + }, + groupName: { + bsonType: "string", + description: "收藏组名称(由大模型生成)" + }, + createTime: { + bsonType: "date", + description: "组创建时间" + } + } + } + } + }); + db.query_collections.createIndex({ "userId": 1, "groupName": 1 }, { unique: true }); + print("创建 query_collections 完成"); +} catch(e) { + print("query_collections 创建错误: " + e); +} + +try { + // 2. collection_records(收藏记录表-具体记录) + db.createCollection("collection_records", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["queryId", "userId", "sqlContent", "dbConnectionId", "llmConfigId", "createTime"], + properties: { + queryId: { + bsonType: "objectId", + description: "组ID,关联query_collections._id" + }, + userId: { + bsonType: "long", + description: "用户ID,关联users.id" + }, + sqlContent: { + bsonType: "string", + description: "收藏的SQL语句" + }, + queryResult: { + bsonType: "object", + description: "结果数据" + }, + dbConnectionId: { + bsonType: "long", + description: "数据库来源,关联db_connections.id" + }, + llmConfigId: { + bsonType: "long", + description: "大模型来源,关联llm_configs.id" + }, + createTime: { + bsonType: "date", + description: "收藏时间" + } + } + } + } + }); + db.collection_records.createIndex({ "queryId": 1, "userId": 1 }); + db.collection_records.createIndex({ "dbConnectionId": 1 }); + db.collection_records.createIndex({ "llmConfigId": 1 }); + print("创建 collection_records 完成"); +} catch(e) { + print("collection_records 创建错误: " + e); +} + +try { + // 3. dialog_records(多轮对话列表) + db.createCollection("dialog_records", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["dialogId", "userId", "topic", "totalRounds", "startTime", "lastTime"], + properties: { + dialogId: { + bsonType: "string", + description: "对话ID,自定义唯一标识" + }, + userId: { + bsonType: "long", + description: "用户ID,关联users.id" + }, + topic: { + bsonType: "string", + description: "对话主题(大模型生成)" + }, + totalRounds: { + bsonType: "int", + description: "对话总轮次", + minimum: 0 + }, + startTime: { + bsonType: "date", + description: "对话开始时间" + }, + lastTime: { + bsonType: "date", + description: "最后一轮对话时间" + } + } + } + } + }); + db.dialog_records.createIndex({ "userId": 1, "lastTime": -1 }); + db.dialog_records.createIndex({ "dialogId": 1 }, { unique: true }); + print("创建 dialog_records 完成"); +} catch(e) { + print("dialog_records 创建错误: " + e); +} + +try { + // 4. dialog_details(多轮对话具体内容表) + db.createCollection("dialog_details", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["dialogId", "rounds"], + properties: { + dialogId: { + bsonType: "string", + description: "对话ID,关联dialog_records.dialogId" + }, + rounds: { + bsonType: "array", + description: "对话轮次数组", + items: { + bsonType: "object", + required: ["roundNum", "userInput", "aiResponse", "generatedSql", "roundTime"], + properties: { + roundNum: { + bsonType: "int", + description: "轮次序号", + minimum: 1 + }, + userInput: { + bsonType: "string", + description: "用户输入" + }, + aiResponse: { + bsonType: "string", + description: "AI回复" + }, + generatedSql: { + bsonType: "string", + description: "生成的SQL" + }, + roundTime: { + bsonType: "date", + description: "轮次时间" + } + } + } + } + } + } + } + }); + db.dialog_details.createIndex({ "dialogId": 1 }); + print("创建 dialog_details 完成"); +} catch(e) { + print("dialog_details 创建错误: " + e); +} + +try { + // 5. sql_cache(SQL缓存集合) + db.createCollection("sql_cache", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["nlHash", "connectionId", "tableIds", "dbType", "generatedSql", "hitCount", "expireTime"], + properties: { + nlHash: { + bsonType: "string", + description: "自然语言查询的MD5哈希" + }, + userId: { + bsonType: ["long", "null"], + description: "用户ID(NULL为全局缓存)" + }, + connectionId: { + bsonType: "long", + description: "数据源ID,关联db_connections.id" + }, + tableIds: { + bsonType: "array", + description: "涉及的表ID列表", + items: { + bsonType: "long" + } + }, + dbType: { + bsonType: "string", + description: "数据库类型" + }, + generatedSql: { + bsonType: "string", + description: "缓存的SQL语句" + }, + hitCount: { + bsonType: "int", + description: "命中次数", + minimum: 0 + }, + expireTime: { + bsonType: "date", + description: "缓存过期时间" + } + } + } + } + }); + db.sql_cache.createIndex({ "nlHash": 1, "connectionId": 1, "tableIds": 1 }); + db.sql_cache.createIndex({ "expireTime": 1 }, { expireAfterSeconds: 0 }); + print("创建 sql_cache 完成"); +} catch(e) { + print("sql_cache 创建错误: " + e); +} + +try { + // 6. ai_interaction_logs(AI交互日志集合) + db.createCollection("ai_interaction_logs", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["userId", "requestType", "llmName", "requestParams", "tokenUsage", "responseTime", "status", "createTime"], + properties: { + userId: { + bsonType: "long", + description: "用户ID,关联users.id" + }, + requestType: { + bsonType: "string", + enum: ["nl2sql", "sql_optimize"], + description: "请求类型" + }, + llmName: { + bsonType: "string", + description: "模型名称,关联llm_configs.name" + }, + requestParams: { + bsonType: "object", + description: "请求参数", + properties: { + naturalLanguage: { + bsonType: "string", + description: "自然语言查询" + }, + metadata: { + bsonType: "object", + description: "元数据信息" + }, + temperature: { + bsonType: "double", + description: "温度参数", + minimum: 0, + maximum: 2 + } + } + }, + responseResult: { + bsonType: "object", + description: "响应结果", + properties: { + sql: { + bsonType: "string", + description: "生成的SQL" + }, + confidence: { + bsonType: "double", + description: "置信度", + minimum: 0, + maximum: 1 + }, + suggestion: { + bsonType: "string", + description: "优化建议" + } + } + }, + tokenUsage: { + bsonType: "object", + description: "Token消耗", + required: ["promptTokens", "completionTokens", "totalTokens"], + properties: { + promptTokens: { + bsonType: "int", + description: "输入token数", + minimum: 0 + }, + completionTokens: { + bsonType: "int", + description: "输出token数", + minimum: 0 + }, + totalTokens: { + bsonType: "int", + description: "总token数", + minimum: 0 + } + } + }, + responseTime: { + bsonType: "int", + description: "响应耗时(ms)", + minimum: 0 + }, + status: { + bsonType: "string", + enum: ["success", "fail"], + description: "交互状态" + }, + errorMsg: { + bsonType: ["string", "null"], + description: "错误信息" + }, + createTime: { + bsonType: "date", + description: "请求发起时间" + } + } + } + } + }); + db.ai_interaction_logs.createIndex({ "userId": 1, "createTime": -1 }); + db.ai_interaction_logs.createIndex({ "llmName": 1, "status": 1 }); + print("创建 ai_interaction_logs 完成"); +} catch(e) { + print("ai_interaction_logs 创建错误: " + e); +} + +try { + // 7. friend_chats(好友聊天记录表)- 完全按照设计文档 + db.createCollection("friend_chats", { + validator: { + $jsonSchema: { + bsonType: "object", + required: ["user_id", "friend_id", "content_type", "content", "send_time", "is_read"], + properties: { + user_id: { + bsonType: "long", + description: "发送人ID,关联users.id" + }, + friend_id: { + bsonType: "long", + description: "接收人ID,关联users.id" + }, + content_type: { + bsonType: "string", + enum: ["text", "query_share", "image"], + description: "消息类型" + }, + content: { + bsonType: "object", + description: "动态消息内容", + properties: { + text: { + bsonType: "string", + description: "文本内容" + }, + query_id: { + bsonType: "string", + description: "查询ID" + }, + title: { + bsonType: "string", + description: "查询标题" + }, + url: { + bsonType: "string", + description: "图片URL" + }, + size: { + bsonType: "int", + description: "文件大小", + minimum: 0 + } + } + }, + send_time: { + bsonType: "date", + description: "消息发送时间" + }, + is_read: { + bsonType: "bool", + description: "是否已读" + }, + extra: { + bsonType: "object", + description: "额外信息(扩展字段)", + properties: { + quote_msg_id: { + bsonType: "objectId", + description: "引用消息ID" + }, + is_recalled: { + bsonType: "bool", + description: "是否撤回" + } + } + } + } + } + } + }); + db.friend_chats.createIndex({ "user_id": 1, "friend_id": 1, "send_time": -1 }); + db.friend_chats.createIndex({ "friend_id": 1, "is_read": 1 }); + print("创建 friend_chats 完成"); +} catch(e) { + print("friend_chats 创建错误: " + e); +} + +// 验证创建结果 +var collections = db.getCollectionNames(); +var userCollections = collections.filter(function(name) { + return !name.startsWith('system.'); +}); + +print("\n=========================================="); +print("MongoDB 集合初始化完成"); +print("用户集合数量: " + userCollections.length); +print("集合列表: " + JSON.stringify(userCollections.sort())); +print("=========================================="); + +// 检查是否所有集合都创建成功 +var expectedCollections = [ + "query_collections", + "collection_records", + "dialog_records", + "dialog_details", + "sql_cache", + "ai_interaction_logs", + "friend_chats" +].sort(); + +var missingCollections = expectedCollections.filter(function(name) { + return userCollections.indexOf(name) === -1; +}); + +if (missingCollections.length > 0) { + print("缺失的集合: " + JSON.stringify(missingCollections)); +} else { + print("所有集合创建成功!"); + + // 显示各集合的索引信息 + print("\n📈 各集合索引详情:"); + userCollections.forEach(function(collectionName) { + var indexes = db[collectionName].getIndexes(); + var userIndexes = indexes.filter(function(index) { + return index.name !== "_id_"; + }); + print(" " + collectionName + ": " + userIndexes.length + " 个索引"); + userIndexes.forEach(function(index) { + print(" - " + index.name + ": " + JSON.stringify(index.key)); + }); + }); +} + +print("\n集合结构验证:"); +userCollections.forEach(function(collectionName) { + var stats = db[collectionName].stats(); + print(" " + collectionName + ": " + stats.count + " 个文档, " + stats.size + " 字节"); +}); + +print("\nMongoDB 严格结构定义初始化完成!"); + +// ======================================== +// 插入初始示例数据(可选) +// ======================================== +print("\n开始插入初始示例数据..."); + +// 注意:MongoDB 的初始数据通常不需要预先插入, +// 因为应用运行时会自动创建文档。 +// 这里仅作为演示,实际项目中可以删除或保留为空。 + +print("初始数据插入完成(当前为空,应用运行时自动生成)"); \ No newline at end of file diff --git a/src/test/mvnw b/src/test/mvnw new file mode 100644 index 00000000..bd8896bf --- /dev/null +++ b/src/test/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/src/test/mvnw.cmd b/src/test/mvnw.cmd new file mode 100644 index 00000000..92450f93 --- /dev/null +++ b/src/test/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/src/test/mysql_schema_from_last.sql b/src/test/mysql_schema_from_last.sql new file mode 100644 index 00000000..b3f55f42 --- /dev/null +++ b/src/test/mysql_schema_from_last.sql @@ -0,0 +1,397 @@ +-- MySQL数据库建表脚本 +-- 基于last.md文档设计 + +-- 1. 基础信息表(用户、角色、字典) + +-- 1.1 roles(角色表)- 需要先创建,因为users依赖它 +CREATE TABLE `roles` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID', + `role_name` VARCHAR(30) NOT NULL UNIQUE COMMENT '角色名称', + `role_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '角色编码', + `description` VARCHAR(500) COMMENT '角色描述' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; + +-- 1.2 users(用户表) +CREATE TABLE `users` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', + `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名', + `password` VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密存储)', + `email` VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱', + `phonenumber` VARCHAR(50) NOT NULL UNIQUE COMMENT '手机号', + `role_id` TINYINT(2) NOT NULL COMMENT '角色ID', + `avatar_url` VARCHAR(255) DEFAULT '/default-avatar.png' COMMENT '头像URL', + `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态:0-禁用,1-正常', + `online_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '在线状态:0-离线,1-在线', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_role_id` (`role_id`), + INDEX `idx_status` (`status`), + UNIQUE INDEX `uk_username` (`username`), + UNIQUE INDEX `uk_email` (`email`), + UNIQUE INDEX `uk_phonenumber` (`phonenumber`), + FOREIGN KEY (`role_id`) REFERENCES `roles`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; + +-- 1.3 db_types(数据库类型表) +CREATE TABLE `db_types` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '类型ID', + `type_name` VARCHAR(50) NOT NULL UNIQUE COMMENT '类型名称', + `type_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '类型编码', + `description` VARCHAR(500) COMMENT '类型描述' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库类型表'; + +-- 1.4 notification_targets(通知目标表) +CREATE TABLE `notification_targets` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '目标ID', + `target_name` VARCHAR(30) NOT NULL UNIQUE COMMENT '目标名称', + `target_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '目标编码', + `description` VARCHAR(200) COMMENT '目标描述' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知目标表'; + +-- 1.5 priorities(通知优先级表) +CREATE TABLE `priorities` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '优先级ID', + `priority_name` VARCHAR(20) NOT NULL UNIQUE COMMENT '优先级名称', + `priority_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '优先级编码', + `sort` INT(11) NOT NULL COMMENT '排序权重', + INDEX `idx_sort` (`sort`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知优先级表'; + +-- 1.6 error_types(错误类型表) +CREATE TABLE `error_types` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '错误ID', + `error_name` VARCHAR(50) NOT NULL UNIQUE COMMENT '错误名称', + `error_code` VARCHAR(50) NOT NULL UNIQUE COMMENT '错误编码', + `description` VARCHAR(500) COMMENT '错误描述' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='错误类型表'; + +-- 1.7 llm_status(大模型状态表) +CREATE TABLE `llm_status` ( + `id` TINYINT(2) PRIMARY KEY AUTO_INCREMENT COMMENT '状态ID', + `status_name` VARCHAR(20) NOT NULL UNIQUE COMMENT '状态名称', + `status_code` VARCHAR(20) NOT NULL UNIQUE COMMENT '状态编码', + `description` VARCHAR(200) COMMENT '状态描述' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大模型状态表'; + +-- 2. 数据资源表 + +-- 2.1 db_connections(数据库连接表) +CREATE TABLE `db_connections` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '连接ID', + `name` VARCHAR(100) NOT NULL UNIQUE COMMENT '连接名称', + `db_type_id` TINYINT(2) NOT NULL COMMENT '数据库类型ID', + `url` VARCHAR(255) NOT NULL COMMENT '连接地址', + `username` VARCHAR(50) NOT NULL COMMENT '数据库账号(加密存储)', + `password` VARCHAR(100) NOT NULL COMMENT '数据库密码(加密存储)', + `status` VARCHAR(20) NOT NULL DEFAULT 'disconnected' COMMENT '连接状态', + `create_user_id` BIGINT(20) NOT NULL COMMENT '创建者ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_db_type_id` (`db_type_id`), + INDEX `idx_status` (`status`), + INDEX `idx_create_user_id` (`create_user_id`), + UNIQUE INDEX `uk_name` (`name`), + FOREIGN KEY (`db_type_id`) REFERENCES `db_types`(`id`), + FOREIGN KEY (`create_user_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库连接表'; + +-- 2.2 table_metadata(表元数据表) +CREATE TABLE `table_metadata` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '表ID', + `db_connection_id` BIGINT(20) NOT NULL COMMENT '数据库连接ID', + `table_name` VARCHAR(100) NOT NULL COMMENT '表名', + `description` VARCHAR(500) COMMENT '表描述', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + UNIQUE INDEX `uk_db_connection_table` (`db_connection_id`, `table_name`), + INDEX `idx_db_connection_id` (`db_connection_id`), + FOREIGN KEY (`db_connection_id`) REFERENCES `db_connections`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表元数据表'; + +-- 2.3 column_metadata(字段元数据表) +CREATE TABLE `column_metadata` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '字段ID', + `table_id` BIGINT(20) NOT NULL COMMENT '表ID', + `column_name` VARCHAR(100) NOT NULL COMMENT '字段名', + `data_type` VARCHAR(50) NOT NULL COMMENT '数据类型', + `description` VARCHAR(500) COMMENT '字段描述', + `is_primary` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否主键:0-否,1-是', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + UNIQUE INDEX `uk_table_column` (`table_id`, `column_name`), + INDEX `idx_table_id` (`table_id`), + FOREIGN KEY (`table_id`) REFERENCES `table_metadata`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字段元数据表'; + +-- 3. 权限与关系表 + +-- 3.1 user_db_permissions(用户数据权限表) +CREATE TABLE `user_db_permissions` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID', + `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', + `permission_details` JSON NOT NULL COMMENT '权限详情', + `last_grant_user_id` BIGINT(20) NOT NULL COMMENT '最后授权管理员ID', + `is_assigned` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已分配:0-未分配,1-已分配', + `last_grant_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后授权时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + UNIQUE INDEX `uk_user_id` (`user_id`), + INDEX `idx_last_grant_user_id` (`last_grant_user_id`), + INDEX `idx_is_assigned` (`is_assigned`), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`last_grant_user_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户数据权限表'; + +-- 3.2 friend_relations(好友关系表) +CREATE TABLE `friend_relations` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '关系ID', + `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', + `friend_id` BIGINT(20) NOT NULL COMMENT '好友ID', + `friend_username` VARCHAR(50) NOT NULL COMMENT '好友用户名', + `online_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '好友在线状态:0-离线,1-在线', + `remark_name` VARCHAR(50) COMMENT '备注名', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间', + UNIQUE INDEX `uk_user_friend` (`user_id`, `friend_id`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_friend_id` (`friend_id`), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`friend_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友关系表'; + +-- 3.3 friend_requests(好友请求表) +CREATE TABLE `friend_requests` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '请求ID', + `applicant_id` BIGINT(20) NOT NULL COMMENT '申请人ID', + `recipient_id` BIGINT(20) NOT NULL COMMENT '接收人ID', + `apply_msg` VARCHAR(200) COMMENT '申请留言', + `status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '请求状态:0-待处理,1-已同意,2-已拒绝', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '申请时间', + `handle_time` DATETIME COMMENT '处理时间', + UNIQUE INDEX `uk_applicant_recipient` (`applicant_id`, `recipient_id`), + INDEX `idx_recipient_status` (`recipient_id`, `status`), + FOREIGN KEY (`applicant_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`recipient_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='好友请求表'; + +-- 3.4 query_shares(查询分享记录表) +CREATE TABLE `query_shares` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '分享ID', + `share_user_id` BIGINT(20) NOT NULL COMMENT '分享人ID', + `receive_user_id` BIGINT(20) NOT NULL COMMENT '接收人ID', + `dialog_id` VARCHAR(50) NOT NULL COMMENT '会话ID', + `target_rounds` JSON NOT NULL COMMENT '会话轮次数组', + `query_title` VARCHAR(200) NOT NULL COMMENT '查询标题', + `share_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '分享时间', + `receive_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '接收状态:0-未处理,1-已保存,2-已删除', + INDEX `idx_receive_user_status` (`receive_user_id`, `receive_status`), + INDEX `idx_share_user_id` (`share_user_id`), + FOREIGN KEY (`share_user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`receive_user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='查询分享记录表'; + +-- 4. 系统日志与监控表 + +-- 4.1 system_health(系统健康表) +CREATE TABLE `system_health` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', + `db_delay` INT(11) NOT NULL COMMENT '数据库延迟(ms)', + `cache_delay` INT(11) NOT NULL COMMENT '缓存延迟(ms)', + `llm_delay` INT(11) NOT NULL COMMENT '大模型延迟(ms)', + `storage_usage` DECIMAL(5,2) NOT NULL COMMENT '存储使用率(%)', + `collect_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '采集时间', + INDEX `idx_collect_time` (`collect_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统健康表'; + +-- 4.2 operation_logs(系统操作日志表) +CREATE TABLE `operation_logs` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', + `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', + `username` VARCHAR(50) NOT NULL COMMENT '用户名', + `operation` VARCHAR(100) NOT NULL COMMENT '操作名称', + `module` VARCHAR(50) NOT NULL COMMENT '操作模块', + `related_llm` VARCHAR(50) COMMENT '涉及模型', + `ip_address` VARCHAR(50) NOT NULL COMMENT 'IP地址', + `operate_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间', + `result` TINYINT(1) NOT NULL COMMENT '操作结果:0-失败,1-成功', + `error_msg` TEXT COMMENT '错误信息', + INDEX `idx_operate_time` (`operate_time`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_module` (`module`), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表'; + +-- 4.3 llm_configs(大模型配置表) +CREATE TABLE `llm_configs` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '配置ID', + `name` VARCHAR(50) NOT NULL UNIQUE COMMENT '模型名称', + `version` VARCHAR(20) NOT NULL COMMENT '模型版本', + `api_key` VARCHAR(200) NOT NULL COMMENT 'API密钥(AES加密)', + `api_url` VARCHAR(255) NOT NULL COMMENT 'API地址', + `status_id` TINYINT(2) NOT NULL COMMENT '状态ID', + `is_disabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否禁用:0-启用,1-禁用', + `timeout` INT(11) NOT NULL COMMENT '超时时间(ms)', + `create_user_id` BIGINT(20) NOT NULL COMMENT '创建人ID', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + INDEX `idx_status_id` (`status_id`), + INDEX `idx_is_disabled` (`is_disabled`), + UNIQUE INDEX `uk_name` (`name`), + FOREIGN KEY (`status_id`) REFERENCES `llm_status`(`id`), + FOREIGN KEY (`create_user_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='大模型配置表'; + +-- 4.4 notifications(通知表) +CREATE TABLE `notifications` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '通知ID', + `title` VARCHAR(100) NOT NULL COMMENT '通知标题', + `content` TEXT NOT NULL COMMENT '通知内容', + `target_id` TINYINT(2) NOT NULL COMMENT '目标ID', + `priority_id` TINYINT(2) NOT NULL COMMENT '优先级ID', + `publisher_id` BIGINT(20) NOT NULL COMMENT '发布者ID', + `is_top` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶:0-否,1-是', + `publish_time` DATETIME COMMENT '发布时间', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `Latest_updateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上次更新时间', + INDEX `idx_target_id` (`target_id`), + INDEX `idx_priority_id` (`priority_id`), + INDEX `idx_is_top_publish_time` (`is_top` DESC, `publish_time` DESC), + FOREIGN KEY (`target_id`) REFERENCES `notification_targets`(`id`), + FOREIGN KEY (`priority_id`) REFERENCES `priorities`(`id`), + FOREIGN KEY (`publisher_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通知表'; + +-- 4.5 token_consume(token消耗表) +CREATE TABLE `token_consume` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', + `llm_name` VARCHAR(50) NOT NULL COMMENT '模型名称', + `total_tokens` BIGINT(11) NOT NULL COMMENT '总消耗', + `prompt_tokens` BIGINT(11) NOT NULL COMMENT '输入消耗', + `completion_tokens` BIGINT(11) NOT NULL COMMENT '输出消耗', + `consume_date` DATE NOT NULL COMMENT '消耗日期', + `growth_rate` DECIMAL(5,2) COMMENT '增长率(%)', + UNIQUE INDEX `uk_llm_date` (`llm_name`, `consume_date`), + INDEX `idx_consume_date` (`consume_date`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='token消耗表'; + +-- 4.6 error_logs(错误分析表) +CREATE TABLE `error_logs` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', + `error_type_id` TINYINT(2) NOT NULL COMMENT '错误类型ID', + `error_count` INT(11) NOT NULL COMMENT '错误次数', + `error_rate` DECIMAL(5,2) COMMENT '错误率(%)', + `period` VARCHAR(20) NOT NULL COMMENT '统计周期', + `stat_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '统计时间', + INDEX `idx_error_type_period` (`error_type_id`, `period`), + INDEX `idx_stat_time` (`stat_time`), + FOREIGN KEY (`error_type_id`) REFERENCES `error_types`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='错误分析表'; + +-- 4.7 performance_metrics(性能趋势表) +CREATE TABLE `performance_metrics` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '指标ID', + `metric_type` VARCHAR(20) NOT NULL COMMENT '指标类型', + `metric_value` DECIMAL(10,2) NOT NULL COMMENT '指标值', + `metric_time` DATETIME NOT NULL COMMENT '指标时间', + `trend` TINYINT(1) COMMENT '趋势标识:0-下降,1-上升', + INDEX `idx_metric_type_time` (`metric_type`, `metric_time`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='性能趋势表'; + +-- 4.8 db_connection_logs(数据库连接日志表) +CREATE TABLE `db_connection_logs` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', + `db_connection_id` BIGINT(20) NOT NULL COMMENT '数据库连接ID', + `db_name` VARCHAR(100) NOT NULL COMMENT '数据库名称', + `connect_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '连接时间', + `status` VARCHAR(20) NOT NULL COMMENT '连接状态', + `remark` TEXT COMMENT '备注信息', + `handler_id` BIGINT(20) COMMENT '处理人ID', + INDEX `idx_db_connection_id` (`db_connection_id`), + INDEX `idx_connect_time` (`connect_time`), + INDEX `idx_status` (`status`), + FOREIGN KEY (`db_connection_id`) REFERENCES `db_connections`(`id`), + FOREIGN KEY (`handler_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库连接日志表'; + +-- 4.9 query_logs(查询日志表) +CREATE TABLE `query_logs` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', + `dialog_id` VARCHAR(50) NOT NULL COMMENT '对话ID', + `data_source_id` BIGINT(20) NOT NULL COMMENT '数据源ID', + `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', + `query_date` DATE NOT NULL COMMENT '查询日期', + `query_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '查询时间', + `execute_result` TINYINT(1) NOT NULL COMMENT '执行结果:0-失败,1-成功', + INDEX `idx_data_source_date` (`data_source_id`, `query_date`), + INDEX `idx_user_id` (`user_id`), + INDEX `idx_dialog_id` (`dialog_id`), + FOREIGN KEY (`data_source_id`) REFERENCES `db_connections`(`id`), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='查询日志表'; + +-- 4.10 user_searches(用户搜索表) +CREATE TABLE `user_searches` ( + `id` BIGINT(20) PRIMARY KEY AUTO_INCREMENT COMMENT '搜索ID', + `user_id` BIGINT(20) NOT NULL COMMENT '用户ID', + `sql_content` TEXT NOT NULL COMMENT 'SQL语句', + `query_title` VARCHAR(200) NOT NULL COMMENT '查询标题', + `search_count` INT(11) NOT NULL DEFAULT 1 COMMENT '搜索次数', + `last_search_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后搜索时间', + INDEX `idx_user_last_search` (`user_id`, `last_search_time`), + INDEX `idx_user_search_count` (`user_id`, `search_count` DESC), + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户搜索表'; + +-- ======================================== +-- 初始化基础数据(必须数据) +-- ======================================== + +-- 1. 角色数据 +INSERT INTO `roles` (`id`, `role_name`, `role_code`, `description`) VALUES +(1, '系统管理员', 'sys_admin', '拥有系统所有权限,可管理用户、配置大模型'), +(2, '数据管理员', 'data_admin', '可管理数据源连接、分配用户数据权限'), +(3, '普通用户', 'normal_user', '可执行自然语言查询、查看历史记录'); + +-- 2. 数据库类型 +INSERT INTO `db_types` (`id`, `type_name`, `type_code`, `description`) VALUES +(1, 'MySQL', 'mysql', 'MySQL关系型数据库'), +(2, 'MongoDB', 'mongodb', 'MongoDB文档型数据库'), +(3, 'SQL Server', 'mssql', 'Microsoft SQL Server'), +(4, 'PostgreSQL', 'postgresql', 'PostgreSQL关系型数据库'); + +-- 3. 大模型状态 +INSERT INTO `llm_status` (`id`, `status_name`, `status_code`, `description`) VALUES +(1, '可用', 'available', 'API成功率≥95%,响应时间正常'), +(2, '不可用', 'unavailable', 'API无法连接或持续失败'), +(3, '不稳定', 'unstable', 'API成功率在60%-95%之间'); + +-- 4. 通知目标 +INSERT INTO `notification_targets` (`id`, `target_name`, `target_code`, `description`) VALUES +(1, '所有用户', 'all', '系统内所有用户'), +(2, '系统管理员', 'sys_admin', '仅系统管理员可见'), +(3, '数据管理员', 'data_admin', '仅数据管理员可见'), +(4, '普通用户', 'normal_user', '仅普通用户可见'); + +-- 5. 通知优先级 +INSERT INTO `priorities` (`id`, `priority_name`, `priority_code`, `sort`) VALUES +(1, '紧急', 'urgent', 1), +(2, '普通', 'normal', 2), +(3, '低', 'low', 3); + +-- 6. 错误类型 +INSERT INTO `error_types` (`id`, `error_name`, `error_code`, `description`) VALUES +(1, '模型调用超时', 'llm_timeout', '大模型API响应超时'), +(2, '数据库连接错误', 'db_connection_error', '数据库连接失败或超时'), +(3, 'SQL语法错误', 'sql_syntax_error', '生成的SQL语句语法错误'), +(4, '权限不足', 'permission_denied', '用户无权访问指定数据源'); + +-- 7. 内置用户(三个角色各一个) +-- 密码统一为 "123456" 的 BCrypt 加密结果:$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi +INSERT INTO `users` (`id`, `username`, `password`, `email`, `phonenumber`, `role_id`, `status`) VALUES +(1, 'sys_admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'sys_admin@example.com', '13800138001', 1, 1), +(2, 'data_admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'data_admin@example.com', '13800138002', 2, 1), +(3, 'normal_user', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi', 'normal_user@example.com', '13800138003', 3, 1); + +-- 8. 用户数据权限初始化(为所有用户创建权限记录) +INSERT INTO `user_db_permissions` (`user_id`, `permission_details`, `is_assigned`, `last_grant_user_id`) VALUES +(1, '[]', 1, 1), +(2, '[]', 1, 1), +(3, '[]', 0, 1); + diff --git a/src/test/pom.xml b/src/test/pom.xml new file mode 100644 index 00000000..cff4379e --- /dev/null +++ b/src/test/pom.xml @@ -0,0 +1,133 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + com.example + springboot_demo + 0.0.1-SNAPSHOT + springboot_demo + springboot_demo + + + + + + + + + + + + + + + 21 + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.mysql + mysql-connector-j + runtime + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + 3.5.9 + + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.projectlombok + lombok + true + + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.43 + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-security + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/test/scripts/export-data.bat b/src/test/scripts/export-data.bat new file mode 100644 index 00000000..9235c27c --- /dev/null +++ b/src/test/scripts/export-data.bat @@ -0,0 +1,25 @@ +@echo off +REM Windows 版本的数据导出脚本 +echo 开始导出数据... + +REM 创建导出目录 +if not exist "data-backup" mkdir data-backup + +REM 导出 MySQL 数据 +echo 导出 MySQL 数据... +docker exec nlq_mysql mysqldump -uroot -proot123456 --no-create-info --skip-triggers --complete-insert natural_language_query_system > data-backup\mysql-data.sql + +REM 导出 MongoDB 数据 +echo 导出 MongoDB 数据... +docker exec nlq_mongodb mongodump --username=admin --password=admin123456 --authenticationDatabase=admin --db=natural_language_query_system --out=/tmp/mongodb-backup +docker cp nlq_mongodb:/tmp/mongodb-backup/natural_language_query_system data-backup\mongodb-data + +echo. +echo 数据导出完成! +echo 文件位置: +echo - MySQL: data-backup\mysql-data.sql +echo - MongoDB: data-backup\mongodb-data\ +echo. +echo 请将 data-backup 文件夹分享给团队成员 +pause + diff --git a/src/test/scripts/export-data.sh b/src/test/scripts/export-data.sh new file mode 100644 index 00000000..8c605864 --- /dev/null +++ b/src/test/scripts/export-data.sh @@ -0,0 +1,37 @@ +#!/bin/bash +# 导出 Docker 容器中的数据到本地文件 +# 用于团队成员之间共享测试数据 + +echo "开始导出数据..." + +# 创建导出目录 +mkdir -p ./data-backup + +# 导出 MySQL 数据(仅数据,不含表结构) +echo "导出 MySQL 数据..." +docker exec nlq_mysql mysqldump \ + -uroot -proot123456 \ + --no-create-info \ + --skip-triggers \ + --complete-insert \ + natural_language_query_system \ + > ./data-backup/mysql-data.sql + +# 导出 MongoDB 数据 +echo "导出 MongoDB 数据..." +docker exec nlq_mongodb mongodump \ + --username=admin \ + --password=admin123456 \ + --authenticationDatabase=admin \ + --db=natural_language_query_system \ + --out=/tmp/mongodb-backup + +docker cp nlq_mongodb:/tmp/mongodb-backup/natural_language_query_system ./data-backup/mongodb-data + +echo "数据导出完成!" +echo "文件位置:" +echo " - MySQL: ./data-backup/mysql-data.sql" +echo " - MongoDB: ./data-backup/mongodb-data/" +echo "" +echo "请将 data-backup 文件夹分享给团队成员" + diff --git a/src/test/scripts/import-data.bat b/src/test/scripts/import-data.bat new file mode 100644 index 00000000..c143388c --- /dev/null +++ b/src/test/scripts/import-data.bat @@ -0,0 +1,30 @@ +@echo off +REM Windows 版本的数据导入脚本 +echo 开始导入数据... + +REM 检查备份文件 +if not exist "data-backup\mysql-data.sql" ( + echo 错误:找不到 MySQL 备份文件 data-backup\mysql-data.sql + pause + exit /b 1 +) + +if not exist "data-backup\mongodb-data" ( + echo 错误:找不到 MongoDB 备份目录 data-backup\mongodb-data + pause + exit /b 1 +) + +REM 导入 MySQL 数据 +echo 导入 MySQL 数据... +docker exec -i nlq_mysql mysql -uroot -proot123456 natural_language_query_system < data-backup\mysql-data.sql + +REM 导入 MongoDB 数据 +echo 导入 MongoDB 数据... +docker cp data-backup\mongodb-data nlq_mongodb:/tmp/mongodb-restore +docker exec nlq_mongodb mongorestore --username=admin --password=admin123456 --authenticationDatabase=admin --db=natural_language_query_system /tmp/mongodb-restore + +echo. +echo 数据导入完成! +pause + diff --git a/src/test/scripts/import-data.sh b/src/test/scripts/import-data.sh new file mode 100644 index 00000000..6b7db786 --- /dev/null +++ b/src/test/scripts/import-data.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# 导入团队共享的测试数据到 Docker 容器 +# 使用前确保已经运行 docker compose up -d + +echo "开始导入数据..." + +# 检查备份文件是否存在 +if [ ! -f "./data-backup/mysql-data.sql" ]; then + echo "错误:找不到 MySQL 备份文件 ./data-backup/mysql-data.sql" + exit 1 +fi + +if [ ! -d "./data-backup/mongodb-data" ]; then + echo "错误:找不到 MongoDB 备份目录 ./data-backup/mongodb-data" + exit 1 +fi + +# 导入 MySQL 数据 +echo "导入 MySQL 数据..." +docker exec -i nlq_mysql mysql \ + -uroot -proot123456 \ + natural_language_query_system \ + < ./data-backup/mysql-data.sql + +# 导入 MongoDB 数据 +echo "导入 MongoDB 数据..." +docker cp ./data-backup/mongodb-data nlq_mongodb:/tmp/mongodb-restore +docker exec nlq_mongodb mongorestore \ + --username=admin \ + --password=admin123456 \ + --authenticationDatabase=admin \ + --db=natural_language_query_system \ + /tmp/mongodb-restore + +echo "数据导入完成!" + diff --git a/src/test/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java b/src/test/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java new file mode 100644 index 00000000..60a1168e --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/SpringbootDemoApplication.java @@ -0,0 +1,13 @@ +package com.example.springboot_demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringbootDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringbootDemoApplication.class, args); + } + +} diff --git a/src/test/src/main/java/com/example/springboot_demo/common/Result.java b/src/test/src/main/java/com/example/springboot_demo/common/Result.java new file mode 100644 index 00000000..ecac05d4 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/common/Result.java @@ -0,0 +1,37 @@ +package com.example.springboot_demo.common; + +import lombok.Data; + +@Data +public class Result { + private Integer code; + private String message; + private T data; + + public static Result success() { + return success(null); + } + + public static Result success(T data) { + Result result = new Result<>(); + result.setCode(200); + result.setMessage("success"); + result.setData(data); + return result; + } + + public static Result error(String message) { + Result result = new Result<>(); + result.setCode(500); + result.setMessage(message); + return result; + } + + public static Result error(Integer code, String message) { + Result result = new Result<>(); + result.setCode(code); + result.setMessage(message); + return result; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/config/CorsConfig.java b/src/test/src/main/java/com/example/springboot_demo/config/CorsConfig.java new file mode 100644 index 00000000..e6c93e4d --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/config/CorsConfig.java @@ -0,0 +1,26 @@ +package com.example.springboot_demo.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOriginPattern("*"); + config.setAllowCredentials(true); + config.addAllowedMethod("*"); + config.addAllowedHeader("*"); + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java b/src/test/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java new file mode 100644 index 00000000..e1a6df5a --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java @@ -0,0 +1,42 @@ +package com.example.springboot_demo.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +import com.example.springboot_demo.utils.JwtUtil; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class JwtInterceptor implements HandlerInterceptor { + + @Autowired + private JwtUtil jwtUtil; + + @Override + public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { + // Allow OPTIONS requests (CORS preflight) + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return true; + } + + String authHeader = request.getHeader("Authorization"); + if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + if (jwtUtil.validateToken(token)) { + // Optionally set user info in request attributes + request.setAttribute("userId", jwtUtil.getUserIdFromToken(token)); + request.setAttribute("username", jwtUtil.getUsernameFromToken(token)); + return true; + } + } + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/config/SecurityConfig.java b/src/test/src/main/java/com/example/springboot_demo/config/SecurityConfig.java new file mode 100644 index 00000000..2b306532 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/config/SecurityConfig.java @@ -0,0 +1,33 @@ +package com.example.springboot_demo.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) // Disable CSRF for REST APIs + .authorizeHttpRequests(auth -> auth + .requestMatchers("/**").permitAll() // Allow all requests for now, we'll handle auth with Interceptor for specific paths or refine this later. + // Note: The plan mentioned "Initially disable Spring Security's default login page". + // Permitting all here effectively bypasses Spring Security's default auth flow, relying on our custom JWT flow. + ); + return http.build(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java b/src/test/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java new file mode 100644 index 00000000..235832bf --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java @@ -0,0 +1,30 @@ +package com.example.springboot_demo.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + @NonNull + private JwtInterceptor jwtInterceptor; + + @Override + @SuppressWarnings("null") + public void addInterceptors(@NonNull InterceptorRegistry registry) { + registry.addInterceptor(jwtInterceptor) + .addPathPatterns("/**") + .excludePathPatterns( + "/auth/**", // 登录接口免认证 + "/actuator/**", // 健康检查接口免认证(开发环境) + "/role", // 仅 POST /role 免认证 + "/user", // 用户注册免认证 + "/error" + ); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/AuthController.java b/src/test/src/main/java/com/example/springboot_demo/controller/AuthController.java new file mode 100644 index 00000000..ef3ab1fa --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/AuthController.java @@ -0,0 +1,31 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.dto.LoginDTO; +import com.example.springboot_demo.service.AuthService; +import com.example.springboot_demo.vo.LoginVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Autowired + private AuthService authService; + + @PostMapping("/login") + public Result login(@RequestBody LoginDTO loginDTO) { + try { + LoginVO loginVO = authService.login(loginDTO); + return Result.success(loginVO); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java b/src/test/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java new file mode 100644 index 00000000..13ba8982 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/ColumnMetadataController.java @@ -0,0 +1,76 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.ColumnMetadata; +import com.example.springboot_demo.service.ColumnMetadataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/column-metadata") +public class ColumnMetadataController { + + @Autowired + private ColumnMetadataService columnMetadataService; + + /** + * 查询所有字段元数据 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(columnMetadataService.list()); + } + + /** + * 根据表ID查询字段元数据 + */ + @GetMapping("/list/{tableId}") + public Result> listByTable(@PathVariable Long tableId) { + return Result.success(columnMetadataService.listByTableId(tableId)); + } + + /** + * 根据ID查询字段元数据 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(columnMetadataService.getById(id)); + } + + /** + * 添加字段元数据 + */ + @PostMapping + public Result save(@RequestBody ColumnMetadata columnMetadata) { + columnMetadata.setCreateTime(LocalDateTime.now()); + if (columnMetadata.getIsPrimary() == null) { + columnMetadata.setIsPrimary(0); + } + columnMetadataService.save(columnMetadata); + return Result.success(columnMetadata); + } + + /** + * 更新字段元数据 + */ + @PutMapping + public Result update(@RequestBody ColumnMetadata columnMetadata) { + columnMetadataService.updateById(columnMetadata); + return Result.success(columnMetadata); + } + + /** + * 删除字段元数据 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + columnMetadataService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java b/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java new file mode 100644 index 00000000..dbbdd817 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionController.java @@ -0,0 +1,109 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.DbConnection; +import com.example.springboot_demo.service.DbConnectionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/db-connection") +public class DbConnectionController { + + @Autowired + private DbConnectionService dbConnectionService; + + /** + * 查询所有数据库连接 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(dbConnectionService.list()); + } + + /** + * 根据创建者ID查询数据库连接 + */ + @GetMapping("/list/{createUserId}") + public Result> listByUser(@PathVariable Long createUserId) { + return Result.success(dbConnectionService.listByCreateUserId(createUserId)); + } + + /** + * 根据ID查询数据库连接 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(dbConnectionService.getById(id)); + } + + /** + * 添加数据库连接 + */ + @PostMapping + public Result save(@RequestBody DbConnection dbConnection) { + // 验证必填字段 + if (dbConnection.getName() == null || dbConnection.getName().trim().isEmpty()) { + return Result.error("连接名称不能为空"); + } + if (dbConnection.getDbTypeId() == null) { + return Result.error("数据库类型不能为空"); + } + if (dbConnection.getUrl() == null || dbConnection.getUrl().trim().isEmpty()) { + return Result.error("连接地址不能为空"); + } + if (dbConnection.getUsername() == null || dbConnection.getUsername().trim().isEmpty()) { + return Result.error("数据库账号不能为空"); + } + if (dbConnection.getPassword() == null || dbConnection.getPassword().trim().isEmpty()) { + return Result.error("数据库密码不能为空"); + } + + // 设置默认值 + dbConnection.setCreateTime(LocalDateTime.now()); + dbConnection.setUpdateTime(LocalDateTime.now()); + if (dbConnection.getStatus() == null) { + dbConnection.setStatus("disconnected"); + } + if (dbConnection.getCreateUserId() == null) { + // 从请求头或session获取当前用户ID,这里暂时使用默认值1 + dbConnection.setCreateUserId(1L); + } + + dbConnectionService.save(dbConnection); + return Result.success(dbConnection); + } + + /** + * 更新数据库连接 + */ + @PutMapping + public Result update(@RequestBody DbConnection dbConnection) { + dbConnection.setUpdateTime(LocalDateTime.now()); + dbConnectionService.updateById(dbConnection); + return Result.success(dbConnection); + } + + /** + * 删除数据库连接 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + dbConnectionService.removeById(id); + return Result.success(); + } + + /** + * 测试数据库连接 + */ + @GetMapping("/test/{id}") + public Result testConnection(@PathVariable Long id) { + boolean result = dbConnectionService.testConnection(id); + return Result.success(result); + } +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/DbTypeController.java b/src/test/src/main/java/com/example/springboot_demo/controller/DbTypeController.java new file mode 100644 index 00000000..6dc048f5 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/DbTypeController.java @@ -0,0 +1,71 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.DbType; +import com.example.springboot_demo.service.DbTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/db-type") +public class DbTypeController { + + @Autowired + private DbTypeService dbTypeService; + + /** + * 查询所有数据库类型 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(dbTypeService.list()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Integer id) { + return Result.success(dbTypeService.getById(id)); + } + + /** + * 根据类型编码查询 + */ + @GetMapping("/code/{typeCode}") + public Result getByTypeCode(@PathVariable String typeCode) { + return Result.success(dbTypeService.getByTypeCode(typeCode)); + } + + /** + * 添加数据库类型 + */ + @PostMapping + public Result save(@RequestBody DbType dbType) { + dbTypeService.save(dbType); + return Result.success(dbType); + } + + /** + * 更新数据库类型 + */ + @PutMapping + public Result update(@RequestBody DbType dbType) { + dbTypeService.updateById(dbType); + return Result.success(dbType); + } + + /** + * 删除数据库类型 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + dbTypeService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/DialogController.java b/src/test/src/main/java/com/example/springboot_demo/controller/DialogController.java new file mode 100644 index 00000000..9010bb95 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/DialogController.java @@ -0,0 +1,34 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mongodb.DialogRecord; +import com.example.springboot_demo.service.DialogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/dialog") +public class DialogController { + + @Autowired + private DialogService dialogService; + + @GetMapping("/list") + public Result> getUserDialogs(@RequestHeader(value = "userId", defaultValue = "1") Long userId) { + List dialogs = dialogService.getUserDialogs(userId); + return Result.success(dialogs); + } + + @GetMapping("/{dialogId}") + public Result getDialogById(@PathVariable String dialogId) { + DialogRecord dialog = dialogService.getDialogById(dialogId); + if (dialog != null) { + return Result.success(dialog); + } + return Result.error("对话不存在"); + } +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java b/src/test/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java new file mode 100644 index 00000000..294b93c4 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/ErrorLogController.java @@ -0,0 +1,86 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.ErrorLog; +import com.example.springboot_demo.service.ErrorLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/error-log") +public class ErrorLogController { + + @Autowired + private ErrorLogService errorLogService; + + /** + * 查询所有错误日志 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(errorLogService.list()); + } + + /** + * 根据错误类型查询 + */ + @GetMapping("/list/type/{errorTypeId}") + public Result> listByErrorType(@PathVariable Integer errorTypeId) { + return Result.success(errorLogService.listByErrorTypeId(errorTypeId)); + } + + /** + * 根据统计周期查询 + */ + @GetMapping("/list/period/{period}") + public Result> listByPeriod(@PathVariable String period) { + return Result.success(errorLogService.listByPeriod(period)); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(errorLogService.getById(id)); + } + + /** + * 添加错误日志 + */ + @PostMapping + public Result save(@RequestBody ErrorLog errorLog) { + if (errorLog.getStatTime() == null) { + errorLog.setStatTime(LocalDateTime.now()); + } + errorLogService.save(errorLog); + return Result.success(errorLog); + } + + /** + * 更新错误日志 + */ + @PutMapping + public Result update(@RequestBody ErrorLog errorLog) { + errorLogService.updateById(errorLog); + return Result.success(errorLog); + } + + /** + * 删除错误日志 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + errorLogService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java b/src/test/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java new file mode 100644 index 00000000..979d7938 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/ErrorTypeController.java @@ -0,0 +1,74 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.ErrorType; +import com.example.springboot_demo.service.ErrorTypeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/error-type") +public class ErrorTypeController { + + @Autowired + private ErrorTypeService errorTypeService; + + /** + * 查询所有错误类型 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(errorTypeService.list()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Integer id) { + return Result.success(errorTypeService.getById(id)); + } + + /** + * 根据错误编码查询 + */ + @GetMapping("/code/{errorCode}") + public Result getByErrorCode(@PathVariable String errorCode) { + return Result.success(errorTypeService.getByErrorCode(errorCode)); + } + + /** + * 添加错误类型 + */ + @PostMapping + public Result save(@RequestBody ErrorType errorType) { + errorTypeService.save(errorType); + return Result.success(errorType); + } + + /** + * 更新错误类型 + */ + @PutMapping + public Result update(@RequestBody ErrorType errorType) { + errorTypeService.updateById(errorType); + return Result.success(errorType); + } + + /** + * 删除错误类型 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + errorTypeService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java b/src/test/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java new file mode 100644 index 00000000..8ad13173 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/LlmConfigController.java @@ -0,0 +1,117 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.LlmConfig; +import com.example.springboot_demo.service.LlmConfigService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/llm-config") +public class LlmConfigController { + + @Autowired + private LlmConfigService llmConfigService; + + /** + * 查询所有大模型配置 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(llmConfigService.list()); + } + + /** + * 查询所有可用的大模型配置 + */ + @GetMapping("/list/available") + public Result> listAvailable() { + return Result.success(llmConfigService.listAvailable()); + } + + /** + * 根据ID查询大模型配置 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(llmConfigService.getById(id)); + } + + /** + * 添加大模型配置 + */ + @PostMapping + public Result save(@RequestBody LlmConfig llmConfig) { + // 验证必填字段 + if (llmConfig.getName() == null || llmConfig.getName().trim().isEmpty()) { + return Result.error("模型名称不能为空"); + } + if (llmConfig.getVersion() == null || llmConfig.getVersion().trim().isEmpty()) { + return Result.error("模型版本不能为空"); + } + if (llmConfig.getApiKey() == null || llmConfig.getApiKey().trim().isEmpty()) { + return Result.error("API密钥不能为空"); + } + if (llmConfig.getApiUrl() == null || llmConfig.getApiUrl().trim().isEmpty()) { + return Result.error("API地址不能为空"); + } + + // 设置默认值 + llmConfig.setCreateTime(LocalDateTime.now()); + llmConfig.setUpdateTime(LocalDateTime.now()); + if (llmConfig.getIsDisabled() == null) { + llmConfig.setIsDisabled(0); + } + if (llmConfig.getStatusId() == null) { + llmConfig.setStatusId(1); // 默认状态ID为1 + } + if (llmConfig.getTimeout() == null) { + llmConfig.setTimeout(60000); // 默认60秒 + } + if (llmConfig.getCreateUserId() == null) { + // 从请求头或session获取当前用户ID,这里暂时使用默认值1 + llmConfig.setCreateUserId(1L); + } + + llmConfigService.save(llmConfig); + return Result.success(llmConfig); + } + + /** + * 更新大模型配置 + */ + @PutMapping + public Result update(@RequestBody LlmConfig llmConfig) { + llmConfig.setUpdateTime(LocalDateTime.now()); + llmConfigService.updateById(llmConfig); + return Result.success(llmConfig); + } + + /** + * 删除大模型配置 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + llmConfigService.removeById(id); + return Result.success(); + } + + /** + * 禁用/启用大模型配置 + */ + @PutMapping("/{id}/toggle") + public Result toggle(@PathVariable Long id) { + LlmConfig config = llmConfigService.getById(id); + if (config != null) { + config.setIsDisabled(config.getIsDisabled() == 0 ? 1 : 0); + config.setUpdateTime(LocalDateTime.now()); + llmConfigService.updateById(config); + } + return Result.success(); + } +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java b/src/test/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java new file mode 100644 index 00000000..e7437954 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/LlmStatusController.java @@ -0,0 +1,71 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.LlmStatus; +import com.example.springboot_demo.service.LlmStatusService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/llm-status") +public class LlmStatusController { + + @Autowired + private LlmStatusService llmStatusService; + + /** + * 查询所有大模型状态 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(llmStatusService.list()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Integer id) { + return Result.success(llmStatusService.getById(id)); + } + + /** + * 根据状态编码查询 + */ + @GetMapping("/code/{statusCode}") + public Result getByStatusCode(@PathVariable String statusCode) { + return Result.success(llmStatusService.getByStatusCode(statusCode)); + } + + /** + * 添加大模型状态 + */ + @PostMapping + public Result save(@RequestBody LlmStatus llmStatus) { + llmStatusService.save(llmStatus); + return Result.success(llmStatus); + } + + /** + * 更新大模型状态 + */ + @PutMapping + public Result update(@RequestBody LlmStatus llmStatus) { + llmStatusService.updateById(llmStatus); + return Result.success(llmStatus); + } + + /** + * 删除大模型状态 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + llmStatusService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/NotificationController.java b/src/test/src/main/java/com/example/springboot_demo/controller/NotificationController.java new file mode 100644 index 00000000..71f65e69 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/NotificationController.java @@ -0,0 +1,125 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.Notification; +import com.example.springboot_demo.service.NotificationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/notification") +public class NotificationController { + + @Autowired + private NotificationService notificationService; + + /** + * 查询所有通知 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(notificationService.list()); + } + + /** + * 查询已发布的通知 + */ + @GetMapping("/list/published") + public Result> listPublished() { + return Result.success(notificationService.listPublished()); + } + + /** + * 查询草稿 + */ + @GetMapping("/list/drafts") + public Result> listDrafts() { + return Result.success(notificationService.listDrafts()); + } + + /** + * 根据目标ID查询通知 + */ + @GetMapping("/list/target/{targetId}") + public Result> listByTarget(@PathVariable Integer targetId) { + return Result.success(notificationService.listByTargetId(targetId)); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(notificationService.getById(id)); + } + + /** + * 添加通知(草稿) + */ + @PostMapping + public Result save(@RequestBody Notification notification) { + notification.setCreateTime(LocalDateTime.now()); + notification.setLatestUpdateTime(LocalDateTime.now()); + if (notification.getIsTop() == null) { + notification.setIsTop(0); + } + notificationService.save(notification); + return Result.success(notification); + } + + /** + * 更新通知 + */ + @PutMapping + public Result update(@RequestBody Notification notification) { + notification.setLatestUpdateTime(LocalDateTime.now()); + notificationService.updateById(notification); + return Result.success(notification); + } + + /** + * 发布通知 + */ + @PutMapping("/{id}/publish") + public Result publish(@PathVariable Long id) { + Notification notification = notificationService.getById(id); + if (notification != null) { + notification.setPublishTime(LocalDateTime.now()); + notification.setLatestUpdateTime(LocalDateTime.now()); + notificationService.updateById(notification); + } + return Result.success(); + } + + /** + * 置顶/取消置顶 + */ + @PutMapping("/{id}/toggle-top") + public Result toggleTop(@PathVariable Long id) { + Notification notification = notificationService.getById(id); + if (notification != null) { + notification.setIsTop(notification.getIsTop() == 0 ? 1 : 0); + notification.setLatestUpdateTime(LocalDateTime.now()); + notificationService.updateById(notification); + } + return Result.success(); + } + + /** + * 删除通知 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + notificationService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java b/src/test/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java new file mode 100644 index 00000000..4053b71a --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/NotificationTargetController.java @@ -0,0 +1,74 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.NotificationTarget; +import com.example.springboot_demo.service.NotificationTargetService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/notification-target") +public class NotificationTargetController { + + @Autowired + private NotificationTargetService notificationTargetService; + + /** + * 查询所有通知目标 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(notificationTargetService.list()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Integer id) { + return Result.success(notificationTargetService.getById(id)); + } + + /** + * 根据目标编码查询 + */ + @GetMapping("/code/{targetCode}") + public Result getByTargetCode(@PathVariable String targetCode) { + return Result.success(notificationTargetService.getByTargetCode(targetCode)); + } + + /** + * 添加通知目标 + */ + @PostMapping + public Result save(@RequestBody NotificationTarget notificationTarget) { + notificationTargetService.save(notificationTarget); + return Result.success(notificationTarget); + } + + /** + * 更新通知目标 + */ + @PutMapping + public Result update(@RequestBody NotificationTarget notificationTarget) { + notificationTargetService.updateById(notificationTarget); + return Result.success(notificationTarget); + } + + /** + * 删除通知目标 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + notificationTargetService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/OperationLogController.java b/src/test/src/main/java/com/example/springboot_demo/controller/OperationLogController.java new file mode 100644 index 00000000..7046f985 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/OperationLogController.java @@ -0,0 +1,82 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.OperationLog; +import com.example.springboot_demo.service.OperationLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/operation-log") +public class OperationLogController { + + @Autowired + private OperationLogService operationLogService; + + /** + * 查询所有操作日志 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(operationLogService.list()); + } + + /** + * 根据用户ID查询操作日志 + */ + @GetMapping("/list/user/{userId}") + public Result> listByUser(@PathVariable Long userId) { + return Result.success(operationLogService.listByUserId(userId)); + } + + /** + * 根据模块查询操作日志 + */ + @GetMapping("/list/module/{module}") + public Result> listByModule(@PathVariable String module) { + return Result.success(operationLogService.listByModule(module)); + } + + /** + * 查询失败的操作日志 + */ + @GetMapping("/list/failed") + public Result> listFailed() { + return Result.success(operationLogService.listFailed()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(operationLogService.getById(id)); + } + + /** + * 添加操作日志 + */ + @PostMapping + public Result save(@RequestBody OperationLog operationLog) { + if (operationLog.getOperateTime() == null) { + operationLog.setOperateTime(LocalDateTime.now()); + } + operationLogService.save(operationLog); + return Result.success(operationLog); + } + + /** + * 删除操作日志 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + operationLogService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/PriorityController.java b/src/test/src/main/java/com/example/springboot_demo/controller/PriorityController.java new file mode 100644 index 00000000..7912c334 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/PriorityController.java @@ -0,0 +1,74 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.Priority; +import com.example.springboot_demo.service.PriorityService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/priority") +public class PriorityController { + + @Autowired + private PriorityService priorityService; + + /** + * 查询所有优先级(按排序) + */ + @GetMapping("/list") + public Result> list() { + return Result.success(priorityService.listOrderBySort()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Integer id) { + return Result.success(priorityService.getById(id)); + } + + /** + * 根据优先级编码查询 + */ + @GetMapping("/code/{priorityCode}") + public Result getByPriorityCode(@PathVariable String priorityCode) { + return Result.success(priorityService.getByPriorityCode(priorityCode)); + } + + /** + * 添加优先级 + */ + @PostMapping + public Result save(@RequestBody Priority priority) { + priorityService.save(priority); + return Result.success(priority); + } + + /** + * 更新优先级 + */ + @PutMapping + public Result update(@RequestBody Priority priority) { + priorityService.updateById(priority); + return Result.success(priority); + } + + /** + * 删除优先级 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Integer id) { + priorityService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/QueryController.java b/src/test/src/main/java/com/example/springboot_demo/controller/QueryController.java new file mode 100644 index 00000000..78d2f7a3 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/QueryController.java @@ -0,0 +1,29 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.dto.QueryRequestDTO; +import com.example.springboot_demo.service.QueryService; +import com.example.springboot_demo.vo.QueryResponseVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/query") +public class QueryController { + + @Autowired + private QueryService queryService; + + @PostMapping("/execute") + public Result executeQuery(@RequestBody QueryRequestDTO request, + @RequestHeader(value = "userId", defaultValue = "1") Long userId) { + try { + QueryResponseVO response = queryService.executeQuery(request, userId); + return Result.success(response); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/QueryLogController.java b/src/test/src/main/java/com/example/springboot_demo/controller/QueryLogController.java new file mode 100644 index 00000000..22f789c3 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/QueryLogController.java @@ -0,0 +1,78 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.QueryLog; +import com.example.springboot_demo.service.QueryLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/query-log") +public class QueryLogController { + + @Autowired + private QueryLogService queryLogService; + + /** + * 查询所有查询日志 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(queryLogService.list()); + } + + /** + * 根据用户ID查询查询日志 + */ + @GetMapping("/list/user/{userId}") + public Result> listByUser(@PathVariable Long userId) { + return Result.success(queryLogService.listByUserId(userId)); + } + + /** + * 根据对话ID查询查询日志 + */ + @GetMapping("/list/dialog/{dialogId}") + public Result> listByDialog(@PathVariable String dialogId) { + return Result.success(queryLogService.listByDialogId(dialogId)); + } + + /** + * 根据ID查询查询日志 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(queryLogService.getById(id)); + } + + /** + * 添加查询日志 + */ + @PostMapping + public Result save(@RequestBody QueryLog queryLog) { + if (queryLog.getQueryDate() == null) { + queryLog.setQueryDate(LocalDate.now()); + } + if (queryLog.getQueryTime() == null) { + queryLog.setQueryTime(LocalDateTime.now()); + } + queryLogService.save(queryLog); + return Result.success(queryLog); + } + + /** + * 删除查询日志 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + queryLogService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/RoleController.java b/src/test/src/main/java/com/example/springboot_demo/controller/RoleController.java new file mode 100644 index 00000000..b6756740 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/RoleController.java @@ -0,0 +1,28 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.Role; +import com.example.springboot_demo.service.RoleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/role") +public class RoleController { + + @Autowired + private RoleService roleService; + + @PostMapping + public Result add(@RequestBody Role role) { + roleService.save(role); + return Result.success(role); + } + + @GetMapping("/list") + public Result> list() { + return Result.success(roleService.list()); + } +} diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java b/src/test/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java new file mode 100644 index 00000000..0c7429bd --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/SystemHealthController.java @@ -0,0 +1,77 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.SystemHealth; +import com.example.springboot_demo.service.SystemHealthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/system-health") +public class SystemHealthController { + + @Autowired + private SystemHealthService systemHealthService; + + /** + * 查询所有健康记录 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(systemHealthService.list()); + } + + /** + * 查询最新的健康记录 + */ + @GetMapping("/latest") + public Result getLatest() { + return Result.success(systemHealthService.getLatest()); + } + + /** + * 查询最近N条健康记录 + */ + @GetMapping("/recent/{limit}") + public Result> listRecent(@PathVariable int limit) { + return Result.success(systemHealthService.listRecent(limit)); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(systemHealthService.getById(id)); + } + + /** + * 添加健康记录 + */ + @PostMapping + public Result save(@RequestBody SystemHealth systemHealth) { + if (systemHealth.getCollectTime() == null) { + systemHealth.setCollectTime(LocalDateTime.now()); + } + systemHealthService.save(systemHealth); + return Result.success(systemHealth); + } + + /** + * 删除健康记录 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + systemHealthService.removeById(id); + return Result.success(); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java b/src/test/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java new file mode 100644 index 00000000..f6f77f72 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/TableMetadataController.java @@ -0,0 +1,75 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.TableMetadata; +import com.example.springboot_demo.service.TableMetadataService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/table-metadata") +public class TableMetadataController { + + @Autowired + private TableMetadataService tableMetadataService; + + /** + * 查询所有表元数据 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(tableMetadataService.list()); + } + + /** + * 根据数据库连接ID查询表元数据 + */ + @GetMapping("/list/{dbConnectionId}") + public Result> listByDbConnection(@PathVariable Long dbConnectionId) { + return Result.success(tableMetadataService.listByDbConnectionId(dbConnectionId)); + } + + /** + * 根据ID查询表元数据 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(tableMetadataService.getById(id)); + } + + /** + * 添加表元数据 + */ + @PostMapping + public Result save(@RequestBody TableMetadata tableMetadata) { + tableMetadata.setCreateTime(LocalDateTime.now()); + tableMetadata.setUpdateTime(LocalDateTime.now()); + tableMetadataService.save(tableMetadata); + return Result.success(tableMetadata); + } + + /** + * 更新表元数据 + */ + @PutMapping + public Result update(@RequestBody TableMetadata tableMetadata) { + tableMetadata.setUpdateTime(LocalDateTime.now()); + tableMetadataService.updateById(tableMetadata); + return Result.success(tableMetadata); + } + + /** + * 删除表元数据 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + tableMetadataService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/TestController.java b/src/test/src/main/java/com/example/springboot_demo/controller/TestController.java new file mode 100644 index 00000000..7e9efbd8 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/TestController.java @@ -0,0 +1,107 @@ +package com.example.springboot_demo.controller; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/test") +public class TestController { + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Autowired + private MongoTemplate mongoTemplate; + + @Autowired + private StringRedisTemplate redisTemplate; + + @GetMapping("/hello") + public String hello() { + return "Hello, Spring Boot!"; + } + + @GetMapping("/mysql") + public Map testMySQL() { + Map result = new HashMap<>(); + try { + // 查询 MySQL 版本 + String version = jdbcTemplate.queryForObject("SELECT VERSION()", String.class); + // 查询数据库名 + String database = jdbcTemplate.queryForObject("SELECT DATABASE()", String.class); + // 查询表数量 + Integer tableCount = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = ?", + Integer.class, + database + ); + + result.put("status", "success"); + result.put("version", version); + result.put("database", database); + result.put("tableCount", tableCount); + } catch (Exception e) { + result.put("status", "error"); + result.put("message", e.getMessage()); + } + return result; + } + + @GetMapping("/mongodb") + public Map testMongoDB() { + Map result = new HashMap<>(); + try { + // 获取数据库名 + String dbName = mongoTemplate.getDb().getName(); + // 获取集合数量 + int collectionCount = mongoTemplate.getDb().listCollectionNames().into(new java.util.ArrayList<>()).size(); + + result.put("status", "success"); + result.put("database", dbName); + result.put("collectionCount", collectionCount); + } catch (Exception e) { + result.put("status", "error"); + result.put("message", e.getMessage()); + } + return result; + } + + @GetMapping("/redis") + public Map testRedis() { + Map result = new HashMap<>(); + try { + // 测试写入 + redisTemplate.opsForValue().set("test:key", "test_value"); + // 测试读取 + String value = redisTemplate.opsForValue().get("test:key"); + // 删除测试数据 + redisTemplate.delete("test:key"); + + result.put("status", "success"); + result.put("message", "Redis 连接正常"); + result.put("testValue", value); + } catch (Exception e) { + result.put("status", "error"); + result.put("message", e.getMessage()); + } + return result; + } + + @GetMapping("/all") + public Map testAll() { + Map result = new HashMap<>(); + result.put("mysql", testMySQL()); + result.put("mongodb", testMongoDB()); + result.put("redis", testRedis()); + return result; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/UserController.java b/src/test/src/main/java/com/example/springboot_demo/controller/UserController.java new file mode 100644 index 00000000..77e982fb --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/UserController.java @@ -0,0 +1,87 @@ +package com.example.springboot_demo.controller; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.User; +import com.example.springboot_demo.service.UserService; + +@RestController +@RequestMapping("/user") +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + User user = userService.getById(id); + if (user != null) { + return Result.success(user); + } + return Result.error("用户不存在"); + } + + @GetMapping("/list") + public Result> list() { + List users = userService.list(); + return Result.success(users); + } + + @GetMapping("/page") + public Result> page( + @RequestParam(defaultValue = "1") int current, + @RequestParam(defaultValue = "10") int size) { + Page page = userService.page(current, size); + return Result.success(page); + } + + @PostMapping + public Result save(@RequestBody User user) { + boolean success = userService.save(user); + if (success) { + return Result.success("添加成功"); + } + return Result.error("添加失败"); + } + + @PutMapping + public Result update(@RequestBody User user) { + boolean success = userService.updateById(user); + if (success) { + return Result.success("更新成功"); + } + return Result.error("更新失败"); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + boolean success = userService.removeById(id); + if (success) { + return Result.success("删除成功"); + } + return Result.error("删除失败"); + } + + @GetMapping("/username/{username}") + public Result getByUsername(@PathVariable String username) { + User user = userService.getByUsername(username); + if (user != null) { + return Result.success(user); + } + return Result.error("用户不存在"); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java b/src/test/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java new file mode 100644 index 00000000..c91565ab --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/UserDbPermissionController.java @@ -0,0 +1,95 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.UserDbPermission; +import com.example.springboot_demo.service.UserDbPermissionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/user-db-permission") +public class UserDbPermissionController { + + @Autowired + private UserDbPermissionService userDbPermissionService; + + /** + * 查询所有权限 + */ + @GetMapping("/list") + public Result> list() { + return Result.success(userDbPermissionService.list()); + } + + /** + * 查询已分配权限的用户 + */ + @GetMapping("/list/assigned") + public Result> listAssigned() { + return Result.success(userDbPermissionService.listAssigned()); + } + + /** + * 查询未分配权限的用户 + */ + @GetMapping("/list/unassigned") + public Result> listUnassigned() { + return Result.success(userDbPermissionService.listUnassigned()); + } + + /** + * 根据ID查询 + */ + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(userDbPermissionService.getById(id)); + } + + /** + * 根据用户ID查询权限 + */ + @GetMapping("/user/{userId}") + public Result getByUserId(@PathVariable Long userId) { + return Result.success(userDbPermissionService.getByUserId(userId)); + } + + /** + * 添加或更新权限 + */ + @PostMapping + public Result save(@RequestBody UserDbPermission permission) { + permission.setLastGrantTime(LocalDateTime.now()); + permission.setUpdateTime(LocalDateTime.now()); + if (permission.getIsAssigned() == null) { + permission.setIsAssigned(1); + } + userDbPermissionService.save(permission); + return Result.success(permission); + } + + /** + * 更新权限 + */ + @PutMapping + public Result update(@RequestBody UserDbPermission permission) { + permission.setLastGrantTime(LocalDateTime.now()); + permission.setUpdateTime(LocalDateTime.now()); + userDbPermissionService.updateById(permission); + return Result.success(permission); + } + + /** + * 删除权限 + */ + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + userDbPermissionService.removeById(id); + return Result.success(); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/dto/LoginDTO.java b/src/test/src/main/java/com/example/springboot_demo/dto/LoginDTO.java new file mode 100644 index 00000000..bc48a109 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/dto/LoginDTO.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.dto; + +import lombok.Data; + +@Data +public class LoginDTO { + private String username; + private String password; +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java b/src/test/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java new file mode 100644 index 00000000..dff2b8c9 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/dto/QueryRequestDTO.java @@ -0,0 +1,13 @@ +package com.example.springboot_demo.dto; + +import lombok.Data; + +@Data +public class QueryRequestDTO { + private String userPrompt; // 用户的自然语言查询 + private String model; // 使用的大模型 + private String database; // 使用的数据库 + private String conversationId; // 对话ID(用于多轮对话) +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java new file mode 100644 index 00000000..e72179ef --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogRecord.java @@ -0,0 +1,29 @@ +package com.example.springboot_demo.entity.mongodb; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Data +@Document(collection = "dialog_records") +public class DialogRecord { + + @Id + private String id; + + private String dialogId; + + private Long userId; + + private String topic; + + private Integer totalRounds; + + private LocalDateTime startTime; + + private LocalDateTime lastTime; +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java new file mode 100644 index 00000000..32f43878 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/ColumnMetadata.java @@ -0,0 +1,30 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("column_metadata") +public class ColumnMetadata { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long tableId; + + private String columnName; + + private String dataType; + + private String description; + + private Integer isPrimary; + + private LocalDateTime createTime; +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java new file mode 100644 index 00000000..6b3274ca --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnection.java @@ -0,0 +1,35 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("db_connections") +public class DbConnection { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String name; + + private Integer dbTypeId; + + private String url; + + private String username; + + private String password; + + private String status; + + private Long createUserId; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java new file mode 100644 index 00000000..bcbff5ef --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbType.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("db_types") +public class DbType { + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + private String typeName; + + private String typeCode; + + private String description; +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java new file mode 100644 index 00000000..6a708bf0 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/ErrorLog.java @@ -0,0 +1,29 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@TableName("error_logs") +public class ErrorLog { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Integer errorTypeId; + + private Integer errorCount; + + private BigDecimal errorRate; + + private String period; + + private LocalDateTime statTime; +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java new file mode 100644 index 00000000..548e619e --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/ErrorType.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("error_types") +public class ErrorType { + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + private String errorName; + + private String errorCode; + + private String description; +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java new file mode 100644 index 00000000..7fba5c66 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/LlmConfig.java @@ -0,0 +1,37 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("llm_configs") +public class LlmConfig { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String name; + + private String version; + + private String apiKey; + + private String apiUrl; + + private Integer statusId; + + private Integer isDisabled; + + private Integer timeout; + + private Long createUserId; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java new file mode 100644 index 00000000..834d787f --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/LlmStatus.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("llm_status") +public class LlmStatus { + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + private String statusName; + + private String statusCode; + + private String description; +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java new file mode 100644 index 00000000..d9dac720 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/Notification.java @@ -0,0 +1,39 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("notifications") +public class Notification { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String title; + + private String content; + + private Integer targetId; + + private Integer priorityId; + + private Long publisherId; + + private Integer isTop; + + private LocalDateTime publishTime; + + private LocalDateTime createTime; + + private LocalDateTime latestUpdateTime; +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java new file mode 100644 index 00000000..47f0d16f --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/NotificationTarget.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("notification_targets") +public class NotificationTarget { + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + private String targetName; + + private String targetCode; + + private String description; +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java new file mode 100644 index 00000000..449a8f10 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/OperationLog.java @@ -0,0 +1,36 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("operation_logs") +public class OperationLog { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long userId; + + private String username; + + private String operation; + + private String module; + + private String relatedLlm; + + private String ipAddress; + + private LocalDateTime operateTime; + + private Integer result; + + private String errorMsg; +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java new file mode 100644 index 00000000..1800e3f5 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/Priority.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("priorities") +public class Priority { + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + private String priorityName; + + private String priorityCode; + + private Integer sort; +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java new file mode 100644 index 00000000..b192dbfe --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryLog.java @@ -0,0 +1,31 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@TableName("query_logs") +public class QueryLog { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private String dialogId; + + private Long dataSourceId; + + private Long userId; + + private LocalDate queryDate; + + private LocalDateTime queryTime; + + private Integer executeResult; +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/Role.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/Role.java new file mode 100644 index 00000000..2685fbef --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/Role.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("roles") +public class Role { + + @TableId(type = IdType.AUTO) + private Integer id; + + private String roleName; + + private String roleCode; + + private String description; +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java new file mode 100644 index 00000000..949bfff0 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/SystemHealth.java @@ -0,0 +1,32 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@TableName("system_health") +public class SystemHealth { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Integer dbDelay; + + private Integer cacheDelay; + + private Integer llmDelay; + + private BigDecimal storageUsage; + + private LocalDateTime collectTime; +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java new file mode 100644 index 00000000..537e2607 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TableMetadata.java @@ -0,0 +1,27 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("table_metadata") +public class TableMetadata { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long dbConnectionId; + + private String tableName; + + private String description; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/User.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/User.java new file mode 100644 index 00000000..6bf4b10f --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/User.java @@ -0,0 +1,38 @@ +package com.example.springboot_demo.entity.mysql; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +@Data +@TableName("users") +public class User { + + @TableId(type = IdType.AUTO) + private Long id; + + private String username; + + private String password; + + private String email; + + private String phonenumber; + + private Integer roleId; + + private String avatarUrl; + + private Integer status; + + private Integer onlineStatus; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java new file mode 100644 index 00000000..77395ad1 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserDbPermission.java @@ -0,0 +1,30 @@ +package com.example.springboot_demo.entity.mysql; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@TableName("user_db_permissions") +public class UserDbPermission { + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + private Long userId; + + private String permissionDetails; // JSON格式字符串 + + private Long lastGrantUserId; + + private Integer isAssigned; + + private LocalDateTime lastGrantTime; + + private LocalDateTime updateTime; +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/exception/BusinessException.java b/src/test/src/main/java/com/example/springboot_demo/exception/BusinessException.java new file mode 100644 index 00000000..d373a8ec --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/exception/BusinessException.java @@ -0,0 +1,42 @@ +package com.example.springboot_demo.exception; + +/** + * 业务异常类 + */ +public class BusinessException extends RuntimeException { + + private int code; + + /** + * 构造函数 + * @param message 错误信息 + */ + public BusinessException(String message) { + super(message); + this.code = 400; // 默认业务错误码 + } + + /** + * 构造函数 + * @param code 错误码 + * @param message 错误信息 + */ + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + /** + * 获取错误码 + */ + public int getCode() { + return code; + } + + /** + * 设置错误码 + */ + public void setCode(int code) { + this.code = code; + } +} \ No newline at end of file diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java new file mode 100644 index 00000000..1dc43695 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/ColumnMetadataMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.ColumnMetadata; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ColumnMetadataMapper extends BaseMapper { +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java new file mode 100644 index 00000000..2fe5045d --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.DbConnection; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DbConnectionMapper extends BaseMapper { +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java new file mode 100644 index 00000000..09d97b6e --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/DbTypeMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.DbType; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DbTypeMapper extends BaseMapper { +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java new file mode 100644 index 00000000..df160331 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/ErrorLogMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.ErrorLog; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ErrorLogMapper extends BaseMapper { +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java new file mode 100644 index 00000000..b9148bca --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/ErrorTypeMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.ErrorType; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ErrorTypeMapper extends BaseMapper { +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java new file mode 100644 index 00000000..53198bf5 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/LlmConfigMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.LlmConfig; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface LlmConfigMapper extends BaseMapper { +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java new file mode 100644 index 00000000..b03abe84 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/LlmStatusMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.LlmStatus; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface LlmStatusMapper extends BaseMapper { +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java new file mode 100644 index 00000000..30259ac6 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/NotificationMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.Notification; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface NotificationMapper extends BaseMapper { +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java new file mode 100644 index 00000000..f228f075 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/NotificationTargetMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.NotificationTarget; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface NotificationTargetMapper extends BaseMapper { +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java new file mode 100644 index 00000000..0c7c52c6 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/OperationLogMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.OperationLog; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OperationLogMapper extends BaseMapper { +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java new file mode 100644 index 00000000..a3a8149f --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/PriorityMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.Priority; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PriorityMapper extends BaseMapper { +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java new file mode 100644 index 00000000..7f335438 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/QueryLogMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.QueryLog; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface QueryLogMapper extends BaseMapper { +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java new file mode 100644 index 00000000..4ea0159a --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/RoleMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.Role; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface RoleMapper extends BaseMapper { +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java new file mode 100644 index 00000000..30b80f6e --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/SystemHealthMapper.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.SystemHealth; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SystemHealthMapper extends BaseMapper { +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java new file mode 100644 index 00000000..1796fd36 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/TableMetadataMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.TableMetadata; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TableMetadataMapper extends BaseMapper { +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java new file mode 100644 index 00000000..83b35f82 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/UserDbPermissionMapper.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.UserDbPermission; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserDbPermissionMapper extends BaseMapper { +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/UserMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/UserMapper.java new file mode 100644 index 00000000..cf537ad9 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/UserMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.User; + +@Mapper +public interface UserMapper extends BaseMapper { +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java new file mode 100644 index 00000000..10a74819 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/DialogRecordRepository.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.repository; + +import com.example.springboot_demo.entity.mongodb.DialogRecord; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface DialogRecordRepository extends MongoRepository { + List findByUserIdOrderByLastTimeDesc(Long userId); + DialogRecord findByDialogId(String dialogId); +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/AuthService.java b/src/test/src/main/java/com/example/springboot_demo/service/AuthService.java new file mode 100644 index 00000000..2b09304f --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/AuthService.java @@ -0,0 +1,10 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.dto.LoginDTO; +import com.example.springboot_demo.vo.LoginVO; + +public interface AuthService { + LoginVO login(LoginDTO loginDTO); +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java b/src/test/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java new file mode 100644 index 00000000..fb20e175 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/ColumnMetadataService.java @@ -0,0 +1,16 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.ColumnMetadata; + +import java.util.List; + +public interface ColumnMetadataService extends IService { + /** + * 根据表ID查询字段元数据列表 + */ + List listByTableId(Long tableId); +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionService.java b/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionService.java new file mode 100644 index 00000000..cc488b7a --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionService.java @@ -0,0 +1,20 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.DbConnection; + +import java.util.List; + +public interface DbConnectionService extends IService { + /** + * 根据创建者ID查询数据库连接列表 + */ + List listByCreateUserId(Long createUserId); + + /** + * 测试数据库连接 + */ + boolean testConnection(Long id); +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/DbTypeService.java b/src/test/src/main/java/com/example/springboot_demo/service/DbTypeService.java new file mode 100644 index 00000000..bf73ad08 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/DbTypeService.java @@ -0,0 +1,14 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.DbType; + +public interface DbTypeService extends IService { + /** + * 根据类型编码查询 + */ + DbType getByTypeCode(String typeCode); +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/DialogService.java b/src/test/src/main/java/com/example/springboot_demo/service/DialogService.java new file mode 100644 index 00000000..1820ac01 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/DialogService.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mongodb.DialogRecord; + +import java.util.List; + +public interface DialogService { + List getUserDialogs(Long userId); + DialogRecord getDialogById(String dialogId); +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/ErrorLogService.java b/src/test/src/main/java/com/example/springboot_demo/service/ErrorLogService.java new file mode 100644 index 00000000..7e52e227 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/ErrorLogService.java @@ -0,0 +1,24 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.ErrorLog; + +import java.util.List; + +public interface ErrorLogService extends IService { + /** + * 根据错误类型查询 + */ + List listByErrorTypeId(Integer errorTypeId); + + /** + * 根据统计周期查询 + */ + List listByPeriod(String period); +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java b/src/test/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java new file mode 100644 index 00000000..7d333874 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/ErrorTypeService.java @@ -0,0 +1,17 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.ErrorType; + +public interface ErrorTypeService extends IService { + /** + * 根据错误编码查询 + */ + ErrorType getByErrorCode(String errorCode); +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/LlmConfigService.java b/src/test/src/main/java/com/example/springboot_demo/service/LlmConfigService.java new file mode 100644 index 00000000..ee679470 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/LlmConfigService.java @@ -0,0 +1,15 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.LlmConfig; + +import java.util.List; + +public interface LlmConfigService extends IService { + /** + * 获取所有可用的大模型配置 + */ + List listAvailable(); +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/LlmService.java b/src/test/src/main/java/com/example/springboot_demo/service/LlmService.java new file mode 100644 index 00000000..2fc9728b --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/LlmService.java @@ -0,0 +1,23 @@ +package com.example.springboot_demo.service; + +import java.util.Map; + +/** + * 大模型调用服务接口 + */ +public interface LlmService { + /** + * 调用大模型生成SQL和结果 + * @param prompt 用户提示词 + * @param modelName 模型名称 + * @param databaseName 数据库名称 + * @return 包含SQL、表格数据和图表数据的Map + */ + Map generateQuery(String prompt, String modelName, String databaseName); +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/LlmStatusService.java b/src/test/src/main/java/com/example/springboot_demo/service/LlmStatusService.java new file mode 100644 index 00000000..ab52b85d --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/LlmStatusService.java @@ -0,0 +1,14 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.LlmStatus; + +public interface LlmStatusService extends IService { + /** + * 根据状态编码查询 + */ + LlmStatus getByStatusCode(String statusCode); +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/NotificationService.java b/src/test/src/main/java/com/example/springboot_demo/service/NotificationService.java new file mode 100644 index 00000000..6cbea2f0 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/NotificationService.java @@ -0,0 +1,29 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.Notification; + +import java.util.List; + +public interface NotificationService extends IService { + /** + * 查询所有已发布的通知(置顶优先,按时间排序) + */ + List listPublished(); + + /** + * 查询所有草稿 + */ + List listDrafts(); + + /** + * 根据目标ID查询通知 + */ + List listByTargetId(Integer targetId); +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java b/src/test/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java new file mode 100644 index 00000000..4cc954cf --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/NotificationTargetService.java @@ -0,0 +1,17 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.NotificationTarget; + +public interface NotificationTargetService extends IService { + /** + * 根据目标编码查询 + */ + NotificationTarget getByTargetCode(String targetCode); +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/OperationLogService.java b/src/test/src/main/java/com/example/springboot_demo/service/OperationLogService.java new file mode 100644 index 00000000..b15542ff --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/OperationLogService.java @@ -0,0 +1,26 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.OperationLog; + +import java.util.List; + +public interface OperationLogService extends IService { + /** + * 根据用户ID查询操作日志 + */ + List listByUserId(Long userId); + + /** + * 根据模块查询操作日志 + */ + List listByModule(String module); + + /** + * 查询失败的操作日志 + */ + List listFailed(); +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/PriorityService.java b/src/test/src/main/java/com/example/springboot_demo/service/PriorityService.java new file mode 100644 index 00000000..fadedad1 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/PriorityService.java @@ -0,0 +1,24 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.Priority; + +import java.util.List; + +public interface PriorityService extends IService { + /** + * 根据优先级编码查询 + */ + Priority getByPriorityCode(String priorityCode); + + /** + * 按排序权重查询所有优先级 + */ + List listOrderBySort(); +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/QueryLogService.java b/src/test/src/main/java/com/example/springboot_demo/service/QueryLogService.java new file mode 100644 index 00000000..7aba0403 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/QueryLogService.java @@ -0,0 +1,21 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.QueryLog; + +import java.util.List; + +public interface QueryLogService extends IService { + /** + * 根据用户ID查询查询日志 + */ + List listByUserId(Long userId); + + /** + * 根据对话ID查询查询日志 + */ + List listByDialogId(String dialogId); +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/QueryService.java b/src/test/src/main/java/com/example/springboot_demo/service/QueryService.java new file mode 100644 index 00000000..c70897c1 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/QueryService.java @@ -0,0 +1,10 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.dto.QueryRequestDTO; +import com.example.springboot_demo.vo.QueryResponseVO; + +public interface QueryService { + QueryResponseVO executeQuery(QueryRequestDTO request, Long userId); +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/RoleService.java b/src/test/src/main/java/com/example/springboot_demo/service/RoleService.java new file mode 100644 index 00000000..f9a5d4ae --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/RoleService.java @@ -0,0 +1,7 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.Role; + +public interface RoleService extends IService { +} diff --git a/src/test/src/main/java/com/example/springboot_demo/service/SystemHealthService.java b/src/test/src/main/java/com/example/springboot_demo/service/SystemHealthService.java new file mode 100644 index 00000000..5377752b --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/SystemHealthService.java @@ -0,0 +1,24 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.SystemHealth; + +import java.util.List; + +public interface SystemHealthService extends IService { + /** + * 查询最新的健康记录 + */ + SystemHealth getLatest(); + + /** + * 查询最近N条健康记录 + */ + List listRecent(int limit); +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/TableMetadataService.java b/src/test/src/main/java/com/example/springboot_demo/service/TableMetadataService.java new file mode 100644 index 00000000..7d66bc37 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/TableMetadataService.java @@ -0,0 +1,14 @@ +package com.example.springboot_demo.service; + +import java.util.List; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.TableMetadata; + +public interface TableMetadataService extends IService { + /** + * 根据数据库连接ID查询表元数据列表 + */ + List listByDbConnectionId(Long dbConnectionId); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java b/src/test/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java new file mode 100644 index 00000000..e776639d --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/UserDbPermissionService.java @@ -0,0 +1,26 @@ +package com.example.springboot_demo.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.springboot_demo.entity.mysql.UserDbPermission; + +import java.util.List; + +public interface UserDbPermissionService extends IService { + /** + * 根据用户ID查询权限 + */ + UserDbPermission getByUserId(Long userId); + + /** + * 查询所有已分配权限的用户 + */ + List listAssigned(); + + /** + * 查询所有未分配权限的用户 + */ + List listUnassigned(); +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/UserService.java b/src/test/src/main/java/com/example/springboot_demo/service/UserService.java new file mode 100644 index 00000000..dae70f44 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/UserService.java @@ -0,0 +1,24 @@ +package com.example.springboot_demo.service; + +import java.util.List; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.example.springboot_demo.entity.mysql.User; + +public interface UserService { + + User getById(Long id); + + List list(); + + Page page(int current, int size); + + boolean save(User user); + + boolean updateById(User user); + + boolean removeById(Long id); + + User getByUsername(String username); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java new file mode 100644 index 00000000..8f57905d --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java @@ -0,0 +1,70 @@ +package com.example.springboot_demo.service.impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.example.springboot_demo.dto.LoginDTO; +import com.example.springboot_demo.entity.mysql.Role; +import com.example.springboot_demo.entity.mysql.User; +import com.example.springboot_demo.mapper.RoleMapper; +import com.example.springboot_demo.mapper.UserMapper; +import com.example.springboot_demo.service.AuthService; +import com.example.springboot_demo.utils.JwtUtil; +import com.example.springboot_demo.vo.LoginVO; + +@Service +public class AuthServiceImpl implements AuthService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private RoleMapper roleMapper; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public LoginVO login(LoginDTO loginDTO) { + // 查询用户 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getUsername, loginDTO.getUsername()); + User user = userMapper.selectOne(wrapper); + + if (user == null) { + throw new RuntimeException("用户不存在"); + } + + // 验证密码 + if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) { + throw new RuntimeException("密码错误"); + } + + // 检查账号状态 + if (user.getStatus() == 0) { + throw new RuntimeException("账号已被禁用"); + } + + // 查询角色信息 + Role role = roleMapper.selectById(user.getRoleId()); + + // 构造返回数据 + LoginVO loginVO = new LoginVO(); + loginVO.setToken(jwtUtil.generateToken(user.getId(), user.getUsername())); + loginVO.setUserId(user.getId()); + loginVO.setUsername(user.getUsername()); + loginVO.setEmail(user.getEmail()); + loginVO.setRoleId(user.getRoleId()); + loginVO.setRoleName(role != null ? role.getRoleName() : null); + loginVO.setAvatarUrl(user.getAvatarUrl()); + + return loginVO; + } +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java new file mode 100644 index 00000000..7cee6d47 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/ColumnMetadataServiceImpl.java @@ -0,0 +1,27 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.ColumnMetadata; +import com.example.springboot_demo.mapper.ColumnMetadataMapper; +import com.example.springboot_demo.service.ColumnMetadataService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ColumnMetadataServiceImpl extends ServiceImpl implements ColumnMetadataService { + + @Override + public List listByTableId(Long tableId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ColumnMetadata::getTableId, tableId); + // 主键字段优先 + wrapper.orderByDesc(ColumnMetadata::getIsPrimary); + wrapper.orderByAsc(ColumnMetadata::getColumnName); + return list(wrapper); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java new file mode 100644 index 00000000..b6782dfa --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionServiceImpl.java @@ -0,0 +1,36 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.DbConnection; +import com.example.springboot_demo.mapper.DbConnectionMapper; +import com.example.springboot_demo.service.DbConnectionService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class DbConnectionServiceImpl extends ServiceImpl implements DbConnectionService { + + @Override + public List listByCreateUserId(Long createUserId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DbConnection::getCreateUserId, createUserId); + wrapper.orderByDesc(DbConnection::getCreateTime); + return list(wrapper); + } + + @Override + public boolean testConnection(Long id) { + // TODO: 实现真实的数据库连接测试逻辑 + // 暂时返回 Mock 结果 + DbConnection connection = getById(id); + if (connection == null) { + return false; + } + // 这里应该根据 db_type_id 和连接信息实际测试连接 + return true; + } +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java new file mode 100644 index 00000000..a4dea719 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/DbTypeServiceImpl.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.DbType; +import com.example.springboot_demo.mapper.DbTypeMapper; +import com.example.springboot_demo.service.DbTypeService; +import org.springframework.stereotype.Service; + +@Service +public class DbTypeServiceImpl extends ServiceImpl implements DbTypeService { + + @Override + public DbType getByTypeCode(String typeCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(DbType::getTypeCode, typeCode); + return getOne(wrapper); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java new file mode 100644 index 00000000..655f4303 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java @@ -0,0 +1,28 @@ +package com.example.springboot_demo.service.impl; + +import com.example.springboot_demo.entity.mongodb.DialogRecord; +import com.example.springboot_demo.repository.DialogRecordRepository; +import com.example.springboot_demo.service.DialogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class DialogServiceImpl implements DialogService { + + @Autowired + private DialogRecordRepository dialogRecordRepository; + + @Override + public List getUserDialogs(Long userId) { + return dialogRecordRepository.findByUserIdOrderByLastTimeDesc(userId); + } + + @Override + public DialogRecord getDialogById(String dialogId) { + return dialogRecordRepository.findByDialogId(dialogId); + } +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java new file mode 100644 index 00000000..97779cac --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/ErrorLogServiceImpl.java @@ -0,0 +1,36 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.ErrorLog; +import com.example.springboot_demo.mapper.ErrorLogMapper; +import com.example.springboot_demo.service.ErrorLogService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ErrorLogServiceImpl extends ServiceImpl implements ErrorLogService { + + @Override + public List listByErrorTypeId(Integer errorTypeId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ErrorLog::getErrorTypeId, errorTypeId); + wrapper.orderByDesc(ErrorLog::getStatTime); + return list(wrapper); + } + + @Override + public List listByPeriod(String period) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ErrorLog::getPeriod, period); + wrapper.orderByDesc(ErrorLog::getStatTime); + return list(wrapper); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java new file mode 100644 index 00000000..40971a86 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/ErrorTypeServiceImpl.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.ErrorType; +import com.example.springboot_demo.mapper.ErrorTypeMapper; +import com.example.springboot_demo.service.ErrorTypeService; +import org.springframework.stereotype.Service; + +@Service +public class ErrorTypeServiceImpl extends ServiceImpl implements ErrorTypeService { + + @Override + public ErrorType getByErrorCode(String errorCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(ErrorType::getErrorCode, errorCode); + return getOne(wrapper); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java new file mode 100644 index 00000000..36257976 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/LlmConfigServiceImpl.java @@ -0,0 +1,24 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.LlmConfig; +import com.example.springboot_demo.mapper.LlmConfigMapper; +import com.example.springboot_demo.service.LlmConfigService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class LlmConfigServiceImpl extends ServiceImpl implements LlmConfigService { + + @Override + public List listAvailable() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(LlmConfig::getIsDisabled, 0); + wrapper.orderByDesc(LlmConfig::getCreateTime); + return list(wrapper); + } +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java new file mode 100644 index 00000000..345e87ab --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java @@ -0,0 +1,380 @@ +package com.example.springboot_demo.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.example.springboot_demo.service.LlmService; +import org.springframework.stereotype.Service; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.*; + +/** + * 大模型调用服务实现 + */ +@Service +public class LlmServiceImpl implements LlmService { + + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .build(); + + @Override + public Map generateQuery(String prompt, String modelName, String databaseName) { + String lowerModelName = modelName.toLowerCase(); + + try { + if (lowerModelName.contains("gemini")) { + return callGemini(prompt, modelName, databaseName); + } else if (lowerModelName.contains("gpt")) { + return callOpenAI(prompt, modelName, databaseName); + } else if (lowerModelName.contains("glm")) { + return callGLM(prompt, modelName, databaseName); + } else if (lowerModelName.contains("qwen")) { + return callQwen(prompt, modelName, databaseName); + } else if (lowerModelName.contains("kimi")) { + return callKimi(prompt, modelName, databaseName); + } else { + throw new RuntimeException("不支持的模型: " + modelName); + } + } catch (Exception e) { + throw new RuntimeException("模型调用失败: " + e.getMessage(), e); + } + } + + /** + * 生成统一的Prompt + */ + private String generatePrompt(String prompt, String databaseName) { + return String.format( + "你是数据查询助手,需将用户请求转换为指定JSON格式。\n" + + "连接的数据库为\"%s\",仅生成该数据库的SQL。\n" + + "响应必须是单个有效的JSON对象,不包含任何额外文本或格式(如```json)。\n\n" + + "用户请求:\"%s\"\n\n" + + "规则:\n" + + "- 数据查询(可SQL回答):success=true,生成SQL、表格数据和图表数据\n" + + "- 非数据查询:success=false,表格数据用[\"Message\"]和[\"抱歉,仅支持数据查询\"]\n\n" + + "返回JSON格式:\n" + + "{\n" + + " \"success\": true/false,\n" + + " \"sqlQuery\": \"SQL语句\",\n" + + " \"tableData\": {\n" + + " \"headers\": [\"列1\", \"列2\"],\n" + + " \"rows\": [[\"值1\", \"值2\"]]\n" + + " },\n" + + " \"chartData\": {\n" + + " \"type\": \"bar/line/pie\",\n" + + " \"labels\": [\"标签1\"],\n" + + " \"datasets\": [{\n" + + " \"label\": \"数据标签\",\n" + + " \"data\": [1, 2, 3],\n" + + " \"backgroundColor\": \"rgba(22, 93, 255, 0.6)\"\n" + + " }]\n" + + " }\n" + + "}", + databaseName, prompt + ); + } + + /** + * 调用Gemini模型 + */ + private Map callGemini(String prompt, String modelName, String databaseName) throws Exception { + // 从配置中获取API Key(这里简化处理,实际应该从数据库读取) + String apiKey = System.getenv("GEMINI_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("Gemini API密钥未配置"); + } + + String url = "https://generativelanguage.googleapis.com/v1beta/models/" + modelName + ":generateContent?key=" + apiKey; + + JSONObject requestBody = new JSONObject(); + requestBody.put("contents", Arrays.asList(Map.of( + "parts", Arrays.asList(Map.of("text", generatePrompt(prompt, databaseName))) + ))); + requestBody.put("generationConfig", Map.of( + "responseMimeType", "application/json" + )); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .timeout(Duration.ofSeconds(60)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Gemini API调用失败: " + response.statusCode()); + } + + JSONObject jsonResponse = JSON.parseObject(response.body()); + + // 检查响应结构 + if (!jsonResponse.containsKey("candidates") || jsonResponse.getJSONArray("candidates").isEmpty()) { + throw new RuntimeException("Gemini API响应格式错误:缺少candidates"); + } + + JSONObject candidate = jsonResponse.getJSONArray("candidates").getJSONObject(0); + if (!candidate.containsKey("content")) { + throw new RuntimeException("Gemini API响应格式错误:缺少content"); + } + + JSONObject contentObj = candidate.getJSONObject("content"); + if (!contentObj.containsKey("parts") || contentObj.getJSONArray("parts").isEmpty()) { + throw new RuntimeException("Gemini API响应格式错误:缺少parts"); + } + + String content = contentObj.getJSONArray("parts") + .getJSONObject(0) + .getString("text"); + + if (content == null || content.isEmpty()) { + throw new RuntimeException("Gemini API返回内容为空"); + } + + return parseJsonResponse(content); + } + + /** + * 调用OpenAI模型 + */ + private Map callOpenAI(String prompt, String modelName, String databaseName) throws Exception { + String apiKey = System.getenv("OPENAI_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("OpenAI API密钥未配置"); + } + + String url = "https://api.openai.com/v1/chat/completions"; + + JSONObject requestBody = new JSONObject(); + requestBody.put("model", modelName); + requestBody.put("messages", Arrays.asList(Map.of( + "role", "user", + "content", generatePrompt(prompt, databaseName) + ))); + requestBody.put("response_format", Map.of("type", "json_object")); + requestBody.put("temperature", 0.0); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .timeout(Duration.ofSeconds(60)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("OpenAI API调用失败: " + response.statusCode()); + } + + JSONObject jsonResponse = JSON.parseObject(response.body()); + + // 检查响应结构 + if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { + throw new RuntimeException("OpenAI API响应格式错误:缺少choices"); + } + + JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); + if (!choice.containsKey("message")) { + throw new RuntimeException("OpenAI API响应格式错误:缺少message"); + } + + String content = choice.getJSONObject("message").getString("content"); + if (content == null || content.isEmpty()) { + throw new RuntimeException("OpenAI API返回内容为空"); + } + + String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); + return parseJsonResponse(cleanedContent); + } + + /** + * 调用GLM模型 + */ + private Map callGLM(String prompt, String modelName, String databaseName) throws Exception { + String apiKey = System.getenv("GLM_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("GLM API密钥未配置"); + } + + String url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"; + + JSONObject requestBody = new JSONObject(); + requestBody.put("model", modelName); + requestBody.put("messages", Arrays.asList(Map.of( + "role", "user", + "content", generatePrompt(prompt, databaseName) + ))); + requestBody.put("response_format", Map.of("type", "json_object")); + requestBody.put("temperature", 0.0); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .timeout(Duration.ofSeconds(60)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("GLM API调用失败: " + response.statusCode()); + } + + JSONObject jsonResponse = JSON.parseObject(response.body()); + + // 检查响应结构 + if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { + throw new RuntimeException("GLM API响应格式错误:缺少choices"); + } + + JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); + if (!choice.containsKey("message")) { + throw new RuntimeException("GLM API响应格式错误:缺少message"); + } + + String content = choice.getJSONObject("message").getString("content"); + if (content == null || content.isEmpty()) { + throw new RuntimeException("GLM API返回内容为空"); + } + + String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); + return parseJsonResponse(cleanedContent); + } + + /** + * 调用Qwen模型 + */ + private Map callQwen(String prompt, String modelName, String databaseName) throws Exception { + String apiKey = System.getenv("QWEN_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("Qwen API密钥未配置"); + } + + String url = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; + + JSONObject requestBody = new JSONObject(); + requestBody.put("model", modelName); + requestBody.put("messages", Arrays.asList(Map.of( + "role", "user", + "content", generatePrompt(prompt, databaseName) + ))); + requestBody.put("response_format", Map.of("type", "json_object")); + requestBody.put("temperature", 0.0); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .timeout(Duration.ofSeconds(60)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Qwen API调用失败: " + response.statusCode()); + } + + JSONObject jsonResponse = JSON.parseObject(response.body()); + + // 检查响应结构 + if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { + throw new RuntimeException("Qwen API响应格式错误:缺少choices"); + } + + JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); + if (!choice.containsKey("message")) { + throw new RuntimeException("Qwen API响应格式错误:缺少message"); + } + + String content = choice.getJSONObject("message").getString("content"); + if (content == null || content.isEmpty()) { + throw new RuntimeException("Qwen API返回内容为空"); + } + + String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); + return parseJsonResponse(cleanedContent); + } + + /** + * 调用Kimi模型 + */ + private Map callKimi(String prompt, String modelName, String databaseName) throws Exception { + String apiKey = System.getenv("KIMI_API_KEY"); + if (apiKey == null || apiKey.isEmpty()) { + throw new RuntimeException("Kimi API密钥未配置"); + } + + String url = "https://api.moonshot.cn/v1/chat/completions"; + + JSONObject requestBody = new JSONObject(); + requestBody.put("model", modelName); + requestBody.put("messages", Arrays.asList(Map.of( + "role", "user", + "content", generatePrompt(prompt, databaseName) + ))); + requestBody.put("response_format", Map.of("type", "json_object")); + requestBody.put("temperature", 0.0); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + apiKey) + .POST(HttpRequest.BodyPublishers.ofString(requestBody.toJSONString())) + .timeout(Duration.ofSeconds(60)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Kimi API调用失败: " + response.statusCode()); + } + + JSONObject jsonResponse = JSON.parseObject(response.body()); + + // 检查响应结构 + if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { + throw new RuntimeException("Kimi API响应格式错误:缺少choices"); + } + + JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); + if (!choice.containsKey("message")) { + throw new RuntimeException("Kimi API响应格式错误:缺少message"); + } + + String content = choice.getJSONObject("message").getString("content"); + if (content == null || content.isEmpty()) { + throw new RuntimeException("Kimi API返回内容为空"); + } + + String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); + return parseJsonResponse(cleanedContent); + } + + /** + * 解析JSON响应 + */ + private Map parseJsonResponse(String jsonContent) { + try { + JSONObject json = JSON.parseObject(jsonContent); + Map result = new HashMap<>(); + result.put("success", json.getBooleanValue("success")); + result.put("sqlQuery", json.getString("sqlQuery")); + result.put("tableData", json.getJSONObject("tableData")); + result.put("chartData", json.getJSONObject("chartData")); + return result; + } catch (Exception e) { + throw new RuntimeException("解析模型响应失败: " + e.getMessage(), e); + } + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java new file mode 100644 index 00000000..c7744390 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/LlmStatusServiceImpl.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.LlmStatus; +import com.example.springboot_demo.mapper.LlmStatusMapper; +import com.example.springboot_demo.service.LlmStatusService; +import org.springframework.stereotype.Service; + +@Service +public class LlmStatusServiceImpl extends ServiceImpl implements LlmStatusService { + + @Override + public LlmStatus getByStatusCode(String statusCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(LlmStatus::getStatusCode, statusCode); + return getOne(wrapper); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java new file mode 100644 index 00000000..a086a63f --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/NotificationServiceImpl.java @@ -0,0 +1,48 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.Notification; +import com.example.springboot_demo.mapper.NotificationMapper; +import com.example.springboot_demo.service.NotificationService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class NotificationServiceImpl extends ServiceImpl implements NotificationService { + + @Override + public List listPublished() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.isNotNull(Notification::getPublishTime); + // 置顶优先,然后按发布时间降序 + wrapper.orderByDesc(Notification::getIsTop); + wrapper.orderByDesc(Notification::getPublishTime); + return list(wrapper); + } + + @Override + public List listDrafts() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.isNull(Notification::getPublishTime); + wrapper.orderByDesc(Notification::getLatestUpdateTime); + return list(wrapper); + } + + @Override + public List listByTargetId(Integer targetId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Notification::getTargetId, targetId); + wrapper.isNotNull(Notification::getPublishTime); + wrapper.orderByDesc(Notification::getIsTop); + wrapper.orderByDesc(Notification::getPublishTime); + return list(wrapper); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java new file mode 100644 index 00000000..78b1a76a --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/NotificationTargetServiceImpl.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.NotificationTarget; +import com.example.springboot_demo.mapper.NotificationTargetMapper; +import com.example.springboot_demo.service.NotificationTargetService; +import org.springframework.stereotype.Service; + +@Service +public class NotificationTargetServiceImpl extends ServiceImpl implements NotificationTargetService { + + @Override + public NotificationTarget getByTargetCode(String targetCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(NotificationTarget::getTargetCode, targetCode); + return getOne(wrapper); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java new file mode 100644 index 00000000..71011c81 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/OperationLogServiceImpl.java @@ -0,0 +1,41 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.OperationLog; +import com.example.springboot_demo.mapper.OperationLogMapper; +import com.example.springboot_demo.service.OperationLogService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class OperationLogServiceImpl extends ServiceImpl implements OperationLogService { + + @Override + public List listByUserId(Long userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OperationLog::getUserId, userId); + wrapper.orderByDesc(OperationLog::getOperateTime); + return list(wrapper); + } + + @Override + public List listByModule(String module) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OperationLog::getModule, module); + wrapper.orderByDesc(OperationLog::getOperateTime); + return list(wrapper); + } + + @Override + public List listFailed() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(OperationLog::getResult, 0); + wrapper.orderByDesc(OperationLog::getOperateTime); + return list(wrapper); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java new file mode 100644 index 00000000..1a67b1ab --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/PriorityServiceImpl.java @@ -0,0 +1,34 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.Priority; +import com.example.springboot_demo.mapper.PriorityMapper; +import com.example.springboot_demo.service.PriorityService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class PriorityServiceImpl extends ServiceImpl implements PriorityService { + + @Override + public Priority getByPriorityCode(String priorityCode) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Priority::getPriorityCode, priorityCode); + return getOne(wrapper); + } + + @Override + public List listOrderBySort() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByAsc(Priority::getSort); + return list(wrapper); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java new file mode 100644 index 00000000..1ea9557b --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryLogServiceImpl.java @@ -0,0 +1,33 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.QueryLog; +import com.example.springboot_demo.mapper.QueryLogMapper; +import com.example.springboot_demo.service.QueryLogService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class QueryLogServiceImpl extends ServiceImpl implements QueryLogService { + + @Override + public List listByUserId(Long userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QueryLog::getUserId, userId); + wrapper.orderByDesc(QueryLog::getQueryTime); + return list(wrapper); + } + + @Override + public List listByDialogId(String dialogId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(QueryLog::getDialogId, dialogId); + wrapper.orderByAsc(QueryLog::getQueryTime); + return list(wrapper); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java new file mode 100644 index 00000000..f595cd92 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java @@ -0,0 +1,247 @@ +package com.example.springboot_demo.service.impl; + +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.example.springboot_demo.dto.QueryRequestDTO; +import com.example.springboot_demo.entity.mongodb.DialogRecord; +import com.example.springboot_demo.repository.DialogRecordRepository; +import com.example.springboot_demo.service.LlmService; +import com.example.springboot_demo.service.QueryService; +import com.example.springboot_demo.vo.QueryResponseVO; +import com.example.springboot_demo.vo.TableDataVO; +import com.example.springboot_demo.vo.ChartDataVO; +import com.example.springboot_demo.vo.DatasetVO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class QueryServiceImpl implements QueryService { + + @Autowired + private DialogRecordRepository dialogRecordRepository; + + @Autowired + private LlmService llmService; + + @Override + public QueryResponseVO executeQuery(QueryRequestDTO request, Long userId) { + long startTime = System.currentTimeMillis(); + + // 生成或获取对话ID + String conversationId = request.getConversationId(); + if (conversationId == null || conversationId.isEmpty()) { + conversationId = "conv_" + UUID.randomUUID().toString().substring(0, 8); + // 创建新对话记录 + DialogRecord dialogRecord = new DialogRecord(); + dialogRecord.setDialogId(conversationId); + dialogRecord.setUserId(userId); + dialogRecord.setTopic(request.getUserPrompt().substring(0, Math.min(20, request.getUserPrompt().length()))); + dialogRecord.setTotalRounds(1); + dialogRecord.setStartTime(LocalDateTime.now()); + dialogRecord.setLastTime(LocalDateTime.now()); + dialogRecordRepository.save(dialogRecord); + } else { + // 更新对话记录 + DialogRecord dialogRecord = dialogRecordRepository.findByDialogId(conversationId); + if (dialogRecord != null) { + dialogRecord.setTotalRounds(dialogRecord.getTotalRounds() + 1); + dialogRecord.setLastTime(LocalDateTime.now()); + dialogRecordRepository.save(dialogRecord); + } + } + + // 调用大模型API生成SQL和结果 + Map llmResult = llmService.generateQuery( + request.getUserPrompt(), + request.getModel(), + request.getDatabase() + ); + + // 计算执行时间 + long endTime = System.currentTimeMillis(); + String executionTime = String.format("%.1f秒", (endTime - startTime) / 1000.0); + + // 构建响应 + QueryResponseVO response = new QueryResponseVO(); + response.setId("query_" + UUID.randomUUID().toString().substring(0, 8)); + response.setUserPrompt(request.getUserPrompt()); + response.setSqlQuery((String) llmResult.getOrDefault("sqlQuery", "")); + response.setConversationId(conversationId); + response.setQueryTime(LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)); + response.setExecutionTime(executionTime); + response.setDatabase(request.getDatabase()); + response.setModel(request.getModel()); + + // 解析表格数据 + Object tableDataObj = llmResult.get("tableData"); + if (tableDataObj != null) { + TableDataVO tableData = parseTableData(tableDataObj); + response.setTableData(tableData); + } + + // 解析图表数据 + Object chartDataObj = llmResult.get("chartData"); + if (chartDataObj != null) { + ChartDataVO chartData = parseChartData(chartDataObj); + response.setChartData(chartData); + } + + return response; + } + + /** + * 解析表格数据 + */ + @SuppressWarnings("unchecked") + private TableDataVO parseTableData(Object tableDataObj) { + TableDataVO tableData = new TableDataVO(); + + if (tableDataObj instanceof Map) { + Map map = (Map) tableDataObj; + + // 解析headers + Object headersObj = map.get("headers"); + if (headersObj instanceof List) { + List headers = ((List) headersObj).stream() + .map(String::valueOf) + .collect(Collectors.toList()); + tableData.setHeaders(headers); + } + + // 解析rows + Object rowsObj = map.get("rows"); + if (rowsObj instanceof List) { + List> rows = new ArrayList<>(); + for (Object row : (List) rowsObj) { + if (row instanceof List) { + List rowList = ((List) row).stream() + .map(String::valueOf) + .collect(Collectors.toList()); + rows.add(rowList); + } else { + rows.add(Collections.emptyList()); + } + } + tableData.setRows(rows); + } + } else if (tableDataObj instanceof JSONObject) { + JSONObject json = (JSONObject) tableDataObj; + JSONArray headersArray = json.getJSONArray("headers"); + JSONArray rowsArray = json.getJSONArray("rows"); + + List headers = headersArray != null ? + headersArray.toJavaList(String.class) : Collections.emptyList(); + + List> rows = new ArrayList<>(); + if (rowsArray != null) { + for (Object row : rowsArray) { + if (row instanceof JSONArray) { + rows.add(((JSONArray) row).toJavaList(String.class)); + } else { + rows.add(Collections.emptyList()); + } + } + } + + tableData.setHeaders(headers); + tableData.setRows(rows); + } + + return tableData; + } + + /** + * 解析图表数据 + */ + @SuppressWarnings("unchecked") + private ChartDataVO parseChartData(Object chartDataObj) { + ChartDataVO chartData = new ChartDataVO(); + + if (chartDataObj instanceof Map) { + Map map = (Map) chartDataObj; + chartData.setType((String) map.getOrDefault("type", "bar")); + + Object labelsObj = map.get("labels"); + if (labelsObj instanceof List) { + List labels = ((List) labelsObj).stream() + .map(String::valueOf) + .collect(Collectors.toList()); + chartData.setLabels(labels); + } + + Object datasetsObj = map.get("datasets"); + if (datasetsObj instanceof List) { + List datasets = ((List) datasetsObj).stream() + .map(datasetObj -> { + DatasetVO dataset = new DatasetVO(); + if (datasetObj instanceof Map) { + Map datasetMap = (Map) datasetObj; + dataset.setLabel((String) datasetMap.get("label")); + + Object dataObj = datasetMap.get("data"); + if (dataObj instanceof List) { + List data = ((List) dataObj).stream() + .map(item -> { + if (item instanceof Number) { + return ((Number) item).doubleValue(); + } + return 0.0; + }) + .collect(Collectors.toList()); + dataset.setData(data); + } + + dataset.setBackgroundColor((String) datasetMap.get("backgroundColor")); + } + return dataset; + }) + .collect(Collectors.toList()); + chartData.setDatasets(datasets); + } + } else if (chartDataObj instanceof JSONObject) { + JSONObject json = (JSONObject) chartDataObj; + chartData.setType(json.getString("type")); + + JSONArray labelsArray = json.getJSONArray("labels"); + if (labelsArray != null) { + chartData.setLabels(labelsArray.toJavaList(String.class)); + } + + JSONArray datasetsArray = json.getJSONArray("datasets"); + if (datasetsArray != null) { + List datasets = datasetsArray.stream() + .map(item -> { + JSONObject datasetJson = (JSONObject) item; + DatasetVO dataset = new DatasetVO(); + dataset.setLabel(datasetJson.getString("label")); + + JSONArray dataArray = datasetJson.getJSONArray("data"); + if (dataArray != null) { + List data = dataArray.stream() + .map(obj -> { + if (obj instanceof Number) { + return ((Number) obj).doubleValue(); + } + return 0.0; + }) + .collect(Collectors.toList()); + dataset.setData(data); + } + + dataset.setBackgroundColor(datasetJson.getString("backgroundColor")); + return dataset; + }) + .collect(Collectors.toList()); + chartData.setDatasets(datasets); + } + } + + return chartData; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java new file mode 100644 index 00000000..5873277e --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/RoleServiceImpl.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.service.impl; + +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.Role; +import com.example.springboot_demo.mapper.RoleMapper; +import com.example.springboot_demo.service.RoleService; + +@Service +public class RoleServiceImpl extends ServiceImpl implements RoleService { +} diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java new file mode 100644 index 00000000..6c245698 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/SystemHealthServiceImpl.java @@ -0,0 +1,36 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.SystemHealth; +import com.example.springboot_demo.mapper.SystemHealthMapper; +import com.example.springboot_demo.service.SystemHealthService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class SystemHealthServiceImpl extends ServiceImpl implements SystemHealthService { + + @Override + public SystemHealth getLatest() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByDesc(SystemHealth::getCollectTime); + wrapper.last("LIMIT 1"); + return getOne(wrapper); + } + + @Override + public List listRecent(int limit) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.orderByDesc(SystemHealth::getCollectTime); + wrapper.last("LIMIT " + limit); + return list(wrapper); + } +} + + + + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java new file mode 100644 index 00000000..66eb9c73 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/TableMetadataServiceImpl.java @@ -0,0 +1,25 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.TableMetadata; +import com.example.springboot_demo.mapper.TableMetadataMapper; +import com.example.springboot_demo.service.TableMetadataService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class TableMetadataServiceImpl extends ServiceImpl implements TableMetadataService { + + @Override + public List listByDbConnectionId(Long dbConnectionId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(TableMetadata::getDbConnectionId, dbConnectionId); + wrapper.orderByAsc(TableMetadata::getTableName); + return list(wrapper); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java new file mode 100644 index 00000000..7a7548de --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/UserDbPermissionServiceImpl.java @@ -0,0 +1,39 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.springboot_demo.entity.mysql.UserDbPermission; +import com.example.springboot_demo.mapper.UserDbPermissionMapper; +import com.example.springboot_demo.service.UserDbPermissionService; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserDbPermissionServiceImpl extends ServiceImpl implements UserDbPermissionService { + + @Override + public UserDbPermission getByUserId(Long userId) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserDbPermission::getUserId, userId); + return getOne(wrapper); + } + + @Override + public List listAssigned() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserDbPermission::getIsAssigned, 1); + wrapper.orderByDesc(UserDbPermission::getLastGrantTime); + return list(wrapper); + } + + @Override + public List listUnassigned() { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserDbPermission::getIsAssigned, 0); + return list(wrapper); + } +} + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java new file mode 100644 index 00000000..38148be3 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java @@ -0,0 +1,69 @@ +package com.example.springboot_demo.service.impl; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.example.springboot_demo.entity.mysql.User; +import com.example.springboot_demo.mapper.UserMapper; +import com.example.springboot_demo.service.UserService; + +@Service +public class UserServiceImpl implements UserService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Override + public User getById(Long id) { + return userMapper.selectById(id); + } + + @Override + public List list() { + return userMapper.selectList(null); + } + + @Override + public Page page(int current, int size) { + Page page = new Page<>(current, size); + return userMapper.selectPage(page, null); + } + + @Override + public boolean save(User user) { + if (StringUtils.hasText(user.getPassword())) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + } + return userMapper.insert(user) > 0; + } + + @Override + public boolean updateById(User user) { + if (StringUtils.hasText(user.getPassword())) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + } + return userMapper.updateById(user) > 0; + } + + @Override + public boolean removeById(Long id) { + return userMapper.deleteById(id) > 0; + } + + @Override + public User getByUsername(String username) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getUsername, username); + return userMapper.selectOne(wrapper); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/utils/JwtUtil.java b/src/test/src/main/java/com/example/springboot_demo/utils/JwtUtil.java new file mode 100644 index 00000000..13206046 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/utils/JwtUtil.java @@ -0,0 +1,57 @@ +package com.example.springboot_demo.utils; + +import java.security.Key; +import java.util.Date; + +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +@Component +public class JwtUtil { + + // Use a secure key for HS256 + private static final Key KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256); + private static final long EXPIRATION_TIME = 86400000; // 24 hours + + public String generateToken(Long userId, String username) { + return Jwts.builder() + .setSubject(username) + .claim("userId", userId) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) + .signWith(KEY) + .compact(); + } + + public Claims getClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(KEY) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public boolean validateToken(String token) { + try { + getClaimsFromToken(token); + return true; + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + public Long getUserIdFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.get("userId", Long.class); + } + + public String getUsernameFromToken(String token) { + return getClaimsFromToken(token).getSubject(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java b/src/test/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java new file mode 100644 index 00000000..f4f687e0 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/vo/ChartDataVO.java @@ -0,0 +1,13 @@ +package com.example.springboot_demo.vo; + +import lombok.Data; +import java.util.List; + +@Data +public class ChartDataVO { + private String type; // bar, line, pie + private List labels; + private List datasets; +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/vo/DatasetVO.java b/src/test/src/main/java/com/example/springboot_demo/vo/DatasetVO.java new file mode 100644 index 00000000..135458dc --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/vo/DatasetVO.java @@ -0,0 +1,13 @@ +package com.example.springboot_demo.vo; + +import lombok.Data; +import java.util.List; + +@Data +public class DatasetVO { + private String label; + private List data; + private Object backgroundColor; // 可以是字符串或数组 +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/vo/LoginVO.java b/src/test/src/main/java/com/example/springboot_demo/vo/LoginVO.java new file mode 100644 index 00000000..ba21ed6c --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/vo/LoginVO.java @@ -0,0 +1,16 @@ +package com.example.springboot_demo.vo; + +import lombok.Data; + +@Data +public class LoginVO { + private String token; + private Long userId; + private String username; + private String email; + private Integer roleId; + private String roleName; + private String avatarUrl; +} + + diff --git a/src/test/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java b/src/test/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java new file mode 100644 index 00000000..7ca36218 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/vo/QueryResponseVO.java @@ -0,0 +1,18 @@ +package com.example.springboot_demo.vo; + +import lombok.Data; + +@Data +public class QueryResponseVO { + private String id; + private String userPrompt; + private String sqlQuery; + private String conversationId; + private String queryTime; + private String executionTime; + private TableDataVO tableData; + private ChartDataVO chartData; + private String database; + private String model; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/vo/TableDataVO.java b/src/test/src/main/java/com/example/springboot_demo/vo/TableDataVO.java new file mode 100644 index 00000000..446dd465 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/vo/TableDataVO.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.vo; + +import lombok.Data; +import java.util.List; + +@Data +public class TableDataVO { + private List headers; + private List> rows; +} + + diff --git a/src/test/src/main/resources/application.yml b/src/test/src/main/resources/application.yml new file mode 100644 index 00000000..b04834c5 --- /dev/null +++ b/src/test/src/main/resources/application.yml @@ -0,0 +1,80 @@ +server: + port: 8080 + +spring: + application: + name: springboot_demo + + # MySQL 数据源配置 + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/natural_language_query_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: root123456 + hikari: + minimum-idle: 5 + maximum-pool-size: 20 + idle-timeout: 600000 + max-lifetime: 1800000 + connection-timeout: 30000 + + # MongoDB 配置 + data: + mongodb: + uri: mongodb://admin:admin123456@127.0.0.1:27017/natural_language_query_system?authSource=admin + + # Redis 配置(可选) + redis: + host: localhost + port: 6379 + password: + database: 0 + timeout: 3000ms + lettuce: + pool: + max-active: 8 + max-wait: -1ms + max-idle: 8 + min-idle: 0 + +# MyBatis Plus 配置 +mybatis-plus: + # Mapper XML 文件位置 + mapper-locations: classpath:mapper/**/*.xml + # 实体类包路径 + type-aliases-package: com.example.springboot_demo.entity.mysql + configuration: + # 日志输出 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # 驼峰命名转换 + map-underscore-to-camel-case: true + # 缓存 + cache-enabled: true + global-config: + db-config: + # 主键策略:自增 + id-type: auto + # 逻辑删除字段 + logic-delete-field: deleted + logic-delete-value: 1 + logic-not-delete-value: 0 + +# Actuator 配置 +management: + endpoints: + web: + exposure: + include: health,info + endpoint: + health: + show-details: always + +# 日志配置 +logging: + level: + root: INFO + com.example.springboot_demo: DEBUG + com.example.springboot_demo.mapper: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + diff --git a/src/test/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java b/src/test/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java new file mode 100644 index 00000000..4ba0d8fa --- /dev/null +++ b/src/test/src/test/java/com/example/springboot_demo/SpringbootDemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.springboot_demo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SpringbootDemoApplicationTests { + + @Test + void contextLoads() { + } + +} -- 2.34.1 From 354bd5fbba937ce6142c9d4a9ea32bd0f871f605 Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Mon, 1 Dec 2025 13:54:18 +0800 Subject: [PATCH 07/14] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E5=AE=9E=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/package-lock.json | 6 ++ .../AiInteractionLogController.java | 41 +++++++++++++ .../CollectionRecordController.java | 52 ++++++++++++++++ .../controller/DbConnectionLogController.java | 48 +++++++++++++++ .../controller/DialogDetailController.java | 39 ++++++++++++ .../controller/FriendChatController.java | 51 ++++++++++++++++ .../controller/FriendRelationController.java | 57 +++++++++++++++++ .../controller/FriendRequestController.java | 59 ++++++++++++++++++ .../PerformanceMetricController.java | 42 +++++++++++++ .../controller/QueryCollectionController.java | 42 +++++++++++++ .../controller/QueryShareController.java | 61 +++++++++++++++++++ .../controller/SqlCacheController.java | 45 ++++++++++++++ .../controller/TokenConsumeController.java | 49 +++++++++++++++ .../controller/UserSearchController.java | 53 ++++++++++++++++ .../entity/mongodb/AiInteractionLog.java | 44 +++++++++++++ .../entity/mongodb/CollectionRecord.java | 31 ++++++++++ .../entity/mongodb/DialogDetail.java | 31 ++++++++++ .../entity/mongodb/FriendChat.java | 31 ++++++++++ .../entity/mongodb/QueryCollection.java | 22 +++++++ .../entity/mongodb/SqlCache.java | 33 ++++++++++ .../entity/mysql/DbConnectionLog.java | 30 +++++++++ .../entity/mysql/FriendRelation.java | 30 +++++++++ .../entity/mysql/FriendRequest.java | 30 +++++++++ .../entity/mysql/PerformanceMetric.java | 27 ++++++++ .../entity/mysql/QueryShare.java | 32 ++++++++++ .../entity/mysql/TokenConsume.java | 31 ++++++++++ .../entity/mysql/UserSearch.java | 28 +++++++++ .../mapper/DbConnectionLogMapper.java | 11 ++++ .../mapper/FriendRelationMapper.java | 11 ++++ .../mapper/FriendRequestMapper.java | 11 ++++ .../mapper/PerformanceMetricMapper.java | 11 ++++ .../mapper/QueryShareMapper.java | 11 ++++ .../mapper/TokenConsumeMapper.java | 11 ++++ .../mapper/UserSearchMapper.java | 11 ++++ .../AiInteractionLogRepository.java | 18 ++++++ .../CollectionRecordRepository.java | 18 ++++++ .../repository/DialogDetailRepository.java | 12 ++++ .../repository/FriendChatRepository.java | 16 +++++ .../repository/QueryCollectionRepository.java | 16 +++++ .../repository/SqlCacheRepository.java | 16 +++++ .../service/AiInteractionLogService.java | 17 ++++++ .../service/CollectionRecordService.java | 21 +++++++ .../service/DbConnectionLogService.java | 19 ++++++ .../service/DialogDetailService.java | 13 ++++ .../service/FriendChatService.java | 17 ++++++ .../service/FriendRelationService.java | 21 +++++++ .../service/FriendRequestService.java | 21 +++++++ .../service/PerformanceMetricService.java | 18 ++++++ .../service/QueryCollectionService.java | 19 ++++++ .../service/QueryShareService.java | 23 +++++++ .../service/SqlCacheService.java | 17 ++++++ .../service/TokenConsumeService.java | 20 ++++++ .../service/UserSearchService.java | 21 +++++++ .../impl/AiInteractionLogServiceImpl.java | 37 +++++++++++ .../impl/CollectionRecordServiceImpl.java | 47 ++++++++++++++ .../impl/DbConnectionLogServiceImpl.java | 47 ++++++++++++++ .../service/impl/DialogDetailServiceImpl.java | 30 +++++++++ .../service/impl/FriendChatServiceImpl.java | 38 ++++++++++++ .../impl/FriendRelationServiceImpl.java | 54 ++++++++++++++++ .../impl/FriendRequestServiceImpl.java | 52 ++++++++++++++++ .../impl/PerformanceMetricServiceImpl.java | 43 +++++++++++++ .../impl/QueryCollectionServiceImpl.java | 42 +++++++++++++ .../service/impl/QueryShareServiceImpl.java | 59 ++++++++++++++++++ .../service/impl/SqlCacheServiceImpl.java | 37 +++++++++++ .../service/impl/TokenConsumeServiceImpl.java | 48 +++++++++++++++ .../service/impl/UserSearchServiceImpl.java | 54 ++++++++++++++++ 66 files changed, 2053 insertions(+) create mode 100644 src/test/package-lock.json create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/AiInteractionLogController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/CollectionRecordController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionLogController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/DialogDetailController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/FriendChatController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/FriendRelationController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/FriendRequestController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/PerformanceMetricController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/QueryCollectionController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/QueryShareController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/SqlCacheController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/TokenConsumeController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/controller/UserSearchController.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mongodb/AiInteractionLog.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mongodb/CollectionRecord.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogDetail.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mongodb/FriendChat.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mongodb/QueryCollection.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mongodb/SqlCache.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnectionLog.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRelation.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRequest.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/PerformanceMetric.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryShare.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/TokenConsume.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserSearch.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionLogMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/FriendRelationMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/FriendRequestMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/PerformanceMetricMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/QueryShareMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/TokenConsumeMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/mapper/UserSearchMapper.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/repository/AiInteractionLogRepository.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/repository/CollectionRecordRepository.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/repository/DialogDetailRepository.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/repository/FriendChatRepository.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/repository/QueryCollectionRepository.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/repository/SqlCacheRepository.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/AiInteractionLogService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/CollectionRecordService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/DbConnectionLogService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/DialogDetailService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/FriendChatService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/FriendRelationService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/FriendRequestService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/PerformanceMetricService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/QueryCollectionService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/QueryShareService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/SqlCacheService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/TokenConsumeService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/UserSearchService.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/AiInteractionLogServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/CollectionRecordServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionLogServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/DialogDetailServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/FriendChatServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRelationServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRequestServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/PerformanceMetricServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/QueryCollectionServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/QueryShareServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/SqlCacheServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/TokenConsumeServiceImpl.java create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/UserSearchServiceImpl.java diff --git a/src/test/package-lock.json b/src/test/package-lock.json new file mode 100644 index 00000000..fed62a41 --- /dev/null +++ b/src/test/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/AiInteractionLogController.java b/src/test/src/main/java/com/example/springboot_demo/controller/AiInteractionLogController.java new file mode 100644 index 00000000..bd04dd30 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/AiInteractionLogController.java @@ -0,0 +1,41 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mongodb.AiInteractionLog; +import com.example.springboot_demo.service.AiInteractionLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/ai-interaction-log") +public class AiInteractionLogController { + + @Autowired + private AiInteractionLogService aiInteractionLogService; + + @GetMapping("/list/user/{userId}") + public Result> listByUserId(@PathVariable Long userId) { + return Result.success(aiInteractionLogService.listByUserId(userId)); + } + + @GetMapping("/list/llm/{llmName}") + public Result> listByLlmName(@PathVariable String llmName) { + return Result.success(aiInteractionLogService.listByLlmName(llmName)); + } + + @GetMapping("/list/llm/{llmName}/{status}") + public Result> listByLlmNameAndStatus(@PathVariable String llmName, @PathVariable String status) { + return Result.success(aiInteractionLogService.listByLlmNameAndStatus(llmName, status)); + } + + @PostMapping + public Result save(@RequestBody AiInteractionLog aiInteractionLog) { + aiInteractionLog.setCreateTime(LocalDateTime.now()); + AiInteractionLog saved = aiInteractionLogService.save(aiInteractionLog); + return Result.success(saved); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/CollectionRecordController.java b/src/test/src/main/java/com/example/springboot_demo/controller/CollectionRecordController.java new file mode 100644 index 00000000..45f7d505 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/CollectionRecordController.java @@ -0,0 +1,52 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mongodb.CollectionRecord; +import com.example.springboot_demo.service.CollectionRecordService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/collection-record") +public class CollectionRecordController { + + @Autowired + private CollectionRecordService collectionRecordService; + + @GetMapping("/list/query/{queryId}") + public Result> listByQueryId(@PathVariable String queryId) { + return Result.success(collectionRecordService.listByQueryId(queryId)); + } + + @GetMapping("/list/user/{userId}") + public Result> listByUserId(@PathVariable Long userId) { + return Result.success(collectionRecordService.listByUserId(userId)); + } + + @GetMapping("/list/db/{dbConnectionId}") + public Result> listByDbConnectionId(@PathVariable Long dbConnectionId) { + return Result.success(collectionRecordService.listByDbConnectionId(dbConnectionId)); + } + + @GetMapping("/{id}") + public Result getById(@PathVariable String id) { + return Result.success(collectionRecordService.getById(id)); + } + + @PostMapping + public Result save(@RequestBody CollectionRecord collectionRecord) { + collectionRecord.setCreateTime(LocalDateTime.now()); + CollectionRecord saved = collectionRecordService.save(collectionRecord); + return Result.success(saved); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + collectionRecordService.deleteById(id); + return Result.success(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionLogController.java b/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionLogController.java new file mode 100644 index 00000000..7f61ccfb --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionLogController.java @@ -0,0 +1,48 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.DbConnectionLog; +import com.example.springboot_demo.service.DbConnectionLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/db-connection-log") +public class DbConnectionLogController { + + @Autowired + private DbConnectionLogService dbConnectionLogService; + + @GetMapping("/list") + public Result> list() { + return Result.success(dbConnectionLogService.list()); + } + + @GetMapping("/list/connection/{dbConnectionId}") + public Result> listByDbConnectionId(@PathVariable Long dbConnectionId) { + return Result.success(dbConnectionLogService.listByDbConnectionId(dbConnectionId)); + } + + @GetMapping("/list/status/{status}") + public Result> listByStatus(@PathVariable String status) { + return Result.success(dbConnectionLogService.listByStatus(status)); + } + + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(dbConnectionLogService.getById(id)); + } + + @PostMapping + public Result save(@RequestBody DbConnectionLog dbConnectionLog) { + if (dbConnectionLog.getConnectTime() == null) { + dbConnectionLog.setConnectTime(LocalDateTime.now()); + } + dbConnectionLogService.save(dbConnectionLog); + return Result.success(dbConnectionLog); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/DialogDetailController.java b/src/test/src/main/java/com/example/springboot_demo/controller/DialogDetailController.java new file mode 100644 index 00000000..095deb55 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/DialogDetailController.java @@ -0,0 +1,39 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mongodb.DialogDetail; +import com.example.springboot_demo.service.DialogDetailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/dialog-detail") +public class DialogDetailController { + + @Autowired + private DialogDetailService dialogDetailService; + + @GetMapping("/{dialogId}") + public Result getByDialogId(@PathVariable String dialogId) { + return Result.success(dialogDetailService.getByDialogId(dialogId)); + } + + @PostMapping + public Result save(@RequestBody DialogDetail dialogDetail) { + DialogDetail saved = dialogDetailService.save(dialogDetail); + return Result.success(saved); + } + + @PutMapping + public Result update(@RequestBody DialogDetail dialogDetail) { + DialogDetail saved = dialogDetailService.save(dialogDetail); + return Result.success(saved); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + dialogDetailService.deleteById(id); + return Result.success(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/FriendChatController.java b/src/test/src/main/java/com/example/springboot_demo/controller/FriendChatController.java new file mode 100644 index 00000000..1b254fa4 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/FriendChatController.java @@ -0,0 +1,51 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mongodb.FriendChat; +import com.example.springboot_demo.service.FriendChatService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/friend-chat") +public class FriendChatController { + + @Autowired + private FriendChatService friendChatService; + + @GetMapping("/list/{userId}/{friendId}") + public Result> listByUserIdAndFriendId(@PathVariable Long userId, @PathVariable Long friendId) { + return Result.success(friendChatService.listByUserIdAndFriendId(userId, friendId)); + } + + @GetMapping("/unread/{friendId}") + public Result> listUnreadByFriendId(@PathVariable Long friendId) { + return Result.success(friendChatService.listUnreadByFriendId(friendId)); + } + + @PostMapping + public Result save(@RequestBody FriendChat friendChat) { + friendChat.setSendTime(LocalDateTime.now()); + if (friendChat.getIsRead() == null) { + friendChat.setIsRead(false); + } + FriendChat saved = friendChatService.save(friendChat); + return Result.success(saved); + } + + @PutMapping + public Result update(@RequestBody FriendChat friendChat) { + FriendChat saved = friendChatService.save(friendChat); + return Result.success(saved); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + friendChatService.deleteById(id); + return Result.success(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/FriendRelationController.java b/src/test/src/main/java/com/example/springboot_demo/controller/FriendRelationController.java new file mode 100644 index 00000000..bae58aee --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/FriendRelationController.java @@ -0,0 +1,57 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.FriendRelation; +import com.example.springboot_demo.service.FriendRelationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/friend-relation") +public class FriendRelationController { + + @Autowired + private FriendRelationService friendRelationService; + + @GetMapping("/list/{userId}") + public Result> listByUserId(@PathVariable Long userId) { + return Result.success(friendRelationService.listByUserId(userId)); + } + + @GetMapping("/{userId}/{friendId}") + public Result getByUserIdAndFriendId(@PathVariable Long userId, @PathVariable Long friendId) { + return Result.success(friendRelationService.getByUserIdAndFriendId(userId, friendId)); + } + + @PostMapping + public Result save(@RequestBody FriendRelation friendRelation) { + friendRelation.setCreateTime(LocalDateTime.now()); + if (friendRelation.getOnlineStatus() == null) { + friendRelation.setOnlineStatus(0); + } + friendRelationService.save(friendRelation); + return Result.success(friendRelation); + } + + @PutMapping + public Result update(@RequestBody FriendRelation friendRelation) { + friendRelationService.updateById(friendRelation); + return Result.success(friendRelation); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + friendRelationService.removeById(id); + return Result.success(); + } + + @DeleteMapping("/{userId}/{friendId}") + public Result deleteByUserIdAndFriendId(@PathVariable Long userId, @PathVariable Long friendId) { + friendRelationService.removeByUserIdAndFriendId(userId, friendId); + return Result.success(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/FriendRequestController.java b/src/test/src/main/java/com/example/springboot_demo/controller/FriendRequestController.java new file mode 100644 index 00000000..83b82410 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/FriendRequestController.java @@ -0,0 +1,59 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.FriendRequest; +import com.example.springboot_demo.service.FriendRequestService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/friend-request") +public class FriendRequestController { + + @Autowired + private FriendRequestService friendRequestService; + + @GetMapping("/list/{recipientId}") + public Result> listByRecipientId(@PathVariable Long recipientId) { + return Result.success(friendRequestService.listByRecipientId(recipientId)); + } + + @GetMapping("/list/{recipientId}/{status}") + public Result> listByRecipientIdAndStatus(@PathVariable Long recipientId, @PathVariable Integer status) { + return Result.success(friendRequestService.listByRecipientIdAndStatus(recipientId, status)); + } + + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(friendRequestService.getById(id)); + } + + @PostMapping + public Result save(@RequestBody FriendRequest friendRequest) { + friendRequest.setCreateTime(LocalDateTime.now()); + if (friendRequest.getStatus() == null) { + friendRequest.setStatus(0); + } + friendRequestService.save(friendRequest); + return Result.success(friendRequest); + } + + @PutMapping + public Result update(@RequestBody FriendRequest friendRequest) { + if (friendRequest.getStatus() != null && friendRequest.getStatus() != 0) { + friendRequest.setHandleTime(LocalDateTime.now()); + } + friendRequestService.updateById(friendRequest); + return Result.success(friendRequest); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + friendRequestService.removeById(id); + return Result.success(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/PerformanceMetricController.java b/src/test/src/main/java/com/example/springboot_demo/controller/PerformanceMetricController.java new file mode 100644 index 00000000..8f3c4476 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/PerformanceMetricController.java @@ -0,0 +1,42 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.PerformanceMetric; +import com.example.springboot_demo.service.PerformanceMetricService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/performance-metric") +public class PerformanceMetricController { + + @Autowired + private PerformanceMetricService performanceMetricService; + + @GetMapping("/list") + public Result> list() { + return Result.success(performanceMetricService.list()); + } + + @GetMapping("/list/{metricType}") + public Result> listByMetricType(@PathVariable String metricType) { + return Result.success(performanceMetricService.listByMetricType(metricType)); + } + + @GetMapping("/range") + public Result> listByTimeRange(@RequestParam String startTime, @RequestParam String endTime) { + LocalDateTime start = LocalDateTime.parse(startTime); + LocalDateTime end = LocalDateTime.parse(endTime); + return Result.success(performanceMetricService.listByTimeRange(start, end)); + } + + @PostMapping + public Result save(@RequestBody PerformanceMetric performanceMetric) { + performanceMetricService.save(performanceMetric); + return Result.success(performanceMetric); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/QueryCollectionController.java b/src/test/src/main/java/com/example/springboot_demo/controller/QueryCollectionController.java new file mode 100644 index 00000000..900197d2 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/QueryCollectionController.java @@ -0,0 +1,42 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mongodb.QueryCollection; +import com.example.springboot_demo.service.QueryCollectionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/query-collection") +public class QueryCollectionController { + + @Autowired + private QueryCollectionService queryCollectionService; + + @GetMapping("/list/{userId}") + public Result> listByUserId(@PathVariable Long userId) { + return Result.success(queryCollectionService.listByUserId(userId)); + } + + @GetMapping("/{id}") + public Result getById(@PathVariable String id) { + return Result.success(queryCollectionService.getById(id)); + } + + @PostMapping + public Result save(@RequestBody QueryCollection queryCollection) { + queryCollection.setCreateTime(LocalDateTime.now()); + QueryCollection saved = queryCollectionService.save(queryCollection); + return Result.success(saved); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + queryCollectionService.deleteById(id); + return Result.success(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/QueryShareController.java b/src/test/src/main/java/com/example/springboot_demo/controller/QueryShareController.java new file mode 100644 index 00000000..ba9e3483 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/QueryShareController.java @@ -0,0 +1,61 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.QueryShare; +import com.example.springboot_demo.service.QueryShareService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/query-share") +public class QueryShareController { + + @Autowired + private QueryShareService queryShareService; + + @GetMapping("/list/receive/{receiveUserId}") + public Result> listByReceiveUserId(@PathVariable Long receiveUserId) { + return Result.success(queryShareService.listByReceiveUserId(receiveUserId)); + } + + @GetMapping("/list/receive/{receiveUserId}/{receiveStatus}") + public Result> listByReceiveUserIdAndStatus(@PathVariable Long receiveUserId, @PathVariable Integer receiveStatus) { + return Result.success(queryShareService.listByReceiveUserIdAndStatus(receiveUserId, receiveStatus)); + } + + @GetMapping("/list/share/{shareUserId}") + public Result> listByShareUserId(@PathVariable Long shareUserId) { + return Result.success(queryShareService.listByShareUserId(shareUserId)); + } + + @GetMapping("/{id}") + public Result getById(@PathVariable Long id) { + return Result.success(queryShareService.getById(id)); + } + + @PostMapping + public Result save(@RequestBody QueryShare queryShare) { + queryShare.setShareTime(LocalDateTime.now()); + if (queryShare.getReceiveStatus() == null) { + queryShare.setReceiveStatus(0); + } + queryShareService.save(queryShare); + return Result.success(queryShare); + } + + @PutMapping + public Result update(@RequestBody QueryShare queryShare) { + queryShareService.updateById(queryShare); + return Result.success(queryShare); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + queryShareService.removeById(id); + return Result.success(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/SqlCacheController.java b/src/test/src/main/java/com/example/springboot_demo/controller/SqlCacheController.java new file mode 100644 index 00000000..fcbe21f1 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/SqlCacheController.java @@ -0,0 +1,45 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mongodb.SqlCache; +import com.example.springboot_demo.service.SqlCacheService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/sql-cache") +public class SqlCacheController { + + @Autowired + private SqlCacheService sqlCacheService; + + @GetMapping("/list/{userId}") + public Result> listByUserId(@PathVariable Long userId) { + return Result.success(sqlCacheService.listByUserId(userId)); + } + + @PostMapping("/query") + public Result getCache(@RequestBody SqlCache request) { + SqlCache cache = sqlCacheService.getByNlHashAndConnectionIdAndTableIds( + request.getNlHash(), + request.getConnectionId(), + request.getTableIds() + ); + return Result.success(cache); + } + + @PostMapping + public Result save(@RequestBody SqlCache sqlCache) { + SqlCache saved = sqlCacheService.save(sqlCache); + return Result.success(saved); + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable String id) { + sqlCacheService.deleteById(id); + return Result.success(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/TokenConsumeController.java b/src/test/src/main/java/com/example/springboot_demo/controller/TokenConsumeController.java new file mode 100644 index 00000000..e3b9cb94 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/TokenConsumeController.java @@ -0,0 +1,49 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.TokenConsume; +import com.example.springboot_demo.service.TokenConsumeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; +import java.util.List; + +@RestController +@RequestMapping("/token-consume") +public class TokenConsumeController { + + @Autowired + private TokenConsumeService tokenConsumeService; + + @GetMapping("/list") + public Result> list() { + return Result.success(tokenConsumeService.list()); + } + + @GetMapping("/{llmName}/{consumeDate}") + public Result getByLlmNameAndDate(@PathVariable String llmName, @PathVariable String consumeDate) { + LocalDate date = LocalDate.parse(consumeDate); + return Result.success(tokenConsumeService.getByLlmNameAndDate(llmName, date)); + } + + @GetMapping("/range/{startDate}/{endDate}") + public Result> listByDateRange(@PathVariable String startDate, @PathVariable String endDate) { + LocalDate start = LocalDate.parse(startDate); + LocalDate end = LocalDate.parse(endDate); + return Result.success(tokenConsumeService.listByDateRange(start, end)); + } + + @PostMapping + public Result save(@RequestBody TokenConsume tokenConsume) { + tokenConsumeService.save(tokenConsume); + return Result.success(tokenConsume); + } + + @PutMapping + public Result update(@RequestBody TokenConsume tokenConsume) { + tokenConsumeService.updateById(tokenConsume); + return Result.success(tokenConsume); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/UserSearchController.java b/src/test/src/main/java/com/example/springboot_demo/controller/UserSearchController.java new file mode 100644 index 00000000..ae143537 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/UserSearchController.java @@ -0,0 +1,53 @@ +package com.example.springboot_demo.controller; + +import com.example.springboot_demo.common.Result; +import com.example.springboot_demo.entity.mysql.UserSearch; +import com.example.springboot_demo.service.UserSearchService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/user-search") +public class UserSearchController { + + @Autowired + private UserSearchService userSearchService; + + @GetMapping("/list/{userId}") + public Result> listByUserId(@PathVariable Long userId) { + return Result.success(userSearchService.listByUserId(userId)); + } + + @GetMapping("/list/top/{userId}/{limit}") + public Result> listTopSearchesByUserId(@PathVariable Long userId, @PathVariable int limit) { + return Result.success(userSearchService.listTopSearchesByUserId(userId, limit)); + } + + @PostMapping + public Result save(@RequestBody UserSearch userSearch) { + UserSearch existing = userSearchService.getByUserIdAndSqlContent(userSearch.getUserId(), userSearch.getSqlContent()); + if (existing != null) { + existing.setSearchCount(existing.getSearchCount() + 1); + existing.setLastSearchTime(LocalDateTime.now()); + userSearchService.updateById(existing); + return Result.success(existing); + } else { + if (userSearch.getSearchCount() == null) { + userSearch.setSearchCount(1); + } + userSearch.setLastSearchTime(LocalDateTime.now()); + userSearchService.save(userSearch); + return Result.success(userSearch); + } + } + + @DeleteMapping("/{id}") + public Result delete(@PathVariable Long id) { + userSearchService.removeById(id); + return Result.success(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/AiInteractionLog.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/AiInteractionLog.java new file mode 100644 index 00000000..63c5cbe7 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/AiInteractionLog.java @@ -0,0 +1,44 @@ +package com.example.springboot_demo.entity.mongodb; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; +import java.util.Map; + +@Data +@Document(collection = "ai_interaction_logs") +public class AiInteractionLog { + + @Id + private String id; + + private Long userId; + + private String requestType; + + private String llmName; + + private Map requestParams; + + private Map responseResult; + + private TokenUsage tokenUsage; + + private Integer responseTime; + + private String status; + + private String errorMsg; + + private LocalDateTime createTime; + + @Data + public static class TokenUsage { + private Integer promptTokens; + private Integer completionTokens; + private Integer totalTokens; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/CollectionRecord.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/CollectionRecord.java new file mode 100644 index 00000000..31462ed0 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/CollectionRecord.java @@ -0,0 +1,31 @@ +package com.example.springboot_demo.entity.mongodb; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; +import java.util.Map; + +@Data +@Document(collection = "collection_records") +public class CollectionRecord { + + @Id + private String id; + + private String queryId; + + private Long userId; + + private String sqlContent; + + private Map queryResult; + + private Long dbConnectionId; + + private Long llmConfigId; + + private LocalDateTime createTime; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogDetail.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogDetail.java new file mode 100644 index 00000000..e52b6186 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogDetail.java @@ -0,0 +1,31 @@ +package com.example.springboot_demo.entity.mongodb; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Data +@Document(collection = "dialog_details") +public class DialogDetail { + + @Id + private String id; + + private String dialogId; + + private List rounds; + + @Data + public static class Round { + private Integer roundNum; + private String userInput; + private String aiResponse; + private String generatedSql; + private LocalDateTime roundTime; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/FriendChat.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/FriendChat.java new file mode 100644 index 00000000..1ddf0b0a --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/FriendChat.java @@ -0,0 +1,31 @@ +package com.example.springboot_demo.entity.mongodb; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; +import java.util.Map; + +@Data +@Document(collection = "friend_chats") +public class FriendChat { + + @Id + private String id; + + private Long userId; + + private Long friendId; + + private String contentType; + + private Map content; + + private LocalDateTime sendTime; + + private Boolean isRead; + + private Map extra; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/QueryCollection.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/QueryCollection.java new file mode 100644 index 00000000..5cef4e49 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/QueryCollection.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo.entity.mongodb; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; + +@Data +@Document(collection = "query_collections") +public class QueryCollection { + + @Id + private String id; + + private Long userId; + + private String groupName; + + private LocalDateTime createTime; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/SqlCache.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/SqlCache.java new file mode 100644 index 00000000..f6e08c67 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/SqlCache.java @@ -0,0 +1,33 @@ +package com.example.springboot_demo.entity.mongodb; + +import lombok.Data; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Document(collection = "sql_cache") +public class SqlCache { + + @Id + private String id; + + private String nlHash; + + private Long userId; + + private Long connectionId; + + private List tableIds; + + private String dbType; + + private String generatedSql; + + private Integer hitCount; + + private LocalDateTime expireTime; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnectionLog.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnectionLog.java new file mode 100644 index 00000000..21747d23 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnectionLog.java @@ -0,0 +1,30 @@ +package com.example.springboot_demo.entity.mysql; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +@Data +@TableName("db_connection_logs") +public class DbConnectionLog { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long dbConnectionId; + + private String dbName; + + private LocalDateTime connectTime; + + private String status; + + private String remark; + + private Long handlerId; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRelation.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRelation.java new file mode 100644 index 00000000..5a6b1b3a --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRelation.java @@ -0,0 +1,30 @@ +package com.example.springboot_demo.entity.mysql; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +@Data +@TableName("friend_relations") +public class FriendRelation { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long userId; + + private Long friendId; + + private String friendUsername; + + private Integer onlineStatus; + + private String remarkName; + + private LocalDateTime createTime; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRequest.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRequest.java new file mode 100644 index 00000000..c1b08a5e --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRequest.java @@ -0,0 +1,30 @@ +package com.example.springboot_demo.entity.mysql; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +@Data +@TableName("friend_requests") +public class FriendRequest { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long applicantId; + + private Long recipientId; + + private String applyMsg; + + private Integer status; + + private LocalDateTime createTime; + + private LocalDateTime handleTime; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/PerformanceMetric.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/PerformanceMetric.java new file mode 100644 index 00000000..7c3d7443 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/PerformanceMetric.java @@ -0,0 +1,27 @@ +package com.example.springboot_demo.entity.mysql; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +@Data +@TableName("performance_metrics") +public class PerformanceMetric { + + @TableId(type = IdType.AUTO) + private Long id; + + private String metricType; + + private BigDecimal metricValue; + + private LocalDateTime metricTime; + + private Integer trend; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryShare.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryShare.java new file mode 100644 index 00000000..480f71b0 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryShare.java @@ -0,0 +1,32 @@ +package com.example.springboot_demo.entity.mysql; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +@Data +@TableName("query_shares") +public class QueryShare { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long shareUserId; + + private Long receiveUserId; + + private String dialogId; + + private String targetRounds; + + private String queryTitle; + + private LocalDateTime shareTime; + + private Integer receiveStatus; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TokenConsume.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TokenConsume.java new file mode 100644 index 00000000..5991d986 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TokenConsume.java @@ -0,0 +1,31 @@ +package com.example.springboot_demo.entity.mysql; + +import java.math.BigDecimal; +import java.time.LocalDate; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +@Data +@TableName("token_consume") +public class TokenConsume { + + @TableId(type = IdType.AUTO) + private Long id; + + private String llmName; + + private Integer totalTokens; + + private Integer promptTokens; + + private Integer completionTokens; + + private LocalDate consumeDate; + + private BigDecimal growthRate; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserSearch.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserSearch.java new file mode 100644 index 00000000..cd5325a1 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserSearch.java @@ -0,0 +1,28 @@ +package com.example.springboot_demo.entity.mysql; + +import java.time.LocalDateTime; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.Data; + +@Data +@TableName("user_searches") +public class UserSearch { + + @TableId(type = IdType.AUTO) + private Long id; + + private Long userId; + + private String sqlContent; + + private String queryTitle; + + private Integer searchCount; + + private LocalDateTime lastSearchTime; +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionLogMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionLogMapper.java new file mode 100644 index 00000000..a6e68436 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionLogMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.DbConnectionLog; + +@Mapper +public interface DbConnectionLogMapper extends BaseMapper { +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRelationMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRelationMapper.java new file mode 100644 index 00000000..bb6d43b1 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRelationMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.FriendRelation; + +@Mapper +public interface FriendRelationMapper extends BaseMapper { +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRequestMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRequestMapper.java new file mode 100644 index 00000000..e589126b --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRequestMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.FriendRequest; + +@Mapper +public interface FriendRequestMapper extends BaseMapper { +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/PerformanceMetricMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/PerformanceMetricMapper.java new file mode 100644 index 00000000..0e6cfc32 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/PerformanceMetricMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.PerformanceMetric; + +@Mapper +public interface PerformanceMetricMapper extends BaseMapper { +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/QueryShareMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/QueryShareMapper.java new file mode 100644 index 00000000..2ea075cd --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/QueryShareMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.QueryShare; + +@Mapper +public interface QueryShareMapper extends BaseMapper { +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/TokenConsumeMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/TokenConsumeMapper.java new file mode 100644 index 00000000..b3e9b5bd --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/TokenConsumeMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.TokenConsume; + +@Mapper +public interface TokenConsumeMapper extends BaseMapper { +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/UserSearchMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/UserSearchMapper.java new file mode 100644 index 00000000..d2a9f054 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/UserSearchMapper.java @@ -0,0 +1,11 @@ +package com.example.springboot_demo.mapper; + +import org.apache.ibatis.annotations.Mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.springboot_demo.entity.mysql.UserSearch; + +@Mapper +public interface UserSearchMapper extends BaseMapper { +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/repository/AiInteractionLogRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/AiInteractionLogRepository.java new file mode 100644 index 00000000..a394d7d1 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/AiInteractionLogRepository.java @@ -0,0 +1,18 @@ +package com.example.springboot_demo.repository; + +import com.example.springboot_demo.entity.mongodb.AiInteractionLog; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface AiInteractionLogRepository extends MongoRepository { + + List findByUserId(Long userId); + + List findByLlmName(String llmName); + + List findByLlmNameAndStatus(String llmName, String status); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/repository/CollectionRecordRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/CollectionRecordRepository.java new file mode 100644 index 00000000..a85eae5a --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/CollectionRecordRepository.java @@ -0,0 +1,18 @@ +package com.example.springboot_demo.repository; + +import com.example.springboot_demo.entity.mongodb.CollectionRecord; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface CollectionRecordRepository extends MongoRepository { + + List findByQueryId(String queryId); + + List findByUserId(Long userId); + + List findByDbConnectionId(Long dbConnectionId); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/repository/DialogDetailRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/DialogDetailRepository.java new file mode 100644 index 00000000..54380bee --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/DialogDetailRepository.java @@ -0,0 +1,12 @@ +package com.example.springboot_demo.repository; + +import com.example.springboot_demo.entity.mongodb.DialogDetail; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface DialogDetailRepository extends MongoRepository { + + DialogDetail findByDialogId(String dialogId); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/repository/FriendChatRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/FriendChatRepository.java new file mode 100644 index 00000000..bbfc0fe5 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/FriendChatRepository.java @@ -0,0 +1,16 @@ +package com.example.springboot_demo.repository; + +import com.example.springboot_demo.entity.mongodb.FriendChat; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface FriendChatRepository extends MongoRepository { + + List findByUserIdAndFriendIdOrderBySendTimeDesc(Long userId, Long friendId); + + List findByFriendIdAndIsRead(Long friendId, Boolean isRead); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/repository/QueryCollectionRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/QueryCollectionRepository.java new file mode 100644 index 00000000..4313db66 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/QueryCollectionRepository.java @@ -0,0 +1,16 @@ +package com.example.springboot_demo.repository; + +import com.example.springboot_demo.entity.mongodb.QueryCollection; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface QueryCollectionRepository extends MongoRepository { + + List findByUserId(Long userId); + + QueryCollection findByUserIdAndGroupName(Long userId, String groupName); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/repository/SqlCacheRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/SqlCacheRepository.java new file mode 100644 index 00000000..37bd0ba6 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/SqlCacheRepository.java @@ -0,0 +1,16 @@ +package com.example.springboot_demo.repository; + +import com.example.springboot_demo.entity.mongodb.SqlCache; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface SqlCacheRepository extends MongoRepository { + + SqlCache findByNlHashAndConnectionIdAndTableIds(String nlHash, Long connectionId, List tableIds); + + List findByUserId(Long userId); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/AiInteractionLogService.java b/src/test/src/main/java/com/example/springboot_demo/service/AiInteractionLogService.java new file mode 100644 index 00000000..c092aafd --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/AiInteractionLogService.java @@ -0,0 +1,17 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mongodb.AiInteractionLog; + +import java.util.List; + +public interface AiInteractionLogService { + + List listByUserId(Long userId); + + List listByLlmName(String llmName); + + List listByLlmNameAndStatus(String llmName, String status); + + AiInteractionLog save(AiInteractionLog aiInteractionLog); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/CollectionRecordService.java b/src/test/src/main/java/com/example/springboot_demo/service/CollectionRecordService.java new file mode 100644 index 00000000..d87e68e6 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/CollectionRecordService.java @@ -0,0 +1,21 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mongodb.CollectionRecord; + +import java.util.List; + +public interface CollectionRecordService { + + List listByQueryId(String queryId); + + List listByUserId(Long userId); + + List listByDbConnectionId(Long dbConnectionId); + + CollectionRecord getById(String id); + + CollectionRecord save(CollectionRecord collectionRecord); + + void deleteById(String id); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionLogService.java b/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionLogService.java new file mode 100644 index 00000000..3bbe386b --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionLogService.java @@ -0,0 +1,19 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mysql.DbConnectionLog; + +import java.util.List; + +public interface DbConnectionLogService { + + List list(); + + List listByDbConnectionId(Long dbConnectionId); + + List listByStatus(String status); + + DbConnectionLog getById(Long id); + + boolean save(DbConnectionLog dbConnectionLog); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/DialogDetailService.java b/src/test/src/main/java/com/example/springboot_demo/service/DialogDetailService.java new file mode 100644 index 00000000..69b36a7e --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/DialogDetailService.java @@ -0,0 +1,13 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mongodb.DialogDetail; + +public interface DialogDetailService { + + DialogDetail getByDialogId(String dialogId); + + DialogDetail save(DialogDetail dialogDetail); + + void deleteById(String id); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/FriendChatService.java b/src/test/src/main/java/com/example/springboot_demo/service/FriendChatService.java new file mode 100644 index 00000000..e54e1158 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/FriendChatService.java @@ -0,0 +1,17 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mongodb.FriendChat; + +import java.util.List; + +public interface FriendChatService { + + List listByUserIdAndFriendId(Long userId, Long friendId); + + List listUnreadByFriendId(Long friendId); + + FriendChat save(FriendChat friendChat); + + void deleteById(String id); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/FriendRelationService.java b/src/test/src/main/java/com/example/springboot_demo/service/FriendRelationService.java new file mode 100644 index 00000000..23a2fe95 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/FriendRelationService.java @@ -0,0 +1,21 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mysql.FriendRelation; + +import java.util.List; + +public interface FriendRelationService { + + List listByUserId(Long userId); + + FriendRelation getByUserIdAndFriendId(Long userId, Long friendId); + + boolean save(FriendRelation friendRelation); + + boolean updateById(FriendRelation friendRelation); + + boolean removeById(Long id); + + boolean removeByUserIdAndFriendId(Long userId, Long friendId); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/FriendRequestService.java b/src/test/src/main/java/com/example/springboot_demo/service/FriendRequestService.java new file mode 100644 index 00000000..f6697b05 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/FriendRequestService.java @@ -0,0 +1,21 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mysql.FriendRequest; + +import java.util.List; + +public interface FriendRequestService { + + List listByRecipientId(Long recipientId); + + List listByRecipientIdAndStatus(Long recipientId, Integer status); + + FriendRequest getById(Long id); + + boolean save(FriendRequest friendRequest); + + boolean updateById(FriendRequest friendRequest); + + boolean removeById(Long id); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/PerformanceMetricService.java b/src/test/src/main/java/com/example/springboot_demo/service/PerformanceMetricService.java new file mode 100644 index 00000000..60c3cb8c --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/PerformanceMetricService.java @@ -0,0 +1,18 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mysql.PerformanceMetric; + +import java.time.LocalDateTime; +import java.util.List; + +public interface PerformanceMetricService { + + List list(); + + List listByMetricType(String metricType); + + List listByTimeRange(LocalDateTime startTime, LocalDateTime endTime); + + boolean save(PerformanceMetric performanceMetric); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/QueryCollectionService.java b/src/test/src/main/java/com/example/springboot_demo/service/QueryCollectionService.java new file mode 100644 index 00000000..f97842c8 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/QueryCollectionService.java @@ -0,0 +1,19 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mongodb.QueryCollection; + +import java.util.List; + +public interface QueryCollectionService { + + List listByUserId(Long userId); + + QueryCollection getByUserIdAndGroupName(Long userId, String groupName); + + QueryCollection getById(String id); + + QueryCollection save(QueryCollection queryCollection); + + void deleteById(String id); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/QueryShareService.java b/src/test/src/main/java/com/example/springboot_demo/service/QueryShareService.java new file mode 100644 index 00000000..ecb04e0f --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/QueryShareService.java @@ -0,0 +1,23 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mysql.QueryShare; + +import java.util.List; + +public interface QueryShareService { + + List listByReceiveUserId(Long receiveUserId); + + List listByReceiveUserIdAndStatus(Long receiveUserId, Integer receiveStatus); + + List listByShareUserId(Long shareUserId); + + QueryShare getById(Long id); + + boolean save(QueryShare queryShare); + + boolean updateById(QueryShare queryShare); + + boolean removeById(Long id); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/SqlCacheService.java b/src/test/src/main/java/com/example/springboot_demo/service/SqlCacheService.java new file mode 100644 index 00000000..72d52415 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/SqlCacheService.java @@ -0,0 +1,17 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mongodb.SqlCache; + +import java.util.List; + +public interface SqlCacheService { + + SqlCache getByNlHashAndConnectionIdAndTableIds(String nlHash, Long connectionId, List tableIds); + + List listByUserId(Long userId); + + SqlCache save(SqlCache sqlCache); + + void deleteById(String id); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/TokenConsumeService.java b/src/test/src/main/java/com/example/springboot_demo/service/TokenConsumeService.java new file mode 100644 index 00000000..f12aa04a --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/TokenConsumeService.java @@ -0,0 +1,20 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mysql.TokenConsume; + +import java.time.LocalDate; +import java.util.List; + +public interface TokenConsumeService { + + List list(); + + TokenConsume getByLlmNameAndDate(String llmName, LocalDate consumeDate); + + List listByDateRange(LocalDate startDate, LocalDate endDate); + + boolean save(TokenConsume tokenConsume); + + boolean updateById(TokenConsume tokenConsume); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/UserSearchService.java b/src/test/src/main/java/com/example/springboot_demo/service/UserSearchService.java new file mode 100644 index 00000000..4d4584ec --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/UserSearchService.java @@ -0,0 +1,21 @@ +package com.example.springboot_demo.service; + +import com.example.springboot_demo.entity.mysql.UserSearch; + +import java.util.List; + +public interface UserSearchService { + + List listByUserId(Long userId); + + List listTopSearchesByUserId(Long userId, int limit); + + UserSearch getByUserIdAndSqlContent(Long userId, String sqlContent); + + boolean save(UserSearch userSearch); + + boolean updateById(UserSearch userSearch); + + boolean removeById(Long id); +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/AiInteractionLogServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/AiInteractionLogServiceImpl.java new file mode 100644 index 00000000..abe846e7 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/AiInteractionLogServiceImpl.java @@ -0,0 +1,37 @@ +package com.example.springboot_demo.service.impl; + +import com.example.springboot_demo.entity.mongodb.AiInteractionLog; +import com.example.springboot_demo.repository.AiInteractionLogRepository; +import com.example.springboot_demo.service.AiInteractionLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AiInteractionLogServiceImpl implements AiInteractionLogService { + + @Autowired + private AiInteractionLogRepository aiInteractionLogRepository; + + @Override + public List listByUserId(Long userId) { + return aiInteractionLogRepository.findByUserId(userId); + } + + @Override + public List listByLlmName(String llmName) { + return aiInteractionLogRepository.findByLlmName(llmName); + } + + @Override + public List listByLlmNameAndStatus(String llmName, String status) { + return aiInteractionLogRepository.findByLlmNameAndStatus(llmName, status); + } + + @Override + public AiInteractionLog save(AiInteractionLog aiInteractionLog) { + return aiInteractionLogRepository.save(aiInteractionLog); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/CollectionRecordServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/CollectionRecordServiceImpl.java new file mode 100644 index 00000000..6505253e --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/CollectionRecordServiceImpl.java @@ -0,0 +1,47 @@ +package com.example.springboot_demo.service.impl; + +import com.example.springboot_demo.entity.mongodb.CollectionRecord; +import com.example.springboot_demo.repository.CollectionRecordRepository; +import com.example.springboot_demo.service.CollectionRecordService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class CollectionRecordServiceImpl implements CollectionRecordService { + + @Autowired + private CollectionRecordRepository collectionRecordRepository; + + @Override + public List listByQueryId(String queryId) { + return collectionRecordRepository.findByQueryId(queryId); + } + + @Override + public List listByUserId(Long userId) { + return collectionRecordRepository.findByUserId(userId); + } + + @Override + public List listByDbConnectionId(Long dbConnectionId) { + return collectionRecordRepository.findByDbConnectionId(dbConnectionId); + } + + @Override + public CollectionRecord getById(String id) { + return collectionRecordRepository.findById(id).orElse(null); + } + + @Override + public CollectionRecord save(CollectionRecord collectionRecord) { + return collectionRecordRepository.save(collectionRecord); + } + + @Override + public void deleteById(String id) { + collectionRecordRepository.deleteById(id); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionLogServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionLogServiceImpl.java new file mode 100644 index 00000000..e6404266 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionLogServiceImpl.java @@ -0,0 +1,47 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.example.springboot_demo.entity.mysql.DbConnectionLog; +import com.example.springboot_demo.mapper.DbConnectionLogMapper; +import com.example.springboot_demo.service.DbConnectionLogService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class DbConnectionLogServiceImpl implements DbConnectionLogService { + + @Autowired + private DbConnectionLogMapper dbConnectionLogMapper; + + @Override + public List list() { + return dbConnectionLogMapper.selectList(null); + } + + @Override + public List listByDbConnectionId(Long dbConnectionId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("db_connection_id", dbConnectionId); + return dbConnectionLogMapper.selectList(wrapper); + } + + @Override + public List listByStatus(String status) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("status", status); + return dbConnectionLogMapper.selectList(wrapper); + } + + @Override + public DbConnectionLog getById(Long id) { + return dbConnectionLogMapper.selectById(id); + } + + @Override + public boolean save(DbConnectionLog dbConnectionLog) { + return dbConnectionLogMapper.insert(dbConnectionLog) > 0; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogDetailServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogDetailServiceImpl.java new file mode 100644 index 00000000..b1300db2 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogDetailServiceImpl.java @@ -0,0 +1,30 @@ +package com.example.springboot_demo.service.impl; + +import com.example.springboot_demo.entity.mongodb.DialogDetail; +import com.example.springboot_demo.repository.DialogDetailRepository; +import com.example.springboot_demo.service.DialogDetailService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DialogDetailServiceImpl implements DialogDetailService { + + @Autowired + private DialogDetailRepository dialogDetailRepository; + + @Override + public DialogDetail getByDialogId(String dialogId) { + return dialogDetailRepository.findByDialogId(dialogId); + } + + @Override + public DialogDetail save(DialogDetail dialogDetail) { + return dialogDetailRepository.save(dialogDetail); + } + + @Override + public void deleteById(String id) { + dialogDetailRepository.deleteById(id); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendChatServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendChatServiceImpl.java new file mode 100644 index 00000000..89aba604 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendChatServiceImpl.java @@ -0,0 +1,38 @@ +package com.example.springboot_demo.service.impl; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.example.springboot_demo.entity.mongodb.FriendChat; +import com.example.springboot_demo.repository.FriendChatRepository; +import com.example.springboot_demo.service.FriendChatService; + +@Service +public class FriendChatServiceImpl implements FriendChatService { + + @Autowired + private FriendChatRepository friendChatRepository; + + @Override + public List listByUserIdAndFriendId(Long userId, Long friendId) { + return friendChatRepository.findByUserIdAndFriendIdOrderBySendTimeDesc(userId, friendId); + } + + @Override + public List listUnreadByFriendId(Long friendId) { + return friendChatRepository.findByFriendIdAndIsRead(friendId, false); + } + + @Override + public FriendChat save(FriendChat friendChat) { + return friendChatRepository.save(friendChat); + } + + @Override + public void deleteById(String id) { + friendChatRepository.deleteById(id); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRelationServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRelationServiceImpl.java new file mode 100644 index 00000000..609db649 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRelationServiceImpl.java @@ -0,0 +1,54 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.example.springboot_demo.entity.mysql.FriendRelation; +import com.example.springboot_demo.mapper.FriendRelationMapper; +import com.example.springboot_demo.service.FriendRelationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class FriendRelationServiceImpl implements FriendRelationService { + + @Autowired + private FriendRelationMapper friendRelationMapper; + + @Override + public List listByUserId(Long userId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("user_id", userId); + return friendRelationMapper.selectList(wrapper); + } + + @Override + public FriendRelation getByUserIdAndFriendId(Long userId, Long friendId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("user_id", userId).eq("friend_id", friendId); + return friendRelationMapper.selectOne(wrapper); + } + + @Override + public boolean save(FriendRelation friendRelation) { + return friendRelationMapper.insert(friendRelation) > 0; + } + + @Override + public boolean updateById(FriendRelation friendRelation) { + return friendRelationMapper.updateById(friendRelation) > 0; + } + + @Override + public boolean removeById(Long id) { + return friendRelationMapper.deleteById(id) > 0; + } + + @Override + public boolean removeByUserIdAndFriendId(Long userId, Long friendId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("user_id", userId).eq("friend_id", friendId); + return friendRelationMapper.delete(wrapper) > 0; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRequestServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRequestServiceImpl.java new file mode 100644 index 00000000..1be04aec --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRequestServiceImpl.java @@ -0,0 +1,52 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.example.springboot_demo.entity.mysql.FriendRequest; +import com.example.springboot_demo.mapper.FriendRequestMapper; +import com.example.springboot_demo.service.FriendRequestService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class FriendRequestServiceImpl implements FriendRequestService { + + @Autowired + private FriendRequestMapper friendRequestMapper; + + @Override + public List listByRecipientId(Long recipientId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("recipient_id", recipientId); + return friendRequestMapper.selectList(wrapper); + } + + @Override + public List listByRecipientIdAndStatus(Long recipientId, Integer status) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("recipient_id", recipientId).eq("status", status); + return friendRequestMapper.selectList(wrapper); + } + + @Override + public FriendRequest getById(Long id) { + return friendRequestMapper.selectById(id); + } + + @Override + public boolean save(FriendRequest friendRequest) { + return friendRequestMapper.insert(friendRequest) > 0; + } + + @Override + public boolean updateById(FriendRequest friendRequest) { + return friendRequestMapper.updateById(friendRequest) > 0; + } + + @Override + public boolean removeById(Long id) { + return friendRequestMapper.deleteById(id) > 0; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/PerformanceMetricServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/PerformanceMetricServiceImpl.java new file mode 100644 index 00000000..06e0479a --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/PerformanceMetricServiceImpl.java @@ -0,0 +1,43 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.example.springboot_demo.entity.mysql.PerformanceMetric; +import com.example.springboot_demo.mapper.PerformanceMetricMapper; +import com.example.springboot_demo.service.PerformanceMetricService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class PerformanceMetricServiceImpl implements PerformanceMetricService { + + @Autowired + private PerformanceMetricMapper performanceMetricMapper; + + @Override + public List list() { + return performanceMetricMapper.selectList(null); + } + + @Override + public List listByMetricType(String metricType) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("metric_type", metricType); + return performanceMetricMapper.selectList(wrapper); + } + + @Override + public List listByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.between("metric_time", startTime, endTime); + return performanceMetricMapper.selectList(wrapper); + } + + @Override + public boolean save(PerformanceMetric performanceMetric) { + return performanceMetricMapper.insert(performanceMetric) > 0; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryCollectionServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryCollectionServiceImpl.java new file mode 100644 index 00000000..86f23879 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryCollectionServiceImpl.java @@ -0,0 +1,42 @@ +package com.example.springboot_demo.service.impl; + +import com.example.springboot_demo.entity.mongodb.QueryCollection; +import com.example.springboot_demo.repository.QueryCollectionRepository; +import com.example.springboot_demo.service.QueryCollectionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class QueryCollectionServiceImpl implements QueryCollectionService { + + @Autowired + private QueryCollectionRepository queryCollectionRepository; + + @Override + public List listByUserId(Long userId) { + return queryCollectionRepository.findByUserId(userId); + } + + @Override + public QueryCollection getByUserIdAndGroupName(Long userId, String groupName) { + return queryCollectionRepository.findByUserIdAndGroupName(userId, groupName); + } + + @Override + public QueryCollection getById(String id) { + return queryCollectionRepository.findById(id).orElse(null); + } + + @Override + public QueryCollection save(QueryCollection queryCollection) { + return queryCollectionRepository.save(queryCollection); + } + + @Override + public void deleteById(String id) { + queryCollectionRepository.deleteById(id); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryShareServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryShareServiceImpl.java new file mode 100644 index 00000000..f8a8a027 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryShareServiceImpl.java @@ -0,0 +1,59 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.example.springboot_demo.entity.mysql.QueryShare; +import com.example.springboot_demo.mapper.QueryShareMapper; +import com.example.springboot_demo.service.QueryShareService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class QueryShareServiceImpl implements QueryShareService { + + @Autowired + private QueryShareMapper queryShareMapper; + + @Override + public List listByReceiveUserId(Long receiveUserId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("receive_user_id", receiveUserId); + return queryShareMapper.selectList(wrapper); + } + + @Override + public List listByReceiveUserIdAndStatus(Long receiveUserId, Integer receiveStatus) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("receive_user_id", receiveUserId).eq("receive_status", receiveStatus); + return queryShareMapper.selectList(wrapper); + } + + @Override + public List listByShareUserId(Long shareUserId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("share_user_id", shareUserId); + return queryShareMapper.selectList(wrapper); + } + + @Override + public QueryShare getById(Long id) { + return queryShareMapper.selectById(id); + } + + @Override + public boolean save(QueryShare queryShare) { + return queryShareMapper.insert(queryShare) > 0; + } + + @Override + public boolean updateById(QueryShare queryShare) { + return queryShareMapper.updateById(queryShare) > 0; + } + + @Override + public boolean removeById(Long id) { + return queryShareMapper.deleteById(id) > 0; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/SqlCacheServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/SqlCacheServiceImpl.java new file mode 100644 index 00000000..e1bffbe4 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/SqlCacheServiceImpl.java @@ -0,0 +1,37 @@ +package com.example.springboot_demo.service.impl; + +import com.example.springboot_demo.entity.mongodb.SqlCache; +import com.example.springboot_demo.repository.SqlCacheRepository; +import com.example.springboot_demo.service.SqlCacheService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class SqlCacheServiceImpl implements SqlCacheService { + + @Autowired + private SqlCacheRepository sqlCacheRepository; + + @Override + public SqlCache getByNlHashAndConnectionIdAndTableIds(String nlHash, Long connectionId, List tableIds) { + return sqlCacheRepository.findByNlHashAndConnectionIdAndTableIds(nlHash, connectionId, tableIds); + } + + @Override + public List listByUserId(Long userId) { + return sqlCacheRepository.findByUserId(userId); + } + + @Override + public SqlCache save(SqlCache sqlCache) { + return sqlCacheRepository.save(sqlCache); + } + + @Override + public void deleteById(String id) { + sqlCacheRepository.deleteById(id); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/TokenConsumeServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/TokenConsumeServiceImpl.java new file mode 100644 index 00000000..945f43f1 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/TokenConsumeServiceImpl.java @@ -0,0 +1,48 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.example.springboot_demo.entity.mysql.TokenConsume; +import com.example.springboot_demo.mapper.TokenConsumeMapper; +import com.example.springboot_demo.service.TokenConsumeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDate; +import java.util.List; + +@Service +public class TokenConsumeServiceImpl implements TokenConsumeService { + + @Autowired + private TokenConsumeMapper tokenConsumeMapper; + + @Override + public List list() { + return tokenConsumeMapper.selectList(null); + } + + @Override + public TokenConsume getByLlmNameAndDate(String llmName, LocalDate consumeDate) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("llm_name", llmName).eq("consume_date", consumeDate); + return tokenConsumeMapper.selectOne(wrapper); + } + + @Override + public List listByDateRange(LocalDate startDate, LocalDate endDate) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.between("consume_date", startDate, endDate); + return tokenConsumeMapper.selectList(wrapper); + } + + @Override + public boolean save(TokenConsume tokenConsume) { + return tokenConsumeMapper.insert(tokenConsume) > 0; + } + + @Override + public boolean updateById(TokenConsume tokenConsume) { + return tokenConsumeMapper.updateById(tokenConsume) > 0; + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/UserSearchServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/UserSearchServiceImpl.java new file mode 100644 index 00000000..2355a9f7 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/UserSearchServiceImpl.java @@ -0,0 +1,54 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.example.springboot_demo.entity.mysql.UserSearch; +import com.example.springboot_demo.mapper.UserSearchMapper; +import com.example.springboot_demo.service.UserSearchService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class UserSearchServiceImpl implements UserSearchService { + + @Autowired + private UserSearchMapper userSearchMapper; + + @Override + public List listByUserId(Long userId) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("user_id", userId); + return userSearchMapper.selectList(wrapper); + } + + @Override + public List listTopSearchesByUserId(Long userId, int limit) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("user_id", userId).orderByDesc("search_count").last("LIMIT " + limit); + return userSearchMapper.selectList(wrapper); + } + + @Override + public UserSearch getByUserIdAndSqlContent(Long userId, String sqlContent) { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("user_id", userId).eq("sql_content", sqlContent); + return userSearchMapper.selectOne(wrapper); + } + + @Override + public boolean save(UserSearch userSearch) { + return userSearchMapper.insert(userSearch) > 0; + } + + @Override + public boolean updateById(UserSearch userSearch) { + return userSearchMapper.updateById(userSearch) > 0; + } + + @Override + public boolean removeById(Long id) { + return userSearchMapper.deleteById(id) > 0; + } +} + -- 2.34.1 From bf9680dbb99e0926cbb2713866f00fad1957f2b7 Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Mon, 1 Dec 2025 14:04:05 +0800 Subject: [PATCH 08/14] =?UTF-8?q?=E8=A1=A5=E4=B8=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/scripts/export-data.bat | 2 ++ src/test/scripts/export-data.sh | 2 ++ src/test/scripts/import-data.bat | 2 ++ src/test/scripts/import-data.sh | 2 ++ 4 files changed, 8 insertions(+) diff --git a/src/test/scripts/export-data.bat b/src/test/scripts/export-data.bat index 9235c27c..4fd8db52 100644 --- a/src/test/scripts/export-data.bat +++ b/src/test/scripts/export-data.bat @@ -23,3 +23,5 @@ echo. echo 请将 data-backup 文件夹分享给团队成员 pause + + diff --git a/src/test/scripts/export-data.sh b/src/test/scripts/export-data.sh index 8c605864..97ff6113 100644 --- a/src/test/scripts/export-data.sh +++ b/src/test/scripts/export-data.sh @@ -35,3 +35,5 @@ echo " - MongoDB: ./data-backup/mongodb-data/" echo "" echo "请将 data-backup 文件夹分享给团队成员" + + diff --git a/src/test/scripts/import-data.bat b/src/test/scripts/import-data.bat index c143388c..a9d61480 100644 --- a/src/test/scripts/import-data.bat +++ b/src/test/scripts/import-data.bat @@ -28,3 +28,5 @@ echo. echo 数据导入完成! pause + + diff --git a/src/test/scripts/import-data.sh b/src/test/scripts/import-data.sh index 6b7db786..a1dc83b8 100644 --- a/src/test/scripts/import-data.sh +++ b/src/test/scripts/import-data.sh @@ -34,3 +34,5 @@ docker exec nlq_mongodb mongorestore \ echo "数据导入完成!" + + -- 2.34.1 From e2d202b9876721ca252c9ef3e4eaf01fe269d79a Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Mon, 1 Dec 2025 14:05:15 +0800 Subject: [PATCH 09/14] =?UTF-8?q?=E8=A1=A5=E4=B8=812?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/QUICK_START.md | 2 ++ src/test/TEST_ACCOUNTS.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/test/QUICK_START.md b/src/test/QUICK_START.md index fb66786d..7d68a946 100644 --- a/src/test/QUICK_START.md +++ b/src/test/QUICK_START.md @@ -204,3 +204,5 @@ password: admin123456 # 不是 admin **最后更新**:2025-12-01 + + diff --git a/src/test/TEST_ACCOUNTS.md b/src/test/TEST_ACCOUNTS.md index ea25564e..b3acb178 100644 --- a/src/test/TEST_ACCOUNTS.md +++ b/src/test/TEST_ACCOUNTS.md @@ -290,3 +290,5 @@ WHERE p.user_id IN (1, 2, 3); **最后更新**:2025-12-01 **密码策略**:统一使用 `123456`(BCrypt 加密存储) + + -- 2.34.1 From c19a025f558a1a495baf904690fc598a43d2ed88 Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Mon, 1 Dec 2025 15:30:21 +0800 Subject: [PATCH 10/14] =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E6=9B=B4=E6=94=B9=EF=BC=8C=E4=B8=80=E4=BA=9B=E8=A1=A5=E4=B8=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/api-test.http | 229 ++++++++++++++++++ .../components/admin/UserManagementPage.tsx | 17 +- src/test/pom.xml | 27 ++- .../springboot_demo/common/Result.java | 27 ++- .../config/SecurityConfig.java | 31 ++- .../AiInteractionLogController.java | 1 + .../CollectionRecordController.java | 1 + .../controller/DbConnectionLogController.java | 1 + .../controller/DialogDetailController.java | 1 + .../controller/FriendChatController.java | 1 + .../controller/FriendRelationController.java | 1 + .../controller/FriendRequestController.java | 1 + .../PerformanceMetricController.java | 1 + .../controller/QueryCollectionController.java | 1 + .../controller/QueryShareController.java | 1 + .../controller/SqlCacheController.java | 1 + .../controller/TokenConsumeController.java | 1 + .../controller/UserSearchController.java | 1 + .../entity/mongodb/AiInteractionLog.java | 1 + .../entity/mongodb/CollectionRecord.java | 1 + .../entity/mongodb/DialogDetail.java | 1 + .../entity/mongodb/FriendChat.java | 1 + .../entity/mongodb/QueryCollection.java | 1 + .../entity/mongodb/SqlCache.java | 1 + .../entity/mysql/DbConnectionLog.java | 1 + .../entity/mysql/FriendRelation.java | 1 + .../entity/mysql/FriendRequest.java | 1 + .../entity/mysql/PerformanceMetric.java | 1 + .../entity/mysql/QueryShare.java | 1 + .../entity/mysql/TokenConsume.java | 1 + .../entity/mysql/UserSearch.java | 1 + .../mapper/DbConnectionLogMapper.java | 1 + .../mapper/FriendRelationMapper.java | 1 + .../mapper/FriendRequestMapper.java | 1 + .../mapper/PerformanceMetricMapper.java | 1 + .../mapper/QueryShareMapper.java | 1 + .../mapper/TokenConsumeMapper.java | 1 + .../mapper/UserSearchMapper.java | 1 + .../AiInteractionLogRepository.java | 1 + .../CollectionRecordRepository.java | 1 + .../repository/DialogDetailRepository.java | 1 + .../repository/FriendChatRepository.java | 1 + .../repository/QueryCollectionRepository.java | 1 + .../repository/SqlCacheRepository.java | 1 + .../service/AiInteractionLogService.java | 1 + .../service/CollectionRecordService.java | 1 + .../service/DbConnectionLogService.java | 1 + .../service/DialogDetailService.java | 1 + .../service/FriendChatService.java | 1 + .../service/FriendRelationService.java | 1 + .../service/FriendRequestService.java | 1 + .../service/PerformanceMetricService.java | 1 + .../service/QueryCollectionService.java | 1 + .../service/QueryShareService.java | 1 + .../service/SqlCacheService.java | 1 + .../service/TokenConsumeService.java | 1 + .../service/UserSearchService.java | 1 + .../impl/AiInteractionLogServiceImpl.java | 1 + .../service/impl/AuthServiceImpl.java | 75 +++--- .../impl/CollectionRecordServiceImpl.java | 1 + .../impl/CustomUserDetailsService.java | 56 +++++ .../impl/DbConnectionLogServiceImpl.java | 1 + .../service/impl/DialogDetailServiceImpl.java | 1 + .../service/impl/FriendChatServiceImpl.java | 1 + .../impl/FriendRelationServiceImpl.java | 1 + .../impl/FriendRequestServiceImpl.java | 1 + .../impl/PerformanceMetricServiceImpl.java | 1 + .../impl/QueryCollectionServiceImpl.java | 1 + .../service/impl/QueryShareServiceImpl.java | 1 + .../service/impl/SqlCacheServiceImpl.java | 1 + .../service/impl/TokenConsumeServiceImpl.java | 1 + .../service/impl/UserSearchServiceImpl.java | 1 + .../example/springboot_demo/PasswordTest.java | 22 ++ 73 files changed, 509 insertions(+), 40 deletions(-) create mode 100644 src/test/src/main/java/com/example/springboot_demo/service/impl/CustomUserDetailsService.java create mode 100644 src/test/src/test/java/com/example/springboot_demo/PasswordTest.java diff --git a/src/test/api-test.http b/src/test/api-test.http index 80801dfb..9d3210c4 100644 --- a/src/test/api-test.http +++ b/src/test/api-test.http @@ -4,6 +4,235 @@ GET http://localhost:8080/test/hello ### 测试所有数据库 GET http://localhost:8080/test/all +### ==================== 新增表接口测试 ==================== + +### 好友关系 - 查询用户好友列表 +GET http://localhost:8080/friend-relation/list/1 + +### 好友关系 - 添加好友 +POST http://localhost:8080/friend-relation +Content-Type: application/json + +{ + "userId": 1, + "friendId": 2, + "friendUsername": "user2", + "remarkName": "小明" +} + +### 好友请求 - 查询待处理请求 +GET http://localhost:8080/friend-request/list/1/0 + +### 好友请求 - 发送好友请求 +POST http://localhost:8080/friend-request +Content-Type: application/json + +{ + "applicantId": 1, + "recipientId": 2, + "applyMsg": "我是张三,加个好友" +} + +### 好友请求 - 处理请求(同意) +PUT http://localhost:8080/friend-request +Content-Type: application/json + +{ + "id": 1, + "status": 1 +} + +### 查询分享 - 查询接收的分享 +GET http://localhost:8080/query-share/list/receive/1 + +### 查询分享 - 分享查询 +POST http://localhost:8080/query-share +Content-Type: application/json + +{ + "shareUserId": 1, + "receiveUserId": 2, + "dialogId": "conv_12345", + "targetRounds": "[1,2,3]", + "queryTitle": "销售数据分析" +} + +### Token消耗 - 查询所有记录 +GET http://localhost:8080/token-consume/list + +### Token消耗 - 查询指定日期 +GET http://localhost:8080/token-consume/gemini-2.5-pro/2025-12-01 + +### Token消耗 - 添加记录 +POST http://localhost:8080/token-consume +Content-Type: application/json + +{ + "llmName": "gemini-2.5-pro", + "totalTokens": 1500, + "promptTokens": 1000, + "completionTokens": 500, + "consumeDate": "2025-12-01" +} + +### 性能指标 - 查询所有 +GET http://localhost:8080/performance-metric/list + +### 性能指标 - 按类型查询 +GET http://localhost:8080/performance-metric/list/query_count + +### 性能指标 - 添加记录 +POST http://localhost:8080/performance-metric +Content-Type: application/json + +{ + "metricType": "query_count", + "metricValue": 150, + "metricTime": "2025-12-01T10:00:00" +} + +### 连接日志 - 查询所有 +GET http://localhost:8080/db-connection-log/list + +### 连接日志 - 按连接ID查询 +GET http://localhost:8080/db-connection-log/list/connection/1 + +### 连接日志 - 添加日志 +POST http://localhost:8080/db-connection-log +Content-Type: application/json + +{ + "dbConnectionId": 1, + "dbName": "订单主库", + "status": "success", + "handlerId": 1 +} + +### 用户搜索 - 查询用户搜索历史 +GET http://localhost:8080/user-search/list/1 + +### 用户搜索 - 查询常用搜索(Top 5) +GET http://localhost:8080/user-search/list/top/1/5 + +### 用户搜索 - 记录搜索 +POST http://localhost:8080/user-search +Content-Type: application/json + +{ + "userId": 1, + "sqlContent": "SELECT * FROM users WHERE status = 1", + "queryTitle": "查询活跃用户" +} + +### 收藏组 - 查询用户收藏组 +GET http://localhost:8080/query-collection/list/1 + +### 收藏组 - 创建收藏组 +POST http://localhost:8080/query-collection +Content-Type: application/json + +{ + "userId": 1, + "groupName": "销售报表查询" +} + +### 收藏记录 - 按组查询 +GET http://localhost:8080/collection-record/list/query/674c8e9f1234567890abcdef + +### 收藏记录 - 添加收藏 +POST http://localhost:8080/collection-record +Content-Type: application/json + +{ + "queryId": "674c8e9f1234567890abcdef", + "userId": 1, + "sqlContent": "SELECT * FROM orders WHERE date > '2025-01-01'", + "dbConnectionId": 1, + "llmConfigId": 1 +} + +### 对话详情 - 查询对话内容 +GET http://localhost:8080/dialog-detail/conv_12345 + +### 对话详情 - 保存对话 +POST http://localhost:8080/dialog-detail +Content-Type: application/json + +{ + "dialogId": "conv_12345", + "rounds": [ + { + "roundNum": 1, + "userInput": "查询所有用户", + "aiResponse": "已生成SQL查询", + "generatedSql": "SELECT * FROM users" + } + ] +} + +### SQL缓存 - 查询用户缓存 +GET http://localhost:8080/sql-cache/list/1 + +### SQL缓存 - 添加缓存 +POST http://localhost:8080/sql-cache +Content-Type: application/json + +{ + "nlHash": "abc123def456", + "userId": 1, + "connectionId": 1, + "tableIds": [1, 2], + "dbType": "mysql", + "generatedSql": "SELECT * FROM users", + "hitCount": 0, + "expireTime": "2025-12-08T00:00:00" +} + +### AI交互日志 - 按用户查询 +GET http://localhost:8080/ai-interaction-log/list/user/1 + +### AI交互日志 - 按模型查询 +GET http://localhost:8080/ai-interaction-log/list/llm/gemini-2.5-pro + +### AI交互日志 - 添加日志 +POST http://localhost:8080/ai-interaction-log +Content-Type: application/json + +{ + "userId": 1, + "requestType": "nl2sql", + "llmName": "gemini-2.5-pro", + "requestParams": { + "naturalLanguage": "查询所有用户" + }, + "tokenUsage": { + "promptTokens": 100, + "completionTokens": 50, + "totalTokens": 150 + }, + "responseTime": 1200, + "status": "success" +} + +### 好友聊天 - 查询聊天记录 +GET http://localhost:8080/friend-chat/list/1/2 + +### 好友聊天 - 查询未读消息 +GET http://localhost:8080/friend-chat/unread/1 + +### 好友聊天 - 发送消息 +POST http://localhost:8080/friend-chat +Content-Type: application/json + +{ + "userId": 1, + "friendId": 2, + "contentType": "text", + "content": { + "text": "你好,最近怎么样?" + } +} + ### ==================== 认证接口 ==================== ### 登录 diff --git a/src/test/frontend/components/admin/UserManagementPage.tsx b/src/test/frontend/components/admin/UserManagementPage.tsx index 91242112..83f9d3b5 100644 --- a/src/test/frontend/components/admin/UserManagementPage.tsx +++ b/src/test/frontend/components/admin/UserManagementPage.tsx @@ -177,22 +177,25 @@ export const UserManagementPage: React.FC = () => { return; } const role = formData.get('role') as UserRole; + const phonenumber = formData.get('phonenumber') as string; await userApi.create({ username: formData.get('username') as string, email: formData.get('email') as string, password: password, roleId: mapRoleToRoleId(role), status: 1, - phonenumber: '', + phonenumber, }); alert('添加成功'); await loadUsers(); // 重新加载列表 } else if (modal === 'edit' && userToProcess) { + const phonenumber = formData.get('phonenumber') as string; await userApi.update({ id: userToProcess.id, username: formData.get('username') as string, email: formData.get('email') as string, + phonenumber, roleId: mapRoleToRoleId(formData.get('role') as UserRole), }); alert('更新成功'); @@ -300,6 +303,18 @@ export const UserManagementPage: React.FC = () => {
    + {/* 手机号 */} +
    + + +
    {modal === 'add' && <>
    } diff --git a/src/test/pom.xml b/src/test/pom.xml index cff4379e..c7d92b10 100644 --- a/src/test/pom.xml +++ b/src/test/pom.xml @@ -66,7 +66,8 @@ org.projectlombok lombok - true + 1.18.42 + provided @@ -123,9 +124,33 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + 1.18.42 + + + + org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + diff --git a/src/test/src/main/java/com/example/springboot_demo/common/Result.java b/src/test/src/main/java/com/example/springboot_demo/common/Result.java index ecac05d4..37e062c4 100644 --- a/src/test/src/main/java/com/example/springboot_demo/common/Result.java +++ b/src/test/src/main/java/com/example/springboot_demo/common/Result.java @@ -1,13 +1,34 @@ package com.example.springboot_demo.common; -import lombok.Data; - -@Data public class Result { private Integer code; private String message; private T data; + public Integer getCode() { + return code; + } + + public void setCode(Integer code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + public static Result success() { return success(null); } diff --git a/src/test/src/main/java/com/example/springboot_demo/config/SecurityConfig.java b/src/test/src/main/java/com/example/springboot_demo/config/SecurityConfig.java index 2b306532..6a676320 100644 --- a/src/test/src/main/java/com/example/springboot_demo/config/SecurityConfig.java +++ b/src/test/src/main/java/com/example/springboot_demo/config/SecurityConfig.java @@ -1,7 +1,11 @@ package com.example.springboot_demo.config; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -9,15 +13,39 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import com.example.springboot_demo.service.impl.CustomUserDetailsService; + @Configuration @EnableWebSecurity public class SecurityConfig { + @Autowired + private CustomUserDetailsService customUserDetailsService; + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + /** + * 配置认证提供者,使用自定义的 UserDetailsService + */ + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(customUserDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + /** + * 暴露 AuthenticationManager Bean,供登录接口使用 + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception { + return authConfig.getAuthenticationManager(); + } + @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http @@ -26,7 +54,8 @@ public class SecurityConfig { .requestMatchers("/**").permitAll() // Allow all requests for now, we'll handle auth with Interceptor for specific paths or refine this later. // Note: The plan mentioned "Initially disable Spring Security's default login page". // Permitting all here effectively bypasses Spring Security's default auth flow, relying on our custom JWT flow. - ); + ) + .authenticationProvider(authenticationProvider()); // 使用自定义的认证提供者 return http.build(); } } diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/AiInteractionLogController.java b/src/test/src/main/java/com/example/springboot_demo/controller/AiInteractionLogController.java index bd04dd30..f46fc03b 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/AiInteractionLogController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/AiInteractionLogController.java @@ -39,3 +39,4 @@ public class AiInteractionLogController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/CollectionRecordController.java b/src/test/src/main/java/com/example/springboot_demo/controller/CollectionRecordController.java index 45f7d505..6e651814 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/CollectionRecordController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/CollectionRecordController.java @@ -50,3 +50,4 @@ public class CollectionRecordController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionLogController.java b/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionLogController.java index 7f61ccfb..89c7b2a3 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionLogController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionLogController.java @@ -46,3 +46,4 @@ public class DbConnectionLogController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/DialogDetailController.java b/src/test/src/main/java/com/example/springboot_demo/controller/DialogDetailController.java index 095deb55..1c91c560 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/DialogDetailController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/DialogDetailController.java @@ -37,3 +37,4 @@ public class DialogDetailController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/FriendChatController.java b/src/test/src/main/java/com/example/springboot_demo/controller/FriendChatController.java index 1b254fa4..70dee82a 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/FriendChatController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/FriendChatController.java @@ -49,3 +49,4 @@ public class FriendChatController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/FriendRelationController.java b/src/test/src/main/java/com/example/springboot_demo/controller/FriendRelationController.java index bae58aee..05fda5cd 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/FriendRelationController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/FriendRelationController.java @@ -55,3 +55,4 @@ public class FriendRelationController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/FriendRequestController.java b/src/test/src/main/java/com/example/springboot_demo/controller/FriendRequestController.java index 83b82410..3bec5f41 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/FriendRequestController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/FriendRequestController.java @@ -57,3 +57,4 @@ public class FriendRequestController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/PerformanceMetricController.java b/src/test/src/main/java/com/example/springboot_demo/controller/PerformanceMetricController.java index 8f3c4476..b5200bb6 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/PerformanceMetricController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/PerformanceMetricController.java @@ -40,3 +40,4 @@ public class PerformanceMetricController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/QueryCollectionController.java b/src/test/src/main/java/com/example/springboot_demo/controller/QueryCollectionController.java index 900197d2..ea0d5900 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/QueryCollectionController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/QueryCollectionController.java @@ -40,3 +40,4 @@ public class QueryCollectionController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/QueryShareController.java b/src/test/src/main/java/com/example/springboot_demo/controller/QueryShareController.java index ba9e3483..00299f8c 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/QueryShareController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/QueryShareController.java @@ -59,3 +59,4 @@ public class QueryShareController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/SqlCacheController.java b/src/test/src/main/java/com/example/springboot_demo/controller/SqlCacheController.java index fcbe21f1..38241708 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/SqlCacheController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/SqlCacheController.java @@ -43,3 +43,4 @@ public class SqlCacheController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/TokenConsumeController.java b/src/test/src/main/java/com/example/springboot_demo/controller/TokenConsumeController.java index e3b9cb94..dd122289 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/TokenConsumeController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/TokenConsumeController.java @@ -47,3 +47,4 @@ public class TokenConsumeController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/controller/UserSearchController.java b/src/test/src/main/java/com/example/springboot_demo/controller/UserSearchController.java index ae143537..f31988ed 100644 --- a/src/test/src/main/java/com/example/springboot_demo/controller/UserSearchController.java +++ b/src/test/src/main/java/com/example/springboot_demo/controller/UserSearchController.java @@ -51,3 +51,4 @@ public class UserSearchController { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/AiInteractionLog.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/AiInteractionLog.java index 63c5cbe7..f62ed696 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/AiInteractionLog.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/AiInteractionLog.java @@ -42,3 +42,4 @@ public class AiInteractionLog { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/CollectionRecord.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/CollectionRecord.java index 31462ed0..2ba566a0 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/CollectionRecord.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/CollectionRecord.java @@ -29,3 +29,4 @@ public class CollectionRecord { private LocalDateTime createTime; } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogDetail.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogDetail.java index e52b6186..e466babc 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogDetail.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogDetail.java @@ -29,3 +29,4 @@ public class DialogDetail { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/FriendChat.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/FriendChat.java index 1ddf0b0a..42b49521 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/FriendChat.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/FriendChat.java @@ -29,3 +29,4 @@ public class FriendChat { private Map extra; } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/QueryCollection.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/QueryCollection.java index 5cef4e49..70ad6402 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/QueryCollection.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/QueryCollection.java @@ -20,3 +20,4 @@ public class QueryCollection { private LocalDateTime createTime; } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/SqlCache.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/SqlCache.java index f6e08c67..e358167e 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/SqlCache.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/SqlCache.java @@ -31,3 +31,4 @@ public class SqlCache { private LocalDateTime expireTime; } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnectionLog.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnectionLog.java index 21747d23..2c04e401 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnectionLog.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnectionLog.java @@ -28,3 +28,4 @@ public class DbConnectionLog { private Long handlerId; } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRelation.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRelation.java index 5a6b1b3a..301c0bd2 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRelation.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRelation.java @@ -28,3 +28,4 @@ public class FriendRelation { private LocalDateTime createTime; } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRequest.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRequest.java index c1b08a5e..102ade55 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRequest.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRequest.java @@ -28,3 +28,4 @@ public class FriendRequest { private LocalDateTime handleTime; } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/PerformanceMetric.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/PerformanceMetric.java index 7c3d7443..da07e300 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/PerformanceMetric.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/PerformanceMetric.java @@ -25,3 +25,4 @@ public class PerformanceMetric { private Integer trend; } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryShare.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryShare.java index 480f71b0..ce421fdc 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryShare.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryShare.java @@ -30,3 +30,4 @@ public class QueryShare { private Integer receiveStatus; } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TokenConsume.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TokenConsume.java index 5991d986..849657b5 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TokenConsume.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TokenConsume.java @@ -29,3 +29,4 @@ public class TokenConsume { private BigDecimal growthRate; } + diff --git a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserSearch.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserSearch.java index cd5325a1..b2402f9b 100644 --- a/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserSearch.java +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserSearch.java @@ -26,3 +26,4 @@ public class UserSearch { private LocalDateTime lastSearchTime; } + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionLogMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionLogMapper.java index a6e68436..08245589 100644 --- a/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionLogMapper.java +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionLogMapper.java @@ -9,3 +9,4 @@ import com.example.springboot_demo.entity.mysql.DbConnectionLog; public interface DbConnectionLogMapper extends BaseMapper { } + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRelationMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRelationMapper.java index bb6d43b1..adf0ff52 100644 --- a/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRelationMapper.java +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRelationMapper.java @@ -9,3 +9,4 @@ import com.example.springboot_demo.entity.mysql.FriendRelation; public interface FriendRelationMapper extends BaseMapper { } + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRequestMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRequestMapper.java index e589126b..40630c26 100644 --- a/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRequestMapper.java +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRequestMapper.java @@ -9,3 +9,4 @@ import com.example.springboot_demo.entity.mysql.FriendRequest; public interface FriendRequestMapper extends BaseMapper { } + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/PerformanceMetricMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/PerformanceMetricMapper.java index 0e6cfc32..3de1991c 100644 --- a/src/test/src/main/java/com/example/springboot_demo/mapper/PerformanceMetricMapper.java +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/PerformanceMetricMapper.java @@ -9,3 +9,4 @@ import com.example.springboot_demo.entity.mysql.PerformanceMetric; public interface PerformanceMetricMapper extends BaseMapper { } + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/QueryShareMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/QueryShareMapper.java index 2ea075cd..01a93d90 100644 --- a/src/test/src/main/java/com/example/springboot_demo/mapper/QueryShareMapper.java +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/QueryShareMapper.java @@ -9,3 +9,4 @@ import com.example.springboot_demo.entity.mysql.QueryShare; public interface QueryShareMapper extends BaseMapper { } + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/TokenConsumeMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/TokenConsumeMapper.java index b3e9b5bd..05813532 100644 --- a/src/test/src/main/java/com/example/springboot_demo/mapper/TokenConsumeMapper.java +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/TokenConsumeMapper.java @@ -9,3 +9,4 @@ import com.example.springboot_demo.entity.mysql.TokenConsume; public interface TokenConsumeMapper extends BaseMapper { } + diff --git a/src/test/src/main/java/com/example/springboot_demo/mapper/UserSearchMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/UserSearchMapper.java index d2a9f054..5e62bda1 100644 --- a/src/test/src/main/java/com/example/springboot_demo/mapper/UserSearchMapper.java +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/UserSearchMapper.java @@ -9,3 +9,4 @@ import com.example.springboot_demo.entity.mysql.UserSearch; public interface UserSearchMapper extends BaseMapper { } + diff --git a/src/test/src/main/java/com/example/springboot_demo/repository/AiInteractionLogRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/AiInteractionLogRepository.java index a394d7d1..c16c569b 100644 --- a/src/test/src/main/java/com/example/springboot_demo/repository/AiInteractionLogRepository.java +++ b/src/test/src/main/java/com/example/springboot_demo/repository/AiInteractionLogRepository.java @@ -16,3 +16,4 @@ public interface AiInteractionLogRepository extends MongoRepository findByLlmNameAndStatus(String llmName, String status); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/repository/CollectionRecordRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/CollectionRecordRepository.java index a85eae5a..5e8f850b 100644 --- a/src/test/src/main/java/com/example/springboot_demo/repository/CollectionRecordRepository.java +++ b/src/test/src/main/java/com/example/springboot_demo/repository/CollectionRecordRepository.java @@ -16,3 +16,4 @@ public interface CollectionRecordRepository extends MongoRepository findByDbConnectionId(Long dbConnectionId); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/repository/DialogDetailRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/DialogDetailRepository.java index 54380bee..12cfaaba 100644 --- a/src/test/src/main/java/com/example/springboot_demo/repository/DialogDetailRepository.java +++ b/src/test/src/main/java/com/example/springboot_demo/repository/DialogDetailRepository.java @@ -10,3 +10,4 @@ public interface DialogDetailRepository extends MongoRepository findByFriendIdAndIsRead(Long friendId, Boolean isRead); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/repository/QueryCollectionRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/QueryCollectionRepository.java index 4313db66..dbc20ad7 100644 --- a/src/test/src/main/java/com/example/springboot_demo/repository/QueryCollectionRepository.java +++ b/src/test/src/main/java/com/example/springboot_demo/repository/QueryCollectionRepository.java @@ -14,3 +14,4 @@ public interface QueryCollectionRepository extends MongoRepository { List findByUserId(Long userId); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/AiInteractionLogService.java b/src/test/src/main/java/com/example/springboot_demo/service/AiInteractionLogService.java index c092aafd..fd49ec40 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/AiInteractionLogService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/AiInteractionLogService.java @@ -15,3 +15,4 @@ public interface AiInteractionLogService { AiInteractionLog save(AiInteractionLog aiInteractionLog); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/CollectionRecordService.java b/src/test/src/main/java/com/example/springboot_demo/service/CollectionRecordService.java index d87e68e6..f491e024 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/CollectionRecordService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/CollectionRecordService.java @@ -19,3 +19,4 @@ public interface CollectionRecordService { void deleteById(String id); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionLogService.java b/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionLogService.java index 3bbe386b..c63a9fd0 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionLogService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionLogService.java @@ -17,3 +17,4 @@ public interface DbConnectionLogService { boolean save(DbConnectionLog dbConnectionLog); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/DialogDetailService.java b/src/test/src/main/java/com/example/springboot_demo/service/DialogDetailService.java index 69b36a7e..7facdfbf 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/DialogDetailService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/DialogDetailService.java @@ -11,3 +11,4 @@ public interface DialogDetailService { void deleteById(String id); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/FriendChatService.java b/src/test/src/main/java/com/example/springboot_demo/service/FriendChatService.java index e54e1158..b9a2fb2f 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/FriendChatService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/FriendChatService.java @@ -15,3 +15,4 @@ public interface FriendChatService { void deleteById(String id); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/FriendRelationService.java b/src/test/src/main/java/com/example/springboot_demo/service/FriendRelationService.java index 23a2fe95..cb5f753f 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/FriendRelationService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/FriendRelationService.java @@ -19,3 +19,4 @@ public interface FriendRelationService { boolean removeByUserIdAndFriendId(Long userId, Long friendId); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/FriendRequestService.java b/src/test/src/main/java/com/example/springboot_demo/service/FriendRequestService.java index f6697b05..5af5fadc 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/FriendRequestService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/FriendRequestService.java @@ -19,3 +19,4 @@ public interface FriendRequestService { boolean removeById(Long id); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/PerformanceMetricService.java b/src/test/src/main/java/com/example/springboot_demo/service/PerformanceMetricService.java index 60c3cb8c..026758e9 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/PerformanceMetricService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/PerformanceMetricService.java @@ -16,3 +16,4 @@ public interface PerformanceMetricService { boolean save(PerformanceMetric performanceMetric); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/QueryCollectionService.java b/src/test/src/main/java/com/example/springboot_demo/service/QueryCollectionService.java index f97842c8..e92d0690 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/QueryCollectionService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/QueryCollectionService.java @@ -17,3 +17,4 @@ public interface QueryCollectionService { void deleteById(String id); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/QueryShareService.java b/src/test/src/main/java/com/example/springboot_demo/service/QueryShareService.java index ecb04e0f..cc032130 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/QueryShareService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/QueryShareService.java @@ -21,3 +21,4 @@ public interface QueryShareService { boolean removeById(Long id); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/SqlCacheService.java b/src/test/src/main/java/com/example/springboot_demo/service/SqlCacheService.java index 72d52415..4e1423cd 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/SqlCacheService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/SqlCacheService.java @@ -15,3 +15,4 @@ public interface SqlCacheService { void deleteById(String id); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/TokenConsumeService.java b/src/test/src/main/java/com/example/springboot_demo/service/TokenConsumeService.java index f12aa04a..5d41a5f3 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/TokenConsumeService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/TokenConsumeService.java @@ -18,3 +18,4 @@ public interface TokenConsumeService { boolean updateById(TokenConsume tokenConsume); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/UserSearchService.java b/src/test/src/main/java/com/example/springboot_demo/service/UserSearchService.java index 4d4584ec..df06640b 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/UserSearchService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/UserSearchService.java @@ -19,3 +19,4 @@ public interface UserSearchService { boolean removeById(Long id); } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/AiInteractionLogServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/AiInteractionLogServiceImpl.java index abe846e7..a8696ebd 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/AiInteractionLogServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/AiInteractionLogServiceImpl.java @@ -35,3 +35,4 @@ public class AiInteractionLogServiceImpl implements AiInteractionLogService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java index 8f57905d..05acc710 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java @@ -1,7 +1,10 @@ package com.example.springboot_demo.service.impl; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -27,43 +30,47 @@ public class AuthServiceImpl implements AuthService { private JwtUtil jwtUtil; @Autowired - private PasswordEncoder passwordEncoder; + private AuthenticationManager authenticationManager; @Override public LoginVO login(LoginDTO loginDTO) { - // 查询用户 - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(User::getUsername, loginDTO.getUsername()); - User user = userMapper.selectOne(wrapper); - - if (user == null) { - throw new RuntimeException("用户不存在"); - } - - // 验证密码 - if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) { - throw new RuntimeException("密码错误"); - } - - // 检查账号状态 - if (user.getStatus() == 0) { - throw new RuntimeException("账号已被禁用"); + try { + // 使用 Spring Security 的 AuthenticationManager 进行认证 + // 这会调用我们自定义的 CustomUserDetailsService 来加载用户并验证密码 + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken( + loginDTO.getUsername(), + loginDTO.getPassword() + ) + ); + + // 认证成功后,从数据库查询完整的用户信息 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getUsername, loginDTO.getUsername()); + User user = userMapper.selectOne(wrapper); + + if (user == null) { + throw new RuntimeException("用户不存在"); + } + + // 查询角色信息 + Role role = roleMapper.selectById(user.getRoleId()); + + // 构造返回数据 + LoginVO loginVO = new LoginVO(); + loginVO.setToken(jwtUtil.generateToken(user.getId(), user.getUsername())); + loginVO.setUserId(user.getId()); + loginVO.setUsername(user.getUsername()); + loginVO.setEmail(user.getEmail()); + loginVO.setRoleId(user.getRoleId()); + loginVO.setRoleName(role != null ? role.getRoleName() : null); + loginVO.setAvatarUrl(user.getAvatarUrl()); + + return loginVO; + } catch (AuthenticationException e) { + // Spring Security 认证失败 + throw new RuntimeException("用户名或密码错误"); } - - // 查询角色信息 - Role role = roleMapper.selectById(user.getRoleId()); - - // 构造返回数据 - LoginVO loginVO = new LoginVO(); - loginVO.setToken(jwtUtil.generateToken(user.getId(), user.getUsername())); - loginVO.setUserId(user.getId()); - loginVO.setUsername(user.getUsername()); - loginVO.setEmail(user.getEmail()); - loginVO.setRoleId(user.getRoleId()); - loginVO.setRoleName(role != null ? role.getRoleName() : null); - loginVO.setAvatarUrl(user.getAvatarUrl()); - - return loginVO; } } diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/CollectionRecordServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/CollectionRecordServiceImpl.java index 6505253e..a5288b26 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/CollectionRecordServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/CollectionRecordServiceImpl.java @@ -45,3 +45,4 @@ public class CollectionRecordServiceImpl implements CollectionRecordService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/CustomUserDetailsService.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/CustomUserDetailsService.java new file mode 100644 index 00000000..f1435aae --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/CustomUserDetailsService.java @@ -0,0 +1,56 @@ +package com.example.springboot_demo.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.example.springboot_demo.entity.mysql.User; +import com.example.springboot_demo.mapper.UserMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +/** + * 自定义用户详情服务 + * 从数据库加载用户信息供 Spring Security 使用 + */ +@Service +public class CustomUserDetailsService implements UserDetailsService { + + @Autowired + private UserMapper userMapper; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 从数据库查询用户 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getUsername, username); + User user = userMapper.selectOne(wrapper); + + if (user == null) { + throw new UsernameNotFoundException("用户不存在: " + username); + } + + // 检查账号状态 + if (user.getStatus() == 0) { + throw new UsernameNotFoundException("账号已被禁用: " + username); + } + + // 构建 Spring Security 的 UserDetails 对象 + // 注意:这里使用数据库中的加密密码 + return org.springframework.security.core.userdetails.User + .withUsername(user.getUsername()) + .password(user.getPassword()) // 数据库中已经是 BCrypt 加密的密码 + .authorities(Collections.singletonList( + new SimpleGrantedAuthority("ROLE_" + user.getRoleId()) + )) + .accountExpired(false) + .accountLocked(user.getStatus() == 0) + .credentialsExpired(false) + .disabled(user.getStatus() == 0) + .build(); + } +} + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionLogServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionLogServiceImpl.java index e6404266..fd16198c 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionLogServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionLogServiceImpl.java @@ -45,3 +45,4 @@ public class DbConnectionLogServiceImpl implements DbConnectionLogService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogDetailServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogDetailServiceImpl.java index b1300db2..e0e1e52d 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogDetailServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogDetailServiceImpl.java @@ -28,3 +28,4 @@ public class DialogDetailServiceImpl implements DialogDetailService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendChatServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendChatServiceImpl.java index 89aba604..9d293cf5 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendChatServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendChatServiceImpl.java @@ -36,3 +36,4 @@ public class FriendChatServiceImpl implements FriendChatService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRelationServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRelationServiceImpl.java index 609db649..3613d4cc 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRelationServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRelationServiceImpl.java @@ -52,3 +52,4 @@ public class FriendRelationServiceImpl implements FriendRelationService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRequestServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRequestServiceImpl.java index 1be04aec..9961c7d8 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRequestServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRequestServiceImpl.java @@ -50,3 +50,4 @@ public class FriendRequestServiceImpl implements FriendRequestService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/PerformanceMetricServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/PerformanceMetricServiceImpl.java index 06e0479a..ffab4934 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/PerformanceMetricServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/PerformanceMetricServiceImpl.java @@ -41,3 +41,4 @@ public class PerformanceMetricServiceImpl implements PerformanceMetricService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryCollectionServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryCollectionServiceImpl.java index 86f23879..db57a1a8 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryCollectionServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryCollectionServiceImpl.java @@ -40,3 +40,4 @@ public class QueryCollectionServiceImpl implements QueryCollectionService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryShareServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryShareServiceImpl.java index f8a8a027..78448af4 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryShareServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryShareServiceImpl.java @@ -57,3 +57,4 @@ public class QueryShareServiceImpl implements QueryShareService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/SqlCacheServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/SqlCacheServiceImpl.java index e1bffbe4..4e30fa33 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/SqlCacheServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/SqlCacheServiceImpl.java @@ -35,3 +35,4 @@ public class SqlCacheServiceImpl implements SqlCacheService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/TokenConsumeServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/TokenConsumeServiceImpl.java index 945f43f1..e2ccd538 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/TokenConsumeServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/TokenConsumeServiceImpl.java @@ -46,3 +46,4 @@ public class TokenConsumeServiceImpl implements TokenConsumeService { } } + diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/UserSearchServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/UserSearchServiceImpl.java index 2355a9f7..9c776530 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/UserSearchServiceImpl.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/UserSearchServiceImpl.java @@ -52,3 +52,4 @@ public class UserSearchServiceImpl implements UserSearchService { } } + diff --git a/src/test/src/test/java/com/example/springboot_demo/PasswordTest.java b/src/test/src/test/java/com/example/springboot_demo/PasswordTest.java new file mode 100644 index 00000000..4ace1b82 --- /dev/null +++ b/src/test/src/test/java/com/example/springboot_demo/PasswordTest.java @@ -0,0 +1,22 @@ +package com.example.springboot_demo; + +import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +public class PasswordTest { + + @Test + public void testPassword() { + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + String rawPassword = "123456"; + String encodedPassword = "$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iKTVKIUi"; + + boolean matches = encoder.matches(rawPassword, encodedPassword); + System.out.println("Password matches: " + matches); + + // 生成新的加密密码 + String newEncoded = encoder.encode(rawPassword); + System.out.println("New encoded password: " + newEncoded); + } +} + -- 2.34.1 From 52908238f4008a0054b4ee0416a835904cd91a8b Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Mon, 1 Dec 2025 18:04:40 +0800 Subject: [PATCH 11/14] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=91=BD=E5=90=8D=EF=BC=8C=E6=B5=8B=E8=AF=95=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E8=BF=9E=E6=8E=A5=EF=BC=8C=E6=9B=B4=E6=94=B9=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/DATABASE_SETUP.md | 269 ---------------- src/test/QUICK_START.md | 208 ------------- src/test/TEST_ACCOUNTS.md | 294 ------------------ src/test/mvnw.cmd | 189 ----------- src/test/mysql_schema_from_last.sql | 2 +- src/test/package-lock.json | 6 - .../impl/CustomUserDetailsService.java | 19 +- 7 files changed, 7 insertions(+), 980 deletions(-) delete mode 100644 src/test/DATABASE_SETUP.md delete mode 100644 src/test/QUICK_START.md delete mode 100644 src/test/TEST_ACCOUNTS.md delete mode 100644 src/test/mvnw.cmd delete mode 100644 src/test/package-lock.json diff --git a/src/test/DATABASE_SETUP.md b/src/test/DATABASE_SETUP.md deleted file mode 100644 index fe09c0fa..00000000 --- a/src/test/DATABASE_SETUP.md +++ /dev/null @@ -1,269 +0,0 @@ -# 数据库环境搭建指南 - -本项目使用 Docker Compose 管理数据库环境,确保团队成员之间的开发环境一致。 - -## 一、首次启动(全新环境) - -### 1. 启动所有服务 - -```bash -# Windows PowerShell / CMD -docker compose up -d - -# 或者只启动必需的服务 -docker compose up -d mysql mongodb redis -``` - -### 2. 验证服务状态 - -```bash -docker ps -``` - -应该看到以下容器都是 `healthy` 状态: -- `nlq_mysql` (端口 3306) -- `nlq_mongodb` (端口 27017) -- `nlq_redis` (端口 6379) - -### 3. 初始数据说明 - -容器首次启动时会自动执行初始化脚本: - -**MySQL (`mysql_schema_from_last.sql`)**: -- ✅ 创建所有表结构 -- ✅ 插入基础数据: - - 3 个角色(系统管理员、数据管理员、普通用户) - - 4 种数据库类型(MySQL、MongoDB、PostgreSQL、SQL Server) - - 3 种大模型状态(可用、不可用、不稳定) - - 3 个内置用户账号(密码统一为 `123456`): - - `sys_admin` - 系统管理员 - - `data_admin` - 数据管理员 - - `normal_user` - 普通用户 - -**MongoDB (`mongodb_schema_from_last.js`)**: -- ✅ 创建 7 个集合(dialog_records、sql_cache 等) -- ✅ 配置索引和数据验证规则 - -### 4. 登录测试 - -**MySQL 管理界面**:http://localhost:8082 -- 系统:MySQL -- 服务器:mysql -- 用户名:root -- 密码:root123456 -- 数据库:natural_language_query_system - -**MongoDB 管理界面**:http://localhost:8081 -- 用户名:admin -- 密码:admin - ---- - -## 二、数据持久化说明 - -### Docker 数据卷 - -项目使用 Docker 命名卷存储数据,**即使删除容器,数据也不会丢失**: - -```bash -# 查看数据卷 -docker volume ls | findstr nlq - -# 输出示例: -# local springboot_demo_mysql_data -# local springboot_demo_mongodb_data -# local springboot_demo_redis_data -``` - -### 常见操作 - -**重启容器(数据不丢失)**: -```bash -docker compose restart -``` - -**停止容器(数据不丢失)**: -```bash -docker compose stop -``` - -**删除容器但保留数据**: -```bash -docker compose down -``` - -**完全清理(包括数据卷)**: -```bash -# ⚠️ 警告:这会删除所有数据! -docker compose down -v -``` - ---- - -## 三、团队协作:数据共享 - -### 场景 1:你配置好了数据,想分享给团队 - -1. **导出数据**: - ```bash - # Windows - cd D:\test - scripts\export-data.bat - - # Linux/Mac - chmod +x scripts/export-data.sh - ./scripts/export-data.sh - ``` - -2. **分享文件**: - 将生成的 `data-backup` 文件夹打包,通过网盘/Git LFS 分享给团队成员。 - -### 场景 2:团队成员导入你的数据 - -1. **获取备份文件**: - 将收到的 `data-backup` 文件夹放到项目根目录。 - -2. **启动容器**: - ```bash - docker compose up -d - ``` - -3. **导入数据**: - ```bash - # Windows - scripts\import-data.bat - - # Linux/Mac - chmod +x scripts/import-data.sh - ./scripts/import-data.sh - ``` - ---- - -## 四、常见问题 - -### Q1: 修改了初始化脚本,但重启后没生效? - -**原因**:初始化脚本只在数据卷为空时执行一次。 - -**解决**: -```bash -# 1. 停止并删除容器和数据卷 -docker compose down -v - -# 2. 重新启动(会重新执行初始化脚本) -docker compose up -d -``` - -### Q2: 端口被占用怎么办? - -**错误示例**: -``` -Error: bind: address already in use -``` - -**解决**:修改 `docker-compose.yml` 中的端口映射: -```yaml -ports: - - "3307:3306" # 将 MySQL 映射到本机 3307 端口 -``` - -### Q3: 如何查看容器日志? - -```bash -# 查看 MySQL 日志 -docker logs -f nlq_mysql - -# 查看 MongoDB 日志 -docker logs -f nlq_mongodb -``` - -### Q4: 如何进入容器内部? - -```bash -# 进入 MySQL 容器 -docker exec -it nlq_mysql bash -mysql -uroot -proot123456 - -# 进入 MongoDB 容器 -docker exec -it nlq_mongodb bash -mongosh -u admin -p admin123456 --authenticationDatabase admin -``` - ---- - -## 五、开发建议 - -### 本地开发流程 - -1. **首次启动**: - ```bash - docker compose up -d - # 等待 30 秒让初始化脚本执行完成 - ``` - -2. **启动后端**: - ```bash - # IDEA 中直接运行 SpringbootDemoApplication - # 或命令行: - mvn spring-boot:run - ``` - -3. **启动前端**: - ```bash - cd frontend - npm install - npm run dev - ``` - -### 数据备份习惯 - -建议每周执行一次数据导出,避免重要测试数据丢失: - -```bash -# 每周五下班前 -scripts\export-data.bat -git add data-backup/ -git commit -m "chore: 备份测试数据 $(date)" -``` - ---- - -## 六、生产环境部署 - -⚠️ **注意**:`docker-compose.yml` 中的密码仅用于开发环境! - -生产环境部署时请: -1. 修改所有默认密码 -2. 使用环境变量管理敏感信息 -3. 配置防火墙规则 -4. 启用 SSL/TLS 加密连接 -5. 定期备份数据 - ---- - -## 附录:默认账号信息 - -### 数据库账号 - -| 服务 | 用户名 | 密码 | 说明 | -|------|--------|------|------| -| MySQL (root) | root | root123456 | 数据库管理员 | -| MySQL (应用) | nlq_user | nlq_pass123 | 应用连接账号 | -| MongoDB | admin | admin123456 | 数据库管理员 | -| Adminer | - | - | 无需登录 | -| Mongo Express | admin | admin | Web 界面登录 | - -### 应用内置账号(密码统一为 123456) - -| 用户名 | 角色 | 权限说明 | -|--------|------|----------| -| sys_admin | 系统管理员 | 拥有系统所有权限,可管理用户、配置大模型 | -| data_admin | 数据管理员 | 可管理数据源连接、分配用户数据权限 | -| normal_user | 普通用户 | 可执行自然语言查询、查看历史记录 | - ---- - -**最后更新**:2025-12-01 -**维护者**:项目组 - diff --git a/src/test/QUICK_START.md b/src/test/QUICK_START.md deleted file mode 100644 index 7d68a946..00000000 --- a/src/test/QUICK_START.md +++ /dev/null @@ -1,208 +0,0 @@ -# 快速启动指南 - -## 一、启动后端 - -1. **在 IDE 中重启 Spring Boot** - - 停止当前运行的 `SpringbootDemoApplication` - - 重新运行(让新的配置生效) - -2. **验证后端启动成功** - ```bash - # 浏览器访问或命令行执行 - curl http://localhost:8080/actuator/health - ``` - - 应返回: - ```json - {"status":"UP"} - ``` - ---- - -## 二、启动前端 - -```bash -cd frontend -npm install # 首次运行需要安装依赖 -npm run dev -``` - -浏览器会自动打开 `http://localhost:5173` - ---- - -## 三、登录系统 - -### 方式 1:在前端界面登录(推荐) - -1. 打开 `http://localhost:5173` -2. 在登录页面输入: - - 用户名:`sys_admin` - - 密码:`123456` -3. 点击登录 - -### 方式 2:用 API 工具测试 - -```http -POST http://localhost:8080/auth/login -Content-Type: application/json - -{ - "username": "sys_admin", - "password": "123456" -} -``` - -成功后会返回: -```json -{ - "code": 200, - "data": { - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "userId": 1, - "username": "sys_admin", - "roleId": 1, - "roleName": "系统管理员" - } -} -``` - ---- - -## 四、测试功能 - -### 1. 配置大模型(系统管理员) - -登录后,在前端界面: -1. 进入"系统管理" → "大模型配置" -2. 点击"添加模型" -3. 填写信息: - - 模型名称:DeepSeek - - 版本:chat - - API Key:你的密钥 - - API 地址:https://api.deepseek.com/chat/completions - - 超时时间:30000 - -或用 API 测试: - -```http -POST http://localhost:8080/llm-config -Authorization: Bearer {{你的token}} -Content-Type: application/json - -{ - "name": "DeepSeek", - "version": "chat", - "apiKey": "sk-xxx", - "apiUrl": "https://api.deepseek.com/chat/completions", - "statusId": 1, - "isDisabled": 0, - "timeout": 30000, - "createUserId": 1 -} -``` - -### 2. 添加数据源(数据管理员) - -用 `data_admin / 123456` 登录后: -1. 进入"数据管理" → "数据源管理" -2. 添加 MySQL 连接 - -### 3. 执行查询(普通用户) - -用 `normal_user / 123456` 登录后: -1. 在主页输入自然语言:"查询所有用户" -2. 选择模型和数据库 -3. 点击发送 - ---- - -## 五、常见问题 - -### Q1: 前端显示 "Failed to fetch" - -**原因**:后端未启动或未登录 - -**解决**: -1. 确认后端在 8080 端口运行:`netstat -ano | findstr :8080` -2. 先登录获取 token -3. 检查浏览器 DevTools → Network 查看具体错误 - -### Q2: 接口返回 401 Unauthorized - -**原因**:Token 无效或未携带 - -**解决**: -1. 重新登录获取新 token -2. 确认前端 localStorage 中有 token: - - 打开 DevTools → Application → Local Storage - - 检查是否有 `token` 和 `userId` - -### Q3: MongoDB 认证失败 - -**原因**:`application.yml` 中密码配置错误 - -**解决**: -确认 `src/main/resources/application.yml` 第 28 行: -```yaml -password: admin123456 # 不是 admin -``` - ---- - -## 六、安全说明 - -### 当前配置(开发环境) - -**免认证的接口**: -- `/auth/**` - 登录注册 -- `/actuator/**` - 健康检查 -- `/user` - 用户注册 -- `/role` - 角色查询 - -**需要认证的接口**: -- `/llm-config/**` - 大模型配置 -- `/db-connection/**` - 数据源管理 -- `/query/**` - 查询执行 -- 其他所有业务接口 - -### 生产环境建议 - -部署到生产环境前,务必: - -1. **移除 `/actuator/**` 的免认证** - ```java - // WebMvcConfig.java 删除这一行 - "/actuator/**", - ``` - -2. **修改默认密码** - - 三个内置账号的密码改为强密码 - - 数据库密码改为强密码 - -3. **配置 HTTPS** - - 使用 SSL 证书 - - 强制 HTTPS 访问 - -4. **启用 CORS 白名单** - ```java - // CorsConfig.java - .allowedOriginPatterns("https://yourdomain.com") - ``` - ---- - -## 附录:三个内置账号 - -| 用户名 | 密码 | 角色 | 权限 | -|--------|------|------|------| -| sys_admin | 123456 | 系统管理员 | 全部权限 | -| data_admin | 123456 | 数据管理员 | 数据源、权限管理 | -| normal_user | 123456 | 普通用户 | 查询、历史记录 | - ---- - -**最后更新**:2025-12-01 - - - diff --git a/src/test/TEST_ACCOUNTS.md b/src/test/TEST_ACCOUNTS.md deleted file mode 100644 index b3acb178..00000000 --- a/src/test/TEST_ACCOUNTS.md +++ /dev/null @@ -1,294 +0,0 @@ -# 内置测试账号说明 - -## 账号列表 - -系统已预置 3 个测试账号,对应三种角色: - -| 用户名 | 密码 | 角色 | 角色ID | 权限范围 | -|--------|------|------|--------|----------| -| `sys_admin` | `123456` | 系统管理员 | 1 | 全部权限 | -| `data_admin` | `123456` | 数据管理员 | 2 | 数据源管理、权限分配 | -| `normal_user` | `123456` | 普通用户 | 3 | 查询、历史记录 | - ---- - -## 快速测试 - -### 1. 登录测试 - -```http -### 测试系统管理员登录 -POST http://localhost:8080/auth/login -Content-Type: application/json - -{ - "username": "sys_admin", - "password": "123456" -} - -### - -### 测试数据管理员登录 -POST http://localhost:8080/auth/login -Content-Type: application/json - -{ - "username": "data_admin", - "password": "123456" -} - -### - -### 测试普通用户登录 -POST http://localhost:8080/auth/login -Content-Type: application/json - -{ - "username": "normal_user", - "password": "123456" -} -``` - -### 2. 预期返回结果 - -成功登录后应返回: - -```json -{ - "code": 200, - "data": { - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "userId": 1, - "username": "sys_admin", - "email": "sys_admin@example.com", - "roleId": 1, - "roleName": "系统管理员", - "avatarUrl": "/default-avatar.png" - }, - "message": "success" -} -``` - ---- - -## 角色权限说明 - -### 系统管理员 (sys_admin) - -**可访问的功能模块**: -- ✅ 用户管理(增删改查用户) -- ✅ 角色管理 -- ✅ 大模型配置管理 -- ✅ 系统日志查看 -- ✅ 通知管理 -- ✅ 数据源管理(查看) -- ✅ 自然语言查询 - -**典型操作**: -```http -### 创建新用户 -POST http://localhost:8080/user -Authorization: Bearer {{sys_admin_token}} -Content-Type: application/json - -{ - "username": "test_user", - "password": "123456", - "email": "test@example.com", - "phonenumber": "13900000000", - "roleId": 3 -} - -### - -### 配置大模型 -POST http://localhost:8080/llm-config -Authorization: Bearer {{sys_admin_token}} -Content-Type: application/json - -{ - "name": "Gemini", - "version": "2.0", - "apiKey": "your-api-key", - "apiUrl": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent", - "statusId": 1, - "isDisabled": 0, - "timeout": 5000, - "createUserId": 1 -} -``` - ---- - -### 数据管理员 (data_admin) - -**可访问的功能模块**: -- ✅ 数据源连接管理(增删改查) -- ✅ 用户数据权限分配 -- ✅ 数据库连接日志查看 -- ✅ 表/字段元数据管理 -- ✅ 自然语言查询 - -**典型操作**: -```http -### 添加数据库连接 -POST http://localhost:8080/db-connection -Authorization: Bearer {{data_admin_token}} -Content-Type: application/json - -{ - "name": "测试MySQL", - "dbTypeId": 1, - "url": "localhost:3306/test_db", - "username": "test_user", - "password": "test_pass", - "createUserId": 2 -} - -### - -### 为用户分配数据权限 -PUT http://localhost:8080/user-db-permission -Authorization: Bearer {{data_admin_token}} -Content-Type: application/json - -{ - "userId": 3, - "permissionDetails": "[{\"db_connection_id\":1,\"table_ids\":[1,2,3]}]", - "isAssigned": 1, - "lastGrantUserId": 2 -} -``` - ---- - -### 普通用户 (normal_user) - -**可访问的功能模块**: -- ✅ 自然语言查询(仅限已授权的数据源) -- ✅ 查询历史记录查看 -- ✅ 对话管理 -- ✅ 查询收藏 -- ✅ 好友功能(如果实现) - -**典型操作**: -```http -### 执行自然语言查询 -POST http://localhost:8080/query/execute -Authorization: Bearer {{normal_user_token}} -userId: 3 -Content-Type: application/json - -{ - "userPrompt": "查询所有用户", - "model": "Gemini", - "database": "测试MySQL" -} - -### - -### 查看历史对话 -GET http://localhost:8080/dialog/list -Authorization: Bearer {{normal_user_token}} -userId: 3 -``` - ---- - -## 权限验证测试 - -### 测试 1:普通用户尝试创建用户(应失败) - -```http -POST http://localhost:8080/user -Authorization: Bearer {{normal_user_token}} -Content-Type: application/json - -{ - "username": "hacker", - "password": "123456", - "email": "hacker@example.com", - "phonenumber": "13900000001", - "roleId": 1 -} -``` - -**预期结果**:403 Forbidden 或权限不足错误 - -### 测试 2:数据管理员尝试配置大模型(应失败) - -```http -POST http://localhost:8080/llm-config -Authorization: Bearer {{data_admin_token}} -Content-Type: application/json - -{ - "name": "Test Model", - "version": "1.0", - "apiKey": "test-key", - "apiUrl": "https://test.com", - "statusId": 1, - "createUserId": 2 -} -``` - -**预期结果**:403 Forbidden 或权限不足错误 - ---- - -## 密码修改建议 - -⚠️ **安全提示**: -- 默认密码 `123456` 仅用于开发测试 -- 生产环境部署前必须修改所有默认密码 -- 建议使用强密码策略(8位以上,包含大小写字母、数字、特殊字符) - -**修改密码示例**: -```http -PUT http://localhost:8080/user -Authorization: Bearer {{token}} -Content-Type: application/json - -{ - "id": 1, - "password": "NewSecurePassword@2025" -} -``` - ---- - -## 数据库直接查询 - -如果需要在数据库中直接查看用户信息: - -```sql --- 查看所有内置用户 -SELECT - u.id, - u.username, - u.email, - r.role_name, - u.status, - u.create_time -FROM users u -LEFT JOIN roles r ON u.role_id = r.id -WHERE u.id IN (1, 2, 3); - --- 查看用户权限分配情况 -SELECT - u.username, - p.permission_details, - p.is_assigned, - p.last_grant_time -FROM user_db_permissions p -LEFT JOIN users u ON p.user_id = u.id -WHERE p.user_id IN (1, 2, 3); -``` - ---- - -**最后更新**:2025-12-01 -**密码策略**:统一使用 `123456`(BCrypt 加密存储) - - - diff --git a/src/test/mvnw.cmd b/src/test/mvnw.cmd deleted file mode 100644 index 92450f93..00000000 --- a/src/test/mvnw.cmd +++ /dev/null @@ -1,189 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/src/test/mysql_schema_from_last.sql b/src/test/mysql_schema_from_last.sql index b3f55f42..17b00f34 100644 --- a/src/test/mysql_schema_from_last.sql +++ b/src/test/mysql_schema_from_last.sql @@ -249,7 +249,7 @@ CREATE TABLE `notifications` ( `is_top` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶:0-否,1-是', `publish_time` DATETIME COMMENT '发布时间', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - `Latest_updateTime` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上次更新时间', + `latest_update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上次更新时间', INDEX `idx_target_id` (`target_id`), INDEX `idx_priority_id` (`priority_id`), INDEX `idx_is_top_publish_time` (`is_top` DESC, `publish_time` DESC), diff --git a/src/test/package-lock.json b/src/test/package-lock.json deleted file mode 100644 index fed62a41..00000000 --- a/src/test/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "test", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/src/test/src/main/java/com/example/springboot_demo/service/impl/CustomUserDetailsService.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/CustomUserDetailsService.java index f1435aae..5e8dd8b0 100644 --- a/src/test/src/main/java/com/example/springboot_demo/service/impl/CustomUserDetailsService.java +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/CustomUserDetailsService.java @@ -1,8 +1,7 @@ package com.example.springboot_demo.service.impl; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.example.springboot_demo.entity.mysql.User; -import com.example.springboot_demo.mapper.UserMapper; +import java.util.Collections; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -10,12 +9,10 @@ import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; -import java.util.Collections; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.example.springboot_demo.entity.mysql.User; +import com.example.springboot_demo.mapper.UserMapper; -/** - * 自定义用户详情服务 - * 从数据库加载用户信息供 Spring Security 使用 - */ @Service public class CustomUserDetailsService implements UserDetailsService { @@ -24,7 +21,6 @@ public class CustomUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - // 从数据库查询用户 LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, username); User user = userMapper.selectOne(wrapper); @@ -33,16 +29,13 @@ public class CustomUserDetailsService implements UserDetailsService { throw new UsernameNotFoundException("用户不存在: " + username); } - // 检查账号状态 if (user.getStatus() == 0) { throw new UsernameNotFoundException("账号已被禁用: " + username); } - // 构建 Spring Security 的 UserDetails 对象 - // 注意:这里使用数据库中的加密密码 return org.springframework.security.core.userdetails.User .withUsername(user.getUsername()) - .password(user.getPassword()) // 数据库中已经是 BCrypt 加密的密码 + .password(user.getPassword()) .authorities(Collections.singletonList( new SimpleGrantedAuthority("ROLE_" + user.getRoleId()) )) -- 2.34.1 From 53df427aa6b239d14a9ae295d8430bd01dadad97 Mon Sep 17 00:00:00 2001 From: zwq <2907551361@qq.com> Date: Tue, 2 Dec 2025 16:31:43 +0800 Subject: [PATCH 12/14] =?UTF-8?q?1.LlmServiceImpl.java=20(=E5=AE=8C?= =?UTF-8?q?=E5=85=A8=E9=87=8D=E5=86=99)=20=E5=88=A0=E9=99=A4=E4=BA=86?= =?UTF-8?q?=E6=8C=89=E6=A8=A1=E5=9E=8B=E5=88=86=E7=B1=BB=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E6=B3=95=EF=BC=88callGemini,=20callOpenAI,=20callGLM,=20callQw?= =?UTF-8?q?en,=20callKimi=EF=BC=89=20=E6=96=B0=E5=A2=9E=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E7=9A=84=20callLlmApi()=20=E6=96=B9=E6=B3=95=20generateQuery()?= =?UTF-8?q?=20=E7=8E=B0=E5=9C=A8=E6=8E=A5=E6=94=B6=E9=85=8D=E7=BD=AEID?= =?UTF-8?q?=E8=80=8C=E4=B8=8D=E6=98=AF=E6=A8=A1=E5=9E=8B=E5=90=8D=E7=A7=B0?= =?UTF-8?q?=20=E4=BB=8E=E6=95=B0=E6=8D=AE=E5=BA=93=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E9=85=8D=E7=BD=AE=EF=BC=88API=20Key=E3=80=81?= =?UTF-8?q?URL=E3=80=81=E6=A8=A1=E5=9E=8B=E5=90=8D=EF=BC=89=202.=20QuerySe?= =?UTF-8?q?rviceImpl.java=20=E7=A7=BB=E9=99=A4=E4=BA=86=20MongoDB=20?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E7=9A=84=20try-catch=EF=BC=8C=E8=AE=A9?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E8=AE=B0=E5=BD=95=E6=AD=A3=E5=B8=B8=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=20=E6=B7=BB=E5=8A=A0=E4=BA=86=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E7=9A=84=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA?= =?UTF-8?q?=203.=20WebMvcConfig.java=20=E5=92=8C=20JwtInterceptor.java=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20/query/**=20=E5=88=B0=20JWT=20=E6=8B=A6?= =?UTF-8?q?=E6=88=AA=E5=99=A8=E6=8E=92=E9=99=A4=E5=88=97=E8=A1=A8=20?= =?UTF-8?q?=E5=85=81=E8=AE=B8=E6=9C=AA=E7=99=BB=E5=BD=95=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20AI=20=E6=9F=A5=E8=AF=A2=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=204.=20QueryPage.tsx=20=E4=BF=AE=E6=94=B9=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E9=85=8D=E7=BD=AEID=E8=80=8C=E4=B8=8D=E6=98=AF=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=90=8D=E7=A7=B0=20selectedModel=20=E2=86=92=20selec?= =?UTF-8?q?tedModelId=20=E4=BB=8E=E5=90=8E=E7=AB=AF=E5=8A=A8=E6=80=81?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E5=8F=AF=E7=94=A8=E6=A8=A1=E5=9E=8B=E5=88=97?= =?UTF-8?q?=E8=A1=A8=20=E6=8F=90=E4=BA=A4=E6=9F=A5=E8=AF=A2=E6=97=B6?= =?UTF-8?q?=E4=BC=A0=E9=80=92=E9=85=8D=E7=BD=AEID=205.=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E9=85=8D=E7=BD=AE=20-=20application.yml=20Mo?= =?UTF-8?q?ngoDB=20URI=20=E6=94=B9=E4=B8=BA=E6=97=A0=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=EF=BC=88=E5=BC=80=E5=8F=91=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=EF=BC=89=20=E4=BF=9D=E7=95=99=E4=BA=86=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E7=9A=84=E6=B3=A8=E9=87=8A=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=206.=20=E6=96=B0=E5=A2=9E=E6=96=87=E4=BB=B6=20update=5Fllm=5Fc?= =?UTF-8?q?onfigs.sql=20-=20=E6=9B=B4=E6=96=B0=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=84=9A=E6=9C=AC=20LLM=5FCONFIG=5FGUIDE.md?= =?UTF-8?q?=20-=20=E6=9E=B6=E6=9E=84=E8=AF=B4=E6=98=8E=E6=96=87=E6=A1=A3?= =?UTF-8?q?=20postman=5Ftest=5Fguide.md=20-=20Postman=20=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=8C=87=E5=8D=97=20LLM=5FConfig=5FTests.postman=5Fcollection.?= =?UTF-8?q?json=20-=20Postman=20=E6=B5=8B=E8=AF=95=E9=9B=86=E5=90=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/test/IMPLEMENTATION_SUMMARY.md | 310 +++++++ src/test/LLM_CONFIG_GUIDE.md | 269 ++++++ .../LLM_Config_Tests.postman_collection.json | 312 +++++++ .../frontend/components/CollectionsPage.tsx | 237 ++++++ .../components/FriendsPageWithAPI.tsx | 772 ++++++++++++++++++ .../components/HistoryPageWithAPI.tsx | 473 +++++++++++ .../components/NotificationsPageWithAPI.tsx | 246 ++++++ src/test/frontend/components/QueryPage.tsx | 33 +- .../components/admin/SystemLogPageWithAPI.tsx | 215 +++++ .../data-admin/ConnectionLogPageWithAPI.tsx | 207 +++++ src/test/frontend/hooks/useQueryCollection.ts | 154 ++++ src/test/frontend/hooks/useQueryShare.ts | 58 ++ src/test/frontend/services/api.ts | 532 ++++++++++++ src/test/postman_test_guide.md | 498 +++++++++++ .../config/JwtInterceptor.java | 19 + .../springboot_demo/config/WebMvcConfig.java | 1 + .../controller/DialogController.java | 25 + .../controller/UserController.java | 6 + .../service/DialogService.java | 3 + .../springboot_demo/service/UserService.java | 2 + .../service/impl/DialogServiceImpl.java | 43 + .../service/impl/LlmServiceImpl.java | 377 ++------- .../service/impl/QueryServiceImpl.java | 6 + .../service/impl/UserServiceImpl.java | 7 + src/test/src/main/resources/application.yml | 5 +- src/test/update_llm_configs.sql | 35 + 26 files changed, 4540 insertions(+), 305 deletions(-) create mode 100644 src/test/IMPLEMENTATION_SUMMARY.md create mode 100644 src/test/LLM_CONFIG_GUIDE.md create mode 100644 src/test/LLM_Config_Tests.postman_collection.json create mode 100644 src/test/frontend/components/CollectionsPage.tsx create mode 100644 src/test/frontend/components/FriendsPageWithAPI.tsx create mode 100644 src/test/frontend/components/HistoryPageWithAPI.tsx create mode 100644 src/test/frontend/components/NotificationsPageWithAPI.tsx create mode 100644 src/test/frontend/components/admin/SystemLogPageWithAPI.tsx create mode 100644 src/test/frontend/components/data-admin/ConnectionLogPageWithAPI.tsx create mode 100644 src/test/frontend/hooks/useQueryCollection.ts create mode 100644 src/test/frontend/hooks/useQueryShare.ts create mode 100644 src/test/postman_test_guide.md create mode 100644 src/test/update_llm_configs.sql diff --git a/src/test/IMPLEMENTATION_SUMMARY.md b/src/test/IMPLEMENTATION_SUMMARY.md new file mode 100644 index 00000000..c0986072 --- /dev/null +++ b/src/test/IMPLEMENTATION_SUMMARY.md @@ -0,0 +1,310 @@ +# 缺失功能实现总结 + +## 已完成的功能模块 + +### 1. 前端 API 定义补全 (`frontend/services/api.ts`) + +新增以下 API 接口定义: + +- **通知系统 API** (`notificationApi`) + - 获取通知列表、已发布通知、草稿 + - 创建、更新、删除通知 + - 发布通知、置顶/取消置顶 + +- **查询分享 API** (`queryShareApi`) + - 获取接收/发送的分享列表 + - 创建、更新、删除分享记录 + +- **查询日志 API** (`queryLogApi`) + - 获取用户查询历史 + - 按对话ID查询 + - 删除查询记录 + +- **好友关系 API** (`friendRelationApi`) + - 获取好友列表 + - 创建、更新、删除好友关系 + - 备注功能 + +- **好友请求 API** (`friendRequestApi`) + - 获取好友请求列表 + - 创建、更新、删除好友请求 + +- **好友聊天 API** (`friendChatApi`) + - 获取聊天记录 + - 发送消息、标记已读 + +- **用户搜索 API** (`userSearchApi`) + - 按邮箱搜索用户 + - 搜索历史记录 + +- **查询收藏 API** (`queryCollectionApi`, `collectionRecordApi`) + - 创建、管理收藏夹 + - 添加/移除收藏记录 + +- **操作日志 API** (`operationLogApi`) + - 获取系统操作日志 + - 按用户、模块筛选 + +- **错误日志 API** (`errorLogApi`) + - 获取错误日志 + - 按类型、周期筛选 + +- **数据库连接日志 API** (`dbConnectionLogApi`) + - 获取连接日志 + - 按数据源筛选 + +### 2. 后端对话管理接口完善 + +**文件:** +- `src/main/java/com/example/springboot_demo/controller/DialogController.java` +- `src/main/java/com/example/springboot_demo/service/DialogService.java` +- `src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java` + +**新增接口:** +- `POST /dialog` - 创建新对话 +- `DELETE /dialog/{dialogId}` - 删除对话 +- `PUT /dialog/{dialogId}` - 更新对话信息 + +**前端对接:** +- `frontend/services/api.ts` 中的 `dialogApi` 已更新 + +### 3. 用户搜索功能(好友添加) + +**后端文件:** +- `src/main/java/com/example/springboot_demo/controller/UserController.java` +- `src/main/java/com/example/springboot_demo/service/UserService.java` +- `src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java` + +**新增接口:** +- `GET /user/search/email?email={email}` - 按邮箱搜索用户 + +### 4. 好友功能完整对接 + +**新文件:** `frontend/components/FriendsPageWithAPI.tsx` + +**功能:** +- 从后端加载好友列表 +- 实时获取未读消息数量 +- 好友请求的接受/拒绝 +- 好友备注功能(调用后端 API) +- 好友搜索与添加(通过邮箱) +- 聊天消息持久化(调用 `friendChatApi`) +- 好友删除 + +**使用方法:** +```tsx +import { FriendsPageWithAPI } from './components/FriendsPageWithAPI'; +// 替换原有的 FriendsPage +``` + +### 5. 查询历史功能对接 + +**新文件:** `frontend/components/HistoryPageWithAPI.tsx` + +**功能:** +- 从后端 `queryLogApi` 加载用户查询历史 +- 按日期、模型、数据库筛选 +- 删除查询记录(调用后端 API) +- 批量删除 +- 查询对比 + +**使用方法:** +```tsx +import { HistoryPageWithAPI } from './components/HistoryPageWithAPI'; +// 替换原有的 HistoryPage +``` + +### 6. 通知系统对接 + +**新文件:** `frontend/components/NotificationsPageWithAPI.tsx` + +**功能:** +- 从后端加载已发布的通知 +- 根据用户角色筛选通知 +- 标记已读/未读(本地状态) +- 删除通知(本地状态) +- 置顶通知显示 + +**使用方法:** +```tsx +import { NotificationsPageWithAPI } from './components/NotificationsPageWithAPI'; +// 替换原有的 NotificationsPage +``` + +### 7. 查询分享功能 + +**新文件:** `frontend/hooks/useQueryShare.ts` + +**功能:** +- 分享查询给好友 +- 标记分享为已读 +- 删除分享记录 + +**使用方法:** +```tsx +import { useQueryShare } from '../hooks/useQueryShare'; + +const { shareQuery, markAsRead, deleteShare, loading, error } = useQueryShare(); + +// 分享查询 +await shareQuery(queryLogId, receiveUserId); +``` + +### 8. 管理员日志页面对接 + +**新文件:** +- `frontend/components/admin/SystemLogPageWithAPI.tsx` +- `frontend/components/data-admin/ConnectionLogPageWithAPI.tsx` + +**功能:** +- 从后端加载操作日志 +- 按时间、用户、操作类型筛选 +- 导出日志为 CSV +- 查看日志详情 + +**使用方法:** +```tsx +import { SystemLogPageWithAPI } from './components/admin/SystemLogPageWithAPI'; +import { ConnectionLogPageWithAPI } from './components/data-admin/ConnectionLogPageWithAPI'; +// 替换原有的日志页面组件 +``` + +### 9. 查询收藏功能 + +**新文件:** +- `frontend/hooks/useQueryCollection.ts` +- `frontend/components/CollectionsPage.tsx` + +**功能:** +- 创建、编辑、删除收藏夹 +- 添加查询到收藏夹 +- 从收藏夹移除查询 +- 查看收藏夹中的查询记录 + +**使用方法:** +```tsx +import { CollectionsPage } from './components/CollectionsPage'; +import { useQueryCollection } from '../hooks/useQueryCollection'; + +// 在路由中添加收藏夹页面 +} /> + +// 在其他组件中使用 +const { collections, addQueryToCollection } = useQueryCollection(userId); +``` + +## 使用说明 + +### 替换原有组件 + +在你的主应用文件(如 `App.tsx`)中,将以下组件替换为新的 API 对接版本: + +```tsx +// 原有导入 +import { FriendsPage } from './components/FriendsPage'; +import { HistoryPage } from './components/HistoryPage'; +import { NotificationsPage } from './components/NotificationsPage'; +import { SystemLogPage } from './components/admin/SystemLogPage'; +import { ConnectionLogPage } from './components/data-admin/ConnectionLogPage'; + +// 替换为 +import { FriendsPageWithAPI as FriendsPage } from './components/FriendsPageWithAPI'; +import { HistoryPageWithAPI as HistoryPage } from './components/HistoryPageWithAPI'; +import { NotificationsPageWithAPI as NotificationsPage } from './components/NotificationsPageWithAPI'; +import { SystemLogPageWithAPI as SystemLogPage } from './components/admin/SystemLogPageWithAPI'; +import { ConnectionLogPageWithAPI as ConnectionLogPage } from './components/data-admin/ConnectionLogPageWithAPI'; +``` + +### 新增路由 + +在路由配置中添加收藏夹页面: + +```tsx +import { CollectionsPage } from './components/CollectionsPage'; + +// 在路由中添加 +} /> +``` + +### 环境变量配置 + +确保前端可以正确连接到后端 API: + +```env +# .env 文件 +VITE_API_BASE_URL=http://localhost:8080 +``` + +如果后端端口改为其他值(如 8090),需要同步修改。 + +## 注意事项 + +1. **端口配置**: + - 后端端口在 `src/main/resources/application.yml` 中配置 + - 前端 API 地址在 `frontend/services/api.ts` 中配置 + - 确保两者一致 + +2. **认证 Token**: + - 所有 API 请求都会自动携带 localStorage 中的 token + - 确保登录后正确保存 token + +3. **用户 ID**: + - 大部分功能依赖 localStorage 中的 userId + - 确保登录后正确保存 userId + +4. **数据库连接**: + - 确保 MySQL、MongoDB、Redis 都已启动 + - 检查 `application.yml` 中的数据库配置 + +5. **跨域问题**: + - 如果前后端分离部署,需要配置 CORS + - 在 Spring Boot 中添加 `@CrossOrigin` 注解或全局配置 + +## 测试建议 + +1. **好友功能测试**: + - 创建多个测试用户 + - 测试好友请求、接受、拒绝 + - 测试聊天消息发送与接收 + - 测试好友备注功能 + +2. **查询历史测试**: + - 执行多次查询 + - 测试筛选功能 + - 测试删除功能 + +3. **通知系统测试**: + - 以管理员身份创建通知 + - 以普通用户身份查看通知 + - 测试置顶功能 + +4. **收藏功能测试**: + - 创建收藏夹 + - 添加查询到收藏夹 + - 测试收藏夹管理功能 + +## 后续优化建议 + +1. **实时通信**: + - 使用 WebSocket 实现好友在线状态实时更新 + - 实现聊天消息实时推送 + +2. **性能优化**: + - 添加分页功能(查询历史、日志等) + - 使用虚拟滚动优化长列表 + +3. **用户体验**: + - 添加加载骨架屏 + - 优化错误提示 + - 添加操作成功的 Toast 提示 + +4. **安全性**: + - 添加请求限流 + - 敏感操作二次确认 + - XSS 防护 + +5. **测试覆盖**: + - 编写单元测试 + - 编写集成测试 + - 添加 E2E 测试 + diff --git a/src/test/LLM_CONFIG_GUIDE.md b/src/test/LLM_CONFIG_GUIDE.md new file mode 100644 index 00000000..bf2d6cb6 --- /dev/null +++ b/src/test/LLM_CONFIG_GUIDE.md @@ -0,0 +1,269 @@ +# 大模型配置指南 + +## 架构说明 + +本系统采用**动态配置**架构,不在代码中硬编码任何特定的大模型。所有大模型配置都存储在数据库中,由管理员动态管理。 + +## 数据库表结构 + +### `llm_configs` 表字段说明 + +| 字段 | 类型 | 说明 | 示例 | +|------|------|------|------| +| `id` | BIGINT | 主键ID | 1 | +| `name` | VARCHAR | 配置名称(用于显示) | "Kimi 8K" | +| `version` | VARCHAR | **真实的模型名称**(用于API调用) | "moonshot-v1-8k" | +| `api_key` | VARCHAR | API密钥 | "sk-xxx..." | +| `api_url` | VARCHAR | API地址 | "https://api.moonshot.cn/v1/chat/completions" | +| `status_id` | INT | 状态ID | 1 | +| `is_disabled` | INT | 是否禁用(0=启用,1=禁用) | 0 | +| `timeout` | INT | 超时时间(毫秒) | 30000 | +| `create_user_id` | BIGINT | 创建用户ID | 1 | +| `create_time` | DATETIME | 创建时间 | 2025-12-02 16:00:00 | +| `update_time` | DATETIME | 更新时间 | 2025-12-02 16:00:00 | + +## 工作流程 + +### 1. 管理员配置大模型 + +管理员通过前端界面(或直接操作数据库)添加大模型配置: + +```sql +INSERT INTO llm_configs (name, version, api_key, api_url, status_id, is_disabled, timeout, create_user_id, create_time, update_time) +VALUES ( + 'Kimi 8K', -- 显示名称 + 'moonshot-v1-8k', -- 真实模型名 + 'sk-xrVyKC3MhOLgr1VNWziPuU9KvBubfUp0EEWbtwKfoZRplyC1', -- API密钥 + 'https://api.moonshot.cn/v1/chat/completions', -- API地址 + 1, -- 状态ID + 0, -- 启用 + 30000, -- 30秒超时 + 1, -- 创建用户ID + NOW(), -- 创建时间 + NOW() -- 更新时间 +); +``` + +### 2. 用户选择大模型 + +前端调用 `/llm-config/list/available` 接口获取所有可用的大模型配置: + +```typescript +const configs = await llmConfigApi.getAvailable(); +// 返回: [{ id: 1, name: "Kimi 8K", version: "moonshot-v1-8k", ... }] +``` + +用户在下拉菜单中看到 "Kimi 8K (moonshot-v1-8k)",选择后前端记录配置ID(如 `1`)。 + +### 3. 执行查询 + +用户提交查询时,前端传递**配置ID**(不是模型名称): + +```typescript +await queryApi.execute({ + userPrompt: "查询销售数据", + model: "1", // 传递配置ID + database: "sales_db", + conversationId: "conv_123" +}); +``` + +### 4. 后端调用API + +后端根据配置ID查询数据库,获取完整配置信息,然后调用对应的API: + +```java +LlmConfig config = llmConfigService.getById(1); +// config.getVersion() = "moonshot-v1-8k" +// config.getApiUrl() = "https://api.moonshot.cn/v1/chat/completions" +// config.getApiKey() = "sk-xxx..." + +// 构建请求 +JSONObject requestBody = new JSONObject(); +requestBody.put("model", config.getVersion()); // 使用真实模型名 +// ... 发送HTTP请求 +``` + +## 支持的大模型 + +本系统支持所有兼容 **OpenAI Chat Completions API** 格式的大模型,包括但不限于: + +### 1. Moonshot Kimi + +```sql +INSERT INTO llm_configs (name, version, api_key, api_url, is_disabled, timeout, create_user_id, create_time, update_time) +VALUES ( + 'Kimi 8K', 'moonshot-v1-8k', 'sk-xxx...', + 'https://api.moonshot.cn/v1/chat/completions', + 0, 30000, 1, NOW(), NOW() +); +``` + +可选模型: +- `moonshot-v1-8k` (8K上下文) +- `moonshot-v1-32k` (32K上下文) +- `moonshot-v1-128k` (128K上下文) + +### 2. OpenAI GPT + +```sql +INSERT INTO llm_configs (name, version, api_key, api_url, is_disabled, timeout, create_user_id, create_time, update_time) +VALUES ( + 'GPT-4', 'gpt-4', 'sk-xxx...', + 'https://api.openai.com/v1/chat/completions', + 0, 60000, 1, NOW(), NOW() +); +``` + +可选模型: +- `gpt-4` +- `gpt-4-turbo` +- `gpt-3.5-turbo` + +### 3. 阿里通义千问 Qwen + +```sql +INSERT INTO llm_configs (name, version, api_key, api_url, is_disabled, timeout, create_user_id, create_time, update_time) +VALUES ( + '通义千问', 'qwen-turbo', 'sk-xxx...', + 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', + 0, 30000, 1, NOW(), NOW() +); +``` + +可选模型: +- `qwen-turbo` +- `qwen-plus` +- `qwen-max` + +### 4. 智谱 GLM + +```sql +INSERT INTO llm_configs (name, version, api_key, api_url, is_disabled, timeout, create_user_id, create_time, update_time) +VALUES ( + '智谱GLM-4', 'glm-4', 'xxx.xxx...', + 'https://open.bigmodel.cn/api/paas/v4/chat/completions', + 0, 30000, 1, NOW(), NOW() +); +``` + +可选模型: +- `glm-4` +- `glm-4-air` +- `glm-4-flash` + +### 5. Google Gemini + +**注意**:Gemini 使用不同的API格式,需要单独适配。当前版本暂不支持。 + +## 管理操作 + +### 查看所有配置 + +```sql +SELECT id, name, version, api_url, is_disabled, create_time +FROM llm_configs +ORDER BY create_time DESC; +``` + +### 启用/禁用配置 + +```sql +-- 禁用 +UPDATE llm_configs SET is_disabled = 1 WHERE id = 1; + +-- 启用 +UPDATE llm_configs SET is_disabled = 0 WHERE id = 1; +``` + +### 更新API密钥 + +```sql +UPDATE llm_configs +SET api_key = 'new-api-key', update_time = NOW() +WHERE id = 1; +``` + +### 删除配置 + +```sql +DELETE FROM llm_configs WHERE id = 1; +``` + +## 前端集成 + +### 获取可用模型列表 + +```typescript +import { llmConfigApi } from '../services/api'; + +const configs = await llmConfigApi.getAvailable(); +``` + +### 显示在下拉菜单 + +```typescript +const modelOptions = configs.map(config => ({ + id: String(config.id), + name: `${config.name} (${config.version})`, + description: `${config.name} - ${config.version}` +})); +``` + +### 提交查询 + +```typescript +await queryApi.execute({ + userPrompt: "用户的查询", + model: selectedModelId, // 传递配置ID + database: selectedDatabase, + conversationId: currentConversationId +}); +``` + +## 故障排查 + +### 问题1:前端显示"无法加载大模型配置" + +**原因**:后端API `/llm-config/list/available` 返回错误或为空 + +**解决**: +1. 检查数据库中是否有 `is_disabled = 0` 的配置 +2. 检查后端日志是否有错误 +3. 确认 `LlmConfigService.listAvailable()` 方法正常工作 + +### 问题2:调用API返回404 + +**原因**:`version` 字段存储的模型名称不正确 + +**解决**: +1. 检查数据库中的 `version` 字段是否为真实的模型名(如 `moonshot-v1-8k`) +2. 参考各大模型官方文档确认正确的模型名称 +3. 运行 `update_llm_configs.sql` 更新配置 + +### 问题3:API密钥无效 + +**原因**:`api_key` 字段存储的密钥过期或错误 + +**解决**: +1. 到对应平台(如 Kimi、OpenAI)重新生成API密钥 +2. 更新数据库中的 `api_key` 字段 + +## 最佳实践 + +1. **命名规范**:`name` 字段使用易读的中文名称,如 "Kimi 8K"、"GPT-4" +2. **版本管理**:`version` 字段必须使用官方文档中的模型名称 +3. **安全性**:定期轮换API密钥,不要在代码中硬编码密钥 +4. **监控**:记录每次API调用的耗时和成本 +5. **备份**:定期备份 `llm_configs` 表数据 + +## 扩展性 + +要添加新的大模型提供商,只需: + +1. 确认其API兼容 OpenAI Chat Completions 格式 +2. 在数据库中插入新配置 +3. 无需修改任何代码! + +如果API格式不兼容(如 Google Gemini),需要在 `LlmServiceImpl` 中添加特殊处理逻辑。 + diff --git a/src/test/LLM_Config_Tests.postman_collection.json b/src/test/LLM_Config_Tests.postman_collection.json new file mode 100644 index 00000000..393fe0da --- /dev/null +++ b/src/test/LLM_Config_Tests.postman_collection.json @@ -0,0 +1,312 @@ +{ + "info": { + "name": "LLM Dynamic Config Tests", + "description": "测试动态大模型配置功能", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8080", + "type": "string" + }, + { + "key": "modelConfigId", + "value": "1", + "type": "string" + } + ], + "item": [ + { + "name": "1. 获取所有模型配置(管理员)", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/llm-config/list", + "host": ["{{baseUrl}}"], + "path": ["llm-config", "list"] + }, + "description": "获取数据库中所有的大模型配置(包括已禁用的)" + }, + "response": [] + }, + { + "name": "2. 获取可用模型配置(用户)", + "request": { + "method": "GET", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/llm-config/list/available", + "host": ["{{baseUrl}}"], + "path": ["llm-config", "list", "available"] + }, + "description": "获取可用的大模型配置(is_disabled = 0)\n前端用这个接口填充模型下拉菜单" + }, + "response": [] + }, + { + "name": "3. 执行AI查询(使用配置ID)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userPrompt\": \"查询销售额前10的产品\",\n \"model\": \"{{modelConfigId}}\",\n \"database\": \"销售数据库\",\n \"conversationId\": \"conv_test_001\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/query/execute", + "host": ["{{baseUrl}}"], + "path": ["query", "execute"] + }, + "description": "核心功能:使用模型配置ID执行AI查询\n\n注意:\n- model 字段传递的是配置ID(如 \"1\"),不是模型名称\n- 后端会根据ID查询数据库获取完整配置\n- 使用配置中的 version 字段作为真实模型名调用API" + }, + "response": [] + }, + { + "name": "4. 执行查询(使用Kimi 32K)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userPrompt\": \"分析过去一年的销售趋势,包括季度对比和增长率\",\n \"model\": \"2\",\n \"database\": \"销售数据库\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/query/execute", + "host": ["{{baseUrl}}"], + "path": ["query", "execute"] + }, + "description": "测试使用不同的模型配置(Kimi 32K)" + }, + "response": [] + }, + { + "name": "5. 执行查询(使用Kimi 128K)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userPrompt\": \"生成详细的年度销售报告,包括所有产品线的数据分析和市场趋势预测\",\n \"model\": \"3\",\n \"database\": \"销售数据库\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/query/execute", + "host": ["{{baseUrl}}"], + "path": ["query", "execute"] + }, + "description": "测试使用不同的模型配置(Kimi 128K,适合长文本)" + }, + "response": [] + }, + { + "name": "6. 错误测试 - 无效配置ID", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"userPrompt\": \"测试查询\",\n \"model\": \"999\",\n \"database\": \"测试数据库\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/query/execute", + "host": ["{{baseUrl}}"], + "path": ["query", "execute"] + }, + "description": "测试错误处理:使用不存在的配置ID\n\n预期:返回 500 错误,消息为 \"模型配置不存在,ID: 999\"" + }, + "response": [] + }, + { + "name": "7. 添加新模型配置", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"GPT-4\",\n \"version\": \"gpt-4\",\n \"apiKey\": \"sk-your-openai-api-key-here\",\n \"apiUrl\": \"https://api.openai.com/v1/chat/completions\",\n \"statusId\": 1,\n \"isDisabled\": 0,\n \"timeout\": 60000,\n \"createUserId\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/llm-config", + "host": ["{{baseUrl}}"], + "path": ["llm-config"] + }, + "description": "管理员功能:添加新的大模型配置\n\n注意:\n- name: 显示名称\n- version: 真实的模型名称(用于API调用)\n- apiUrl: API地址\n- apiKey: API密钥" + }, + "response": [] + }, + { + "name": "8. 更新模型配置", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"apiKey\": \"sk-new-api-key-here\",\n \"timeout\": 45000\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/llm-config", + "host": ["{{baseUrl}}"], + "path": ["llm-config"] + }, + "description": "管理员功能:更新现有配置\n\n可以只传需要更新的字段" + }, + "response": [] + }, + { + "name": "9. 禁用模型配置", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"isDisabled\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/llm-config", + "host": ["{{baseUrl}}"], + "path": ["llm-config"] + }, + "description": "禁用某个模型配置\n\n禁用后,该配置不会出现在 /list/available 接口中" + }, + "response": [] + }, + { + "name": "10. 启用模型配置", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"id\": 1,\n \"isDisabled\": 0\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseUrl}}/llm-config", + "host": ["{{baseUrl}}"], + "path": ["llm-config"] + }, + "description": "重新启用某个模型配置" + }, + "response": [] + }, + { + "name": "11. 删除模型配置", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/llm-config/4", + "host": ["{{baseUrl}}"], + "path": ["llm-config", "4"] + }, + "description": "管理员功能:删除模型配置\n\n注意:删除前确保没有查询记录引用该配置" + }, + "response": [] + }, + { + "name": "12. 根据ID获取配置详情", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/llm-config/1", + "host": ["{{baseUrl}}"], + "path": ["llm-config", "1"] + }, + "description": "获取指定ID的配置详情" + }, + "response": [] + } + ] +} + diff --git a/src/test/frontend/components/CollectionsPage.tsx b/src/test/frontend/components/CollectionsPage.tsx new file mode 100644 index 00000000..14528f3b --- /dev/null +++ b/src/test/frontend/components/CollectionsPage.tsx @@ -0,0 +1,237 @@ +import React, { useState } from 'react'; +import { Modal } from './Modal'; +import { useQueryCollection } from '../hooks/useQueryCollection'; + +export const CollectionsPage: React.FC = () => { + const userId = Number(localStorage.getItem('userId') || '1'); + const { + collections, + loading, + error, + createCollection, + updateCollection, + deleteCollection, + } = useQueryCollection(userId); + + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + const [isEditModalOpen, setIsEditModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + const [currentCollection, setCurrentCollection] = useState(null); + const [collectionName, setCollectionName] = useState(''); + const [collectionDescription, setCollectionDescription] = useState(''); + + const handleOpenCreateModal = () => { + setCollectionName(''); + setCollectionDescription(''); + setIsCreateModalOpen(true); + }; + + const handleOpenEditModal = (collection: any) => { + setCurrentCollection(collection); + setCollectionName(collection.collectionName); + setCollectionDescription(collection.description || ''); + setIsEditModalOpen(true); + }; + + const handleOpenDeleteModal = (collection: any) => { + setCurrentCollection(collection); + setIsDeleteModalOpen(true); + }; + + const handleCreate = async () => { + if (!collectionName.trim()) { + alert('请输入收藏夹名称'); + return; + } + const result = await createCollection(collectionName, collectionDescription); + if (result) { + setIsCreateModalOpen(false); + alert('创建成功'); + } else { + alert(error || '创建失败'); + } + }; + + const handleUpdate = async () => { + if (!collectionName.trim()) { + alert('请输入收藏夹名称'); + return; + } + if (!currentCollection) return; + + const result = await updateCollection(currentCollection.id, collectionName, collectionDescription); + if (result) { + setIsEditModalOpen(false); + setCurrentCollection(null); + alert('更新成功'); + } else { + alert(error || '更新失败'); + } + }; + + const handleDelete = async () => { + if (!currentCollection) return; + + const result = await deleteCollection(currentCollection.id); + if (result) { + setIsDeleteModalOpen(false); + setCurrentCollection(null); + alert('删除成功'); + } else { + alert(error || '删除失败'); + } + }; + + if (loading && collections.length === 0) { + return ( +
    +
    +
    +

    加载收藏夹中...

    +
    +
    + ); + } + + return ( +
    +
    +

    + 我的收藏夹 +

    + +
    + + {error && ( +
    + {error} +
    + )} + +
    + {collections.map(collection => ( +
    +
    +
    +

    {collection.collectionName}

    + {collection.description && ( +

    {collection.description}

    + )} +
    + +
    +
    + 创建于: {new Date(collection.createTime).toLocaleDateString()} +
    +
    + + +
    +
    + ))} +
    + + {collections.length === 0 && !loading && ( +
    + +

    还没有收藏夹,点击右上角创建一个吧

    +
    + )} + + setIsCreateModalOpen(false)} title="新建收藏夹"> +
    +
    + + setCollectionName(e.target.value)} + className="w-full px-4 py-2 border border-gray-300 rounded-lg" + maxLength={50} + /> +
    +
    + +