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/api-test.http b/src/test/api-test.http new file mode 100644 index 00000000..f6dd22ba --- /dev/null +++ b/src/test/api-test.http @@ -0,0 +1,944 @@ +### 测试 Hello +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": "你好,最近怎么样?" + } +} + +### ==================== 认证接口 ==================== + +### 登录 +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/user/search/phone?phoneNumber=13800138000 + +### 修改密码 +POST http://localhost:8080/user/change-password +Content-Type: application/json + +{ + "userId": 1, + "oldPassword": "123456", + "newPassword": "newpassword123" +} + +### ==================== 好友管理接口 ==================== + +### 查询用户的好友列表 +GET http://localhost:8080/friend-relation/list/1 + +### 查询特定好友关系 +GET http://localhost:8080/friend-relation/1/2 + +### 添加好友关系(通常由系统自动创建) +POST http://localhost:8080/friend-relation +Content-Type: application/json + +{ + "userId": 1, + "friendId": 2, + "friendUsername": "user2", + "onlineStatus": 1, + "remarkName": "小明" +} + +### 更新好友备注 +PUT http://localhost:8080/friend-relation +Content-Type: application/json + +{ + "id": 1, + "remarkName": "我的好朋友小明" +} + +### 删除好友关系(通过关系ID) +DELETE http://localhost:8080/friend-relation/1 + +### 删除好友关系(通过用户ID和好友ID) +DELETE http://localhost:8080/friend-relation/1/2 + +### ==================== 好友请求接口 ==================== + +### 查询用户收到的所有好友请求 +GET http://localhost:8080/friend-request/list/1 + +### 查询用户收到的待处理好友请求(status=0) +GET http://localhost:8080/friend-request/list/1/0 + +### 查询特定好友请求 +GET http://localhost:8080/friend-request/1 + +### 发送好友请求 +POST http://localhost:8080/friend-request +Content-Type: application/json + +{ + "applicantId": 1, + "recipientId": 2, + "applyMsg": "你好,我想加你为好友", + "status": 0 +} + +### 接受好友请求(自动创建双向好友关系) +POST http://localhost:8080/friend-request/1/accept + +### 拒绝好友请求 +POST http://localhost:8080/friend-request/1/reject + +### 删除好友请求记录 +DELETE http://localhost:8080/friend-request/1 + +### ==================== 数据库连接管理接口 ==================== + +### 查询所有数据库连接 +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..1954025d --- /dev/null +++ b/src/test/frontend/App.tsx @@ -0,0 +1,777 @@ +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 { FriendsPageWithRealAPI } from './components/FriendsPageWithRealAPI'; +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_NOTIFICATIONS, + MOCK_QUERY_SHARES, + MOCK_FRIENDS_LIST +} from './constants'; + +// API 导入 +import { userApi } from './services/api'; + +// 类型定义导入 +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 [currentUser, setCurrentUser] = useState({ name: '', avatarUrl: '' }); + // 所有对话列表 + 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 = async (role: UserRole) => { + const savedRole = sessionStorage.getItem('userRole') as UserRole; + const roleToSet = role || savedRole; + if (roleToSet) { + setUserRole(roleToSet); + // 加载用户信息 + await loadUserInfo(); + } + }; + + /** + * 加载当前用户信息 + */ + const loadUserInfo = async () => { + try { + const userId = Number(sessionStorage.getItem('userId') || '1'); + const userData = await userApi.getById(userId); + setCurrentUser({ + name: userData.username, + avatarUrl: userData.avatarUrl || '/default-avatar.png', + }); + } catch (error) { + console.error('加载用户信息失败:', error); + // 使用默认值 + setCurrentUser({ name: '用户', avatarUrl: '/default-avatar.png' }); + } + }; + + /** + * 组件挂载时初始化登录状态 + * 从sessionStorage读取保存的角色,自动登录 + */ + useEffect(() => { + handleLogin(userRole); + + // 监听头像更新事件 + const handleAvatarUpdate = (event: CustomEvent) => { + setCurrentUser(prev => ({ + ...prev, + avatarUrl: event.detail.avatarUrl + })); + }; + + window.addEventListener('userAvatarUpdated', handleAvatarUpdate as EventListener); + + return () => { + window.removeEventListener('userAvatarUpdated', handleAvatarUpdate as EventListener); + }; + }, []); + + /** + * 退出登录处理函数 + * 清除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: { + id: sessionStorage.getItem('userId') || '1', + name: currentUser.name, + email: '', + avatarUrl: currentUser.avatarUrl, + isOnline: true, + remark: '' + }, // 发送者为当前用户 + 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: `${currentUser.name} 分享了一个查询给你`, + content: `"${queryToShare.userPrompt}"`, + timestamp: new Date().toISOString(), + isRead: false, + isPinned: false, + fromUser: { name: currentUser.name, avatarUrl: currentUser.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..5b03da24 --- /dev/null +++ b/src/test/frontend/components/AccountPage.tsx @@ -0,0 +1,351 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { UserProfile } from '../types'; +import { Modal } from './Modal'; +import { userApi, ChangePasswordRequest } from '../services/api'; + +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(null); + const [loading, setLoading] = useState(true); + const [isEditModalOpen, setEditModalOpen] = useState(false); + const [isPasswordModalOpen, setPasswordModalOpen] = useState(false); + const [isPhoneModalOpen, setPhoneModalOpen] = useState(false); + const [editForm, setEditForm] = useState({ name: '', email: '', phoneNumber: '' }); + const [passwordForm, setPasswordForm] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' }); + const [avatarPreview, setAvatarPreview] = useState(''); + const fileInputRef = useRef(null); + + useEffect(() => { + loadUserProfile(); + + // sessionStorage是每个标签页独立的,不需要监听storage事件 + }, []); + + const loadUserProfile = async () => { + try { + setLoading(true); + // 每次加载时都从localStorage获取最新的userId + const userId = Number(sessionStorage.getItem('userId') || '1'); + const userData = await userApi.getById(userId); + const profile: UserProfile = { + id: String(userData.id), + userId: String(userData.id), + name: userData.username, + email: userData.email, + phoneNumber: userData.phonenumber, + avatarUrl: userData.avatarUrl || '/default-avatar.png', + registrationDate: (userData as any).createTime?.split('T')[0] || '', + accountStatus: userData.status === 1 ? 'normal' : 'disabled', + preferences: { + defaultModel: '', + defaultDatabase: '', + }, + }; + setUser(profile); + setAvatarPreview(profile.avatarUrl); + } catch (error) { + console.error('加载用户信息失败:', error); + } finally { + setLoading(false); + } + }; + + const handleOpenEditModal = () => { + if (user) { + setEditForm({ + name: user.name, + email: user.email, + phoneNumber: user.phoneNumber, + }); + setEditModalOpen(true); + } + }; + + const handleSaveProfile = async () => { + try { + const userId = Number(sessionStorage.getItem('userId') || '1'); + await userApi.update({ + id: userId, + username: editForm.name, + email: editForm.email, + phonenumber: editForm.phoneNumber, + avatarUrl: avatarPreview, + }); + await loadUserProfile(); + setEditModalOpen(false); + alert('个人信息更新成功!'); + } catch (error) { + console.error('更新个人信息失败:', error); + alert('更新失败,请重试'); + } + }; + + const handleAvatarChange = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onloadend = async () => { + const base64Avatar = reader.result as string; + setAvatarPreview(base64Avatar); + + // 立即保存到后端 + try { + const userId = Number(sessionStorage.getItem('userId') || '1'); + await userApi.update({ + id: userId, + avatarUrl: base64Avatar, + }); + await loadUserProfile(); + } catch (error) { + console.error('更新头像失败:', error); + alert('更新头像失败'); + } + }; + reader.readAsDataURL(file); + } + }; + + const handleChangePassword = async () => { + if (passwordForm.newPassword !== passwordForm.confirmPassword) { + alert('两次输入的新密码不一致!'); + return; + } + if (passwordForm.newPassword.length < 6) { + alert('新密码长度不能少于6位!'); + return; + } + if (!passwordForm.oldPassword) { + alert('请输入当前密码!'); + return; + } + try { + const userId = Number(sessionStorage.getItem('userId') || '1'); + const changePasswordRequest: ChangePasswordRequest = { + userId: userId, + oldPassword: passwordForm.oldPassword, + newPassword: passwordForm.newPassword, + }; + await userApi.changePassword(changePasswordRequest); + alert('密码修改成功!'); + setPasswordModalOpen(false); + setPasswordForm({ oldPassword: '', newPassword: '', confirmPassword: '' }); + } catch (error) { + console.error('修改密码失败:', error); + alert(error instanceof Error ? error.message : '修改失败,请检查当前密码是否正确'); + } + }; + + if (loading) { + return ( +
+
加载中...
+
+ ); + } + + if (!user) { + return ( +
+
加载用户信息失败
+
+ ); + } + + 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="修改密码"> +
+
+ + setPasswordForm(prev => ({...prev, oldPassword: e.target.value}))} + className="w-full px-4 py-2 border border-gray-300 rounded-lg" + /> +
+
+ + setPasswordForm(prev => ({...prev, newPassword: e.target.value}))} + className="w-full px-4 py-2 border border-gray-300 rounded-lg" + /> +
+
+ + setPasswordForm(prev => ({...prev, confirmPassword: e.target.value}))} + className="w-full px-4 py-2 border border-gray-300 rounded-lg" + /> +
+
+
+ + +
+
+ + {/* 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/CollectionsPage.tsx b/src/test/frontend/components/CollectionsPage.tsx new file mode 100644 index 00000000..d89249cf --- /dev/null +++ b/src/test/frontend/components/CollectionsPage.tsx @@ -0,0 +1,241 @@ +import React, { useState } from 'react'; +import { Modal } from './Modal'; +import { useQueryCollection } from '../hooks/useQueryCollection'; + +export const CollectionsPage: React.FC = () => { + const userId = Number(sessionStorage.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} + /> +
+
+ +
+
+
+
+
+
+
+ + + + 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/SystemLogPageWithAPI.tsx b/src/test/frontend/components/admin/SystemLogPageWithAPI.tsx new file mode 100644 index 00000000..0b9c61a5 --- /dev/null +++ b/src/test/frontend/components/admin/SystemLogPageWithAPI.tsx @@ -0,0 +1,219 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { AdminModal } from './AdminModal'; +import { operationLogApi, userApi } from '../../services/api'; + +interface SystemLog { + id: string; + time: string; + user: string; + action: string; + module: string; + status: '成功' | '失败'; + ip: string; + details: string; +} + +interface SystemLogPageProps { + initialStatusFilter: string; + clearInitialFilter: () => void; +} + +export const SystemLogPageWithAPI: React.FC = ({ initialStatusFilter, clearInitialFilter }) => { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [filters, setFilters] = useState({ startDate: '', endDate: '', user: '', action: '', status: initialStatusFilter }); + const [isExportModalOpen, setExportModalOpen] = useState(false); + const [viewingLog, setViewingLog] = useState(null); + + useEffect(() => { + if (initialStatusFilter) { + clearInitialFilter(); + } + }, [initialStatusFilter, clearInitialFilter]); + + useEffect(() => { + loadLogs(); + }, []); + + const loadLogs = async () => { + try { + setLoading(true); + const operationLogs = await operationLogApi.getList(); + + const systemLogs: SystemLog[] = await Promise.all( + operationLogs.map(async (log) => { + let username = 'Unknown'; + try { + const user = await userApi.getById(log.userId); + username = user.username; + } catch (error) { + console.error('获取用户信息失败:', error); + } + + return { + id: String(log.id), + time: new Date(log.operateTime).toLocaleString(), + user: username, + action: log.operateType, + module: log.module, + status: log.status === 1 ? '成功' : '失败', + ip: log.ip, + details: log.operateDesc, + }; + }) + ); + + setLogs(systemLogs); + } catch (error) { + console.error('加载系统日志失败:', error); + } finally { + setLoading(false); + } + }; + + 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 = () => { + const csvContent = "data:text/csv;charset=utf-8," + + "时间,用户,操作,模块,状态,IP,详情\n" + + filteredLogs.map(log => + `${log.time},${log.user},${log.action},${log.module},${log.status},${log.ip},"${log.details}"` + ).join('\n'); + + const encodedUri = encodeURI(csvContent); + const link = document.createElement("a"); + link.setAttribute("href", encodedUri); + link.setAttribute("download", `system_logs_${new Date().toISOString().split('T')[0]}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + setExportModalOpen(false); + alert('日志已导出'); + } + + if (loading) { + return ( +
+
+
+

加载系统日志中...

+
+
+ ); + } + + return ( +
+
+ +
+ +
+
+ + + + + +
+ +
+ +
+
+ + + + + + + + + + + + + + {filteredLogs.length > 0 ? filteredLogs.map(log => ( + + + + + + + + + + )) : ( + + + + )} + +
时间用户操作模块状态IP操作
{log.time}{log.user}{log.action}{log.module} + + {log.status} + + {log.ip} + +
+ + 暂无日志记录 +
+
+
+ + setExportModalOpen(false)} title="导出日志"> +

确定要导出当前筛选的 {filteredLogs.length} 条日志记录吗?

+
+ + +
+
+ + setViewingLog(null)} title="日志详情"> + {viewingLog && ( +
+
时间:{viewingLog.time}
+
用户:{viewingLog.user}
+
操作:{viewingLog.action}
+
模块:{viewingLog.module}
+
状态: + + {viewingLog.status} + +
+
IP:{viewingLog.ip}
+
详情:{viewingLog.details}
+
+ )} +
+
+ ); +}; + + + + + diff --git a/src/test/frontend/components/admin/UserManagementPage.tsx b/src/test/frontend/components/admin/UserManagementPage.tsx new file mode 100644 index 00000000..83f9d3b5 --- /dev/null +++ b/src/test/frontend/components/admin/UserManagementPage.tsx @@ -0,0 +1,368 @@ +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; + 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, + }); + 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('更新成功'); + 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/ConnectionLogPageWithAPI.tsx b/src/test/frontend/components/data-admin/ConnectionLogPageWithAPI.tsx new file mode 100644 index 00000000..dfd28ad8 --- /dev/null +++ b/src/test/frontend/components/data-admin/ConnectionLogPageWithAPI.tsx @@ -0,0 +1,211 @@ +import React, { useState, useMemo, useEffect } from 'react'; +import { AdminModal } from '../admin/AdminModal'; +import { dbConnectionLogApi, dbConnectionApi } from '../../services/api'; + +interface ConnectionLog { + id: string; + time: string; + datasource: string; + status: '成功' | '失败'; + details: string; +} + +export const ConnectionLogPageWithAPI: React.FC = () => { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [isExportModalOpen, setExportModalOpen] = useState(false); + const [viewingLog, setViewingLog] = useState(null); + const [searchTerm, setSearchTerm] = useState(''); + const [searchType, setSearchType] = useState<'all' | 'time' | 'datasource' | 'status'>('all'); + + useEffect(() => { + loadLogs(); + }, []); + + const loadLogs = async () => { + try { + setLoading(true); + const connectionLogs = await dbConnectionLogApi.getList(); + + const logData: ConnectionLog[] = await Promise.all( + connectionLogs.map(async (log) => { + let datasourceName = 'Unknown'; + try { + const connection = await dbConnectionApi.getById(log.dbConnectionId); + datasourceName = connection.name; + } catch (error) { + console.error('获取数据源信息失败:', error); + } + + return { + id: String(log.id), + time: new Date(log.connectTime).toLocaleString(), + datasource: datasourceName, + status: log.status === 1 ? '成功' : '失败', + details: log.errorMessage || '连接成功', + }; + }) + ); + + setLogs(logData); + } catch (error) { + console.error('加载连接日志失败:', error); + } finally { + setLoading(false); + } + }; + + const getStatusClass = (status: '成功' | '失败') => { + return status === '成功' ? 'text-success' : 'text-danger'; + }; + + const handleExport = () => { + const csvContent = "data:text/csv;charset=utf-8," + + "时间,数据源,状态,详情\n" + + filteredLogs.map(log => + `${log.time},${log.datasource},${log.status},"${log.details}"` + ).join('\n'); + + const encodedUri = encodeURI(csvContent); + const link = document.createElement("a"); + link.setAttribute("href", encodedUri); + link.setAttribute("download", `connection_logs_${new Date().toISOString().split('T')[0]}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + setExportModalOpen(false); + alert('日志已导出'); + }; + + 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: + return ( + new Date(log.time).toLocaleString().toLowerCase().includes(term) || + log.datasource.toLowerCase().includes(term) || + log.status.toLowerCase().includes(term) + ); + } + }); + }, [logs, searchTerm, searchType]); + + if (loading) { + return ( +
+
+
+

加载连接日志中...

+
+
+ ); + } + + return ( +
+
+
+
+ + setSearchTerm(e.target.value)} + className="w-full pl-9 pr-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-primary/30" + /> +
+ +
+ +
+ +
+
+ + + + + + + + + + + {filteredLogs.length > 0 ? filteredLogs.map(log => ( + + + + + + + )) : ( + + + + )} + +
时间数据源状态操作
{log.time}{log.datasource} + + {log.status} + + + +
+ + 暂无日志记录 +
+
+
+ + setExportModalOpen(false)} title="导出日志"> +

确定要导出当前筛选的 {filteredLogs.length} 条日志记录吗?

+
+ + +
+
+ + setViewingLog(null)} title="日志详情"> + {viewingLog && ( +
+
时间:{viewingLog.time}
+
数据源:{viewingLog.datasource}
+
状态: + + {viewingLog.status} + +
+
详情:{viewingLog.details}
+
+ )} +
+
+ ); +}; + + + + + 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..0b2e9961 --- /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(sessionStorage.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..b0f61b0f --- /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(sessionStorage.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(sessionStorage.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/hooks/useQueryCollection.ts b/src/test/frontend/hooks/useQueryCollection.ts new file mode 100644 index 00000000..72d0916d --- /dev/null +++ b/src/test/frontend/hooks/useQueryCollection.ts @@ -0,0 +1,158 @@ +import { useState, useEffect } from 'react'; +import { queryCollectionApi, collectionRecordApi } from '../services/api'; + +export interface QueryCollection { + id: number; + userId: number; + collectionName: string; + description?: string; + createTime: string; +} + +export interface CollectionRecord { + id: string; + collectionId: number; + queryLogId: number; + addTime: string; +} + +export const useQueryCollection = (userId: number) => { + const [collections, setCollections] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadCollections = async () => { + setLoading(true); + setError(null); + try { + const data = await queryCollectionApi.getByUser(userId); + setCollections(data); + } catch (err) { + setError(err instanceof Error ? err.message : '加载收藏夹失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + if (userId) { + loadCollections(); + } + }, [userId]); + + const createCollection = async (name: string, description?: string) => { + setLoading(true); + setError(null); + try { + const newCollection = await queryCollectionApi.create({ + userId, + collectionName: name, + description, + }); + setCollections(prev => [...prev, newCollection]); + return newCollection; + } catch (err) { + setError(err instanceof Error ? err.message : '创建收藏夹失败'); + return null; + } finally { + setLoading(false); + } + }; + + const updateCollection = async (id: number, name: string, description?: string) => { + setLoading(true); + setError(null); + try { + const updated = await queryCollectionApi.update({ + id, + collectionName: name, + description, + }); + setCollections(prev => prev.map(c => c.id === id ? updated : c)); + return updated; + } catch (err) { + setError(err instanceof Error ? err.message : '更新收藏夹失败'); + return null; + } finally { + setLoading(false); + } + }; + + const deleteCollection = async (id: number) => { + setLoading(true); + setError(null); + try { + await queryCollectionApi.delete(id); + setCollections(prev => prev.filter(c => c.id !== id)); + return true; + } catch (err) { + setError(err instanceof Error ? err.message : '删除收藏夹失败'); + return false; + } finally { + setLoading(false); + } + }; + + const addQueryToCollection = async (collectionId: number, queryLogId: number) => { + setLoading(true); + setError(null); + try { + await collectionRecordApi.create({ + collectionId, + queryLogId, + }); + return true; + } catch (err) { + setError(err instanceof Error ? err.message : '添加到收藏夹失败'); + return false; + } finally { + setLoading(false); + } + }; + + const removeQueryFromCollection = async (recordId: string) => { + setLoading(true); + setError(null); + try { + await collectionRecordApi.delete(recordId); + return true; + } catch (err) { + setError(err instanceof Error ? err.message : '从收藏夹移除失败'); + return false; + } finally { + setLoading(false); + } + }; + + const getCollectionRecords = async (collectionId: string) => { + setLoading(true); + setError(null); + try { + const records = await collectionRecordApi.getByCollection(collectionId); + return records; + } catch (err) { + setError(err instanceof Error ? err.message : '获取收藏记录失败'); + return []; + } finally { + setLoading(false); + } + }; + + return { + collections, + loading, + error, + loadCollections, + createCollection, + updateCollection, + deleteCollection, + addQueryToCollection, + removeQueryFromCollection, + getCollectionRecords, + }; +}; + + + + + diff --git a/src/test/frontend/hooks/useQueryShare.ts b/src/test/frontend/hooks/useQueryShare.ts new file mode 100644 index 00000000..af10d01d --- /dev/null +++ b/src/test/frontend/hooks/useQueryShare.ts @@ -0,0 +1,62 @@ +import { useState } from 'react'; +import { queryShareApi, queryLogApi } from '../services/api'; + +export const useQueryShare = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const shareQuery = async (queryLogId: number, receiveUserId: number) => { + setLoading(true); + setError(null); + try { + await queryShareApi.create({ + shareUserId: Number(sessionStorage.getItem('userId') || '1'), + receiveUserId, + queryLogId, + receiveStatus: 0, + }); + return true; + } catch (err) { + setError(err instanceof Error ? err.message : '分享失败'); + return false; + } finally { + setLoading(false); + } + }; + + const markAsRead = async (shareId: number) => { + try { + await queryShareApi.update({ + id: shareId, + receiveStatus: 1, + }); + return true; + } catch (err) { + setError(err instanceof Error ? err.message : '标记失败'); + return false; + } + }; + + const deleteShare = async (shareId: number) => { + try { + await queryShareApi.delete(shareId); + return true; + } catch (err) { + setError(err instanceof Error ? err.message : '删除失败'); + return false; + } + }; + + return { + shareQuery, + markAsRead, + deleteShare, + loading, + error, + }; +}; + + + + + 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..fdcae0e3 --- /dev/null +++ b/src/test/frontend/services/api.ts @@ -0,0 +1,933 @@ +// API 基础配置 +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'; + +// 从sessionStorage获取token(每个标签页独立) +const getToken = (): string | null => { + return sessionStorage.getItem('token'); +}; + +// 从sessionStorage获取userId(每个标签页独立) +const getUserId = (): string | null => { + return sessionStorage.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和用户信息到sessionStorage(每个标签页独立) + if (response.token) { + sessionStorage.setItem('token', response.token); + sessionStorage.setItem('userId', String(response.userId)); + sessionStorage.setItem('username', response.username); + sessionStorage.setItem('roleId', String(response.roleId)); + sessionStorage.setItem('roleName', response.roleName || ''); + } + + return response; + }, + + logout: () => { + sessionStorage.removeItem('token'); + sessionStorage.removeItem('userId'); + sessionStorage.removeItem('username'); + sessionStorage.removeItem('roleId'); + sessionStorage.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}`); + }, + + create: async (dialog: Partial): Promise => { + return await request('/dialog', { + method: 'POST', + body: JSON.stringify(dialog), + }); + }, + + update: async (dialogId: string, dialog: Partial): Promise => { + return await request(`/dialog/${dialogId}`, { + method: 'PUT', + body: JSON.stringify(dialog), + }); + }, + + delete: async (dialogId: string): Promise => { + return await request(`/dialog/${dialogId}`, { + method: 'DELETE', + }); + }, +}; + +// ==================== 用户接口 ==================== +export interface User { + id: number; + username: string; + email: string; + phonenumber: string; + roleId: number; + avatarUrl: string; + status: number; +} + +export interface ChangePasswordRequest { + userId: number; + oldPassword: string; + newPassword: string; +} + +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', + }); + }, + + changePassword: async (changePasswordRequest: ChangePasswordRequest): Promise => { + return await request('/user/change-password', { + method: 'POST', + body: JSON.stringify(changePasswordRequest), + }); + }, +}; + +// ==================== 数据库连接接口 ==================== +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', + }); + }, +}; + +// ==================== 通知接口 ==================== +export interface Notification { + id: number; + title: string; + content: string; + targetId: number; + priorityId: number; + publishTime?: string; + isTop: number; + createUserId: number; + createTime: string; + latestUpdateTime: string; +} + +export const notificationApi = { + getList: async (): Promise => { + return await request('/notification/list'); + }, + + getPublished: async (): Promise => { + return await request('/notification/list/published'); + }, + + getDrafts: async (): Promise => { + return await request('/notification/list/drafts'); + }, + + getByTarget: async (targetId: number): Promise => { + return await request(`/notification/list/target/${targetId}`); + }, + + getById: async (id: number): Promise => { + return await request(`/notification/${id}`); + }, + + create: async (notification: Partial): Promise => { + return await request('/notification', { + method: 'POST', + body: JSON.stringify(notification), + }); + }, + + update: async (notification: Partial): Promise => { + return await request('/notification', { + method: 'PUT', + body: JSON.stringify(notification), + }); + }, + + publish: async (id: number): Promise => { + return await request(`/notification/${id}/publish`, { + method: 'PUT', + }); + }, + + toggleTop: async (id: number): Promise => { + return await request(`/notification/${id}/toggle-top`, { + method: 'PUT', + }); + }, + + delete: async (id: number): Promise => { + return await request(`/notification/${id}`, { + method: 'DELETE', + }); + }, +}; + +// ==================== 查询分享接口 ==================== +export interface QueryShare { + id: number; + shareUserId: number; + receiveUserId: number; + queryLogId: number; + shareTime: string; + receiveStatus: number; +} + +export const queryShareApi = { + getByReceiveUser: async (receiveUserId: number): Promise => { + return await request(`/query-share/list/receive/${receiveUserId}`); + }, + + getByReceiveUserAndStatus: async (receiveUserId: number, receiveStatus: number): Promise => { + return await request(`/query-share/list/receive/${receiveUserId}/${receiveStatus}`); + }, + + getByShareUser: async (shareUserId: number): Promise => { + return await request(`/query-share/list/share/${shareUserId}`); + }, + + getById: async (id: number): Promise => { + return await request(`/query-share/${id}`); + }, + + create: async (queryShare: Partial): Promise => { + return await request('/query-share', { + method: 'POST', + body: JSON.stringify(queryShare), + }); + }, + + update: async (queryShare: Partial): Promise => { + return await request('/query-share', { + method: 'PUT', + body: JSON.stringify(queryShare), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/query-share/${id}`, { + method: 'DELETE', + }); + }, +}; + +// ==================== 查询日志接口 ==================== +export interface QueryLog { + id: number; + userId: number; + dialogId: string; + userPrompt: string; + sqlQuery: string; + queryResult: string; + queryTime: string; + executionTime: string; + llmConfigId: number; + dbConnectionId: number; +} + +export const queryLogApi = { + getList: async (): Promise => { + return await request('/query-log/list'); + }, + + getByUser: async (userId: number): Promise => { + return await request(`/query-log/list/user/${userId}`); + }, + + getByDialog: async (dialogId: string): Promise => { + return await request(`/query-log/list/dialog/${dialogId}`); + }, + + getById: async (id: number): Promise => { + return await request(`/query-log/${id}`); + }, + + create: async (queryLog: Partial): Promise => { + return await request('/query-log', { + method: 'POST', + body: JSON.stringify(queryLog), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/query-log/${id}`, { + method: 'DELETE', + }); + }, +}; + +// ==================== 好友关系接口 ==================== +export interface FriendRelation { + id: number; + userId: number; + friendId: number; + remark?: string; + onlineStatus: number; + createTime: string; +} + +export interface FriendRelationWithUser extends FriendRelation { + friendUser?: User; +} + +export const friendRelationApi = { + getByUser: async (userId: number): Promise => { + return await request(`/friend-relation/list/${userId}`); + }, + + getByUserAndFriend: async (userId: number, friendId: number): Promise => { + return await request(`/friend-relation/${userId}/${friendId}`); + }, + + create: async (friendRelation: Partial): Promise => { + return await request('/friend-relation', { + method: 'POST', + body: JSON.stringify(friendRelation), + }); + }, + + update: async (friendRelation: Partial): Promise => { + return await request('/friend-relation', { + method: 'PUT', + body: JSON.stringify(friendRelation), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/friend-relation/${id}`, { + method: 'DELETE', + }); + }, + + deleteByUserAndFriend: async (userId: number, friendId: number): Promise => { + return await request(`/friend-relation/${userId}/${friendId}`, { + method: 'DELETE', + }); + }, +}; + +// ==================== 好友请求接口 ==================== +export interface FriendRequest { + id: number; + sendUserId: number; + recipientId: number; + status: number; + createTime: string; + handleTime?: string; +} + +export interface FriendRequestWithUser extends FriendRequest { + applicantUser?: User; +} + +export const friendRequestApi = { + getByRecipient: async (recipientId: number): Promise => { + return await request(`/friend-request/list/${recipientId}`); + }, + + getByRecipientAndStatus: async (recipientId: number, status: number): Promise => { + return await request(`/friend-request/list/${recipientId}/${status}`); + }, + + getById: async (id: number): Promise => { + return await request(`/friend-request/${id}`); + }, + + create: async (friendRequest: Partial): Promise => { + return await request('/friend-request', { + method: 'POST', + body: JSON.stringify(friendRequest), + }); + }, + + update: async (friendRequest: Partial): Promise => { + return await request('/friend-request', { + method: 'PUT', + body: JSON.stringify(friendRequest), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/friend-request/${id}`, { + method: 'DELETE', + }); + }, + + accept: async (id: number): Promise => { + return await request(`/friend-request/${id}/accept`, { + method: 'POST', + }); + }, + + reject: async (id: number): Promise => { + return await request(`/friend-request/${id}/reject`, { + method: 'POST', + }); + }, +}; + +// ==================== 好友聊天接口 ==================== +export interface FriendChat { + id: string; + sendUserId: number; + receiveUserId: number; + content: string; + sendTime: string; + isRead: boolean; +} + +export const friendChatApi = { + getByUserAndFriend: async (userId: number, friendId: number): Promise => { + return await request(`/friend-chat/list/${userId}/${friendId}`); + }, + + getUnreadByFriend: async (friendId: number): Promise => { + return await request(`/friend-chat/unread/${friendId}`); + }, + + create: async (friendChat: Partial): Promise => { + return await request('/friend-chat', { + method: 'POST', + body: JSON.stringify(friendChat), + }); + }, + + update: async (friendChat: Partial): Promise => { + return await request('/friend-chat', { + method: 'PUT', + body: JSON.stringify(friendChat), + }); + }, + + delete: async (id: string): Promise => { + return await request(`/friend-chat/${id}`, { + method: 'DELETE', + }); + }, +}; + +// ==================== 用户搜索接口 ==================== +export interface UserSearchRecord { + id: number; + userId: number; + searchKeyword: string; + searchTime: string; +} + +export const userSearchApi = { + searchByEmail: async (email: string): Promise => { + return await request(`/user/search/email?email=${encodeURIComponent(email)}`); + }, + + searchByPhoneNumber: async (phoneNumber: string): Promise => { + return await request(`/user/search/phone?phoneNumber=${encodeURIComponent(phoneNumber)}`); + }, + + getSearchHistory: async (userId: number): Promise => { + return await request(`/user-search/list/${userId}`); + }, + + getTopSearches: async (userId: number, limit: number): Promise => { + return await request(`/user-search/list/top/${userId}/${limit}`); + }, + + saveSearch: async (userSearch: Partial): Promise => { + return await request('/user-search', { + method: 'POST', + body: JSON.stringify(userSearch), + }); + }, +}; + +// ==================== 查询收藏接口 ==================== +export interface QueryCollection { + id: number; + userId: number; + collectionName: string; + description?: string; + createTime: string; +} + +export interface CollectionRecord { + id: string; + collectionId: number; + queryLogId: number; + addTime: string; +} + +export const queryCollectionApi = { + getByUser: async (userId: number): Promise => { + return await request(`/query-collection/list/${userId}`); + }, + + create: async (collection: Partial): Promise => { + return await request('/query-collection', { + method: 'POST', + body: JSON.stringify(collection), + }); + }, + + update: async (collection: Partial): Promise => { + return await request('/query-collection', { + method: 'PUT', + body: JSON.stringify(collection), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/query-collection/${id}`, { + method: 'DELETE', + }); + }, +}; + +export const collectionRecordApi = { + getByCollection: async (collectionId: string): Promise => { + return await request(`/collection-record/list/query/${collectionId}`); + }, + + create: async (record: Partial): Promise => { + return await request('/collection-record', { + method: 'POST', + body: JSON.stringify(record), + }); + }, + + delete: async (id: string): Promise => { + return await request(`/collection-record/${id}`, { + method: 'DELETE', + }); + }, +}; + +// ==================== 操作日志接口 ==================== +export interface OperationLog { + id: number; + userId: number; + module: string; + operateType: string; + operateDesc: string; + operateTime: string; + ip: string; + status: number; +} + +export const operationLogApi = { + getList: async (): Promise => { + return await request('/operation-log/list'); + }, + + getByUser: async (userId: number): Promise => { + return await request(`/operation-log/list/user/${userId}`); + }, + + getByModule: async (module: string): Promise => { + return await request(`/operation-log/list/module/${encodeURIComponent(module)}`); + }, + + getFailed: async (): Promise => { + return await request('/operation-log/list/failed'); + }, + + getById: async (id: number): Promise => { + return await request(`/operation-log/${id}`); + }, + + create: async (log: Partial): Promise => { + return await request('/operation-log', { + method: 'POST', + body: JSON.stringify(log), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/operation-log/${id}`, { + method: 'DELETE', + }); + }, +}; + +// ==================== 错误日志接口 ==================== +export interface ErrorLog { + id: number; + errorTypeId: number; + errorCount: number; + statPeriod: string; + statTime: string; +} + +export const errorLogApi = { + getList: async (): Promise => { + return await request('/error-log/list'); + }, + + getByErrorType: async (errorTypeId: number): Promise => { + return await request(`/error-log/list/type/${errorTypeId}`); + }, + + getByPeriod: async (period: string): Promise => { + return await request(`/error-log/list/period/${encodeURIComponent(period)}`); + }, + + getById: async (id: number): Promise => { + return await request(`/error-log/${id}`); + }, + + create: async (log: Partial): Promise => { + return await request('/error-log', { + method: 'POST', + body: JSON.stringify(log), + }); + }, + + update: async (log: Partial): Promise => { + return await request('/error-log', { + method: 'PUT', + body: JSON.stringify(log), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/error-log/${id}`, { + method: 'DELETE', + }); + }, +}; + +// ==================== 数据库连接日志接口 ==================== +export interface DbConnectionLog { + id: number; + dbConnectionId: number; + connectTime: string; + disconnectTime?: string; + status: number; + errorMessage?: string; +} + +export const dbConnectionLogApi = { + getList: async (): Promise => { + return await request('/db-connection-log/list'); + }, + + getByConnection: async (dbConnectionId: number): Promise => { + return await request(`/db-connection-log/list/connection/${dbConnectionId}`); + }, + + getById: async (id: number): Promise => { + return await request(`/db-connection-log/${id}`); + }, + + create: async (log: Partial): Promise => { + return await request('/db-connection-log', { + method: 'POST', + body: JSON.stringify(log), + }); + }, + + delete: async (id: number): Promise => { + return await request(`/db-connection-log/${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/mysql_schema_from_last.sql b/src/test/mysql_schema_from_last.sql new file mode 100644 index 00000000..c903ce5c --- /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` MEDIUMTEXT COMMENT '头像URL或Base64编码', + `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_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), + 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..c7d92b10 --- /dev/null +++ b/src/test/pom.xml @@ -0,0 +1,158 @@ + + + 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 + 1.18.42 + provided + + + + + 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.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/scripts/export-data.bat b/src/test/scripts/export-data.bat new file mode 100644 index 00000000..4fd8db52 --- /dev/null +++ b/src/test/scripts/export-data.bat @@ -0,0 +1,27 @@ +@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..97ff6113 --- /dev/null +++ b/src/test/scripts/export-data.sh @@ -0,0 +1,39 @@ +#!/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..a9d61480 --- /dev/null +++ b/src/test/scripts/import-data.bat @@ -0,0 +1,32 @@ +@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..a1dc83b8 --- /dev/null +++ b/src/test/scripts/import-data.sh @@ -0,0 +1,38 @@ +#!/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..37e062c4 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/common/Result.java @@ -0,0 +1,58 @@ +package com.example.springboot_demo.common; + +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); + } + + 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..75cb0cbf --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/config/JwtInterceptor.java @@ -0,0 +1,61 @@ +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; + } + } + + // 如果没有 token,尝试从 userId header 获取(用于开发环境) + String userIdHeader = request.getHeader("userId"); + if (StringUtils.hasText(userIdHeader)) { + try { + Long userId = Long.parseLong(userIdHeader); + request.setAttribute("userId", userId); + return true; + } catch (NumberFormatException e) { + // 忽略 + } + } + + // 对于查询接口,如果没有 token 也允许通过(使用默认 userId=1) + // 生产环境应该移除这个逻辑 + if (request.getRequestURI().startsWith("/query/")) { + request.setAttribute("userId", 1L); + 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..6a676320 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/config/SecurityConfig.java @@ -0,0 +1,62 @@ +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; +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 + .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. + ) + .authenticationProvider(authenticationProvider()); // 使用自定义的认证提供者 + 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..3f84ddc6 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/config/WebMvcConfig.java @@ -0,0 +1,31 @@ +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", // 用户注册免认证 + "/query/**", // 查询接口(暂时免认证,或确保前端发送token) + "/error" + ); + } +} + 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..f46fc03b --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/AiInteractionLogController.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.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/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/CollectionRecordController.java b/src/test/src/main/java/com/example/springboot_demo/controller/CollectionRecordController.java new file mode 100644 index 00000000..6e651814 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/CollectionRecordController.java @@ -0,0 +1,53 @@ +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/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/DbConnectionLogController.java b/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionLogController.java new file mode 100644 index 00000000..89c7b2a3 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/DbConnectionLogController.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.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/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..b7977722 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/DialogController.java @@ -0,0 +1,59 @@ +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("对话不存在"); + } + + @PostMapping + public Result createDialog(@RequestBody DialogRecord dialogRecord, + @RequestHeader(value = "userId", defaultValue = "1") Long userId) { + dialogRecord.setUserId(userId); + DialogRecord created = dialogService.createDialog(dialogRecord); + return Result.success(created); + } + + @DeleteMapping("/{dialogId}") + public Result deleteDialog(@PathVariable String dialogId, + @RequestHeader(value = "userId", defaultValue = "1") Long userId) { + dialogService.deleteDialog(dialogId, userId); + return Result.success(); + } + + @PutMapping("/{dialogId}") + public Result updateDialog(@PathVariable String dialogId, + @RequestBody DialogRecord dialogRecord, + @RequestHeader(value = "userId", defaultValue = "1") Long userId) { + dialogRecord.setDialogId(dialogId); + dialogRecord.setUserId(userId); + DialogRecord updated = dialogService.updateDialog(dialogRecord); + return Result.success(updated); + } +} + + 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..1c91c560 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/DialogDetailController.java @@ -0,0 +1,40 @@ +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/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/FriendChatController.java b/src/test/src/main/java/com/example/springboot_demo/controller/FriendChatController.java new file mode 100644 index 00000000..70dee82a --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/FriendChatController.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.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..05fda5cd --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/FriendRelationController.java @@ -0,0 +1,58 @@ +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..5ca38743 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/FriendRequestController.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.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(); + } + + @PostMapping("/{id}/accept") + public Result acceptRequest(@PathVariable Long id) { + try { + boolean success = friendRequestService.acceptRequest(id); + if (success) { + return Result.success("已接受好友请求"); + } + return Result.error("接受好友请求失败"); + } catch (RuntimeException e) { + return Result.error(e.getMessage()); + } + } + + @PostMapping("/{id}/reject") + public Result rejectRequest(@PathVariable Long id) { + try { + boolean success = friendRequestService.rejectRequest(id); + if (success) { + return Result.success("已拒绝好友请求"); + } + return Result.error("拒绝好友请求失败"); + } catch (RuntimeException e) { + return Result.error(e.getMessage()); + } + } +} + + 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/PerformanceMetricController.java b/src/test/src/main/java/com/example/springboot_demo/controller/PerformanceMetricController.java new file mode 100644 index 00000000..b5200bb6 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/PerformanceMetricController.java @@ -0,0 +1,43 @@ +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/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/QueryCollectionController.java b/src/test/src/main/java/com/example/springboot_demo/controller/QueryCollectionController.java new file mode 100644 index 00000000..ea0d5900 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/QueryCollectionController.java @@ -0,0 +1,43 @@ +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/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/QueryShareController.java b/src/test/src/main/java/com/example/springboot_demo/controller/QueryShareController.java new file mode 100644 index 00000000..00299f8c --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/QueryShareController.java @@ -0,0 +1,62 @@ +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/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/SqlCacheController.java b/src/test/src/main/java/com/example/springboot_demo/controller/SqlCacheController.java new file mode 100644 index 00000000..38241708 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/SqlCacheController.java @@ -0,0 +1,46 @@ +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/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/TokenConsumeController.java b/src/test/src/main/java/com/example/springboot_demo/controller/TokenConsumeController.java new file mode 100644 index 00000000..dd122289 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/TokenConsumeController.java @@ -0,0 +1,50 @@ +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/UserController.java b/src/test/src/main/java/com/example/springboot_demo/controller/UserController.java new file mode 100644 index 00000000..36362bce --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/UserController.java @@ -0,0 +1,120 @@ +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.dto.ChangePasswordDTO; +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("用户不存在"); + } + + @GetMapping("/search/email") + public Result> searchByEmail(@RequestParam String email) { + List users = userService.searchByEmail(email); + return Result.success(users); + } + + @GetMapping("/search/phone") + public Result searchByPhoneNumber(@RequestParam String phoneNumber) { + User user = userService.searchByPhoneNumber(phoneNumber); + if (user != null) { + return Result.success(user); + } + return Result.error("未找到该用户"); + } + + @PostMapping("/change-password") + public Result changePassword(@RequestBody ChangePasswordDTO changePasswordDTO) { + try { + boolean success = userService.changePassword( + changePasswordDTO.getUserId(), + changePasswordDTO.getOldPassword(), + changePasswordDTO.getNewPassword() + ); + if (success) { + return Result.success("密码修改成功"); + } + return Result.error("密码修改失败"); + } catch (RuntimeException e) { + return Result.error(e.getMessage()); + } + } +} + 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/controller/UserSearchController.java b/src/test/src/main/java/com/example/springboot_demo/controller/UserSearchController.java new file mode 100644 index 00000000..f31988ed --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/controller/UserSearchController.java @@ -0,0 +1,54 @@ +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/dto/ChangePasswordDTO.java b/src/test/src/main/java/com/example/springboot_demo/dto/ChangePasswordDTO.java new file mode 100644 index 00000000..1a8a9e6c --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/dto/ChangePasswordDTO.java @@ -0,0 +1,16 @@ +package com.example.springboot_demo.dto; + +import lombok.Data; + +@Data +public class ChangePasswordDTO { + + private Long userId; + + private String oldPassword; + + private String newPassword; +} + + + 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/dto/SendFriendRequestDTO.java b/src/test/src/main/java/com/example/springboot_demo/dto/SendFriendRequestDTO.java new file mode 100644 index 00000000..50aeb6f9 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/dto/SendFriendRequestDTO.java @@ -0,0 +1,16 @@ +package com.example.springboot_demo.dto; + +import lombok.Data; + +@Data +public class SendFriendRequestDTO { + + private Long sendUserId; + + private String searchKey; // 邮箱或手机号 + + private String message; // 申请消息 +} + + + 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..f62ed696 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/AiInteractionLog.java @@ -0,0 +1,45 @@ +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..2ba566a0 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/CollectionRecord.java @@ -0,0 +1,32 @@ +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..e466babc --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/DialogDetail.java @@ -0,0 +1,32 @@ +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/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/mongodb/FriendChat.java b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/FriendChat.java new file mode 100644 index 00000000..42b49521 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/FriendChat.java @@ -0,0 +1,32 @@ +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..70ad6402 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/QueryCollection.java @@ -0,0 +1,23 @@ +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..e358167e --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mongodb/SqlCache.java @@ -0,0 +1,34 @@ +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/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/DbConnectionLog.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnectionLog.java new file mode 100644 index 00000000..2c04e401 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/DbConnectionLog.java @@ -0,0 +1,31 @@ +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/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/FriendRelation.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRelation.java new file mode 100644 index 00000000..301c0bd2 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRelation.java @@ -0,0 +1,31 @@ +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..102ade55 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/FriendRequest.java @@ -0,0 +1,31 @@ +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/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/PerformanceMetric.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/PerformanceMetric.java new file mode 100644 index 00000000..da07e300 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/PerformanceMetric.java @@ -0,0 +1,28 @@ +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/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/QueryShare.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryShare.java new file mode 100644 index 00000000..ce421fdc --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/QueryShare.java @@ -0,0 +1,33 @@ +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/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/TokenConsume.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TokenConsume.java new file mode 100644 index 00000000..849657b5 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/TokenConsume.java @@ -0,0 +1,32 @@ +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/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/entity/mysql/UserSearch.java b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserSearch.java new file mode 100644 index 00000000..b2402f9b --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/entity/mysql/UserSearch.java @@ -0,0 +1,29 @@ +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/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/DbConnectionLogMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionLogMapper.java new file mode 100644 index 00000000..08245589 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/DbConnectionLogMapper.java @@ -0,0 +1,12 @@ +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/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/FriendRelationMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRelationMapper.java new file mode 100644 index 00000000..adf0ff52 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRelationMapper.java @@ -0,0 +1,12 @@ +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..40630c26 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/FriendRequestMapper.java @@ -0,0 +1,12 @@ +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/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/PerformanceMetricMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/PerformanceMetricMapper.java new file mode 100644 index 00000000..3de1991c --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/PerformanceMetricMapper.java @@ -0,0 +1,12 @@ +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/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/QueryShareMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/QueryShareMapper.java new file mode 100644 index 00000000..01a93d90 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/QueryShareMapper.java @@ -0,0 +1,12 @@ +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/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/TokenConsumeMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/TokenConsumeMapper.java new file mode 100644 index 00000000..05813532 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/TokenConsumeMapper.java @@ -0,0 +1,12 @@ +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/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/mapper/UserSearchMapper.java b/src/test/src/main/java/com/example/springboot_demo/mapper/UserSearchMapper.java new file mode 100644 index 00000000..5e62bda1 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/mapper/UserSearchMapper.java @@ -0,0 +1,12 @@ +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..c16c569b --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/AiInteractionLogRepository.java @@ -0,0 +1,19 @@ +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..5e8f850b --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/CollectionRecordRepository.java @@ -0,0 +1,19 @@ +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..12cfaaba --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/DialogDetailRepository.java @@ -0,0 +1,13 @@ +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/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/repository/FriendChatRepository.java b/src/test/src/main/java/com/example/springboot_demo/repository/FriendChatRepository.java new file mode 100644 index 00000000..18c0e53c --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/FriendChatRepository.java @@ -0,0 +1,17 @@ +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..dbc20ad7 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/QueryCollectionRepository.java @@ -0,0 +1,17 @@ +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..f0c8d7c7 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/repository/SqlCacheRepository.java @@ -0,0 +1,17 @@ +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..fd49ec40 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/AiInteractionLogService.java @@ -0,0 +1,18 @@ +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/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/CollectionRecordService.java b/src/test/src/main/java/com/example/springboot_demo/service/CollectionRecordService.java new file mode 100644 index 00000000..f491e024 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/CollectionRecordService.java @@ -0,0 +1,22 @@ +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/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/DbConnectionLogService.java b/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionLogService.java new file mode 100644 index 00000000..c63a9fd0 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/DbConnectionLogService.java @@ -0,0 +1,20 @@ +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/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/DialogDetailService.java b/src/test/src/main/java/com/example/springboot_demo/service/DialogDetailService.java new file mode 100644 index 00000000..7facdfbf --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/DialogDetailService.java @@ -0,0 +1,14 @@ +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/DialogService.java b/src/test/src/main/java/com/example/springboot_demo/service/DialogService.java new file mode 100644 index 00000000..98fe5ca2 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/DialogService.java @@ -0,0 +1,15 @@ +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); + DialogRecord createDialog(DialogRecord dialogRecord); + void deleteDialog(String dialogId, Long userId); + DialogRecord updateDialog(DialogRecord dialogRecord); +} + + 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/FriendChatService.java b/src/test/src/main/java/com/example/springboot_demo/service/FriendChatService.java new file mode 100644 index 00000000..b9a2fb2f --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/FriendChatService.java @@ -0,0 +1,18 @@ +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..cb5f753f --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/FriendRelationService.java @@ -0,0 +1,22 @@ +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..e8a428b0 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/FriendRequestService.java @@ -0,0 +1,26 @@ +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); + + boolean acceptRequest(Long requestId); + + boolean rejectRequest(Long requestId); +} + + 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/PerformanceMetricService.java b/src/test/src/main/java/com/example/springboot_demo/service/PerformanceMetricService.java new file mode 100644 index 00000000..026758e9 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/PerformanceMetricService.java @@ -0,0 +1,19 @@ +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/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/QueryCollectionService.java b/src/test/src/main/java/com/example/springboot_demo/service/QueryCollectionService.java new file mode 100644 index 00000000..e92d0690 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/QueryCollectionService.java @@ -0,0 +1,20 @@ +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/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/QueryShareService.java b/src/test/src/main/java/com/example/springboot_demo/service/QueryShareService.java new file mode 100644 index 00000000..cc032130 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/QueryShareService.java @@ -0,0 +1,24 @@ +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/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/SqlCacheService.java b/src/test/src/main/java/com/example/springboot_demo/service/SqlCacheService.java new file mode 100644 index 00000000..4e1423cd --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/SqlCacheService.java @@ -0,0 +1,18 @@ +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/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/TokenConsumeService.java b/src/test/src/main/java/com/example/springboot_demo/service/TokenConsumeService.java new file mode 100644 index 00000000..5d41a5f3 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/TokenConsumeService.java @@ -0,0 +1,21 @@ +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/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/UserSearchService.java b/src/test/src/main/java/com/example/springboot_demo/service/UserSearchService.java new file mode 100644 index 00000000..df06640b --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/UserSearchService.java @@ -0,0 +1,22 @@ +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/UserService.java b/src/test/src/main/java/com/example/springboot_demo/service/UserService.java new file mode 100644 index 00000000..5e04bbcd --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/UserService.java @@ -0,0 +1,30 @@ +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); + + List searchByEmail(String email); + + User searchByPhoneNumber(String phoneNumber); + + boolean changePassword(Long userId, String oldPassword, String newPassword); +} + 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..a8696ebd --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/AiInteractionLogServiceImpl.java @@ -0,0 +1,38 @@ +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/AuthServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java new file mode 100644 index 00000000..05acc710 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/AuthServiceImpl.java @@ -0,0 +1,77 @@ +package com.example.springboot_demo.service.impl; + +import org.springframework.beans.factory.annotation.Autowired; +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; +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 AuthenticationManager authenticationManager; + + @Override + public LoginVO login(LoginDTO loginDTO) { + 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("用户名或密码错误"); + } + } +} + + 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..a5288b26 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/CollectionRecordServiceImpl.java @@ -0,0 +1,48 @@ +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/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/CustomUserDetailsService.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/CustomUserDetailsService.java new file mode 100644 index 00000000..1ef088b9 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/CustomUserDetailsService.java @@ -0,0 +1,53 @@ +package com.example.springboot_demo.service.impl; + +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; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.example.springboot_demo.entity.mysql.User; +import com.example.springboot_demo.mapper.UserMapper; + +@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); + } + + return org.springframework.security.core.userdetails.User + .withUsername(user.getUsername()) + .password(user.getPassword()) + .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 new file mode 100644 index 00000000..fd16198c --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/DbConnectionLogServiceImpl.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.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/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/DialogDetailServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogDetailServiceImpl.java new file mode 100644 index 00000000..e0e1e52d --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogDetailServiceImpl.java @@ -0,0 +1,31 @@ +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/DialogServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java new file mode 100644 index 00000000..dad7256e --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/DialogServiceImpl.java @@ -0,0 +1,71 @@ +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.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@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); + } + + @Override + public DialogRecord createDialog(DialogRecord dialogRecord) { + if (dialogRecord.getDialogId() == null || dialogRecord.getDialogId().isEmpty()) { + dialogRecord.setDialogId("conv_" + UUID.randomUUID().toString().replace("-", "")); + } + if (dialogRecord.getStartTime() == null) { + dialogRecord.setStartTime(LocalDateTime.now()); + } + if (dialogRecord.getLastTime() == null) { + dialogRecord.setLastTime(LocalDateTime.now()); + } + if (dialogRecord.getTotalRounds() == null) { + dialogRecord.setTotalRounds(0); + } + return dialogRecordRepository.save(dialogRecord); + } + + @Override + public void deleteDialog(String dialogId, Long userId) { + DialogRecord dialog = dialogRecordRepository.findByDialogId(dialogId); + if (dialog != null && dialog.getUserId().equals(userId)) { + dialogRecordRepository.delete(dialog); + } + } + + @Override + public DialogRecord updateDialog(DialogRecord dialogRecord) { + DialogRecord existing = dialogRecordRepository.findByDialogId(dialogRecord.getDialogId()); + if (existing != null) { + if (dialogRecord.getTopic() != null) { + existing.setTopic(dialogRecord.getTopic()); + } + if (dialogRecord.getTotalRounds() != null) { + existing.setTotalRounds(dialogRecord.getTotalRounds()); + } + existing.setLastTime(LocalDateTime.now()); + return dialogRecordRepository.save(existing); + } + return null; + } +} + + 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/FriendChatServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendChatServiceImpl.java new file mode 100644 index 00000000..9d293cf5 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendChatServiceImpl.java @@ -0,0 +1,39 @@ +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..3613d4cc --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRelationServiceImpl.java @@ -0,0 +1,55 @@ +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..e298e2d3 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/FriendRequestServiceImpl.java @@ -0,0 +1,137 @@ +package com.example.springboot_demo.service.impl; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.example.springboot_demo.entity.mysql.FriendRelation; +import com.example.springboot_demo.entity.mysql.FriendRequest; +import com.example.springboot_demo.entity.mysql.User; +import com.example.springboot_demo.mapper.FriendRequestMapper; +import com.example.springboot_demo.service.FriendRelationService; +import com.example.springboot_demo.service.FriendRequestService; +import com.example.springboot_demo.service.UserService; + +@Service +public class FriendRequestServiceImpl implements FriendRequestService { + + @Autowired + private FriendRequestMapper friendRequestMapper; + + @Autowired + private FriendRelationService friendRelationService; + + @Autowired + private UserService userService; + + @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; + } + + @Override + @Transactional + public boolean acceptRequest(Long requestId) { + // 1. 查询好友请求 + FriendRequest request = friendRequestMapper.selectById(requestId); + if (request == null) { + throw new RuntimeException("好友请求不存在"); + } + + if (request.getStatus() != 0) { + throw new RuntimeException("该请求已被处理"); + } + + // 检查是否给自己发送好友请求 + if (request.getApplicantId().equals(request.getRecipientId())) { + throw new RuntimeException("不能添加自己为好友"); + } + + // 2. 更新请求状态为已接受(1) + request.setStatus(1); + request.setHandleTime(LocalDateTime.now()); + friendRequestMapper.updateById(request); + + // 3. 获取用户信息 + User applicant = userService.getById(request.getApplicantId()); + User recipient = userService.getById(request.getRecipientId()); + + if (applicant == null || recipient == null) { + throw new RuntimeException("用户不存在"); + } + + // 4. 创建双向好友关系 + // 申请人 -> 接收人 + FriendRelation relation1 = new FriendRelation(); + relation1.setUserId(request.getApplicantId()); + relation1.setFriendId(request.getRecipientId()); + relation1.setFriendUsername(recipient.getUsername()); + relation1.setOnlineStatus(recipient.getOnlineStatus()); + relation1.setCreateTime(LocalDateTime.now()); + friendRelationService.save(relation1); + + // 接收人 -> 申请人 + FriendRelation relation2 = new FriendRelation(); + relation2.setUserId(request.getRecipientId()); + relation2.setFriendId(request.getApplicantId()); + relation2.setFriendUsername(applicant.getUsername()); + relation2.setOnlineStatus(applicant.getOnlineStatus()); + relation2.setCreateTime(LocalDateTime.now()); + friendRelationService.save(relation2); + + return true; + } + + @Override + public boolean rejectRequest(Long requestId) { + // 查询好友请求 + FriendRequest request = friendRequestMapper.selectById(requestId); + if (request == null) { + throw new RuntimeException("好友请求不存在"); + } + + if (request.getStatus() != 0) { + throw new RuntimeException("该请求已被处理"); + } + + // 更新请求状态为已拒绝(2) + request.setStatus(2); + request.setHandleTime(LocalDateTime.now()); + return friendRequestMapper.updateById(request) > 0; + } +} + + 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..2306ebf5 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/LlmServiceImpl.java @@ -0,0 +1,175 @@ +package com.example.springboot_demo.service.impl; + +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.Arrays; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.example.springboot_demo.entity.mysql.LlmConfig; +import com.example.springboot_demo.service.LlmConfigService; +import com.example.springboot_demo.service.LlmService; + +/** + * 大模型调用服务实现 + * 支持所有兼容 OpenAI Chat Completions API 的模型 + */ +@Service +public class LlmServiceImpl implements LlmService { + + @Autowired + private LlmConfigService llmConfigService; + + private final HttpClient httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(30)) + .build(); + + @Override + public Map generateQuery(String prompt, String modelConfigId, String databaseName) { + try { + // 根据配置ID从数据库获取模型配置 + LlmConfig config = llmConfigService.getById(Long.valueOf(modelConfigId)); + if (config == null) { + throw new RuntimeException("模型配置不存在,ID: " + modelConfigId); + } + if (config.getIsDisabled() == 1) { + throw new RuntimeException("该模型配置已被禁用"); + } + + // 统一调用大模型API + return callLlmApi(prompt, config, databaseName); + } catch (NumberFormatException e) { + throw new RuntimeException("无效的模型配置ID: " + modelConfigId); + } catch (Exception e) { + throw new RuntimeException("模型调用失败: " + e.getMessage(), e); + } + } + + /** + * 统一调用大模型API + * 支持所有兼容 OpenAI Chat Completions API 的模型 + */ + private Map callLlmApi(String prompt, LlmConfig config, String databaseName) throws Exception { + String apiKey = config.getApiKey().trim(); + String url = config.getApiUrl().trim(); + String modelName = config.getVersion().trim(); + + // 打印调试信息 + System.out.println("=== LLM API 调用信息 ==="); + System.out.println("配置名称: " + config.getName() + " (ID: " + config.getId() + ")"); + System.out.println("API URL: " + url); + System.out.println("模型名称: " + modelName); + System.out.println("API Key 前10位: " + (apiKey.length() > 10 ? apiKey.substring(0, 10) + "..." : apiKey)); + + // 构建请求体(OpenAI Chat Completions API 格式) + 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); + + System.out.println("请求体: " + requestBody.toJSONString()); + + // 发送HTTP请求 + 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(config.getTimeout() != null ? config.getTimeout() / 1000 : 60)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + System.out.println("响应状态码: " + response.statusCode()); + System.out.println("响应内容: " + response.body()); + System.out.println("========================="); + + if (response.statusCode() != 200) { + throw new RuntimeException("API调用失败: " + response.statusCode() + ", 响应: " + response.body()); + } + + JSONObject jsonResponse = JSON.parseObject(response.body()); + + // 解析响应(OpenAI格式) + if (!jsonResponse.containsKey("choices") || jsonResponse.getJSONArray("choices").isEmpty()) { + throw new RuntimeException("API响应格式错误:缺少choices字段"); + } + + JSONObject choice = jsonResponse.getJSONArray("choices").getJSONObject(0); + if (!choice.containsKey("message")) { + throw new RuntimeException("API响应格式错误:缺少message字段"); + } + + String content = choice.getJSONObject("message").getString("content"); + if (content == null || content.isEmpty()) { + throw new RuntimeException("API返回内容为空"); + } + + // 清理可能的markdown代码块标记 + String cleanedContent = content.replaceAll("^```json\\n|```$", "").trim(); + return parseJsonResponse(cleanedContent); + } + + /** + * 生成统一的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 + ); + } + + /** + * 解析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/PerformanceMetricServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/PerformanceMetricServiceImpl.java new file mode 100644 index 00000000..ffab4934 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/PerformanceMetricServiceImpl.java @@ -0,0 +1,44 @@ +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/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/QueryCollectionServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryCollectionServiceImpl.java new file mode 100644 index 00000000..db57a1a8 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryCollectionServiceImpl.java @@ -0,0 +1,43 @@ +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/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..294fbc6c --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryServiceImpl.java @@ -0,0 +1,253 @@ +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); + } + + // 保存对话记录到 MongoDB + if (request.getConversationId() == null || request.getConversationId().isEmpty()) { + // 创建新对话记录 + 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); + System.out.println("✓ 创建新对话记录: " + conversationId); + } else { + // 更新对话记录 + DialogRecord dialogRecord = dialogRecordRepository.findByDialogId(conversationId); + if (dialogRecord != null) { + dialogRecord.setTotalRounds(dialogRecord.getTotalRounds() + 1); + dialogRecord.setLastTime(LocalDateTime.now()); + dialogRecordRepository.save(dialogRecord); + System.out.println("✓ 更新对话记录: " + conversationId + " (轮次: " + dialogRecord.getTotalRounds() + ")"); + } + } + + // 调用大模型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/QueryShareServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryShareServiceImpl.java new file mode 100644 index 00000000..78448af4 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/QueryShareServiceImpl.java @@ -0,0 +1,60 @@ +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/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/SqlCacheServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/SqlCacheServiceImpl.java new file mode 100644 index 00000000..4e30fa33 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/SqlCacheServiceImpl.java @@ -0,0 +1,38 @@ +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/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/TokenConsumeServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/TokenConsumeServiceImpl.java new file mode 100644 index 00000000..e2ccd538 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/TokenConsumeServiceImpl.java @@ -0,0 +1,49 @@ +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/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/UserSearchServiceImpl.java b/src/test/src/main/java/com/example/springboot_demo/service/impl/UserSearchServiceImpl.java new file mode 100644 index 00000000..9c776530 --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/UserSearchServiceImpl.java @@ -0,0 +1,55 @@ +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; + } +} + + 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..0b1ffb6f --- /dev/null +++ b/src/test/src/main/java/com/example/springboot_demo/service/impl/UserServiceImpl.java @@ -0,0 +1,104 @@ +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); + } + + @Override + public List searchByEmail(String email) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.like(User::getEmail, email); + return userMapper.selectList(wrapper); + } + + @Override + public User searchByPhoneNumber(String phoneNumber) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(User::getPhonenumber, phoneNumber); + return userMapper.selectOne(wrapper); + } + + @Override + public boolean changePassword(Long userId, String oldPassword, String newPassword) { + // 1. 查询用户 + User user = userMapper.selectById(userId); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + + // 2. 验证旧密码 + if (!passwordEncoder.matches(oldPassword, user.getPassword())) { + throw new RuntimeException("当前密码错误"); + } + + // 3. 加密新密码 + String encodedNewPassword = passwordEncoder.encode(newPassword); + + // 4. 更新密码 + user.setPassword(encodedNewPassword); + return userMapper.updateById(user) > 0; + } +} + 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..5d9235e6 --- /dev/null +++ b/src/test/src/main/resources/application.yml @@ -0,0 +1,83 @@ +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: + # 如果 MongoDB 没有启用认证,使用下面的配置(无用户名密码) + uri: mongodb://127.0.0.1:27017/natural_language_query_system + # 如果 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/PasswordTest.java b/src/test/src/test/java/com/example/springboot_demo/PasswordTest.java new file mode 100644 index 00000000..3ddbb623 --- /dev/null +++ b/src/test/src/test/java/com/example/springboot_demo/PasswordTest.java @@ -0,0 +1,26 @@ +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); + } +} + + + + + 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() { + } + +}