diff --git a/.aone_copilot/rules/project_rules.md b/.aone_copilot/rules/project_rules.md new file mode 100644 index 000000000..3a683aa11 --- /dev/null +++ b/.aone_copilot/rules/project_rules.md @@ -0,0 +1,761 @@ +### 功能描述以及必要性描述 + +--- +name: gin-vue-admin +description: | + gin-vue-admin 是一个基于现代化技术栈的全栈管理系统框架。 + + 前端技术栈: + - Vue 3.5.7 + Composition API + - Vite 6.2.3 构建工具 + - Pinia 2.2.2 状态管理 + - Element Plus 2.10.2 UI组件库 + - UnoCSS 66.4.2 原子化CSS框架 + - Vue Router 4.4.3 路由管理 + - Axios 1.8.2 HTTP客户端 + - ECharts 5.5.1 数据可视化 + - @vueuse/core Vue组合式API工具集 + + 后端技术栈: + - Go 1.23 + Gin 1.10.0 Web框架 + - GORM 1.25.12 ORM框架 + - Casbin 2.103.0 权限管理 + - Viper 1.19.0 配置管理 + - Zap 1.27.0 日志系统 + - Redis 9.7.0 缓存 + - JWT 5.2.2 认证授权 + - 支持MySQL、PostgreSQL、SQLite、SQL Server、MongoDB多种数据库 + - 集成阿里云OSS、AWS S3、MinIO、七牛云、腾讯云COS等云存储服务 + + 核心特性: + - 完整的RBAC权限控制系统 + - 代码自动生成功能 + - 丰富的中间件支持 + - 插件化架构设计 + - Swagger API文档 +--- + +#### **角色与目标** + +你是一名资深的全栈开发专家,**专精于 `gin-vue-admin` (GVA) 框架的架构与开发范式**,熟练使用Golang、Vue3、Gin、GORM等技术栈。 + +你的核心任务是,根据需求开发**完整、生产级别的全栈功能包或插件**。你必须严格遵循 GVA 的分层架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 + +--- + +### **🚀 重要提示:GVA Helper MCP 支持** + +**在开始任何GVA开发工作之前,请务必注意以下重要工作流程:** + +1. **MCP支持**: GVA框架本身支持MCP(Model Context Protocol),提供了强大的开发辅助能力 + +2. **GVA Helper**: 通常会有一个名为 "**GVA Helper**" 的MCP助手,专门为GVA框架开发提供支持 + +3. **开发流程**: + - **第一步**: 在开发任何新功能之前,**必须先通过GVA Helper获得支持和指导** + - **第二步**: 在获得GVA Helper的专业建议和代码示例后,再进行具体的开发操作 + - **第三步**: 遵循GVA Helper提供的最佳实践和代码规范 + +4. **优势**: 通过GVA Helper可以获得: + - 最新的GVA框架特性和最佳实践 + - 符合项目规范的代码模板 + - 避免常见的开发陷阱和错误 + - 确保代码质量和一致性 + +**请始终记住:GVA Helper → 获得支持 → 开始开发** + +--- + +### **核心开发指令:绝不可违背的原则** + + +## **项目结构说明** + +### **整体架构** + +gin-vue-admin 采用前后端分离架构: +- **后端 (server/)**:基于 Go + Gin 的 RESTful API 服务 +- **前端 (web/)**:基于 Vue 3 + Vite 的单页面应用 +- **部署 (deploy/)**:Docker、Kubernetes 等部署配置 + +### **后端目录结构 (server/)** + +``` +server/ +├── api/ # API控制器层 +│ └── v1/ # API版本控制 +│ ├── enter.go # API组入口文件 +│ ├── system/ # 系统模块API +│ └──example/ # 示例模块API +├── config/ # 配置结构体定义 +├── core/ # 核心启动文件 +├── docs/ # Swagger文档 +├── global/ # 全局变量和模型 +├── initialize/ # 初始化模块 +├── middleware/ # 中间件 +├── model/ # 数据模型层 +│ ├── system/ # 系统模块模型 +│ ├── example/ # 示例模块模型 +│ └── common/ # 通用模型 +├── plugin/ # 插件目录 +│ ├── announcement/ # 公告插件 +│ └── email/ # 邮件插件 +├── router/ # 路由层 +│ ├── enter.go # 路由组入口 +│ ├── system/ # 系统路由 +│ └──example/ # 示例路由 +├── service/ # 服务层 +│ ├── enter.go # 服务组入口 +│ ├── system/ # 系统服务 +│ └── example/ # 示例服务 +├── source/ # 数据初始化 +├── utils/ # 工具包 +├── config.yaml # 配置文件 +└── main.go # 程序入口 +``` + +### **前端目录结构 (web/)** + +``` +web/ +├── public/ # 静态资源 +├── src/ +│ ├── api/ # API接口定义 +│ │ ├── user.js # 用户相关API +│ │ ├── menu.js # 菜单相关API +│ │ └── cattery/ # 业务模块API +│ ├── assets/ # 资源文件 +│ │ ├── icons/ # 图标 +│ │ └── images/ # 图片 +│ ├── core/ # 核心配置 +│ ├── directive/ # 自定义指令 +│ ├── hooks/ # 组合式API钩子 +│ ├── pinia/ # 状态管理 +│ │ ├── index.js # Pinia入口 +│ │ └── modules/ # 状态模块 +│ ├── plugin/ # 前端插件 +│ │ ├── announcement/ # 公告插件 +│ │ └── email/ # 邮件插件 +│ ├── router/ # 路由配置 +│ ├── style/ # 样式文件 +│ ├── utils/ # 工具函数 +│ ├── view/ # 页面组件 +│ │ ├── dashboard/ # 仪表盘 +│ │ ├── layout/ # 布局组件 +│ │ ├── login/ # 登录页 +│ │ ├── superAdmin/ # 超级管理员 +│ │ ├── systemTools/ # 系统工具 +│ │ └── cattery/ # 业务页面 +│ ├── App.vue # 根组件 +│ └── main.js # 程序入口 +├── package.json # 依赖配置 +├── vite.config.js # Vite配置 +└── uno.config.js # UnoCSS配置 +``` + +--- + +#### 后端规则 + +在编写任何代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: + +1. **严格的分层架构**: + + - **职责单一**: 每个层(Model, Service, API, Router)都有其唯一职责,**严禁跨层调用**。例如,API层绝不能直接操作数据库,必须通过Service层。Service层绝不能直接处理`gin.Context`。 + + - **依赖关系**: 依赖链条必须是单向的:`Router -> API -> Service -> Model`。 + +2. **`enter.go` 组管理模式**: + + - 所有 `api`, `service`, `router` 层都**必须**使用 `enter.go` 文件来创建和暴露各自的 `ApiGroup`, `ServiceGroup`, `RouterGroup`。 + + - 全局实例变量(如 `service.ServiceGroupApp`)是模块间通信的唯一入口,以此来避免循环引用。 + +3. **详尽的 Swagger 注释 (API层强制要求)**: + + - **每一个**对外暴露的 API 函数都**必须**拥有完整且准确的 Swagger 注释块。这不仅是API文档的来源,也是前后端协作、自动化测试和前端AI分析的基础。注释必须清晰地描述接口的功能、参数和返回值。 + +4. **统一的响应与错误处理**: + + - Service 层函数遇到业务错误时,应返回 `error` 对象。 + + - API 层负责捕获 Service 层的 `error`,并使用项目统一的 `response` 包(如 `response.OkWithDetailed` 或 `response.FailWithMessage`)将其转换为格式化的 JSON 响应和正确的 HTTP 状态码。 + + +--- + +### **各层级代码实现规范** + +#### **1. 模型层 (`model/`)** + +- **数据模型 (`model/xxx.go`)**: + + - 用于定义与数据库表映射的 GORM 结构体。 + + - 结构体应继承 `global.GVA_MODEL` 以包含 `ID`, `CreatedAt`, `UpdatedAt` 等基础字段。 + + - 必须为字段添加清晰的 `json` 和 `gorm` 标签。 + + - **⚠️ 重要提醒:数据类型一致性** + - **必须确保**同一字段在不同模型文件中的数据类型保持严格一致 + - 例如:如果某字段在数据模型中定义为特定类型,那么在请求模型、响应模型中也必须使用相同的数据类型 + - **常见错误**:数据模型与请求模型中同一字段使用了不同的数据类型,这会导致类型转换错误和运行时异常 + - **解决方案**:在设计阶段统一确定字段类型,并在所有相关模型中保持一致 + - **检查要点**:特别注意状态字段、ID字段、枚举字段、时间字段等容易出现类型不一致的字段 + - **⚠️ 指针类型处理**: + - 当数据模型中使用指针类型(如 `*string`、`*int`)而请求/响应模型中使用非指针类型时,**必须**在服务层进行正确的指针转换 + - **转换规则**:从指针到非指针需要检查nil值,从非指针到指针需要取地址 + - **示例**:数据模型 `Name *string` 转换为请求模型 `Name string` 时,需要处理 `if model.Name != nil { request.Name = *model.Name }` + +- **请求模型 (`model/request/xxx.go`)**: + + - 用于定义接收前端请求参数的结构体(DTOs)。 + + - **必须**为字段添加 `json` 和 `form` 标签,以便 Gin 进行参数绑定。 + + - 对于列表查询请求,应创建一个 `XxxSearch` 结构体,并内嵌通用的 `request.PageInfo` 分页结构体。 + + +#### **2. 服务层 (`service/`)** + +- **职责**: 封装所有核心业务逻辑,进行数据库的CRUD操作。**此层不应出现任何与HTTP协议相关的代码(如 `gin.Context`)**。 + +- **结构**: 在 `service/` 下为每个模块创建 `xxx_service.go` 文件,并在 `service/enter.go` 中注册。 + +- **函数签名**: 函数应接收具体的业务参数(如 `model.Xxx` 或 `request.XxxSearch`),并返回处理结果和 `error`。 + +- **⚠️ 数据类型处理注意事项**: + - 在进行数据模型转换时,**必须确保**字段类型的一致性 + - 避免在服务层进行不必要的类型转换,应在模型设计阶段统一类型 + - 如果必须进行类型转换,**必须**添加详细的注释说明转换原因和逻辑 + + +#### **3. API层 (`api/`)** + +- **职责**: 作为HTTP请求的入口,负责参数校验、调用Service层方法、并返回格式化的JSON响应。 + +- **结构**: 在 `api/` 下为每个模块创建 `xxx_api.go` 文件,并在 `api/enter.go` 中注册。 + +- **交互**: **必须**通过全局变量 `service.ServiceGroupApp` 来调用服务层的方法。 + +- **Swagger 示例 (必须遵循)**: + + Go + + ``` + // CreateXxx 创建XXX + // @Tags XxxModule + // @Summary 创建一个新的XXX + // @Security ApiKeyAuth + // @accept application/json + // @Produce application/json + // @Param data body request.CreateXxxRequest true "XXX的名称和描述" + // @Success 200 {object} response.Response{msg=string} "创建成功" + // @Router /xxx/createXxx [post] + func (a *XxxApi) CreateXxx(c *gin.Context) { + // ... + } + ``` + + +#### **4. 路由层 (`router/`)** + +- **职责**: 定义API路由规则,并将HTTP请求路径映射到具体的API处理函数上,同时配置中间件。 + +- **结构**: 在 `router/` 下为每个模块创建 `xxx_router.go` 文件,并在 `router/enter.go` 中注册。 + +- **交互**: **必须**通过全局变量 `api.ApiGroupApp` 来引用API层的处理函数。 + +- **路由分组**: 应根据业务需求和权限,合理使用路由组 (`Router.Group()`),并挂载不同的中间件(如鉴权、操作记录等)。 + +#### **5. 初始化层 (`initialize/`)** + +- **职责**: 提供插件资源(数据库、路由、菜单等)的初始化入口,供主程序调用。 + +- **`gorm.go`**: 实现 `InitializeDB` 函数,**必须**调用 `db.AutoMigrate` 自动迁移本插件所有 `model` 的表结构。 + +- **`router.go`**: 实现 `InitializeRouter` 函数,**必须**调用 `router.RouterGroupApp` 中本插件路由的初始化方法,注册所有API路由。 + +- **`menu.go`**: 实现 `InitializeMenu` 函数,负责在数据库中创建或更新本插件的侧边栏菜单、按钮和对应的API权限。 +- viper.go: 加载插件配置文件 +- api.go: 注册API到系统 + + +#### **6. 插件入口 (`plugin.go`) + +- **职责**: 作为插件的唯一入口,实现 GVA 的插件接口,让框架能够识别和加载本插件。 + +- **接口实现**: **必须**定义一个结构体并实现 `system.Plugin` 接口。 + +- **`Register`方法**: 实现 `Register` 方法,该方法接收一个 `*gin.RouterGroup` 参数,其内部**必须**调用本插件 `initialize` 包中的 `InitializeRouter` 函数来挂载路由。 + +- **`RouterPath`方法**: 实现 `RouterPath` 方法,返回该插件所有API的根路径,例如 `"/myPlugin"`。 + +### 模块间引用关系: +- API层引用Service层:在API文件中定义变量如 `var xxxService = service.ServiceGroupApp.XxxService` +- Router层引用API层:在路由函数中使用 `api.ApiGroupApp.XxxApi.XxxMethod` +- Initialize/Router引用Router层:通过 `router.RouterGroupApp.XxxRouter.InitXxxRouter` +- 各模块通过enter.go文件组织和暴露功能,避免循环引用 + +### 代码组织示例: + +1. Service入口 (service/enter.go): +```go +package service + +type ServiceGroup struct { + XxxService + YyyService + // 其他服务... +} + +var ServiceGroupApp = new(ServiceGroup) +``` + +2. API入口 (api/enter.go): +```go +package api + +type ApiGroup struct { + XxxApi + YyyApi + // 其他API... +} + +var ApiGroupApp = new(ApiGroup) +``` + +3. Router入口 (router/enter.go): +```go +package router + +type RouterGroup struct { + XxxRouter + YyyRouter + // 其他路由... +} + +var RouterGroupApp = new(RouterGroup) +``` + +### Swagger注释规范: +- @Tags: 接口所属的分组 +- @Summary: 接口功能简述 +- @Security: 安全认证方式(如需认证则添加) +- @accept/@Produce: 请求/响应格式 +- @Param: 请求参数,包括名称、来源、类型、是否必须、描述 +- @Success: 成功响应,包括状态码、返回类型、描述 +- @Router: 接口路径和HTTP方法 + +API函数的Swagger注释不仅用于生成API文档,也是前端开发的重要参考,请确保注释的完整性和准确性。 + + +--- + +### **开发工作流** + +1. **接收任务**: 我会向你下达一个具体的功能插件开发任务,例如:“请为项目创建一个‘商品管理 (Product)’插件”。 + +2. **【第一步】模型设计 (奠定基础)**: + + - 你的**首要行动**是分析需求,设计并提供 `model` 和 `model/request` 下的所有 Go 结构体定义。这是后续所有开发的基础。 + +3. **【第二步】自下而上,分层实现**: + - 具体项目结构可以参考:server/plugin/announcement 这个插件,非常经典! + + - 在模型确认后,你将按照 `Service -> API -> Router` 的顺序,逐层生成代码。 + + - 确保每一层的代码都完整、健壮,并严格遵守上述规范。 + +4. **【第三步】插件初始化与注册**: + + - 在完成核心功能层的代码后,你将生成 `initialize/` 目录下的相关初始化文件(如 `db.go`, `router.go`)以及插件的主入口文件 `plugin.go`。 + +5. **【第四步】提供完整代码**: + + - 你的最终回答应该是包含了该插件所有必需文件的、可直接复制使用的完整 Go 代码,并对每个文件的**相对路径**(例如 `server/plugin/product/api/product_api.go`)和用途进行清晰的说明。 + + +--- + +## **前端开发规范** + +### **角色与目标** + +你是一名资深的 Vue.js 前端开发专家,**专精于 `gin-vue-admin` (GVA) 框架的前端架构与开发范式**。 + +你的核心任务是,根据需求开发**完整、生产级别的前端功能模块或插件**。你必须严格遵循 GVA 的前端架构、代码规范和核心设计模式,确保你生成的每一部分代码都能无缝集成到现有项目中。 + +### **核心开发指令:绝不可违背的原则** + +#### 前端规则 + +在编写任何前端代码之前,你必须将以下 GVA 的核心设计原则作为最高行为准则: + +1. **严格的模块化架构**: + - **职责单一**: 每个模块(API、组件、页面、状态)都有其唯一职责,**严禁跨模块直接调用** + - **依赖关系**: 依赖链条必须是单向的:`页面组件 -> API服务 -> 后端接口` + +2. **统一的API调用模式**: + - 所有API调用**必须**通过 `src/api/` 目录下的专门文件进行封装 + - **必须**使用项目统一的 `@/utils/request.js` 进行HTTP请求 + - API函数**必须**包含完整的JSDoc注释,描述接口功能、参数和返回值 + +3. **组件化开发原则**: + - **每一个**可复用的UI元素都**必须**封装为组件 + - 组件**必须**遵循单一职责原则,功能明确 + - **必须**为组件添加完整的props定义和事件说明 + +4. **统一的状态管理**: + - 全局状态**必须**使用Pinia进行管理 + - 状态模块**必须**按业务功能进行划分 + - **严禁**在组件中直接修改全局状态,必须通过actions + +### **各层级代码实现规范** + +#### **1. API层 (`src/api/`)** + +- **职责**: 封装所有后端API调用,提供统一的接口服务 +- **结构**: 按业务模块创建API文件,如 `user.js`、`menu.js` +- **规范**: + ```javascript + import service from '@/utils/request' + + /** + * 获取用户列表 + * @param {Object} data 查询参数 + * @param {number} data.page 页码 + * @param {number} data.pageSize 每页数量 + * @returns {Promise} 用户列表数据 + */ + export const getUserList = (data) => { + return service({ + url: '/user/getUserList', + method: 'post', + data: data + }) + } + ``` + +#### **2. 组件层 (`src/components/`)** + +- **职责**: 提供可复用的UI组件 +- **结构**: 按功能分类组织,每个组件一个文件夹 +- **规范**: + ```vue + + + + ``` + +#### **3. 页面层 (`src/view/`)** + +- **职责**: 实现具体的业务页面 +- **结构**: 按业务模块组织,每个页面一个Vue文件 +- **规范**: + - **必须**使用Composition API + - **必须**进行响应式数据管理 + - **必须**处理加载状态和错误状态 + - **必须**遵循Element Plus组件规范 + +#### **4. 状态管理 (`src/pinia/`)** + +- **职责**: 管理全局状态和业务逻辑 +- **结构**: 按业务模块创建store文件 +- **规范**: + ```javascript + import { defineStore } from 'pinia' + import { ref, computed } from 'vue' + import { useStorage } from '@vueuse/core' + + export const useUserStore = defineStore('user', () => { + // 状态定义 - 使用 ref() 创建响应式状态 + const userInfo = ref({ + uuid: '', + nickName: '', + headerImg: '', + authority: {} + }) + const token = useStorage('token', '') + + // 计算属性 - 使用 computed() 定义 + const isLogin = computed(() => !!token.value) + + // 方法定义 - 直接定义函数作为 actions + const setUserInfo = (val) => { + userInfo.value = val + } + + const setToken = (val) => { + token.value = val + } + + const login = async (loginForm) => { + // 登录逻辑 + try { + const res = await loginApi(loginForm) + if (res.code === 0) { + setUserInfo(res.data.user) + setToken(res.data.token) + return true + } + return false + } catch (error) { + console.error('Login error:', error) + return false + } + } + + const logout = async () => { + // 登出逻辑 + token.value = '' + userInfo.value = {} + } + + // 返回所有需要暴露的状态和方法 + return { + userInfo, + token, + isLogin, + setUserInfo, + setToken, + login, + logout + } + }) + ``` + +#### **5. 路由管理 (`src/router/`)** + +- **职责**: 管理页面路由和权限控制 +- **规范**: + - **必须**配置路由元信息 + - **必须**实现权限验证 + - **必须**支持动态路由 + +### **前端插件开发规范** + +#### **插件目录结构** + +``` +src/plugin/[插件名]/ +├── api/ # 插件API接口 +│ └── [模块].js +├── components/ # 插件组件(可选) +│ └── [组件名].vue +├── view/ # 插件页面 +│ └── [页面名].vue +├── form/ # 插件表单(可选) +│ └── [表单名].vue +└── index.js # 插件入口文件(可选) +``` + +#### **插件开发原则** + +1. **独立性**: 插件应该是自包含的,不依赖其他业务模块 +2. **可配置性**: 插件应该支持配置化,便于定制 +3. **可扩展性**: 插件应该预留扩展接口 +4. **一致性**: 插件UI风格应与主系统保持一致 + +### **代码质量要求** + +1. **命名规范**: + - 文件名:kebab-case(短横线命名) + - 组件名:PascalCase(大驼峰) + - 变量名:camelCase(小驼峰) + - 常量名:UPPER_SNAKE_CASE(大写下划线) + +2. **注释规范**: + - **必须**为所有API函数添加JSDoc注释 + - **必须**为复杂组件添加功能说明 + - **必须**为关键业务逻辑添加行内注释 + +3. **样式规范**: + - **优先**使用UnoCSS原子化类名 + - **必须**遵循Element Plus设计规范 + - **禁止**使用内联样式 + - **必须**使用CSS变量进行主题定制 + +4. **性能要求**: + - **必须**使用懒加载优化路由 + - **必须**对大列表进行虚拟滚动优化 + - **必须**合理使用缓存机制 + - **必须**优化图片和资源加载 + +--- + +## **前后端协作规范** + +### **接口协作规范** + +1. **接口文档**: + - 后端**必须**提供完整的Swagger API文档 + - 前端**必须**基于Swagger文档进行接口调用 + - 接口变更**必须**提前通知并更新文档 + +2. **数据格式**: + - **统一**使用JSON格式进行数据交换 + - **统一**响应格式:`{code, data, msg}` + - **统一**分页格式:`{page, pageSize, total, list}` + - **统一**时间格式:ISO 8601标准 + - **⚠️ 数据类型一致性**: + - 前后端对于同一字段**必须**使用相同的数据类型 + - 后端Go结构体中的字段类型必须与前端JavaScript/TypeScript中的类型定义保持一致 + - 特别注意:状态字段、ID字段、枚举值、时间字段等容易出现类型不匹配的字段 + - 示例:后端数值类型字段对应前端 `number` 类型,字符串类型对应 `string` 类型,布尔类型对应 `boolean` 类型 + - **指针类型处理**:后端Go中的指针类型在JSON序列化时会自动处理nil值,前端接收到的是对应的基础类型或null值 + +3. **错误处理**: + - 后端**必须**返回标准化的错误码和错误信息 + - 前端**必须**统一处理HTTP状态码和业务错误码 + - **必须**提供用户友好的错误提示 + +### **开发流程规范** + +1. **需求分析阶段**: + - 确定功能需求和接口设计 + - 定义数据模型和业务流程 + - 制定前后端开发计划 + +2. **开发阶段**: + - 后端优先开发API接口 + - 前端基于Mock数据进行并行开发 + - 定期进行接口联调测试 + +3. **测试阶段**: + - 单元测试:前后端各自负责 + - 集成测试:前后端协作完成 + - 用户验收测试:产品团队主导 + +### **版本管理规范** + +1. **分支策略**: + - `main`:生产环境分支 + - `develop`:开发环境分支 + - `feature/*`:功能开发分支 + - `hotfix/*`:紧急修复分支 + +2. **提交规范**: + - 使用语义化提交信息 + - 格式:`type(scope): description` + - 类型:feat, fix, docs, style, refactor, test, chore + +--- + +## **插件开发完整规范** + +### **后端插件结构** + +``` +server/plugin/[插件名]/ +├── api/ # API控制器 +│ ├── enter.go # API组入口 +│ └── [模块].go # 具体API实现 +├── config/ # 插件配置 +│ └── config.go +├── initialize/ # 初始化模块 +│ ├── api.go # API注册 +│ ├── gorm.go # 数据库初始化 +│ ├── menu.go # 菜单初始化 +│ ├── router.go # 路由初始化 +│ └── viper.go # 配置初始化 +├── model/ # 数据模型 +│ ├── [模型].go # 数据库模型 +│ └── request/ # 请求模型 +├── router/ # 路由定义 +│ ├── enter.go # 路由组入口 +│ └── [模块].go # 具体路由 +├── service/ # 业务服务 +│ ├── enter.go # 服务组入口 +│ └── [模块].go # 具体服务 +└── plugin.go # 插件入口 +``` + +### **前端插件结构** + +``` +web/src/plugin/[插件名]/ +├── api/ # API接口 +│ └── [模块].js +├── components/ # 插件组件 +│ └── [组件].vue +├── view/ # 插件页面 +│ └── [页面].vue +├── form/ # 表单组件 +│ └── [表单].vue +└── config.js # 插件配置 +``` + +### **插件开发工作流** + +1. **【第一步】需求分析**: + - 明确插件功能和业务需求 + - 设计数据模型和接口规范 + - 规划前端页面和交互流程 + +2. **【第二步】后端开发**: + - 创建数据模型和请求模型 + - 实现服务层业务逻辑 + - 开发API控制器和路由 + - 编写初始化和配置代码 + +3. **【第三步】前端开发**: + - 创建API接口封装 + - 开发页面组件和表单 + - 实现业务逻辑和状态管理 + - 集成到主系统菜单 + +4. **【第四步】测试集成**: + - 单元测试和集成测试 + - 前后端联调测试 + - 用户体验测试 + - 性能和安全测试 + +### **插件质量标准** + +1. **功能完整性**: 插件功能完整,满足业务需求 +2. **代码质量**: 代码规范,注释完整,易于维护 +3. **数据类型一致性**: 前后端数据模型字段类型保持严格一致,避免类型转换错误 +4. **性能表现**: 响应速度快,资源占用合理 +5. **用户体验**: 界面友好,操作流畅,错误处理完善 +6. **兼容性**: 与主系统兼容,不影响其他功能 +7. **安全性**: 数据安全,权限控制,防止安全漏洞 + +--- + +### **建议和方案** + +基于以上规范,建议AI在开发gin-vue-admin项目时: + +1. **严格遵循分层架构**:确保前后端代码都按照规定的层次结构组织 +2. **保持代码一致性**:使用统一的命名规范、注释格式和代码风格 +3. **注重文档完整性**:确保API文档、代码注释和使用说明的完整性 +4. **优化用户体验**:关注页面加载速度、交互流畅性和错误处理 +5. **考虑扩展性**:设计时预留扩展接口,便于后续功能增强 +6. **重视安全性**:实现完善的权限控制和数据验证机制 \ No newline at end of file diff --git a/server/api/v1/example/exa_breakpoint_continue.go b/server/api/v1/example/exa_breakpoint_continue.go index 8f39cb160..565ffe6bd 100644 --- a/server/api/v1/example/exa_breakpoint_continue.go +++ b/server/api/v1/example/exa_breakpoint_continue.go @@ -5,6 +5,7 @@ import ( "io" "mime/multipart" "strconv" + "strings" "github.com/flipped-aurora/gin-vue-admin/server/model/example" @@ -135,6 +136,11 @@ func (b *FileUploadAndDownloadApi) RemoveChunk(c *gin.Context) { response.FailWithMessage(err.Error(), c) return } + // 路径穿越拦截 + if strings.Contains(file.FilePath, "..") || strings.Contains(file.FilePath, "../") || strings.Contains(file.FilePath, "./") || strings.Contains(file.FilePath, ".\\") { + response.FailWithMessage("非法路径,禁止删除", c) + return + } err = utils.RemoveChunk(file.FileMd5) if err != nil { global.GVA_LOG.Error("缓存切片删除失败!", zap.Error(err)) diff --git a/server/api/v1/system/enter.go b/server/api/v1/system/enter.go index dbc721b16..e6d37e2c0 100644 --- a/server/api/v1/system/enter.go +++ b/server/api/v1/system/enter.go @@ -23,6 +23,7 @@ type ApiGroup struct { AutoCodeTemplateApi SysParamsApi SysVersionApi + SysErrorApi } var ( @@ -46,4 +47,5 @@ var ( autoCodeHistoryService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeHistory autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService + sysErrorService = service.ServiceGroupApp.SystemServiceGroup.SysErrorService ) diff --git a/server/api/v1/system/sys_auto_code.go b/server/api/v1/system/sys_auto_code.go index 02f458bb3..1283b0208 100644 --- a/server/api/v1/system/sys_auto_code.go +++ b/server/api/v1/system/sys_auto_code.go @@ -1,15 +1,10 @@ package system import ( - "fmt" "github.com/flipped-aurora/gin-vue-admin/server/model/common" - "github.com/goccy/go-json" - "io" - "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" - "github.com/flipped-aurora/gin-vue-admin/server/utils/request" "github.com/gin-gonic/gin" "go.uber.org/zap" ) @@ -108,48 +103,15 @@ func (autoApi *AutoCodeApi) GetColumn(c *gin.Context) { func (autoApi *AutoCodeApi) LLMAuto(c *gin.Context) { var llm common.JSONMap - err := c.ShouldBindJSON(&llm) - if err != nil { + if err := c.ShouldBindJSON(&llm); err != nil { response.FailWithMessage(err.Error(), c) return } - if global.GVA_CONFIG.AutoCode.AiPath == "" { - response.FailWithMessage("请先前往插件市场个人中心获取AiPath并填入config.yaml中", c) - return - } - - path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", llm["mode"])) - res, err := request.HttpRequest( - path, - "POST", - nil, - nil, - llm, - ) + data, err := autoCodeService.LLMAuto(c.Request.Context(), llm) if err != nil { global.GVA_LOG.Error("大模型生成失败!", zap.Error(err)) response.FailWithMessage("大模型生成失败"+err.Error(), c) return } - var resStruct response.Response - b, err := io.ReadAll(res.Body) - defer res.Body.Close() - if err != nil { - global.GVA_LOG.Error("大模型生成失败!", zap.Error(err)) - response.FailWithMessage("大模型生成失败"+err.Error(), c) - return - } - err = json.Unmarshal(b, &resStruct) - if err != nil { - global.GVA_LOG.Error("大模型生成失败!", zap.Error(err)) - response.FailWithMessage("大模型生成失败"+err.Error(), c) - return - } - - if resStruct.Code == 7 { - global.GVA_LOG.Error("大模型生成失败!"+resStruct.Msg, zap.Error(err)) - response.FailWithMessage("大模型生成失败"+resStruct.Msg, c) - return - } - response.OkWithData(resStruct.Data, c) + response.OkWithData(data, c) } diff --git a/server/api/v1/system/sys_dictionary.go b/server/api/v1/system/sys_dictionary.go index 5e6ca79c3..9673e7f28 100644 --- a/server/api/v1/system/sys_dictionary.go +++ b/server/api/v1/system/sys_dictionary.go @@ -135,3 +135,57 @@ func (s *DictionaryApi) GetSysDictionaryList(c *gin.Context) { } response.OkWithDetailed(list, "获取成功", c) } + +// ExportSysDictionary +// @Tags SysDictionary +// @Summary 导出字典JSON(包含字典详情) +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query system.SysDictionary true "字典ID" +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "导出字典JSON" +// @Router /sysDictionary/exportSysDictionary [get] +func (s *DictionaryApi) ExportSysDictionary(c *gin.Context) { + var dictionary system.SysDictionary + err := c.ShouldBindQuery(&dictionary) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + if dictionary.ID == 0 { + response.FailWithMessage("字典ID不能为空", c) + return + } + exportData, err := dictionaryService.ExportSysDictionary(dictionary.ID) + if err != nil { + global.GVA_LOG.Error("导出失败!", zap.Error(err)) + response.FailWithMessage("导出失败", c) + return + } + response.OkWithDetailed(exportData, "导出成功", c) +} + +// ImportSysDictionary +// @Tags SysDictionary +// @Summary 导入字典JSON(包含字典详情) +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.ImportSysDictionaryRequest true "字典JSON数据" +// @Success 200 {object} response.Response{msg=string} "导入字典" +// @Router /sysDictionary/importSysDictionary [post] +func (s *DictionaryApi) ImportSysDictionary(c *gin.Context) { + var req request.ImportSysDictionaryRequest + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = dictionaryService.ImportSysDictionary(req.Json) + if err != nil { + global.GVA_LOG.Error("导入失败!", zap.Error(err)) + response.FailWithMessage("导入失败: "+err.Error(), c) + return + } + response.OkWithMessage("导入成功", c) +} diff --git a/server/api/v1/system/sys_error.go b/server/api/v1/system/sys_error.go new file mode 100644 index 000000000..966cd5088 --- /dev/null +++ b/server/api/v1/system/sys_error.go @@ -0,0 +1,199 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SysErrorApi struct{} + +// CreateSysError 创建错误日志 +// @Tags SysError +// @Summary 创建错误日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body system.SysError true "创建错误日志" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /sysError/createSysError [post] +func (sysErrorApi *SysErrorApi) CreateSysError(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var sysError system.SysError + err := c.ShouldBindJSON(&sysError) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = sysErrorService.CreateSysError(ctx, &sysError) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:"+err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// DeleteSysError 删除错误日志 +// @Tags SysError +// @Summary 删除错误日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body system.SysError true "删除错误日志" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /sysError/deleteSysError [delete] +func (sysErrorApi *SysErrorApi) DeleteSysError(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + err := sysErrorService.DeleteSysError(ctx, ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteSysErrorByIds 批量删除错误日志 +// @Tags SysError +// @Summary 批量删除错误日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /sysError/deleteSysErrorByIds [delete] +func (sysErrorApi *SysErrorApi) DeleteSysErrorByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + IDs := c.QueryArray("IDs[]") + err := sysErrorService.DeleteSysErrorByIds(ctx, IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// UpdateSysError 更新错误日志 +// @Tags SysError +// @Summary 更新错误日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body system.SysError true "更新错误日志" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /sysError/updateSysError [put] +func (sysErrorApi *SysErrorApi) UpdateSysError(c *gin.Context) { + // 从ctx获取标准context进行业务行为 + ctx := c.Request.Context() + + var sysError system.SysError + err := c.ShouldBindJSON(&sysError) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = sysErrorService.UpdateSysError(ctx, sysError) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:"+err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// FindSysError 用id查询错误日志 +// @Tags SysError +// @Summary 用id查询错误日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param ID query uint true "用id查询错误日志" +// @Success 200 {object} response.Response{data=system.SysError,msg=string} "查询成功" +// @Router /sysError/findSysError [get] +func (sysErrorApi *SysErrorApi) FindSysError(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + resysError, err := sysErrorService.GetSysError(ctx, ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithData(resysError, c) +} + +// GetSysErrorList 分页获取错误日志列表 +// @Tags SysError +// @Summary 分页获取错误日志列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query systemReq.SysErrorSearch true "分页获取错误日志列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /sysError/getSysErrorList [get] +func (sysErrorApi *SysErrorApi) GetSysErrorList(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo systemReq.SysErrorSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := sysErrorService.GetSysErrorInfoList(ctx, pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetSysErrorSolution 触发错误日志的异步处理 +// @Tags SysError +// @Summary 根据ID触发处理:标记为处理中,1分钟后自动改为处理完成 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param id query string true "错误日志ID" +// @Success 200 {object} response.Response{msg=string} "处理已提交" +// @Router /sysError/getSysErrorSolution [get] +func (sysErrorApi *SysErrorApi) GetSysErrorSolution(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 兼容 id 与 ID 两种参数 + ID := c.Query("id") + if ID == "" { + response.FailWithMessage("缺少参数: id", c) + return + } + + err := sysErrorService.GetSysErrorSolution(ctx, ID) + if err != nil { + global.GVA_LOG.Error("处理触发失败!", zap.Error(err)) + response.FailWithMessage("处理触发失败:"+err.Error(), c) + return + } + + response.OkWithMessage("已提交至AI处理", c) +} diff --git a/server/api/v1/system/sys_export_template.go b/server/api/v1/system/sys_export_template.go index 0d61588ee..604309ddc 100644 --- a/server/api/v1/system/sys_export_template.go +++ b/server/api/v1/system/sys_export_template.go @@ -50,6 +50,34 @@ type SysExportTemplateApi struct { var sysExportTemplateService = service.ServiceGroupApp.SystemServiceGroup.SysExportTemplateService +// PreviewSQL 预览最终生成的SQL +// @Tags SysExportTemplate +// @Summary 预览最终生成的SQL(不执行查询,仅返回SQL字符串) +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param templateID query string true "导出模板ID" +// @Param params query string false "查询参数编码字符串,参考 ExportExcel 组件" +// @Success 200 {object} response.Response{data=map[string]string} "获取成功" +// @Router /sysExportTemplate/previewSQL [get] +func (sysExportTemplateApi *SysExportTemplateApi) PreviewSQL(c *gin.Context) { + templateID := c.Query("templateID") + if templateID == "" { + response.FailWithMessage("模板ID不能为空", c) + return + } + + // 直接复用导出接口的参数组织方式:使用 URL Query,其中 params 为内部编码的查询字符串 + queryParams := c.Request.URL.Query() + + if sqlPreview, err := sysExportTemplateService.PreviewSQL(templateID, queryParams); err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败", c) + } else { + response.OkWithData(gin.H{"sql": sqlPreview}, c) + } +} + // CreateSysExportTemplate 创建导出模板 // @Tags SysExportTemplate // @Summary 创建导出模板 diff --git a/server/core/internal/zap_core.go b/server/core/internal/zap_core.go index 4648e60cb..2e5455745 100644 --- a/server/core/internal/zap_core.go +++ b/server/core/internal/zap_core.go @@ -1,11 +1,18 @@ package internal import ( - "github.com/flipped-aurora/gin-vue-admin/server/global" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "os" - "time" + "context" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service" + astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + "github.com/flipped-aurora/gin-vue-admin/server/utils/stacktrace" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "strings" + "time" ) type ZapCore struct { @@ -54,13 +61,75 @@ func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapco } func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { - for i := 0; i < len(fields); i++ { - if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" { - syncer := z.WriteSyncer(fields[i].String) - z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level) - } - } - return z.Core.Write(entry, fields) + for i := 0; i < len(fields); i++ { + if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" { + syncer := z.WriteSyncer(fields[i].String) + z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level) + } + } + // 先写入原日志目标 + err := z.Core.Write(entry, fields) + + // 捕捉 Error 及以上级别日志并入库,且可提取 zap.Error(err) 的错误内容 + if entry.Level >= zapcore.ErrorLevel { + // 避免与 GORM zap 写入互相递归:跳过由 gorm logger writer 触发的日志 + if strings.Contains(entry.Caller.File, "gorm_logger_writer.go") { + return err + } + // 避免重复记录 panic 恢复日志,panic 由 GinRecovery 单独捕捉入库 + if strings.Contains(entry.Message, "[Recovery from panic]") { + return err + } + + form := "后端" + level := entry.Level.String() + // 生成基础信息 + info := entry.Message + + // 提取 zap.Error(err) 内容 + var errStr string + for i := 0; i < len(fields); i++ { + f := fields[i] + if f.Type == zapcore.ErrorType || f.Key == "error" || f.Key == "err" { + if f.Interface != nil { + errStr = fmt.Sprintf("%v", f.Interface) + } else if f.String != "" { + errStr = f.String + } + break + } + } + if errStr != "" { + info = fmt.Sprintf("%s | 错误: %s", info, errStr) + } + + // 附加来源与堆栈信息 + if entry.Caller.File != "" { + info = fmt.Sprintf("%s \n 源文件:%s:%d", info, entry.Caller.File, entry.Caller.Line) + } + stack := entry.Stack + if stack != "" { + info = fmt.Sprintf("%s \n 调用栈:%s", info, stack) + // 解析最终业务调用方,并提取其方法源码 + if frame, ok := stacktrace.FindFinalCaller(stack); ok { + fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line) + if exErr == nil { + info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc) + } else { + info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr) + } + } + } + + // 使用后台上下文,避免依赖 gin.Context + ctx := context.Background() + _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(ctx, &system.SysError{ + Form: &form, + Info: &info, + Level: level, + }) + } + return err } func (z *ZapCore) Sync() error { diff --git a/server/core/zap.go b/server/core/zap.go index d7e08a44a..09aa16528 100644 --- a/server/core/zap.go +++ b/server/core/zap.go @@ -1,13 +1,13 @@ package core import ( - "fmt" - "github.com/flipped-aurora/gin-vue-admin/server/core/internal" - "github.com/flipped-aurora/gin-vue-admin/server/global" - "github.com/flipped-aurora/gin-vue-admin/server/utils" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "os" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/core/internal" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" ) // Zap 获取 zap.Logger @@ -24,9 +24,13 @@ func Zap() (logger *zap.Logger) { core := internal.NewZapCore(levels[i]) cores = append(cores, core) } - logger = zap.New(zapcore.NewTee(cores...)) + // 构建基础 logger(错误级别的入库逻辑已在自定义 ZapCore 中处理) + logger = zap.New(zapcore.NewTee(cores...)) + // 启用 Error 及以上级别的堆栈捕捉,确保 entry.Stack 可用 + opts := []zap.Option{zap.AddStacktrace(zapcore.ErrorLevel)} if global.GVA_CONFIG.Zap.ShowLine { - logger = logger.WithOptions(zap.AddCaller()) + opts = append(opts, zap.AddCaller()) } + logger = logger.WithOptions(opts...) return logger } diff --git a/server/global/version.go b/server/global/version.go index c3b888219..855ec4fbe 100644 --- a/server/global/version.go +++ b/server/global/version.go @@ -4,7 +4,7 @@ package global // 目前只有Version正式使用 其余为预留 const ( // Version 当前版本号 - Version = "v2.8.6" + Version = "v2.8.7" // AppName 应用名称 AppName = "Gin-Vue-Admin" // Description 应用描述 diff --git a/server/go.mod b/server/go.mod index a93759827..1b04d3157 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,8 +1,8 @@ module github.com/flipped-aurora/gin-vue-admin/server -go 1.23 +go 1.24.0 -toolchain go1.23.9 +toolchain go1.24.2 require ( github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible @@ -20,6 +20,7 @@ require ( github.com/gookit/color v1.5.4 github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible + github.com/localrivet/gomcp v1.7.2 github.com/mark3labs/mcp-go v0.41.1 github.com/mholt/archives v0.1.1 github.com/minio/minio-go/v7 v7.0.84 @@ -43,9 +44,9 @@ require ( go.mongodb.org/mongo-driver v1.17.2 go.uber.org/automaxprocs v1.6.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.32.0 - golang.org/x/sync v0.10.0 - golang.org/x/text v0.21.0 + golang.org/x/crypto v0.37.0 + golang.org/x/sync v0.13.0 + golang.org/x/text v0.24.0 gorm.io/datatypes v1.2.5 gorm.io/driver/mysql v1.5.7 gorm.io/driver/postgres v1.5.11 @@ -77,6 +78,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/eclipse/paho.mqtt.golang v1.5.0 // indirect github.com/emirpasic/gods v1.12.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gammazero/toposort v0.1.1 // indirect @@ -91,12 +93,16 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.24.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/gobwas/ws v1.4.0 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -111,10 +117,11 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/localrivet/wilduri v0.0.0-20250504021349-6ce732e97cca // indirect github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/magiconair/properties v1.8.9 // indirect github.com/mailru/easyjson v0.9.0 // indirect @@ -128,6 +135,9 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/mozillazg/go-httpheader v0.4.0 // indirect + github.com/nats-io/nats.go v1.42.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nwaples/rardecode/v2 v2.1.0 // indirect github.com/otiai10/mint v1.6.3 // indirect @@ -172,11 +182,13 @@ require ( golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect golang.org/x/image v0.23.0 // indirect golang.org/x/mod v0.22.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.32.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.29.0 // indirect - google.golang.org/protobuf v1.36.3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect + google.golang.org/grpc v1.72.1 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/hints v1.1.2 // indirect diff --git a/server/go.sum b/server/go.sum index 33301c5a2..f19b7f040 100644 --- a/server/go.sum +++ b/server/go.sum @@ -113,6 +113,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dzwvip/gorm-oracle v0.1.2 h1:811aFDY7oDfKWHc0Z0lHdXzzr89EmKBSwc/jLJ8GU5g= github.com/dzwvip/gorm-oracle v0.1.2/go.mod h1:TbF7idnO9UgGpJ0qJpDZby1/wGquzP5GYof88ScBITE= +github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= +github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -139,6 +141,10 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -168,6 +174,12 @@ github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= @@ -198,6 +210,8 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -234,6 +248,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -288,8 +304,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= @@ -315,6 +331,10 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/localrivet/gomcp v1.7.2 h1:dJtKCvbI8Gr/L0N7cZlo3XOMyCc7GCahdtbI/Y/K9Ig= +github.com/localrivet/gomcp v1.7.2/go.mod h1:7MBYbqypfmEzDuLWdz2FSkAeX19ZX9cSe6qD6mZgOEc= +github.com/localrivet/wilduri v0.0.0-20250504021349-6ce732e97cca h1:q0KYRv+ktfm8KnMROXcRNJEnfXSI3NZ45aMC8T/mg14= +github.com/localrivet/wilduri v0.0.0-20250504021349-6ce732e97cca/go.mod h1:8B25VIq6WUPYAdY3aodQnj/hDNmYTcPgzzc7ZZ1++NI= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= @@ -358,6 +378,12 @@ github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= github.com/mozillazg/go-httpheader v0.4.0 h1:aBn6aRXtFzyDLZ4VIRLsZbbJloagQfMnCiYgOq6hK4w= github.com/mozillazg/go-httpheader v0.4.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA= +github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM= +github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= @@ -511,6 +537,18 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -541,8 +579,8 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -607,8 +645,8 @@ golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -626,8 +664,9 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -661,8 +700,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -696,8 +735,9 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= @@ -761,6 +801,8 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -768,8 +810,10 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= +google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/server/initialize/gorm.go b/server/initialize/gorm.go index 2aacb84ab..d044580e0 100644 --- a/server/initialize/gorm.go +++ b/server/initialize/gorm.go @@ -62,6 +62,7 @@ func RegisterTables() { system.JoinTemplate{}, system.SysParams{}, system.SysVersion{}, + system.SysError{}, example.ExaFile{}, example.ExaCustomer{}, diff --git a/server/initialize/router.go b/server/initialize/router.go index d297df874..e5334d60d 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -34,11 +34,12 @@ func (fs justFilesFilesystem) Open(name string) (http.File, error) { // 初始化总路由 func Routers() *gin.Engine { - Router := gin.New() - Router.Use(gin.Recovery()) - if gin.Mode() == gin.DebugMode { - Router.Use(gin.Logger()) - } + Router := gin.New() + // 使用自定义的 Recovery 中间件,记录 panic 并入库 + Router.Use(middleware.GinRecovery(true)) + if gin.Mode() == gin.DebugMode { + Router.Use(gin.Logger()) + } if !global.GVA_CONFIG.MCP.Separate { @@ -107,6 +108,7 @@ func Routers() *gin.Engine { systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 按钮权限管理 systemRouter.InitSysExportTemplateRouter(PrivateGroup, PublicGroup) // 导出模板 systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理 + systemRouter.InitSysErrorRouter(PrivateGroup, PublicGroup) // 错误日志 exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由 exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由 exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类 diff --git a/server/main.go b/server/main.go index b04da36a4..ab0693a53 100644 --- a/server/main.go +++ b/server/main.go @@ -21,7 +21,7 @@ import ( // @Tag.Description 用户 // @title Gin-Vue-Admin Swagger API接口文档 -// @version v2.8.6 +// @version v2.8.7 // @description 使用gin+vue进行极速开发的全栈开发基础平台 // @securityDefinitions.apikey ApiKeyAuth // @in header diff --git a/server/middleware/error.go b/server/middleware/error.go index f68b7a562..871efb3c8 100644 --- a/server/middleware/error.go +++ b/server/middleware/error.go @@ -1,12 +1,12 @@ package middleware import ( - "net" - "net/http" - "net/http/httputil" - "os" - "runtime/debug" - "strings" + "net" + "net/http" + "net/http/httputil" + "os" + "runtime/debug" + "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/gin-gonic/gin" diff --git a/server/middleware/jwt.go b/server/middleware/jwt.go index 7715ed9c7..f4c029625 100644 --- a/server/middleware/jwt.go +++ b/server/middleware/jwt.go @@ -2,11 +2,12 @@ package middleware import ( "errors" + "strconv" + "time" + "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/golang-jwt/jwt/v5" - "strconv" - "time" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" "github.com/gin-gonic/gin" @@ -59,7 +60,7 @@ func JWTAuth() gin.HandlerFunc { newClaims, _ := j.ParseToken(newToken) c.Header("new-token", newToken) c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10)) - utils.SetToken(c, newToken, int(dr.Seconds())) + utils.SetToken(c, newToken, int(dr.Seconds()/60)) if global.GVA_CONFIG.System.UseMultipoint { // 记录新的活跃jwt _ = utils.SetRedisJWT(newToken, newClaims.Username) diff --git a/server/model/system/request/sys_dictionary.go b/server/model/system/request/sys_dictionary.go index 2b5fa1a9d..5a84796ec 100644 --- a/server/model/system/request/sys_dictionary.go +++ b/server/model/system/request/sys_dictionary.go @@ -3,3 +3,7 @@ package request type SysDictionarySearch struct { Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)"` // 字典名(中) } + +type ImportSysDictionaryRequest struct { + Json string `json:"json" binding:"required"` // JSON字符串 +} diff --git a/server/model/system/request/sys_error.go b/server/model/system/request/sys_error.go new file mode 100644 index 000000000..ca1044d98 --- /dev/null +++ b/server/model/system/request/sys_error.go @@ -0,0 +1,14 @@ + +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "time" +) + +type SysErrorSearch struct{ + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` + Form *string `json:"form" form:"form"` + Info *string `json:"info" form:"info"` + request.PageInfo +} diff --git a/server/model/system/sys_error.go b/server/model/system/sys_error.go new file mode 100644 index 000000000..7f5671b04 --- /dev/null +++ b/server/model/system/sys_error.go @@ -0,0 +1,21 @@ +// 自动生成模板SysError +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 错误日志 结构体 SysError +type SysError struct { + global.GVA_MODEL + Form *string `json:"form" form:"form" gorm:"comment:错误来源;column:form;type:text;" binding:"required"` //错误来源 + Info *string `json:"info" form:"info" gorm:"comment:错误内容;column:info;type:text;"` //错误内容 + Level string `json:"level" form:"level" gorm:"comment:日志等级;column:level;"` + Solution *string `json:"solution" form:"solution" gorm:"comment:解决方案;column:solution;type:text"` //解决方案 + Status string `json:"status" form:"status" gorm:"comment:处理状态;column:status;type:varchar(20);default:未处理;"` //处理状态:未处理/处理中/处理完成 +} + +// TableName 错误日志 SysError自定义表名 sys_error +func (SysError) TableName() string { + return "sys_error" +} diff --git a/server/plugin/plugin-tool/utils/check.go b/server/plugin/plugin-tool/utils/check.go index 4ea21921e..82e31a0bf 100644 --- a/server/plugin/plugin-tool/utils/check.go +++ b/server/plugin/plugin-tool/utils/check.go @@ -1,50 +1,53 @@ package utils import ( - "fmt" + "github.com/pkg/errors" + "go.uber.org/zap" + "gorm.io/gorm" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) -func RegisterApis(apis ...system.SysApi) { - var count int64 - var apiPaths []string - for i := range apis { - apiPaths = append(apiPaths, apis[i].Path) - } - global.GVA_DB.Find(&[]system.SysApi{}, "path in (?)", apiPaths).Count(&count) - if count > 0 { - return - } - err := global.GVA_DB.Create(&apis).Error +func RegisterApis( apis ...system.SysApi) { + err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, api := range apis { + err := tx.Model(system.SysApi{}).Where("path = ? AND method = ? AND api_group = ? ", api.Path, api.Method, api.ApiGroup).FirstOrCreate(&api).Error + if err != nil { + zap.L().Error("注册API失败", zap.Error(err), zap.String("api", api.Path), zap.String("method", api.Method), zap.String("apiGroup", api.ApiGroup)) + return err + } + } + return nil + }) if err != nil { - fmt.Println(err) + zap.L().Error("注册API失败", zap.Error(err)) } } -func RegisterMenus(menus ...system.SysBaseMenu) { - var count int64 - var menuNames []string +func RegisterMenus( menus ...system.SysBaseMenu) { parentMenu := menus[0] otherMenus := menus[1:] - for i := range menus { - menuNames = append(menuNames, menus[i].Name) - } - global.GVA_DB.Find(&[]system.SysBaseMenu{}, "name in (?)", menuNames).Count(&count) - if count > 0 { - return - } - err := global.GVA_DB.Create(&parentMenu).Error - if err != nil { - fmt.Println(err) - } - for i := range otherMenus { + err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { + err := tx.Model(system.SysBaseMenu{}).Where("name = ? ", parentMenu.Name).FirstOrCreate(&parentMenu).Error + if err != nil { + zap.L().Error("注册菜单失败", zap.Error(err)) + return errors.Wrap(err, "注册菜单失败") + } pid := parentMenu.ID - otherMenus[i].ParentId = pid - } - err = global.GVA_DB.Create(&otherMenus).Error + for i := range otherMenus { + otherMenus[i].ParentId = pid + err = tx.Model(system.SysBaseMenu{}).Where("name = ? ", otherMenus[i].Name).FirstOrCreate(&otherMenus[i]).Error + if err != nil { + zap.L().Error("注册菜单失败", zap.Error(err)) + return errors.Wrap(err, "注册菜单失败") + } + } + + return nil + }) if err != nil { - fmt.Println(err) + zap.L().Error("注册菜单失败", zap.Error(err)) } + } diff --git a/server/router/system/enter.go b/server/router/system/enter.go index a0a23f54a..970f6630b 100644 --- a/server/router/system/enter.go +++ b/server/router/system/enter.go @@ -20,6 +20,7 @@ type RouterGroup struct { SysExportTemplateRouter SysParamsRouter SysVersionRouter + SysErrorRouter } var ( @@ -43,4 +44,5 @@ var ( autoCodeTemplateApi = api.ApiGroupApp.SystemApiGroup.AutoCodeTemplateApi exportTemplateApi = api.ApiGroupApp.SystemApiGroup.SysExportTemplateApi sysVersionApi = api.ApiGroupApp.SystemApiGroup.SysVersionApi + sysErrorApi = api.ApiGroupApp.SystemApiGroup.SysErrorApi ) diff --git a/server/router/system/sys_dictionary.go b/server/router/system/sys_dictionary.go index 41ce85ec9..c95e8ffdd 100644 --- a/server/router/system/sys_dictionary.go +++ b/server/router/system/sys_dictionary.go @@ -14,6 +14,8 @@ func (s *DictionaryRouter) InitSysDictionaryRouter(Router *gin.RouterGroup) { sysDictionaryRouter.POST("createSysDictionary", dictionaryApi.CreateSysDictionary) // 新建SysDictionary sysDictionaryRouter.DELETE("deleteSysDictionary", dictionaryApi.DeleteSysDictionary) // 删除SysDictionary sysDictionaryRouter.PUT("updateSysDictionary", dictionaryApi.UpdateSysDictionary) // 更新SysDictionary + sysDictionaryRouter.POST("importSysDictionary", dictionaryApi.ImportSysDictionary) // 导入SysDictionary + sysDictionaryRouter.GET("exportSysDictionary", dictionaryApi.ExportSysDictionary) // 导出SysDictionary } { sysDictionaryRouterWithoutRecord.GET("findSysDictionary", dictionaryApi.FindSysDictionary) // 根据ID获取SysDictionary diff --git a/server/router/system/sys_error.go b/server/router/system/sys_error.go new file mode 100644 index 000000000..ea3860207 --- /dev/null +++ b/server/router/system/sys_error.go @@ -0,0 +1,28 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysErrorRouter struct{} + +// InitSysErrorRouter 初始化 错误日志 路由信息 +func (s *SysErrorRouter) InitSysErrorRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) { + sysErrorRouter := Router.Group("sysError").Use(middleware.OperationRecord()) + sysErrorRouterWithoutRecord := Router.Group("sysError") + sysErrorRouterWithoutAuth := PublicRouter.Group("sysError") + { + sysErrorRouter.DELETE("deleteSysError", sysErrorApi.DeleteSysError) // 删除错误日志 + sysErrorRouter.DELETE("deleteSysErrorByIds", sysErrorApi.DeleteSysErrorByIds) // 批量删除错误日志 + sysErrorRouter.PUT("updateSysError", sysErrorApi.UpdateSysError) // 更新错误日志 + sysErrorRouter.GET("getSysErrorSolution", sysErrorApi.GetSysErrorSolution) // 触发错误日志处理 + } + { + sysErrorRouterWithoutRecord.GET("findSysError", sysErrorApi.FindSysError) // 根据ID获取错误日志 + sysErrorRouterWithoutRecord.GET("getSysErrorList", sysErrorApi.GetSysErrorList) // 获取错误日志列表 + } + { + sysErrorRouterWithoutAuth.POST("createSysError", sysErrorApi.CreateSysError) // 新建错误日志 + } +} diff --git a/server/router/system/sys_export_template.go b/server/router/system/sys_export_template.go index e7fd5beef..fb82fb167 100644 --- a/server/router/system/sys_export_template.go +++ b/server/router/system/sys_export_template.go @@ -26,6 +26,7 @@ func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.Router sysExportTemplateRouterWithoutRecord.GET("getSysExportTemplateList", exportTemplateApi.GetSysExportTemplateList) // 获取导出模板列表 sysExportTemplateRouterWithoutRecord.GET("exportExcel", exportTemplateApi.ExportExcel) // 获取导出token sysExportTemplateRouterWithoutRecord.GET("exportTemplate", exportTemplateApi.ExportTemplate) // 导出表格模板 + sysExportTemplateRouterWithoutRecord.GET("previewSQL", exportTemplateApi.PreviewSQL) // 预览SQL } { sysExportTemplateRouterWithoutAuth.GET("exportExcelByToken", exportTemplateApi.ExportExcelByToken) // 通过token导出表格 diff --git a/server/service/example/exa_file_upload_download.go b/server/service/example/exa_file_upload_download.go index c5519fc08..536734eee 100644 --- a/server/service/example/exa_file_upload_download.go +++ b/server/service/example/exa_file_upload_download.go @@ -9,6 +9,7 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/model/example" "github.com/flipped-aurora/gin-vue-admin/server/model/example/request" "github.com/flipped-aurora/gin-vue-admin/server/utils/upload" + "gorm.io/gorm" ) //@author: [piexlmax](https://github.com/piexlmax) @@ -107,7 +108,13 @@ func (e *FileUploadAndDownloadService) UploadFile(header *multipart.FileHeader, Key: key, } if noSave == "0" { - return f, e.Upload(f) + // 检查是否已存在相同key的记录 + var existingFile example.ExaFileUploadAndDownload + err = global.GVA_DB.Where("`key` = ?", key).First(&existingFile).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return f, e.Upload(f) + } + return f, err } return f, nil } diff --git a/server/service/system/auto_code_llm.go b/server/service/system/auto_code_llm.go new file mode 100644 index 000000000..76b4fb4b5 --- /dev/null +++ b/server/service/system/auto_code_llm.go @@ -0,0 +1,51 @@ +package system + +import ( + "context" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + commonResp "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils/request" + "github.com/goccy/go-json" + "io" + "strings" +) + +// LLMAuto 调用大模型服务,返回生成结果数据 +// 入参为通用 JSONMap,需包含 mode(例如 ai/butler/eye/painter 等)以及业务 prompt/payload +func (s *AutoCodeService) LLMAuto(ctx context.Context, llm common.JSONMap) (interface{}, error) { + if global.GVA_CONFIG.AutoCode.AiPath == "" { + return nil, errors.New("请先前往插件市场个人中心获取AiPath并填入config.yaml中") + } + + // 构建调用路径:{AiPath} 中的 {FUNC} 由 mode 替换 + mode := fmt.Sprintf("%v", llm["mode"]) // 统一转字符串,避免 nil 造成路径异常 + path := strings.ReplaceAll(global.GVA_CONFIG.AutoCode.AiPath, "{FUNC}", fmt.Sprintf("api/chat/%s", mode)) + + res, err := request.HttpRequest( + path, + "POST", + nil, + nil, + llm, + ) + if err != nil { + return nil, fmt.Errorf("大模型生成失败: %w", err) + } + defer res.Body.Close() + + var resStruct commonResp.Response + b, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("读取大模型响应失败: %w", err) + } + if err = json.Unmarshal(b, &resStruct); err != nil { + return nil, fmt.Errorf("解析大模型响应失败: %w", err) + } + if resStruct.Code == 7 { // 业务约定:7 表示模型生成失败 + return nil, fmt.Errorf("大模型生成失败: %s", resStruct.Msg) + } + return resStruct.Data, nil +} diff --git a/server/service/system/enter.go b/server/service/system/enter.go index d91f2796d..f09123cf3 100644 --- a/server/service/system/enter.go +++ b/server/service/system/enter.go @@ -22,4 +22,5 @@ type ServiceGroup struct { AutoCodePackage autoCodePackage AutoCodeHistory autoCodeHistory AutoCodeTemplate autoCodeTemplate + SysErrorService } diff --git a/server/service/system/sys_dictionary.go b/server/service/system/sys_dictionary.go index 10b2d264f..d5d068d91 100644 --- a/server/service/system/sys_dictionary.go +++ b/server/service/system/sys_dictionary.go @@ -1,6 +1,7 @@ package system import ( + "encoding/json" "errors" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" @@ -152,3 +153,145 @@ func (dictionaryService *DictionaryService) checkCircularReference(currentID uin return nil } + +//@author: [pixelMax] +//@function: ExportSysDictionary +//@description: 导出字典JSON(包含字典详情) +//@param: id uint +//@return: exportData map[string]interface{}, err error + +func (dictionaryService *DictionaryService) ExportSysDictionary(id uint) (exportData map[string]interface{}, err error) { + var dictionary system.SysDictionary + // 查询字典及其所有详情 + err = global.GVA_DB.Where("id = ?", id).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB { + return db.Order("sort") + }).First(&dictionary).Error + if err != nil { + return nil, err + } + + // 清空字典详情中的ID、创建时间、更新时间等字段 + var cleanDetails []map[string]interface{} + for _, detail := range dictionary.SysDictionaryDetails { + cleanDetail := map[string]interface{}{ + "label": detail.Label, + "value": detail.Value, + "extend": detail.Extend, + "status": detail.Status, + "sort": detail.Sort, + "level": detail.Level, + "path": detail.Path, + } + cleanDetails = append(cleanDetails, cleanDetail) + } + + // 构造导出数据 + exportData = map[string]interface{}{ + "name": dictionary.Name, + "type": dictionary.Type, + "status": dictionary.Status, + "desc": dictionary.Desc, + "sysDictionaryDetails": cleanDetails, + } + + return exportData, nil +} + +//@author: [pixelMax] +//@function: ImportSysDictionary +//@description: 导入字典JSON(包含字典详情) +//@param: jsonStr string +//@return: err error + +func (dictionaryService *DictionaryService) ImportSysDictionary(jsonStr string) error { + // 直接解析到 SysDictionary 结构体 + var importData system.SysDictionary + if err := json.Unmarshal([]byte(jsonStr), &importData); err != nil { + return errors.New("JSON 格式错误: " + err.Error()) + } + + // 验证必填字段 + if importData.Name == "" { + return errors.New("字典名称不能为空") + } + if importData.Type == "" { + return errors.New("字典类型不能为空") + } + + // 检查字典类型是否已存在 + if !errors.Is(global.GVA_DB.First(&system.SysDictionary{}, "type = ?", importData.Type).Error, gorm.ErrRecordNotFound) { + return errors.New("存在相同的type,不允许导入") + } + + // 创建字典(清空导入数据的ID和时间戳) + dictionary := system.SysDictionary{ + Name: importData.Name, + Type: importData.Type, + Status: importData.Status, + Desc: importData.Desc, + } + + // 开启事务 + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 创建字典 + if err := tx.Create(&dictionary).Error; err != nil { + return err + } + + // 处理字典详情 + if len(importData.SysDictionaryDetails) > 0 { + // 创建一个映射来跟踪旧ID到新ID的对应关系 + idMap := make(map[uint]uint) + + // 第一遍:创建所有详情记录 + for _, detail := range importData.SysDictionaryDetails { + // 验证必填字段 + if detail.Label == "" || detail.Value == "" { + continue + } + + // 记录旧ID + oldID := detail.ID + + // 创建新的详情记录(ID会被GORM自动设置) + detailRecord := system.SysDictionaryDetail{ + Label: detail.Label, + Value: detail.Value, + Extend: detail.Extend, + Status: detail.Status, + Sort: detail.Sort, + Level: detail.Level, + Path: detail.Path, + SysDictionaryID: int(dictionary.ID), + } + + // 创建详情记录 + if err := tx.Create(&detailRecord).Error; err != nil { + return err + } + + // 记录旧ID到新ID的映射 + if oldID > 0 { + idMap[oldID] = detailRecord.ID + } + } + + // 第二遍:更新parent_id关系 + for _, detail := range importData.SysDictionaryDetails { + if detail.ParentID != nil && *detail.ParentID > 0 && detail.ID > 0 { + if newID, exists := idMap[detail.ID]; exists { + if newParentID, parentExists := idMap[*detail.ParentID]; parentExists { + if err := tx.Model(&system.SysDictionaryDetail{}). + Where("id = ?", newID). + Update("parent_id", newParentID).Error; err != nil { + return err + } + } + } + } + } + } + + return nil + }) +} diff --git a/server/service/system/sys_error.go b/server/service/system/sys_error.go new file mode 100644 index 000000000..c9d1b1cb8 --- /dev/null +++ b/server/service/system/sys_error.go @@ -0,0 +1,124 @@ +package system + +import ( + "context" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +type SysErrorService struct{} + +// CreateSysError 创建错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) CreateSysError(ctx context.Context, sysError *system.SysError) (err error) { + err = global.GVA_DB.Create(sysError).Error + return err +} + +// DeleteSysError 删除错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) DeleteSysError(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.Delete(&system.SysError{}, "id = ?", ID).Error + return err +} + +// DeleteSysErrorByIds 批量删除错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) DeleteSysErrorByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.Delete(&[]system.SysError{}, "id in ?", IDs).Error + return err +} + +// UpdateSysError 更新错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) UpdateSysError(ctx context.Context, sysError system.SysError) (err error) { + err = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", sysError.ID).Updates(&sysError).Error + return err +} + +// GetSysError 根据ID获取错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) GetSysError(ctx context.Context, ID string) (sysError system.SysError, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&sysError).Error + return +} + +// GetSysErrorInfoList 分页获取错误日志记录 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) GetSysErrorInfoList(ctx context.Context, info systemReq.SysErrorSearch) (list []system.SysError, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysError{}).Order("created_at desc") + var sysErrors []system.SysError + // 如果有条件搜索 下方会自动创建搜索语句 + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) + } + + if info.Form != nil && *info.Form != "" { + db = db.Where("form = ?", *info.Form) + } + if info.Info != nil && *info.Info != "" { + db = db.Where("info LIKE ?", "%"+*info.Info+"%") + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysErrors).Error + return sysErrors, total, err +} + +// GetSysErrorSolution 异步处理错误 +// Author [yourname](https://github.com/yourname) +func (sysErrorService *SysErrorService) GetSysErrorSolution(ctx context.Context, ID string) (err error) { + // 立即更新为处理中 + err = global.GVA_DB.WithContext(ctx).Model(&system.SysError{}).Where("id = ?", ID).Update("status", "处理中").Error + if err != nil { + return err + } + + // 异步协程在一分钟后更新为处理完成 + go func(id string) { + // 查询当前错误信息用于生成方案 + var se system.SysError + _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).First(&se).Error + + // 构造 LLM 请求参数,使用管家模式(butler)根据错误信息生成解决方案 + var form, info string + if se.Form != nil { + form = *se.Form + } + if se.Info != nil { + info = *se.Info + } + + llmReq := common.JSONMap{ + "mode": "solution", + "command": "solution", + "info": info, + "form": form, + } + + // 调用服务层 LLMAuto,忽略错误但尽量写入方案 + var solution string + if data, err := (&AutoCodeService{}).LLMAuto(context.Background(), llmReq); err == nil { + solution = fmt.Sprintf("%v", data) + _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Updates(map[string]interface{}{"status": "处理完成", "solution": solution}).Error + } else { + // 即使生成失败也标记为完成,避免任务卡住 + _ = global.GVA_DB.Model(&system.SysError{}).Where("id = ?", id).Update("status", "处理失败").Error + } + }(ID) + + return nil +} diff --git a/server/service/system/sys_export_template.go b/server/service/system/sys_export_template.go index 182ce6298..9344291e1 100644 --- a/server/service/system/sys_export_template.go +++ b/server/service/system/sys_export_template.go @@ -213,6 +213,16 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator) } + if condition.Operator == "BETWEEN" { + sql = fmt.Sprintf("%s BETWEEN ? AND ?", condition.Column) + startValue := paramsValues.Get("start" + condition.From) + endValue := paramsValues.Get("end" + condition.From) + if startValue != "" && endValue != "" { + db = db.Where(sql, startValue, endValue) + } + continue + } + if value != "" { if condition.Operator == "LIKE" { value = "%" + value + "%" @@ -317,14 +327,14 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID for j, colCell := range row { cell := fmt.Sprintf("%s%d", getColumnName(j+1), i+1) - var sErr error - if v, err := strconv.ParseFloat(colCell, 64); err == nil { - sErr = f.SetCellValue("Sheet1", cell, v) - } else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil { - sErr = f.SetCellValue("Sheet1", cell, v) - } else { - sErr = f.SetCellValue("Sheet1", cell, colCell) - } + var sErr error + if v, err := strconv.ParseFloat(colCell, 64); err == nil { + sErr = f.SetCellValue("Sheet1", cell, v) + } else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil { + sErr = f.SetCellValue("Sheet1", cell, v) + } else { + sErr = f.SetCellValue("Sheet1", cell, colCell) + } if sErr != nil { return nil, "", sErr @@ -340,6 +350,185 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID return file, template.Name, nil } +// PreviewSQL 预览最终生成的 SQL(不执行查询,仅返回 SQL 字符串) +// Author [piexlmax](https://github.com/piexlmax) & [trae-ai] +func (sysExportTemplateService *SysExportTemplateService) PreviewSQL(templateID string, values url.Values) (sqlPreview string, err error) { + // 解析 params(与导出逻辑保持一致) + var params = values.Get("params") + paramsValues, _ := url.ParseQuery(params) + + // 加载模板 + var template system.SysExportTemplate + err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error + if err != nil { + return "", err + } + + // 解析模板列 + var templateInfoMap = make(map[string]string) + columns, err := utils.GetJSONKeys(template.TemplateInfo) + if err != nil { + return "", err + } + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return "", err + } + var selectKeyFmt []string + for _, key := range columns { + selectKeyFmt = append(selectKeyFmt, key) + } + selects := strings.Join(selectKeyFmt, ", ") + + // 生成 FROM 与 JOIN 片段 + var sb strings.Builder + sb.WriteString("SELECT ") + sb.WriteString(selects) + sb.WriteString(" FROM ") + sb.WriteString(template.TableName) + + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + sb.WriteString(" ") + sb.WriteString(join.JOINS) + sb.WriteString(" ") + sb.WriteString(join.Table) + sb.WriteString(" ON ") + sb.WriteString(join.ON) + } + } + + // WHERE 条件 + var wheres []string + + // 软删除过滤 + filterDeleted := false + if paramsValues != nil { + filterParam := paramsValues.Get("filterDeleted") + if filterParam == "true" { + filterDeleted = true + } + } + if filterDeleted { + wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", template.TableName)) + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + if sysExportTemplateService.hasDeletedAtColumn(join.Table) { + wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table)) + } + } + } + } + + // 模板条件(保留与 ExportExcel 同步的解析规则) + if len(template.Conditions) > 0 { + for _, condition := range template.Conditions { + op := strings.ToUpper(strings.TrimSpace(condition.Operator)) + col := strings.TrimSpace(condition.Column) + + // 预览优先展示传入值,没有则展示占位符 + val := "" + if paramsValues != nil { + val = paramsValues.Get(condition.From) + } + + switch op { + case "BETWEEN": + startValue := "" + endValue := "" + if paramsValues != nil { + startValue = paramsValues.Get("start" + condition.From) + endValue = paramsValues.Get("end" + condition.From) + } + if startValue != "" && endValue != "" { + wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", col, startValue, endValue)) + } else { + wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", col, condition.From, condition.From)) + } + case "IN", "NOT IN": + if val != "" { + // 逗号分隔值做简单展示 + parts := strings.Split(val, ",") + for i := range parts { parts[i] = strings.TrimSpace(parts[i]) } + wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", col, op, strings.Join(parts, "','"))) + } else { + wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", col, op, condition.From)) + } + case "LIKE": + if val != "" { + wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", col, val)) + } else { + wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", col, condition.From)) + } + default: + if val != "" { + wheres = append(wheres, fmt.Sprintf("%s %s '%s'", col, op, val)) + } else { + wheres = append(wheres, fmt.Sprintf("%s %s {%s}", col, op, condition.From)) + } + } + } + } + + if len(wheres) > 0 { + sb.WriteString(" WHERE ") + sb.WriteString(strings.Join(wheres, " AND ")) + } + + // 排序 + order := "" + if paramsValues != nil { + order = paramsValues.Get("order") + } + if order == "" && template.Order != "" { + order = template.Order + } + if order != "" { + sb.WriteString(" ORDER BY ") + sb.WriteString(order) + } + + // limit/offset(如果传入或默认值为0,则不生成) + limitStr := "" + offsetStr := "" + if paramsValues != nil { + limitStr = paramsValues.Get("limit") + offsetStr = paramsValues.Get("offset") + } + + // 处理模板默认limit(仅当非0时) + if limitStr == "" && template.Limit != nil && *template.Limit != 0 { + limitStr = strconv.Itoa(*template.Limit) + } + + // 解析为数值,用于判断是否生成 + limitInt := 0 + offsetInt := 0 + if limitStr != "" { + if v, e := strconv.Atoi(limitStr); e == nil { limitInt = v } + } + if offsetStr != "" { + if v, e := strconv.Atoi(offsetStr); e == nil { offsetInt = v } + } + + if limitInt > 0 { + sb.WriteString(" LIMIT ") + sb.WriteString(strconv.Itoa(limitInt)) + if offsetInt > 0 { + sb.WriteString(" OFFSET ") + sb.WriteString(strconv.Itoa(offsetInt)) + } + } else { + // 当limit未设置或为0时,仅当offset>0才生成OFFSET + if offsetInt > 0 { + sb.WriteString(" OFFSET ") + sb.WriteString(strconv.Itoa(offsetInt)) + } + } + + return sb.String(), nil +} + // ExportTemplate 导出Excel模板 // Author [piexlmax](https://github.com/piexlmax) func (sysExportTemplateService *SysExportTemplateService) ExportTemplate(templateID string) (file *bytes.Buffer, name string, err error) { diff --git a/server/source/system/api.go b/server/source/system/api.go index 87a47695c..863847bd4 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -149,6 +149,8 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "系统字典", Method: "PUT", Path: "/sysDictionary/updateSysDictionary", Description: "更新字典"}, {ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/findSysDictionary", Description: "根据ID获取字典(建议选择)"}, {ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/getSysDictionaryList", Description: "获取字典列表"}, + {ApiGroup: "系统字典", Method: "POST", Path: "/sysDictionary/importSysDictionary", Description: "导入字典JSON"}, + {ApiGroup: "系统字典", Method: "GET", Path: "/sysDictionary/exportSysDictionary", Description: "导出字典JSON"}, {ApiGroup: "操作记录", Method: "POST", Path: "/sysOperationRecord/createSysOperationRecord", Description: "新增操作记录"}, {ApiGroup: "操作记录", Method: "GET", Path: "/sysOperationRecord/findSysOperationRecord", Description: "根据ID获取操作记录"}, @@ -175,8 +177,17 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/getSysExportTemplateList", Description: "获取导出模板列表"}, {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportExcel", Description: "导出Excel"}, {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/exportTemplate", Description: "下载模板"}, + {ApiGroup: "导出模板", Method: "GET", Path: "/sysExportTemplate/previewSQL", Description: "预览SQL"}, {ApiGroup: "导出模板", Method: "POST", Path: "/sysExportTemplate/importExcel", Description: "导入Excel"}, + {ApiGroup: "错误日志", Method: "POST", Path: "/sysError/createSysError", Description: "新建错误日志"}, + {ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysError", Description: "删除错误日志"}, + {ApiGroup: "错误日志", Method: "DELETE", Path: "/sysError/deleteSysErrorByIds", Description: "批量删除错误日志"}, + {ApiGroup: "错误日志", Method: "PUT", Path: "/sysError/updateSysError", Description: "更新错误日志"}, + {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/findSysError", Description: "根据ID获取错误日志"}, + {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorList", Description: "获取错误日志列表"}, + {ApiGroup: "错误日志", Method: "GET", Path: "/sysError/getSysErrorSolution", Description: "触发错误处理(异步)"}, + {ApiGroup: "公告", Method: "POST", Path: "/info/createInfo", Description: "新建公告"}, {ApiGroup: "公告", Method: "DELETE", Path: "/info/deleteInfo", Description: "删除公告"}, {ApiGroup: "公告", Method: "DELETE", Path: "/info/deleteInfoByIds", Description: "批量删除公告"}, diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go index 6bd3c0499..a7e49d757 100644 --- a/server/source/system/casbin.go +++ b/server/source/system/casbin.go @@ -149,6 +149,8 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/sysDictionary/getSysDictionaryList", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysDictionary/createSysDictionary", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/sysDictionary/deleteSysDictionary", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/importSysDictionary", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysDictionary/exportSysDictionary", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysOperationRecord/findSysOperationRecord", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysOperationRecord/updateSysOperationRecord", V2: "PUT"}, @@ -176,8 +178,17 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/sysExportTemplate/getSysExportTemplateList", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportExcel", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysExportTemplate/exportTemplate", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysExportTemplate/previewSQL", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysExportTemplate/importExcel", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysError/createSysError", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysError/deleteSysError", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysError/deleteSysErrorByIds", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysError/updateSysError", V2: "PUT"}, + {Ptype: "p", V0: "888", V1: "/sysError/findSysError", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysError/getSysErrorList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysError/getSysErrorSolution", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/info/createInfo", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/info/deleteInfo", V2: "DELETE"}, {Ptype: "p", V0: "888", V1: "/info/deleteInfoByIds", V2: "DELETE"}, diff --git a/server/source/system/menu.go b/server/source/system/menu.go index 9817c13af..078716bbc 100644 --- a/server/source/system/menu.go +++ b/server/source/system/menu.go @@ -103,6 +103,7 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTool", Name: "mcpTool", Component: "view/systemTools/autoCode/mcp.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools模板", Icon: "magnet"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTest", Name: "mcpTest", Component: "view/systemTools/autoCode/mcpTest.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools测试", Icon: "partly-cloudy"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "sysVersion", Name: "sysVersion", Component: "view/systemTools/version/version.vue", Sort: 8, Meta: Meta{Title: "版本管理", Icon: "server"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "sysError", Name: "sysError", Component: "view/systemTools/sysError/sysError.vue", Sort: 9, Meta: Meta{Title: "错误日志", Icon: "warn"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "插件市场", Icon: "shop"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}}, diff --git a/server/utils/ast/extract_func.go b/server/utils/ast/extract_func.go new file mode 100644 index 000000000..4b880c1f5 --- /dev/null +++ b/server/utils/ast/extract_func.go @@ -0,0 +1,62 @@ +package ast + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" +) + +// ExtractFuncSourceByPosition 根据文件路径与行号,提取包含该行的整个方法源码 +// 返回:方法名、完整源码、起止行号 +func ExtractFuncSourceByPosition(filePath string, line int) (name string, source string, startLine int, endLine int, err error) { + // 读取源文件 + src, readErr := os.ReadFile(filePath) + if readErr != nil { + err = fmt.Errorf("read file failed: %w", readErr) + return + } + + // 解析 AST + fset := token.NewFileSet() + file, parseErr := parser.ParseFile(fset, filePath, src, parser.ParseComments) + if parseErr != nil { + err = fmt.Errorf("parse file failed: %w", parseErr) + return + } + + // 在 AST 中定位包含指定行号的函数声明 + var target *ast.FuncDecl + ast.Inspect(file, func(n ast.Node) bool { + fd, ok := n.(*ast.FuncDecl) + if !ok { + return true + } + s := fset.Position(fd.Pos()).Line + e := fset.Position(fd.End()).Line + if line >= s && line <= e { + target = fd + startLine = s + endLine = e + return false + } + return true + }) + + if target == nil { + err = fmt.Errorf("no function encloses line %d in %s", line, filePath) + return + } + + // 使用字节偏移精确提取源码片段(包含注释与原始格式) + start := fset.Position(target.Pos()).Offset + end := fset.Position(target.End()).Offset + if start < 0 || end > len(src) || start >= end { + err = fmt.Errorf("invalid offsets for function: start=%d end=%d len=%d", start, end, len(src)) + return + } + source = string(src[start:end]) + name = target.Name.Name + return +} \ No newline at end of file diff --git a/server/utils/stacktrace/stacktrace.go b/server/utils/stacktrace/stacktrace.go new file mode 100644 index 000000000..dbae2c966 --- /dev/null +++ b/server/utils/stacktrace/stacktrace.go @@ -0,0 +1,79 @@ +package stacktrace + +import ( + "regexp" + "strconv" + "strings" +) + +// Frame 表示一次栈帧解析结果 +type Frame struct { + File string + Line int + Func string +} + +var fileLineRe = regexp.MustCompile(`\s*(.+\.go):(\d+)\s*$`) + +// FindFinalCaller 从 zap 的 entry.Stack 文本中,解析“最终业务调用方”的文件与行号 +// 策略:自顶向下解析,优先选择第一条项目代码帧,过滤第三方库/标准库/框架中间件 +func FindFinalCaller(stack string) (Frame, bool) { + if stack == "" { + return Frame{}, false + } + lines := strings.Split(stack, "\n") + var currFunc string + for i := 0; i < len(lines); i++ { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + if m := fileLineRe.FindStringSubmatch(line); m != nil { + file := m[1] + ln, _ := strconv.Atoi(m[2]) + if shouldSkip(file) { + // 跳过此帧,同时重置函数名以避免错误配对 + currFunc = "" + continue + } + return Frame{File: file, Line: ln, Func: currFunc}, true + } + // 记录函数名行,下一行通常是文件:行 + currFunc = line + } + return Frame{}, false +} + +func shouldSkip(file string) bool { + // 第三方库与 Go 模块缓存 + if strings.Contains(file, "/go/pkg/mod/") { + return true + } + if strings.Contains(file, "/go.uber.org/") { + return true + } + if strings.Contains(file, "/gorm.io/") { + return true + } + // 标准库 + if strings.Contains(file, "/go/go") && strings.Contains(file, "/src/") { // e.g. /Users/name/go/go1.24.2/src/net/http/server.go + return true + } + // 框架内不需要作为最终调用方的路径 + if strings.Contains(file, "/server/core/zap.go") { + return true + } + if strings.Contains(file, "/server/core/") { + return true + } + if strings.Contains(file, "/server/utils/errorhook/") { + return true + } + if strings.Contains(file, "/server/middleware/") { + return true + } + if strings.Contains(file, "/server/router/") { + return true + } + return false +} \ No newline at end of file diff --git a/web/package.json b/web/package.json index f2449e9f1..5f5035fac 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "gin-vue-admin", - "version": "2.8.6", + "version": "2.8.7", "private": true, "scripts": { "dev": "node openDocument.js && vite --host --mode development", @@ -45,7 +45,7 @@ "spark-md5": "^3.0.2", "universal-cookie": "^7", "vform3-builds": "^3.0.10", - "vite-auto-import-svg": "^1.9.0", + "vite-auto-import-svg": "^2.1.0", "vue": "^3.5.7", "vue-cropper": "^1.1.4", "vue-echarts": "^7.0.3", diff --git a/web/src/App.vue b/web/src/App.vue index 5a2214e9f..63cbbdd15 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -3,7 +3,7 @@ id="app" class="bg-gray-50 text-slate-700 !dark:text-slate-500 dark:bg-slate-800" > - + @@ -15,7 +15,7 @@ import Application from '@/components/application/index.vue' import { useAppStore } from '@/pinia' - useAppStore() + const appStore = useAppStore() defineOptions({ name: 'App' }) diff --git a/web/src/api/autoCode.js b/web/src/api/autoCode.js index cb3443f1d..1ee16fcd1 100644 --- a/web/src/api/autoCode.js +++ b/web/src/api/autoCode.js @@ -162,7 +162,6 @@ export const butler = (data) => { }) } - export const eye = (data) => { return service({ url: '/autoCode/llmAuto', diff --git a/web/src/api/exportTemplate.js b/web/src/api/exportTemplate.js index 9f8729b01..753547de7 100644 --- a/web/src/api/exportTemplate.js +++ b/web/src/api/exportTemplate.js @@ -126,3 +126,20 @@ export const exportTemplate = (params) => { params }) } + +// PreviewSQL 预览最终生成的SQL +// @Tags SysExportTemplate +// @Summary 预览最终生成的SQL +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/previewSQL [get] +// @Param templateID query string true "导出模板ID" +// @Param params query string false "查询参数编码字符串,参考 ExportExcel 组件" +export const previewSQL = (params) => { + return service({ + url: '/sysExportTemplate/previewSQL', + method: 'get', + params + }) +} diff --git a/web/src/api/sysDictionary.js b/web/src/api/sysDictionary.js index f5d6c8620..90a2583e4 100644 --- a/web/src/api/sysDictionary.js +++ b/web/src/api/sysDictionary.js @@ -78,3 +78,35 @@ export const getSysDictionaryList = (params) => { params }) } + +// @Tags SysDictionary +// @Summary 导出字典JSON(包含字典详情) +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data query model.SysDictionary true "字典ID" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"导出成功"}" +// @Router /sysDictionary/exportSysDictionary [get] +export const exportSysDictionary = (params) => { + return service({ + url: '/sysDictionary/exportSysDictionary', + method: 'get', + params + }) +} + +// @Tags SysDictionary +// @Summary 导入字典JSON(包含字典详情) +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body object true "字典JSON数据" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"导入成功"}" +// @Router /sysDictionary/importSysDictionary [post] +export const importSysDictionary = (data) => { + return service({ + url: '/sysDictionary/importSysDictionary', + method: 'post', + data + }) +} diff --git a/web/src/api/system/sysError.js b/web/src/api/system/sysError.js new file mode 100644 index 000000000..4b3271bc3 --- /dev/null +++ b/web/src/api/system/sysError.js @@ -0,0 +1,126 @@ +import service from '@/utils/request' +// @Tags SysError +// @Summary 创建错误日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.SysError true "创建错误日志" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /sysError/createSysError [post] +export const createSysError = (data) => { + return service({ + url: '/sysError/createSysError', + method: 'post', + data + }) +} + +// @Tags SysError +// @Summary 删除错误日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.SysError true "删除错误日志" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysError/deleteSysError [delete] +export const deleteSysError = (params) => { + return service({ + url: '/sysError/deleteSysError', + method: 'delete', + params + }) +} + +// @Tags SysError +// @Summary 批量删除错误日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除错误日志" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysError/deleteSysError [delete] +export const deleteSysErrorByIds = (params) => { + return service({ + url: '/sysError/deleteSysErrorByIds', + method: 'delete', + params + }) +} + +// @Tags SysError +// @Summary 更新错误日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.SysError true "更新错误日志" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /sysError/updateSysError [put] +export const updateSysError = (data) => { + return service({ + url: '/sysError/updateSysError', + method: 'put', + data + }) +} + +// @Tags SysError +// @Summary 用id查询错误日志 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query model.SysError true "用id查询错误日志" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /sysError/findSysError [get] +export const findSysError = (params) => { + return service({ + url: '/sysError/findSysError', + method: 'get', + params + }) +} + +// @Tags SysError +// @Summary 分页获取错误日志列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取错误日志列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysError/getSysErrorList [get] +export const getSysErrorList = (params) => { + return service({ + url: '/sysError/getSysErrorList', + method: 'get', + params + }) +} + +// @Tags SysError +// @Summary 不需要鉴权的错误日志接口 +// @Accept application/json +// @Produce application/json +// @Param data query systemReq.SysErrorSearch true "分页获取错误日志列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /sysError/getSysErrorPublic [get] +export const getSysErrorPublic = () => { + return service({ + url: '/sysError/getSysErrorPublic', + method: 'get', + }) +} + +// @Tags SysError +// @Summary 触发错误处理(异步) +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param id query string true "错误日志ID" +// @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"处理已提交\"}" +// @Router /sysError/getSysErrorSolution [get] +export const getSysErrorSolution = (params) => { + return service({ + url: '/sysError/getSysErrorSolution', + method: 'get', + params + }) +} \ No newline at end of file diff --git a/web/src/core/error-handel.js b/web/src/core/error-handel.js index 9c06e9535..bc85b06e1 100644 --- a/web/src/core/error-handel.js +++ b/web/src/core/error-handel.js @@ -1,51 +1,24 @@ +import { createSysError } from '@/api/system/sysError' + function sendErrorTip(errorInfo) { - console.groupCollapsed(`捕获到错误: ${errorInfo.type}`); - console.log('错误类型:', errorInfo.type); - console.log('错误信息:', errorInfo.message); - console.log('调用栈:', errorInfo.stack); - if (errorInfo.component) { - console.log('组件名:', errorInfo.component.name) - console.log('组件地址:', errorInfo.component.__file) - } - if (errorInfo.vueInfo) console.log('Vue 信息:', errorInfo.vueInfo); - if (errorInfo.source) console.log('来源文件:', errorInfo.source); - if (errorInfo.lineno) console.log('行号:', errorInfo.lineno); - if (errorInfo.colno) console.log('列号:', errorInfo.colno); - console.groupEnd(); + setTimeout(() => { + const errorData = { + form: errorInfo.type, + info: `${errorInfo.message}\nStack: ${errorInfo.stack}${errorInfo.component ? `\nComponent: ${errorInfo.component.name || 'Unknown'}` : ''}${errorInfo.vueInfo ? `\nVue Info: ${errorInfo.vueInfo}` : ''}${errorInfo.source ? `\nSource: ${errorInfo.source}:${errorInfo.lineno}:${errorInfo.colno}` : ''}`, + level: 'error', + solution: null + } + + createSysError(errorData).catch(apiErr => { + console.error('Failed to create error record:', apiErr) + }) + }, 0) } - -function initVueErrorHandler(app) { - app.config.errorHandler = (err, vm, info) => { - let errorType = 'Vue Error'; + window.addEventListener('unhandledrejection', (event) => { sendErrorTip({ - type: errorType, - message: err.message, - stack: err.stack, - component: vm.$options || 'Unknown Vue Component', - vueInfo: info + type: '前端', + message: `错误信息: ${event.reason}`, + stack: `调用栈: ${event.reason?.stack || '没有调用栈信息'}`, }); - }; -} - -function initJsErrorHandler() { - window.onerror = (message, source, lineno, colno, error) => { - let errorType = 'JS Error'; - - sendErrorTip({ - type: errorType, - message: message, - stack: error ? error.stack : 'No stack available', - source: source, - lineno: lineno, - colno: colno - }); - - return false; - }; -} - -export function initErrorHandler(app) { - initVueErrorHandler(app) - initJsErrorHandler() -} + }); diff --git a/web/src/main.js b/web/src/main.js index 107fa680c..5855b570c 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -15,13 +15,10 @@ import auth from '@/directive/auth' import clickOutSide from '@/directive/clickOutSide' import { store } from '@/pinia' import App from './App.vue' -// import { initErrorHandler } from '@/core/error-handel' - +import '@/core/error-handel' const app = createApp(App) -// 注入错误处理捕获 -// initErrorHandler(app) app.config.productionTip = false app diff --git a/web/src/pathInfo.json b/web/src/pathInfo.json index 780f9a6e3..318e8113f 100644 --- a/web/src/pathInfo.json +++ b/web/src/pathInfo.json @@ -73,6 +73,7 @@ "/src/view/systemTools/index.vue": "System", "/src/view/systemTools/installPlugin/index.vue": "Index", "/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug", + "/src/view/systemTools/sysError/sysError.vue": "SysError", "/src/view/systemTools/system/system.vue": "Config", "/src/view/systemTools/version/version.vue": "SysVersion", "/src/plugin/announcement/form/info.vue": "InfoForm", diff --git a/web/src/pinia/modules/app.js b/web/src/pinia/modules/app.js index 9eb9dea1f..374e4ebed 100644 --- a/web/src/pinia/modules/app.js +++ b/web/src/pinia/modules/app.js @@ -19,7 +19,8 @@ export const useAppStore = defineStore('app', () => { show_watermark: true, side_mode: 'normal', // 页面过渡动画配置 - transition_type: 'slide' + transition_type: 'slide', + global_size: 'default' }) const isDark = useDark({ @@ -99,6 +100,10 @@ export const useAppStore = defineStore('app', () => { config.transition_type = e } + const toggleGlobalSize = (e) => { + config.global_size = e + } + const baseCoinfg = { weakness: false, grey: false, @@ -111,7 +116,8 @@ export const useAppStore = defineStore('app', () => { show_watermark: true, side_mode: 'normal', // 页面过渡动画配置 - transition_type: 'slide' + transition_type: 'slide', + global_size: 'default' } const resetConfig = () => { @@ -150,6 +156,7 @@ export const useAppStore = defineStore('app', () => { toggleConfigWatermark, toggleSideMode, toggleTransition, - resetConfig + resetConfig, + toggleGlobalSize } }) diff --git a/web/src/pinia/modules/user.js b/web/src/pinia/modules/user.js index 5b1f55c73..974e85ec2 100644 --- a/web/src/pinia/modules/user.js +++ b/web/src/pinia/modules/user.js @@ -33,7 +33,6 @@ export const useUserStore = defineStore('user', () => { } }) } - console.log(appStore.config) } const setToken = (val) => { diff --git a/web/src/style/main.scss b/web/src/style/main.scss index 21a3d121e..749d977c4 100644 --- a/web/src/style/main.scss +++ b/web/src/style/main.scss @@ -40,7 +40,7 @@ } .gva-search-box { - @apply p-4 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2; + @apply p-4 pb-0 bg-white text-slate-700 dark:text-slate-400 dark:bg-slate-900 rounded my-2; } .gva-form-box { diff --git a/web/src/view/layout/setting/components/layoutModeCard.vue b/web/src/view/layout/setting/components/layoutModeCard.vue index 5f1fd1b79..abe8fcc0f 100644 --- a/web/src/view/layout/setting/components/layoutModeCard.vue +++ b/web/src/view/layout/setting/components/layoutModeCard.vue @@ -1,9 +1,9 @@