diff --git a/.claude/rules/project_rules.md b/.claude/rules/project_rules.md new file mode 100644 index 000000000..3a683aa11 --- /dev/null +++ b/.claude/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/.cursor/rules/project_rules.md b/.cursor/rules/project_rules.md new file mode 100644 index 000000000..f8fb9471b --- /dev/null +++ b/.cursor/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/.trae/rules/project_rules.md b/.trae/rules/project_rules.md new file mode 100644 index 000000000..f8fb9471b --- /dev/null +++ b/.trae/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/README.md b/README.md index 6dc2e4eb5..6eea7bbd9 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ +
@@ -21,7 +22,8 @@ -
+ +

⭐️ 高度适配AI编辑器的MCP

📄 创建基础模板

🤖 AI生成结构

⏰ 生成代码

@@ -377,7 +379,7 @@ fmt.Println(decodeBytes, err) 如果你觉得这个项目对你有帮助,你可以请作者喝饮料 :tropical_drink: [点我](https://www.gin-vue-admin.com/coffee/index.html) -## 10. 商用注意事项 +## 10. 注意事项 -请严格遵守Apache 2.0协议并保留作品声明,商业用途请务必[获取授权](https://www.gin-vue-admin.com/empower/) -未授权商用将依法追究法律责任 +请严格遵守Apache 2.0协议并保留作品声明,去除版权信息请务必[获取授权](https://www.gin-vue-admin.com/empower/) +未授权去除版权信息将依法追究法律责任 diff --git a/server/api/v1/system/enter.go b/server/api/v1/system/enter.go index c268ccc7b..dbc721b16 100644 --- a/server/api/v1/system/enter.go +++ b/server/api/v1/system/enter.go @@ -22,6 +22,7 @@ type ApiGroup struct { AutoCodeHistoryApi AutoCodeTemplateApi SysParamsApi + SysVersionApi } var ( @@ -44,4 +45,5 @@ var ( autoCodePackageService = service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage autoCodeHistoryService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeHistory autoCodeTemplateService = service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate + sysVersionService = service.ServiceGroupApp.SystemServiceGroup.SysVersionService ) diff --git a/server/api/v1/system/sys_user.go b/server/api/v1/system/sys_user.go index d6ca6bde1..b6a84c121 100644 --- a/server/api/v1/system/sys_user.go +++ b/server/api/v1/system/sys_user.go @@ -27,8 +27,6 @@ import ( func (b *BaseApi) Login(c *gin.Context) { var l systemReq.Login err := c.ShouldBindJSON(&l) - key := c.ClientIP() - if err != nil { response.FailWithMessage(err.Error(), c) return @@ -39,6 +37,7 @@ func (b *BaseApi) Login(c *gin.Context) { return } + key := c.ClientIP() // 判断验证码是否开启 openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数 openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间 @@ -48,30 +47,30 @@ func (b *BaseApi) Login(c *gin.Context) { } var oc bool = openCaptcha == 0 || openCaptcha < interfaceToInt(v) - - if !oc || (l.CaptchaId != "" && l.Captcha != "" && store.Verify(l.CaptchaId, l.Captcha, true)) { - u := &system.SysUser{Username: l.Username, Password: l.Password} - user, err := userService.Login(u) - if err != nil { - global.GVA_LOG.Error(global.Translate("sys_user.loginFail"), zap.Error(err)) - // 验证码次数+1 - global.BlackCache.Increment(key, 1) - response.FailWithMessage(global.Translate("sys_user.userNameOrPasswordError"), c) - return - } - if user.Enable != 1 { - global.GVA_LOG.Error(global.Translate("sys_user.loginFailUserBanned")) - // 验证码次数+1 - global.BlackCache.Increment(key, 1) - response.FailWithMessage(global.Translate("sys_user.userBanned"), c) - return - } - b.TokenNext(c, *user) + if oc && (l.Captcha == "" || l.CaptchaId == "" || !store.Verify(l.CaptchaId, l.Captcha, true)) { + // 验证码次数+1 + global.BlackCache.Increment(key, 1) + response.FailWithMessage(global.Translate("sys_user.vCodeErr"), c) return } - // 验证码次数+1 - global.BlackCache.Increment(key, 1) - response.FailWithMessage(global.Translate("sys_user.vCodeErr"), c) + + u := &system.SysUser{Username: l.Username, Password: l.Password} + user, err := userService.Login(u) + if err != nil { + global.GVA_LOG.Error(global.Translate("sys_user.loginFail"), zap.Error(err)) + // 验证码次数+1 + global.BlackCache.Increment(key, 1) + response.FailWithMessage(global.Translate("sys_user.userNameOrPasswordError"), c) + return + } + if user.Enable != 1 { + global.GVA_LOG.Error(global.Translate("sys_user.loginFailUserBanned")) + // 验证码次数+1 + global.BlackCache.Increment(key, 1) + response.FailWithMessage(global.Translate("sys_user.userBanned"), c) + return + } + b.TokenNext(c, *user) } // TokenNext 登录以后签发jwt @@ -184,7 +183,7 @@ func (b *BaseApi) ChangePassword(c *gin.Context) { } uid := utils.GetUserID(c) u := &system.SysUser{GVA_MODEL: global.GVA_MODEL{ID: uid}, Password: req.Password} - _, err = userService.ChangePassword(u, req.NewPassword) + err = userService.ChangePassword(u, req.NewPassword) if err != nil { global.GVA_LOG.Error(global.Translate("general.modifyFail"), zap.Error(err)) response.FailWithMessage(global.Translate("general.changePWErr"), c) diff --git a/server/api/v1/system/sys_version.go b/server/api/v1/system/sys_version.go new file mode 100644 index 000000000..1e228089b --- /dev/null +++ b/server/api/v1/system/sys_version.go @@ -0,0 +1,486 @@ +package system + +import ( + "encoding/json" + "fmt" + "net/http" + "sort" + "strconv" + "time" + + "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" + systemRes "github.com/flipped-aurora/gin-vue-admin/server/model/system/response" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SysVersionApi struct{} + +// buildMenuTree 构建菜单树结构 +func buildMenuTree(menus []system.SysBaseMenu) []system.SysBaseMenu { + // 创建菜单映射 + menuMap := make(map[uint]*system.SysBaseMenu) + for i := range menus { + menuMap[menus[i].ID] = &menus[i] + } + + // 构建树结构 + var rootMenus []system.SysBaseMenu + for _, menu := range menus { + if menu.ParentId == 0 { + // 根菜单 + menuData := convertMenuToStruct(menu, menuMap) + rootMenus = append(rootMenus, menuData) + } + } + + // 按sort排序根菜单 + sort.Slice(rootMenus, func(i, j int) bool { + return rootMenus[i].Sort < rootMenus[j].Sort + }) + + return rootMenus +} + +// convertMenuToStruct 将菜单转换为结构体并递归处理子菜单 +func convertMenuToStruct(menu system.SysBaseMenu, menuMap map[uint]*system.SysBaseMenu) system.SysBaseMenu { + result := system.SysBaseMenu{ + Path: menu.Path, + Name: menu.Name, + Hidden: menu.Hidden, + Component: menu.Component, + Sort: menu.Sort, + Meta: menu.Meta, + } + + // 清理并复制参数数据 + if len(menu.Parameters) > 0 { + cleanParameters := make([]system.SysBaseMenuParameter, 0, len(menu.Parameters)) + for _, param := range menu.Parameters { + cleanParam := system.SysBaseMenuParameter{ + Type: param.Type, + Key: param.Key, + Value: param.Value, + // 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID + } + cleanParameters = append(cleanParameters, cleanParam) + } + result.Parameters = cleanParameters + } + + // 清理并复制菜单按钮数据 + if len(menu.MenuBtn) > 0 { + cleanMenuBtns := make([]system.SysBaseMenuBtn, 0, len(menu.MenuBtn)) + for _, btn := range menu.MenuBtn { + cleanBtn := system.SysBaseMenuBtn{ + Name: btn.Name, + Desc: btn.Desc, + // 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID + } + cleanMenuBtns = append(cleanMenuBtns, cleanBtn) + } + result.MenuBtn = cleanMenuBtns + } + + // 查找并处理子菜单 + var children []system.SysBaseMenu + for _, childMenu := range menuMap { + if childMenu.ParentId == menu.ID { + childData := convertMenuToStruct(*childMenu, menuMap) + children = append(children, childData) + } + } + + // 按sort排序子菜单 + if len(children) > 0 { + sort.Slice(children, func(i, j int) bool { + return children[i].Sort < children[j].Sort + }) + result.Children = children + } + + return result +} + +// DeleteSysVersion 删除版本管理 +// @Tags SysVersion +// @Summary 删除版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body system.SysVersion true "删除版本管理" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /sysVersion/deleteSysVersion [delete] +func (sysVersionApi *SysVersionApi) DeleteSysVersion(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + err := sysVersionService.DeleteSysVersion(ctx, ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteSysVersionByIds 批量删除版本管理 +// @Tags SysVersion +// @Summary 批量删除版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /sysVersion/deleteSysVersionByIds [delete] +func (sysVersionApi *SysVersionApi) DeleteSysVersionByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + IDs := c.QueryArray("IDs[]") + err := sysVersionService.DeleteSysVersionByIds(ctx, IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// FindSysVersion 用id查询版本管理 +// @Tags SysVersion +// @Summary 用id查询版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param ID query uint true "用id查询版本管理" +// @Success 200 {object} response.Response{data=system.SysVersion,msg=string} "查询成功" +// @Router /sysVersion/findSysVersion [get] +func (sysVersionApi *SysVersionApi) FindSysVersion(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + resysVersion, err := sysVersionService.GetSysVersion(ctx, ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithData(resysVersion, c) +} + +// GetSysVersionList 分页获取版本管理列表 +// @Tags SysVersion +// @Summary 分页获取版本管理列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query systemReq.SysVersionSearch true "分页获取版本管理列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /sysVersion/getSysVersionList [get] +func (sysVersionApi *SysVersionApi) GetSysVersionList(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo systemReq.SysVersionSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := sysVersionService.GetSysVersionInfoList(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) +} + +// GetSysVersionPublic 不需要鉴权的版本管理接口 +// @Tags SysVersion +// @Summary 不需要鉴权的版本管理接口 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /sysVersion/getSysVersionPublic [get] +func (sysVersionApi *SysVersionApi) GetSysVersionPublic(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 + // 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + sysVersionService.GetSysVersionPublic(ctx) + response.OkWithDetailed(gin.H{ + "info": "不需要鉴权的版本管理接口信息", + }, "获取成功", c) +} + +// ExportVersion 创建发版数据 +// @Tags SysVersion +// @Summary 创建发版数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body systemReq.ExportVersionRequest true "创建发版数据" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /sysVersion/exportVersion [post] +func (sysVersionApi *SysVersionApi) ExportVersion(c *gin.Context) { + ctx := c.Request.Context() + + var req systemReq.ExportVersionRequest + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + // 获取选中的菜单数据 + var menuData []system.SysBaseMenu + if len(req.MenuIds) > 0 { + menuData, err = sysVersionService.GetMenusByIds(ctx, req.MenuIds) + if err != nil { + global.GVA_LOG.Error("获取菜单数据失败!", zap.Error(err)) + response.FailWithMessage("获取菜单数据失败:"+err.Error(), c) + return + } + } + + // 获取选中的API数据 + var apiData []system.SysApi + if len(req.ApiIds) > 0 { + apiData, err = sysVersionService.GetApisByIds(ctx, req.ApiIds) + if err != nil { + global.GVA_LOG.Error("获取API数据失败!", zap.Error(err)) + response.FailWithMessage("获取API数据失败:"+err.Error(), c) + return + } + } + + // 获取选中的字典数据 + var dictData []system.SysDictionary + if len(req.DictIds) > 0 { + dictData, err = sysVersionService.GetDictionariesByIds(ctx, req.DictIds) + if err != nil { + global.GVA_LOG.Error("获取字典数据失败!", zap.Error(err)) + response.FailWithMessage("获取字典数据失败:"+err.Error(), c) + return + } + } + + // 处理菜单数据,构建递归的children结构 + processedMenus := buildMenuTree(menuData) + + // 处理API数据,清除ID和时间戳字段 + processedApis := make([]system.SysApi, 0, len(apiData)) + for _, api := range apiData { + cleanApi := system.SysApi{ + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + } + processedApis = append(processedApis, cleanApi) + } + + // 处理字典数据,清除ID和时间戳字段,包含字典详情 + processedDicts := make([]system.SysDictionary, 0, len(dictData)) + for _, dict := range dictData { + cleanDict := system.SysDictionary{ + Name: dict.Name, + Type: dict.Type, + Status: dict.Status, + Desc: dict.Desc, + } + + // 处理字典详情数据,清除ID和时间戳字段 + cleanDetails := make([]system.SysDictionaryDetail, 0, len(dict.SysDictionaryDetails)) + for _, detail := range dict.SysDictionaryDetails { + cleanDetail := system.SysDictionaryDetail{ + Label: detail.Label, + Value: detail.Value, + Extend: detail.Extend, + Status: detail.Status, + Sort: detail.Sort, + // 不复制 ID, CreatedAt, UpdatedAt, SysDictionaryID + } + cleanDetails = append(cleanDetails, cleanDetail) + } + cleanDict.SysDictionaryDetails = cleanDetails + + processedDicts = append(processedDicts, cleanDict) + } + + // 构建导出数据 + exportData := systemRes.ExportVersionResponse{ + Version: systemReq.VersionInfo{ + Name: req.VersionName, + Code: req.VersionCode, + Description: req.Description, + ExportTime: time.Now().Format("2006-01-02 15:04:05"), + }, + Menus: processedMenus, + Apis: processedApis, + Dictionaries: processedDicts, + } + + // 转换为JSON + jsonData, err := json.MarshalIndent(exportData, "", " ") + if err != nil { + global.GVA_LOG.Error("JSON序列化失败!", zap.Error(err)) + response.FailWithMessage("JSON序列化失败:"+err.Error(), c) + return + } + + // 保存版本记录 + version := system.SysVersion{ + VersionName: utils.Pointer(req.VersionName), + VersionCode: utils.Pointer(req.VersionCode), + Description: utils.Pointer(req.Description), + VersionData: utils.Pointer(string(jsonData)), + } + + err = sysVersionService.CreateSysVersion(ctx, &version) + if err != nil { + global.GVA_LOG.Error("保存版本记录失败!", zap.Error(err)) + response.FailWithMessage("保存版本记录失败:"+err.Error(), c) + return + } + + response.OkWithMessage("创建发版成功", c) +} + +// DownloadVersionJson 下载版本JSON数据 +// @Tags SysVersion +// @Summary 下载版本JSON数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param ID query string true "版本ID" +// @Success 200 {object} response.Response{data=object,msg=string} "下载成功" +// @Router /sysVersion/downloadVersionJson [get] +func (sysVersionApi *SysVersionApi) DownloadVersionJson(c *gin.Context) { + ctx := c.Request.Context() + + ID := c.Query("ID") + if ID == "" { + response.FailWithMessage("版本ID不能为空", c) + return + } + + // 获取版本记录 + version, err := sysVersionService.GetSysVersion(ctx, ID) + if err != nil { + global.GVA_LOG.Error("获取版本记录失败!", zap.Error(err)) + response.FailWithMessage("获取版本记录失败:"+err.Error(), c) + return + } + + // 构建JSON数据 + var jsonData []byte + if version.VersionData != nil && *version.VersionData != "" { + jsonData = []byte(*version.VersionData) + } else { + // 如果没有存储的JSON数据,构建一个基本的结构 + basicData := systemRes.ExportVersionResponse{ + Version: systemReq.VersionInfo{ + Name: *version.VersionName, + Code: *version.VersionCode, + Description: *version.Description, + ExportTime: version.CreatedAt.Format("2006-01-02 15:04:05"), + }, + Menus: []system.SysBaseMenu{}, + Apis: []system.SysApi{}, + } + jsonData, _ = json.MarshalIndent(basicData, "", " ") + } + + // 设置下载响应头 + filename := fmt.Sprintf("version_%s_%s.json", *version.VersionCode, time.Now().Format("20060102150405")) + c.Header("Content-Type", "application/json") + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + c.Header("Content-Length", strconv.Itoa(len(jsonData))) + + c.Data(http.StatusOK, "application/json", jsonData) +} + +// ImportVersion 导入版本数据 +// @Tags SysVersion +// @Summary 导入版本数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body systemReq.ImportVersionRequest true "版本JSON数据" +// @Success 200 {object} response.Response{msg=string} "导入成功" +// @Router /sysVersion/importVersion [post] +func (sysVersionApi *SysVersionApi) ImportVersion(c *gin.Context) { + ctx := c.Request.Context() + + // 获取JSON数据 + var importData systemReq.ImportVersionRequest + err := c.ShouldBindJSON(&importData) + if err != nil { + response.FailWithMessage("解析JSON数据失败:"+err.Error(), c) + return + } + + // 验证数据格式 + if importData.VersionInfo.Name == "" || importData.VersionInfo.Code == "" { + response.FailWithMessage("版本信息格式错误", c) + return + } + + // 导入菜单数据 + if len(importData.ExportMenu) > 0 { + if err := sysVersionService.ImportMenus(ctx, importData.ExportMenu); err != nil { + global.GVA_LOG.Error("导入菜单失败!", zap.Error(err)) + response.FailWithMessage("导入菜单失败: "+err.Error(), c) + return + } + } + + // 导入API数据 + if len(importData.ExportApi) > 0 { + if err := sysVersionService.ImportApis(importData.ExportApi); err != nil { + global.GVA_LOG.Error("导入API失败!", zap.Error(err)) + response.FailWithMessage("导入API失败: "+err.Error(), c) + return + } + } + + // 导入字典数据 + if len(importData.ExportDictionary) > 0 { + if err := sysVersionService.ImportDictionaries(importData.ExportDictionary); err != nil { + global.GVA_LOG.Error("导入字典失败!", zap.Error(err)) + response.FailWithMessage("导入字典失败: "+err.Error(), c) + return + } + } + + // 创建导入记录 + jsonData, _ := json.Marshal(importData) + version := system.SysVersion{ + VersionName: utils.Pointer(importData.VersionInfo.Name), + VersionCode: utils.Pointer(fmt.Sprintf("%s_imported_%s", importData.VersionInfo.Code, time.Now().Format("20060102150405"))), + Description: utils.Pointer(fmt.Sprintf("导入版本: %s", importData.VersionInfo.Description)), + VersionData: utils.Pointer(string(jsonData)), + } + + err = sysVersionService.CreateSysVersion(ctx, &version) + if err != nil { + global.GVA_LOG.Error("保存导入记录失败!", zap.Error(err)) + // 这里不返回错误,因为数据已经导入成功 + } + + response.OkWithMessage("导入成功", c) +} diff --git a/server/config.docker.yaml b/server/config.docker.yaml index a7783716f..662c5cad1 100644 --- a/server/config.docker.yaml +++ b/server/config.docker.yaml @@ -2,107 +2,103 @@ # jwt configuration jwt: - signing-key: qmPlus - expires-time: 7d - buffer-time: 1d - issuer: qmPlus + signing-key: qmPlus + expires-time: 7d + buffer-time: 1d + issuer: qmPlus # zap logger configuration zap: - level: info - format: console - prefix: "[github.com/flipped-aurora/gin-vue-admin/server]" - director: log - show-line: true - encode-level: LowercaseColorLevelEncoder - stacktrace-key: stacktrace - log-in-console: true + level: info + format: console + prefix: "[github.com/flipped-aurora/gin-vue-admin/server]" + director: log + show-line: true + encode-level: LowercaseColorLevelEncoder + stacktrace-key: stacktrace + log-in-console: true + retention-day: -1 # redis configuration redis: - db: 0 - addr: 177.7.0.14:6379 - password: "" + #是否使用redis集群模式 + useCluster: false + #使用集群模式addr和db默认无效 + addr: 177.7.0.14:6379 + password: "" + db: 0 + clusterAddrs: + - "177.7.0.14:7000" + - "177.7.0.15:7001" + - "177.7.0.13:7002" + +# redis-list configuration +redis-list: + - name: cache # 数据库的名称,注意: name 需要在 redis-list 中唯一 + useCluster: false # 是否使用redis集群模式 + addr: 177.7.0.14:6379 # 使用集群模式addr和db默认无效 + password: "" + db: 0 + clusterAddrs: + - "177.7.0.14:7000" + - "177.7.0.15:7001" + - "177.7.0.13:7002" # mongo configuration mongo: - coll: '' - options: '' - database: '' - username: '' - password: '' - min-pool-size: 0 - max-pool-size: 100 - socket-timeout-ms: 0 - connect-timeout-ms: 0 - is-zap: false - hosts: - - host: '' - port: '' + coll: '' + options: '' + database: '' + username: '' + password: '' + auth-source: '' + min-pool-size: 0 + max-pool-size: 100 + socket-timeout-ms: 0 + connect-timeout-ms: 0 + is-zap: false + hosts: + - host: '' + port: '' # email configuration email: - to: xxx@qq.com - port: 465 - from: xxx@163.com - host: smtp.163.com - is-ssl: true - secret: xxx - nickname: test + to: xxx@qq.com + port: 465 + from: xxx@163.com + host: smtp.163.com + is-ssl: true + secret: xxx + nickname: test # system configuration system: - env: public # Change to "develop" to skip authentication for development mode - addr: 8888 - db-type: mysql - oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置 - use-redis: false # 使用redis - use-mongo: false # 使用mongo - use-multipoint: false - # IP限制次数 一个小时15000次 - iplimit-count: 15000 - # IP限制一个小时 - iplimit-time: 3600 + env: local # 修改为public可以关闭路由日志输出 + addr: 8888 + db-type: mysql + oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置 + use-redis: false # 使用redis + use-mongo: false # 使用mongo + use-multipoint: false + # IP限制次数 一个小时15000次 + iplimit-count: 15000 + # IP限制一个小时 + iplimit-time: 3600 + # 路由全局前缀 + router-prefix: "" + # 严格角色模式 打开后权限将会存在上下级关系 + use-strict-auth: false # captcha configuration captcha: - key-long: 6 - img-width: 240 - img-height: 80 - open-captcha: 0 # 0代表一直开启,大于0代表限制次数 - open-captcha-timeout: 3600 # open-captcha大于0时才生效 + key-long: 6 + img-width: 240 + img-height: 80 + open-captcha: 0 # 0代表一直开启,大于0代表限制次数 + open-captcha-timeout: 3600 # open-captcha大于0时才生效 # mysql connect configuration # 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) mysql: - path: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - max-idle-conns: 10 - max-open-conns: 100 - log-mode: "" - log-zap: false - -# pgsql connect configuration -# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) -pgsql: - path: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - max-idle-conns: 10 - max-open-conns: 100 - log-mode: "" - log-zap: false - -db-list: - - disable: true # 是否禁用 - type: "" # 数据库的类型,目前支持mysql、pgsql - alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一 path: "" port: "" config: "" @@ -114,107 +110,174 @@ db-list: log-mode: "" log-zap: false +# pgsql connect configuration +# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) +pgsql: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +oracle: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +mssql: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +sqlite: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +db-list: + - disable: true # 是否禁用 + type: "" # 数据库的类型,目前支持mysql、pgsql、mssql、oracle + alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一 + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false # local configuration local: - path: uploads/file - store-path: uploads/file + path: uploads/file + store-path: uploads/file # autocode configuration autocode: - transfer-restart: true - # root 自动适配项目根目录 - # 请不要手动配置,他会在项目加载的时候识别出根路径 - root: "" - server: /server - server-plug: /plugin/%s - server-api: /api/v1/%s - server-initialize: /initialize - server-model: /model/%s - server-request: /model/%s/request/ - server-router: /router/%s - server-service: /service/%s - web: /web/src - web-api: /api - web-form: /view - web-table: /view + web: web/src + root: "" # root 自动适配项目根目录, 请不要手动配置,他会在项目加载的时候识别出根路径 + server: server + module: 'github.com/flipped-aurora/gin-vue-admin/server' + ai-path: "" # AI服务路径 # qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址) qiniu: - zone: ZoneHuaDong - bucket: "" - img-path: "" - use-https: false - access-key: "" - secret-key: "" - use-cdn-domains: false + zone: ZoneHuaDong + bucket: "" + img-path: "" + use-https: false + access-key: "" + secret-key: "" + use-cdn-domains: false + +# minio oss configuration +minio: + endpoint: yourEndpoint + access-key-id: yourAccessKeyId + access-key-secret: yourAccessKeySecret + bucket-name: yourBucketName + use-ssl: false + base-path: "" + bucket-url: "http://host:9000/yourBucketName" # aliyun oss configuration aliyun-oss: - endpoint: yourEndpoint - access-key-id: yourAccessKeyId - access-key-secret: yourAccessKeySecret - bucket-name: yourBucketName - bucket-url: yourBucketUrl - base-path: yourBasePath + endpoint: yourEndpoint + access-key-id: yourAccessKeyId + access-key-secret: yourAccessKeySecret + bucket-name: yourBucketName + bucket-url: yourBucketUrl + base-path: yourBasePath # tencent cos configuration tencent-cos: - bucket: xxxxx-10005608 - region: ap-shanghai - secret-id: your-secret-id - secret-key: your-secret-key - base-url: https://gin.vue.admin - path-prefix: github.com/flipped-aurora/gin-vue-admin/server + bucket: xxxxx-10005608 + region: ap-shanghai + secret-id: your-secret-id + secret-key: your-secret-key + base-url: https://gin.vue.admin + path-prefix: github.com/flipped-aurora/gin-vue-admin/server # aws s3 configuration (minio compatible) aws-s3: - bucket: xxxxx-10005608 - region: ap-shanghai - endpoint: "" - s3-force-path-style: false - disable-ssl: false - secret-id: your-secret-id - secret-key: your-secret-key - base-url: https://gin.vue.admin - path-prefix: github.com/flipped-aurora/gin-vue-admin/server + bucket: xxxxx-10005608 + region: ap-shanghai + endpoint: "" + s3-force-path-style: false + disable-ssl: false + secret-id: your-secret-id + secret-key: your-secret-key + base-url: https://gin.vue.admin + path-prefix: github.com/flipped-aurora/gin-vue-admin/server + +# cloudflare r2 configuration +cloudflare-r2: + bucket: xxxx0bucket + base-url: https://gin.vue.admin.com + path: uploads + account-id: xxx_account_id + access-key-id: xxx_key_id + secret-access-key: xxx_secret_key # huawei obs configuration hua-wei-obs: - path: you-path - bucket: you-bucket - endpoint: you-endpoint - access-key: you-access-key - secret-key: you-secret-key + path: you-path + bucket: you-bucket + endpoint: you-endpoint + access-key: you-access-key + secret-key: you-secret-key # excel configuration excel: - dir: ./resource/excel/ + dir: ./resource/excel/ -# timer task db clear table -Timer: - start: true - spec: "@daily" # 定时任务详细配置参考 https://pkg.go.dev/github.com/robfig/cron/v3 - detail: - - tableName: sys_operation_records - compareField: created_at - interval: 2160h - - tableName: jwt_blacklists - compareField: created_at - interval: 168h +# disk usage configuration +disk-list: + - mount-point: "/" # 跨域配置 # 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用 cors: - mode: whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝 - whitelist: - - allow-origin: example1.com - allow-headers: content-type - allow-methods: GET, POST - expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - allow-credentials: true # 布尔值 - - allow-origin: example2.com - allow-headers: content-type - allow-methods: GET, POST - expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - allow-credentials: true # 布尔值 \ No newline at end of file + mode: strict-whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝 + whitelist: + - allow-origin: example1.com + allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id + allow-methods: POST, GET + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + + allow-credentials: true # 布尔值 + - allow-origin: example2.com + allow-headers: content-type + allow-methods: GET, POST + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + allow-credentials: true # 布尔值 +mcp: + name: GVA_MCP + version: v1.0.0 + sse_path: /sse + message_path: /message + url_prefix: '' \ No newline at end of file diff --git a/server/config.yaml b/server/config.yaml index a5f58e3a5..385e8d8b3 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -1,106 +1,200 @@ -aliyun-oss: - endpoint: yourEndpoint - access-key-id: yourAccessKeyId - access-key-secret: yourAccessKeySecret - bucket-name: yourBucketName - bucket-url: yourBucketUrl - base-path: yourBasePath -autocode: - web: web/src - root: D:\Projects\Go\gin-vue-admin - server: server - module: github.com/flipped-aurora/gin-vue-admin/server - ai-path: "" -aws-s3: - bucket: xxxxx-10005608 - region: ap-shanghai - endpoint: "" - secret-id: your-secret-id - secret-key: your-secret-key - base-url: https://gin.vue.admin - path-prefix: github.com/flipped-aurora/gin-vue-admin/server - s3-force-path-style: false - disable-ssl: false +# github.com/flipped-aurora/gin-vue-admin/server Global Configuration + +# jwt configuration +jwt: + signing-key: qmPlus + expires-time: 7d + buffer-time: 1d + issuer: qmPlus +# zap logger configuration +zap: + level: info + format: console + prefix: "[github.com/flipped-aurora/gin-vue-admin/server]" + director: log + show-line: true + encode-level: LowercaseColorLevelEncoder + stacktrace-key: stacktrace + log-in-console: true + retention-day: -1 + +# redis configuration +redis: + #是否使用redis集群模式 + useCluster: false + #使用集群模式addr和db默认无效 + addr: 127.0.0.1:6379 + password: "" + db: 0 + clusterAddrs: + - "172.21.0.3:7000" + - "172.21.0.4:7001" + - "172.21.0.2:7002" + +# redis-list configuration +redis-list: + - name: cache # 数据库的名称,注意: name 需要在 redis-list 中唯一 + useCluster: false # 是否使用redis集群模式 + addr: 127.0.0.1:6379 # 使用集群模式addr和db默认无效 + password: "" + db: 0 + clusterAddrs: + - "172.21.0.3:7000" + - "172.21.0.4:7001" + - "172.21.0.2:7002" + +# mongo configuration +mongo: + coll: '' + options: '' + database: '' + username: '' + password: '' + auth-source: '' + min-pool-size: 0 + max-pool-size: 100 + socket-timeout-ms: 0 + connect-timeout-ms: 0 + is-zap: false + hosts: + - host: '' + port: '' + +# email configuration +email: + to: xxx@qq.com + port: 465 + from: xxx@163.com + host: smtp.163.com + is-ssl: true + secret: xxx + nickname: test + +# system configuration +system: + env: local # 修改为public可以关闭路由日志输出 + addr: 8888 + db-type: mysql + oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置 + use-redis: false # 使用redis + use-mongo: false # 使用mongo + use-multipoint: false + # IP限制次数 一个小时15000次 + iplimit-count: 15000 + # IP限制一个小时 + iplimit-time: 3600 + # 路由全局前缀 + router-prefix: "" + # 严格角色模式 打开后权限将会存在上下级关系 + use-strict-auth: false + +# captcha configuration captcha: key-long: 6 img-width: 240 img-height: 80 - open-captcha: 0 - open-captcha-timeout: 3600 -cloudflare-r2: - bucket: xxxx0bucket - base-url: https://gin.vue.admin.com - path: uploads - account-id: xxx_account_id - access-key-id: xxx_key_id - secret-access-key: xxx_secret_key -cors: - mode: strict-whitelist - whitelist: - - allow-origin: example1.com - allow-methods: POST, GET - allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id - expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - allow-credentials: true - - allow-origin: example2.com - allow-methods: GET, POST - allow-headers: content-type - expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - allow-credentials: true + open-captcha: 0 # 0代表一直开启,大于0代表限制次数 + open-captcha-timeout: 3600 # open-captcha大于0时才生效 + +# mysql connect configuration +# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) +mysql: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false + +# pgsql connect configuration +# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) +pgsql: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +oracle: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +mssql: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +sqlite: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false db-list: - - type: "" - alias-name: "" - prefix: "" + - disable: true # 是否禁用 + type: "" # 数据库的类型,目前支持mysql、pgsql、mssql、oracle + alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一 + path: "" port: "" config: "" db-name: "" username: "" password: "" - path: "" - engine: "" - log-mode: "" max-idle-conns: 10 max-open-conns: 100 - singular: false + log-mode: "" log-zap: false - disable: true -disk-list: - - mount-point: / -email: - to: xxx@qq.com - from: xxx@163.com - host: smtp.163.com - secret: xxx - nickname: test - port: 465 - is-ssl: true - is-loginauth: false -excel: - dir: ./resource/excel/ -hua-wei-obs: - path: you-path - bucket: you-bucket - endpoint: you-endpoint - access-key: you-access-key - secret-key: you-secret-key -jwt: - signing-key: aa114301-5cf5-4f5f-95ba-5352d951031d - expires-time: 7d - buffer-time: 1d - issuer: qmPlus -language: - language: en - default-language: zh - dir: ./resource/lang/ + +# local configuration local: path: uploads/file store-path: uploads/file -mcp: - name: GVA_MCP - version: v1.0.0 - sse_path: /sse - message_path: /message - url_prefix: "" + +# autocode configuration +autocode: + web: web/src + root: "" # root 自动适配项目根目录, 请不要手动配置,他会在项目加载的时候识别出根路径 + server: server + module: 'github.com/flipped-aurora/gin-vue-admin/server' + ai-path: "" # AI服务路径 + +# qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址) +qiniu: + zone: ZoneHuaDong + bucket: "" + img-path: "" + use-https: false + access-key: "" + secret-key: "" + use-cdn-domains: false + +# minio oss configuration minio: endpoint: yourEndpoint access-key-id: yourAccessKeyId @@ -108,131 +202,18 @@ minio: bucket-name: yourBucketName use-ssl: false base-path: "" - bucket-url: http://host:9000/yourBucketName -mongo: - coll: "" - options: "" - database: "" - username: "" - password: "" - auth-source: "" - min-pool-size: 0 - max-pool-size: 100 - socket-timeout-ms: 0 - connect-timeout-ms: 0 - is-zap: false - hosts: - - host: "" - port: "" -mssql: - prefix: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - path: "" - engine: "" - log-mode: "" - max-idle-conns: 10 - max-open-conns: 100 - singular: false - log-zap: false -mysql: - prefix: "" - port: "3306" - config: charset=utf8mb4&parseTime=True&loc=Local - db-name: gva - username: mhassan - password: P@$$w0rD - path: 127.0.0.1 - engine: "" - log-mode: error - max-idle-conns: 10 - max-open-conns: 100 - singular: false - log-zap: false -oracle: - prefix: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - path: "" - engine: "" - log-mode: "" - max-idle-conns: 10 - max-open-conns: 100 - singular: false - log-zap: false -pgsql: - prefix: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - path: "" - engine: "" - log-mode: "" - max-idle-conns: 10 - max-open-conns: 100 - singular: false - log-zap: false -qiniu: - zone: ZoneHuaDong - bucket: "" - img-path: "" - access-key: "" - secret-key: "" - use-https: false - use-cdn-domains: false -redis: - name: "" - addr: 127.0.0.1:6379 - password: "" - db: 0 - useCluster: false - clusterAddrs: - - 172.21.0.3:7000 - - 172.21.0.4:7001 - - 172.21.0.2:7002 -redis-list: - - name: cache - addr: 127.0.0.1:6379 - password: "" - db: 0 - useCluster: false - clusterAddrs: - - 172.21.0.3:7000 - - 172.21.0.4:7001 - - 172.21.0.2:7002 -sqlite: - prefix: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - path: "" - engine: "" - log-mode: "" - max-idle-conns: 10 - max-open-conns: 100 - singular: false - log-zap: false -system: - db-type: mysql - oss-type: local - router-prefix: "" - addr: 8888 - iplimit-count: 15000 - iplimit-time: 3600 - use-multipoint: false - use-redis: false - use-mongo: false - use-strict-auth: false + bucket-url: "http://host:9000/yourBucketName" + +# aliyun oss configuration +aliyun-oss: + endpoint: yourEndpoint + access-key-id: yourAccessKeyId + access-key-secret: yourAccessKeySecret + bucket-name: yourBucketName + bucket-url: yourBucketUrl + base-path: yourBasePath + +# tencent cos configuration tencent-cos: bucket: xxxxx-10005608 region: ap-shanghai @@ -240,13 +221,68 @@ tencent-cos: secret-key: your-secret-key base-url: https://gin.vue.admin path-prefix: github.com/flipped-aurora/gin-vue-admin/server -zap: - level: info - prefix: '[github.com/flipped-aurora/gin-vue-admin/server]' - format: console - director: log - encode-level: LowercaseColorLevelEncoder - stacktrace-key: stacktrace - show-line: true - log-in-console: true - retention-day: -1 + +# aws s3 configuration (minio compatible) +aws-s3: + bucket: xxxxx-10005608 + region: ap-shanghai + endpoint: "" + s3-force-path-style: false + disable-ssl: false + secret-id: your-secret-id + secret-key: your-secret-key + base-url: https://gin.vue.admin + path-prefix: github.com/flipped-aurora/gin-vue-admin/server + +# cloudflare r2 configuration +cloudflare-r2: + bucket: xxxx0bucket + base-url: https://gin.vue.admin.com + path: uploads + account-id: xxx_account_id + access-key-id: xxx_key_id + secret-access-key: xxx_secret_key + +# huawei obs configuration +hua-wei-obs: + path: you-path + bucket: you-bucket + endpoint: you-endpoint + access-key: you-access-key + secret-key: you-secret-key + +# excel configuration +excel: + dir: ./resource/excel/ + +# disk usage configuration +disk-list: + - mount-point: "/" + +# 跨域配置 +# 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用 +cors: + mode: strict-whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝 + whitelist: + - allow-origin: example1.com + allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id + allow-methods: POST, GET + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + + allow-credentials: true # 布尔值 + - allow-origin: example2.com + allow-headers: content-type + allow-methods: GET, POST + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + allow-credentials: true # 布尔值 +mcp: + name: GVA_MCP + version: v1.0.0 + sse_path: /sse + message_path: /message + url_prefix: '' + +language: + language: en + default-language: zh + dir: ./resource/lang/ \ No newline at end of file diff --git a/server/config/gorm_oracle.go b/server/config/gorm_oracle.go index 1bbeb46ab..52cf21c00 100644 --- a/server/config/gorm_oracle.go +++ b/server/config/gorm_oracle.go @@ -1,10 +1,18 @@ package config +import ( + "fmt" + "net" + "net/url" +) + type Oracle struct { GeneralDB `yaml:",inline" mapstructure:",squash"` } func (m *Oracle) Dsn() string { - return "oracle://" + m.Username + ":" + m.Password + "@" + m.Path + ":" + m.Port + "/" + m.Dbname + "?" + m.Config + dsn := fmt.Sprintf("oracle://%s:%s@%s/%s?%s", url.PathEscape(m.Username), url.PathEscape(m.Password), + net.JoinHostPort(m.Path, m.Port), url.PathEscape(m.Dbname), m.Config) + return dsn } diff --git a/server/core/server.go b/server/core/server.go index 05cd85286..b692c004f 100644 --- a/server/core/server.go +++ b/server/core/server.go @@ -36,27 +36,26 @@ func RunServer() { address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr) fmt.Printf(` - %s gin-vue-admin - %s:v2.8.2 - %s - %s:https://github.com/flipped-aurora/gin-vue-admin - %s:https://plugin.gin-vue-admin.com - %s:https://support.qq.com/products/371961 - %s:http://127.0.0.1%s/swagger/index.html - 默认MCP SSE地址:http://127.0.0.1%s%s - 默认MCP Message地址:http://127.0.0.1%s%s - %s:http://127.0.0.1:8080 - %s - %s - %s - %s + %s gin-vue-admin + %s:%s + %s + %s://github.com/flipped-aurora/gin-vue-admin + %s:https://plugin.gin-vue-admin.com + %s:https://support.qq.com/products/371961 + %s:http://127.0.0.1%s/swagger/index.html + 默认MCP SSE地址:http://127.0.0.1%s%s + 默认MCP Message地址:http://127.0.0.1%s%s + %s:http://127.0.0.1:8080 + %s + %s + %s + %s + ** 感谢您对Gin-Vue-Admin的支持与关注 合法授权使用更有利于项目的长久发展** `, global.Translate("core.server.welcomeTo"), global.Translate("core.server.currentVersion"), + global.Version, global.Translate("core.server.joinGroup"), - global.Translate("core.server.website"), - global.Translate("core.server.pluginMarket"), - global.Translate("core.server.community"), - global.Translate("core.server.swagger"), + global.Translate("core.server.website"), global.Translate("core.server.pluginMarket"), global.Translate("core.server.community"), global.Translate("core.server.swagger"), address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath, global.Translate("core.server.frontend"), global.Translate("core.server.copyright1"), diff --git a/server/docs/docs.go b/server/docs/docs.go index 6f0eb2a0d..e5fc176f3 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -2,7 +2,10 @@ package docs -import "github.com/swaggo/swag" +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/swaggo/swag" +) const docTemplate = `{ "schemes": {{ marshal .Schemes }}, @@ -9296,7 +9299,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "v2.8.2", + Version: global.Version, Host: "", BasePath: "", Schemes: []string{}, diff --git a/server/global/version.go b/server/global/version.go new file mode 100644 index 000000000..68314d1e6 --- /dev/null +++ b/server/global/version.go @@ -0,0 +1,12 @@ +package global + +// Version 版本信息 +// 目前只有Version正式使用 其余为预留 +const ( + // Version 当前版本号 + Version = "v2.8.5" + // AppName 应用名称 + AppName = "Gin-Vue-Admin" + // Description 应用描述 + Description = "使用gin+vue进行极速开发的全栈开发基础平台" +) \ No newline at end of file diff --git a/server/go.mod b/server/go.mod index f9ac52deb..2f1de05b5 100644 --- a/server/go.mod +++ b/server/go.mod @@ -9,6 +9,7 @@ require ( github.com/aws/aws-sdk-go v1.55.6 github.com/casbin/casbin/v2 v2.103.0 github.com/casbin/gorm-adapter/v3 v3.32.0 + github.com/dzwvip/gorm-oracle v0.1.2 github.com/fsnotify/fsnotify v1.8.0 github.com/gin-gonic/gin v1.10.0 github.com/glebarez/sqlite v1.11.0 @@ -76,6 +77,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/emirpasic/gods v1.12.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gammazero/toposort v0.1.1 // indirect github.com/gin-contrib/sse v1.0.0 // indirect @@ -139,6 +141,7 @@ require ( github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sijms/go-ora/v2 v2.7.17 // indirect github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect @@ -146,7 +149,8 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/therootcompany/xz v1.0.1 // indirect - github.com/tidwall/gjson v1.17.3 // indirect + github.com/thoas/go-funk v0.7.0 // indirect + github.com/tidwall/gjson v1.14.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect diff --git a/server/go.sum b/server/go.sum index 6592a892f..082a88ac6 100644 --- a/server/go.sum +++ b/server/go.sum @@ -108,6 +108,10 @@ github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj6 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 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/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= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -261,6 +265,7 @@ github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJk github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -411,6 +416,8 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sijms/go-ora/v2 v2.7.17 h1:M/pYIqjaMUeBxyzOWp2oj4ntF6fHSBloJWGNH9vbmsU= +github.com/sijms/go-ora/v2 v2.7.17/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= github.com/songzhibin97/gkit v1.2.13 h1:paY0XJkdRuy9/8k9nTnbdrzo8pC22jIIFldUkOQv5nU= github.com/songzhibin97/gkit v1.2.13/go.mod h1:38CreNR27eTGaG1UMGihrXqI4xc3nGfYxLVKKVx6Ngg= github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= @@ -454,9 +461,10 @@ github.com/tencentyun/cos-go-sdk-v5 v0.7.60 h1:/e/tmvRmfKexr/QQIBzWhOkZWsmY3EK72 github.com/tencentyun/cos-go-sdk-v5 v0.7.60/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y= +github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= -github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= @@ -794,6 +802,7 @@ gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY= gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE= +gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/server/initialize/ensure_tables.go b/server/initialize/ensure_tables.go index c58227e3b..7a5acf736 100644 --- a/server/initialize/ensure_tables.go +++ b/server/initialize/ensure_tables.go @@ -53,7 +53,7 @@ func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error sysModel.Condition{}, sysModel.JoinTemplate{}, sysModel.SysParams{}, - + sysModel.SysVersion{}, adapter.CasbinRule{}, example.ExaFile{}, diff --git a/server/initialize/gorm.go b/server/initialize/gorm.go index e5cdfa7c7..45d8a3145 100644 --- a/server/initialize/gorm.go +++ b/server/initialize/gorm.go @@ -56,6 +56,7 @@ func RegisterTables() { system.Condition{}, system.JoinTemplate{}, system.SysParams{}, + system.SysVersion{}, example.ExaFile{}, example.ExaCustomer{}, diff --git a/server/initialize/gorm_mssql.go b/server/initialize/gorm_mssql.go index e1478ea41..530275ab1 100644 --- a/server/initialize/gorm_mssql.go +++ b/server/initialize/gorm_mssql.go @@ -28,7 +28,9 @@ func GormMssql() *gorm.DB { DSN: m.Dsn(), // DSN data source name DefaultStringSize: 191, // string 类型字段的默认长度 } - if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); err != nil { return nil } else { db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) @@ -48,7 +50,9 @@ func GormMssqlByConfig(m config.Mssql) *gorm.DB { DSN: m.Dsn(), // DSN data source name DefaultStringSize: 191, // string 类型字段的默认长度 } - if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); err != nil { panic(err) } else { db.InstanceSet("gorm:table_options", "ENGINE=InnoDB") diff --git a/server/initialize/gorm_mysql.go b/server/initialize/gorm_mysql.go index 61e50ba29..cf949aa18 100644 --- a/server/initialize/gorm_mysql.go +++ b/server/initialize/gorm_mysql.go @@ -34,8 +34,9 @@ func initMysqlDatabase(m config.Mysql) *gorm.DB { DefaultStringSize: 191, // string 类型字段的默认长度 SkipInitializeWithVersion: false, // 根据版本自动配置 } - - if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(general)); err != nil { panic(err) } else { db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) diff --git a/server/initialize/gorm_oracle.go b/server/initialize/gorm_oracle.go index 513359f0f..9d3d66359 100644 --- a/server/initialize/gorm_oracle.go +++ b/server/initialize/gorm_oracle.go @@ -1,18 +1,14 @@ package initialize import ( - //"github.com/dzwvip/oracle" + oracle "github.com/dzwvip/gorm-oracle" "github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" - - //_ "github.com/godror/godror" - "gorm.io/driver/mysql" "gorm.io/gorm" ) // GormOracle 初始化oracle数据库 -// 如果需要Oracle库 放开import里的注释 把下方 mysql.Config 改为 oracle.Config ; mysql.New 改为 oracle.New func GormOracle() *gorm.DB { m := global.GVA_CONFIG.Oracle return initOracleDatabase(m) @@ -28,13 +24,9 @@ func initOracleDatabase(m config.Oracle) *gorm.DB { if m.Dbname == "" { return nil } - - oracleConfig := mysql.Config{ - DSN: m.Dsn(), // DSN data source name - DefaultStringSize: 191, // string 类型字段的默认长度 - } - - if db, err := gorm.Open(mysql.New(oracleConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(oracle.Open(m.Dsn()), internal.Gorm.Config(general)); err != nil { panic(err) } else { sqlDB, _ := db.DB() diff --git a/server/initialize/gorm_pgsql.go b/server/initialize/gorm_pgsql.go index 6abde5892..b5022db2f 100644 --- a/server/initialize/gorm_pgsql.go +++ b/server/initialize/gorm_pgsql.go @@ -30,7 +30,9 @@ func initPgSqlDatabase(p config.Pgsql) *gorm.DB { DSN: p.Dsn(), // DSN data source name PreferSimpleProtocol: false, } - if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(p.Prefix, p.Singular)); err != nil { + // 数据库配置 + general := p.GeneralDB + if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(general)); err != nil { panic(err) } else { sqlDB, _ := db.DB() diff --git a/server/initialize/gorm_sqlite.go b/server/initialize/gorm_sqlite.go index 9d158bf16..33510eea5 100644 --- a/server/initialize/gorm_sqlite.go +++ b/server/initialize/gorm_sqlite.go @@ -25,7 +25,9 @@ func initSqliteDatabase(s config.Sqlite) *gorm.DB { return nil } - if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(s.Prefix, s.Singular)); err != nil { + // 数据库配置 + general := s.GeneralDB + if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(general)); err != nil { panic(err) } else { sqlDB, _ := db.DB() diff --git a/server/initialize/internal/gorm.go b/server/initialize/internal/gorm.go index ebf5526c3..28f40979a 100644 --- a/server/initialize/internal/gorm.go +++ b/server/initialize/internal/gorm.go @@ -1,12 +1,12 @@ package internal import ( + "time" + "github.com/flipped-aurora/gin-vue-admin/server/config" - "github.com/flipped-aurora/gin-vue-admin/server/global" "gorm.io/gorm" "gorm.io/gorm/logger" "gorm.io/gorm/schema" - "time" ) var Gorm = new(_gorm) @@ -15,22 +15,7 @@ type _gorm struct{} // Config gorm 自定义配置 // Author [SliverHorn](https://github.com/SliverHorn) -func (g *_gorm) Config(prefix string, singular bool) *gorm.Config { - var general config.GeneralDB - switch global.GVA_CONFIG.System.DbType { - case "mysql": - general = global.GVA_CONFIG.Mysql.GeneralDB - case "pgsql": - general = global.GVA_CONFIG.Pgsql.GeneralDB - case "oracle": - general = global.GVA_CONFIG.Oracle.GeneralDB - case "sqlite": - general = global.GVA_CONFIG.Sqlite.GeneralDB - case "mssql": - general = global.GVA_CONFIG.Mssql.GeneralDB - default: - general = global.GVA_CONFIG.Mysql.GeneralDB - } +func (g *_gorm) Config(general config.GeneralDB) *gorm.Config { return &gorm.Config{ Logger: logger.New(NewWriter(general), logger.Config{ SlowThreshold: 200 * time.Millisecond, @@ -38,8 +23,8 @@ func (g *_gorm) Config(prefix string, singular bool) *gorm.Config { Colorful: true, }), NamingStrategy: schema.NamingStrategy{ - TablePrefix: prefix, - SingularTable: singular, + TablePrefix: general.Prefix, + SingularTable: general.Singular, }, DisableForeignKeyConstraintWhenMigrating: true, } diff --git a/server/initialize/router.go b/server/initialize/router.go index 2803594d3..cb50bfaea 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -97,6 +97,7 @@ func Routers() *gin.Engine { systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由 systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由 systemRouter.InitSystemRouter(PrivateGroup) // system相关路由 + systemRouter.InitSysVersionRouter(PrivateGroup) // 发版相关路由 systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由 systemRouter.InitAutoCodeRouter(PrivateGroup, PublicGroup) // 创建自动化代码 systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由 diff --git a/server/main.go b/server/main.go index 47ff720bd..5e3335db8 100644 --- a/server/main.go +++ b/server/main.go @@ -22,7 +22,7 @@ import ( // @Tag.Description 用户 // @title Gin-Vue-Admin Swagger API接口文档 -// @version v2.8.2 +// @version v2.8.5 // @description 使用gin+vue进行极速开发的全栈开发基础平台 // @securityDefinitions.apikey ApiKeyAuth // @in header diff --git a/server/mcp/api_creator.go b/server/mcp/api_creator.go new file mode 100644 index 000000000..1a1503d91 --- /dev/null +++ b/server/mcp/api_creator.go @@ -0,0 +1,201 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "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" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + RegisterTool(&ApiCreator{}) +} + +// ApiCreateRequest API创建请求结构 +type ApiCreateRequest struct { + Path string `json:"path"` // API路径 + Description string `json:"description"` // API中文描述 + ApiGroup string `json:"apiGroup"` // API组 + Method string `json:"method"` // HTTP方法 +} + +// ApiCreateResponse API创建响应结构 +type ApiCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + ApiID uint `json:"apiId"` + Path string `json:"path"` + Method string `json:"method"` +} + +// ApiCreator API创建工具 +type ApiCreator struct{} + +// New 创建API创建工具 +func (a *ApiCreator) New() mcp.Tool { + return mcp.NewTool("create_api", + mcp.WithDescription(`创建后端API记录,用于AI编辑器自动添加API接口时自动创建对应的API权限记录。 + +**重要限制:** +- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成API权限,不应调用此工具 +- 仅在以下情况使用:1) 单独创建API(不涉及模块创建);2) AI编辑器自动添加API;3) router下的文件产生路径变化时`), + mcp.WithString("path", + mcp.Required(), + mcp.Description("API路径,如:/user/create"), + ), + mcp.WithString("description", + mcp.Required(), + mcp.Description("API中文描述,如:创建用户"), + ), + mcp.WithString("apiGroup", + mcp.Required(), + mcp.Description("API组名称,用于分类管理,如:用户管理"), + ), + mcp.WithString("method", + mcp.Description("HTTP方法"), + mcp.DefaultString("POST"), + ), + mcp.WithString("apis", + mcp.Description("批量创建API的JSON字符串,格式:[{\"path\":\"/user/create\",\"description\":\"创建用户\",\"apiGroup\":\"用户管理\",\"method\":\"POST\"}]"), + ), + ) +} + +// Handle 处理API创建请求 +func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := request.GetArguments() + + var apis []ApiCreateRequest + + // 检查是否是批量创建 + if apisStr, ok := args["apis"].(string); ok && apisStr != "" { + if err := json.Unmarshal([]byte(apisStr), &apis); err != nil { + return nil, fmt.Errorf("apis 参数格式错误: %v", err) + } + } else { + // 单个API创建 + path, ok := args["path"].(string) + if !ok || path == "" { + return nil, errors.New("path 参数是必需的") + } + + description, ok := args["description"].(string) + if !ok || description == "" { + return nil, errors.New("description 参数是必需的") + } + + apiGroup, ok := args["apiGroup"].(string) + if !ok || apiGroup == "" { + return nil, errors.New("apiGroup 参数是必需的") + } + + method := "POST" + if val, ok := args["method"].(string); ok && val != "" { + method = val + } + + apis = append(apis, ApiCreateRequest{ + Path: path, + Description: description, + ApiGroup: apiGroup, + Method: method, + }) + } + + if len(apis) == 0 { + return nil, errors.New("没有要创建的API") + } + + // 创建API记录 + apiService := service.ServiceGroupApp.SystemServiceGroup.ApiService + var responses []ApiCreateResponse + successCount := 0 + + for _, apiReq := range apis { + api := system.SysApi{ + Path: apiReq.Path, + Description: apiReq.Description, + ApiGroup: apiReq.ApiGroup, + Method: apiReq.Method, + } + + err := apiService.CreateApi(api) + if err != nil { + global.GVA_LOG.Warn("创建API失败", + zap.String("path", apiReq.Path), + zap.String("method", apiReq.Method), + zap.Error(err)) + + responses = append(responses, ApiCreateResponse{ + Success: false, + Message: fmt.Sprintf("创建API失败: %v", err), + Path: apiReq.Path, + Method: apiReq.Method, + }) + } else { + // 获取创建的API ID + var createdApi system.SysApi + err = global.GVA_DB.Where("path = ? AND method = ?", apiReq.Path, apiReq.Method).First(&createdApi).Error + if err != nil { + global.GVA_LOG.Warn("获取创建的API ID失败", zap.Error(err)) + } + + responses = append(responses, ApiCreateResponse{ + Success: true, + Message: fmt.Sprintf("成功创建API %s %s", apiReq.Method, apiReq.Path), + ApiID: createdApi.ID, + Path: apiReq.Path, + Method: apiReq.Method, + }) + successCount++ + } + } + + // 构建总体响应 + var resultMessage string + if len(apis) == 1 { + resultMessage = responses[0].Message + } else { + resultMessage = fmt.Sprintf("批量创建API完成,成功 %d 个,失败 %d 个", successCount, len(apis)-successCount) + } + + result := map[string]interface{}{ + "success": successCount > 0, + "message": resultMessage, + "totalCount": len(apis), + "successCount": successCount, + "failedCount": len(apis) - successCount, + "details": responses, + } + + resultJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + // 添加权限分配提醒 + permissionReminder := "\n\n⚠️ 重要提醒:\n" + + "API创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的API权限," + + "以确保用户能够正常访问新接口。\n" + + "具体步骤:\n" + + "1. 进入角色管理页面\n" + + "2. 选择需要授权的角色\n" + + "3. 在API权限中勾选新创建的API接口\n" + + "4. 保存权限配置" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("API创建结果:\n\n%s%s", string(resultJSON), permissionReminder), + }, + }, + }, nil +} diff --git a/server/mcp/api_lister.go b/server/mcp/api_lister.go new file mode 100644 index 000000000..d8a87d1f1 --- /dev/null +++ b/server/mcp/api_lister.go @@ -0,0 +1,165 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + // 注册工具将在enter.go中统一处理 + RegisterTool(&ApiLister{}) +} + +// ApiInfo API信息结构 +type ApiInfo struct { + ID uint `json:"id,omitempty"` // 数据库ID(仅数据库API有) + Path string `json:"path"` // API路径 + Description string `json:"description,omitempty"` // API描述 + ApiGroup string `json:"apiGroup,omitempty"` // API组 + Method string `json:"method"` // HTTP方法 + Source string `json:"source"` // 来源:database 或 gin +} + +// ApiListResponse API列表响应结构 +type ApiListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + DatabaseApis []ApiInfo `json:"databaseApis"` // 数据库中的API + GinApis []ApiInfo `json:"ginApis"` // gin框架中的API + TotalCount int `json:"totalCount"` // 总数量 +} + +// ApiLister API列表工具 +type ApiLister struct{} + +// New 创建API列表工具 +func (a *ApiLister) New() mcp.Tool { + return mcp.NewTool("list_all_apis", + mcp.WithDescription(`获取系统中所有的API接口,分为两组: + +**功能说明:** +- 返回数据库中已注册的API列表 +- 返回gin框架中实际注册的路由API列表 +- 帮助前端判断是使用现有API还是需要创建新的API,如果api在前端未使用且需要前端调用的时候,请到api文件夹下对应模块的js中添加方法并暴露给当前业务调用 + +**返回数据结构:** +- databaseApis: 数据库中的API记录(包含ID、描述、分组等完整信息) +- ginApis: gin路由中的API(仅包含路径和方法),需要AI根据路径自行揣摩路径的业务含义,例如:/api/user/:id 表示根据用户ID获取用户信息`), + ) +} + +// Handle 处理API列表请求 +func (a *ApiLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + + // 获取数据库中的API + databaseApis, err := a.getDatabaseApis() + if err != nil { + global.GVA_LOG.Error("获取数据库API失败", zap.Error(err)) + errorResponse := ApiListResponse{ + Success: false, + Message: "获取数据库API失败: " + err.Error(), + } + resultJSON, _ := json.Marshal(errorResponse) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(resultJSON), + }, + }, + }, nil + } + + // 获取gin路由中的API + ginApis, err := a.getGinApis() + if err != nil { + global.GVA_LOG.Error("获取gin路由API失败", zap.Error(err)) + errorResponse := ApiListResponse{ + Success: false, + Message: "获取gin路由API失败: " + err.Error(), + } + resultJSON, _ := json.Marshal(errorResponse) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(resultJSON), + }, + }, + }, nil + } + + // 构建响应 + response := ApiListResponse{ + Success: true, + Message: "获取API列表成功", + DatabaseApis: databaseApis, + GinApis: ginApis, + TotalCount: len(databaseApis) + len(ginApis), + } + + global.GVA_LOG.Info("API列表获取成功", + zap.Int("数据库API数量", len(databaseApis)), + zap.Int("gin路由API数量", len(ginApis)), + zap.Int("总数量", response.TotalCount)) + + resultJSON, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(resultJSON), + }, + }, + }, nil +} + +// getDatabaseApis 获取数据库中的所有API +func (a *ApiLister) getDatabaseApis() ([]ApiInfo, error) { + var apis []system.SysApi + err := global.GVA_DB.Model(&system.SysApi{}).Order("api_group ASC, path ASC").Find(&apis).Error + if err != nil { + return nil, err + } + + // 转换为ApiInfo格式 + var result []ApiInfo + for _, api := range apis { + result = append(result, ApiInfo{ + ID: api.ID, + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + Source: "database", + }) + } + + return result, nil +} + +// getGinApis 获取gin路由中的所有API(包含被忽略的API) +func (a *ApiLister) getGinApis() ([]ApiInfo, error) { + // 从gin路由信息中获取所有API + var result []ApiInfo + for _, route := range global.GVA_ROUTERS { + result = append(result, ApiInfo{ + Path: route.Path, + Method: route.Method, + Source: "gin", + }) + } + + return result, nil +} diff --git a/server/mcp/current_time.go b/server/mcp/current_time.go deleted file mode 100644 index 41eaea308..000000000 --- a/server/mcp/current_time.go +++ /dev/null @@ -1,81 +0,0 @@ -package mcpTool - -import ( - "context" - "errors" - "fmt" - "github.com/mark3labs/mcp-go/mcp" - "time" -) - -func init() { - RegisterTool(&CurrentTime{}) -} - -type CurrentTime struct { -} - -// 获取当前系统时间 -func (t *CurrentTime) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - // 获取当前系统时间 - - timezone, ok := request.GetArguments()["timezone"].(string) - - if !ok { - return nil, errors.New("参数错误:timezone 必须是字符串类型") - } - // 根据timezone参数加载对应时区 - loc, err := loadTimeZone(timezone) - if err != nil { - return nil, err - } - - // 获取当前时间并转换为指定时区 - currentTime := time.Now().In(loc).Format("2006-01-02 15:04:05") - //返回 - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf("%s 时区的当前时间是:%s", timezone, currentTime), - }, - }, - }, nil -} - -func (t *CurrentTime) New() mcp.Tool { - return mcp.NewTool("currentTime", - mcp.WithDescription("获取当前系统时间"), - mcp.WithString("timezone", - mcp.Required(), - mcp.Description("时区"), - mcp.Enum("UTC", "CST", "PST", "EST", "GMT", "CET", "JST", "MST", "IST", "AST", "HST"), - )) -} - -// 将简写时区转换为IANA标准时区 -func loadTimeZone(timezone string) (*time.Location, error) { - // 时区映射表 - timezoneMap := map[string]string{ - "UTC": "UTC", - "CST": "Asia/Shanghai", // 中国标准时间 - "PST": "America/Los_Angeles", - "EST": "America/New_York", - "GMT": "GMT", - "CET": "Europe/Paris", - "JST": "Asia/Tokyo", - "MST": "America/Denver", - "IST": "Asia/Kolkata", - "AST": "Asia/Riyadh", // 阿拉伯标准时间 - "HST": "Pacific/Honolulu", - } - - // 获取标准时区名称 - tzName, exists := timezoneMap[timezone] - if !exists { - return nil, errors.New("不支持的时区: " + timezone) - } - - // 加载时区 - return time.LoadLocation(tzName) -} diff --git a/server/mcp/dictionary_generator.go b/server/mcp/dictionary_generator.go new file mode 100644 index 000000000..e7c655806 --- /dev/null +++ b/server/mcp/dictionary_generator.go @@ -0,0 +1,310 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "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" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" + "gorm.io/gorm" +) + +func init() { + RegisterTool(&DictionaryOptionsGenerator{}) +} + +// DictionaryOptionsGenerator 字典选项生成器 +type DictionaryOptionsGenerator struct{} + +// DictionaryGenerateRequest 字典生成请求 +type DictionaryGenerateRequest struct { + DictType string `json:"dictType"` // 字典类型 + FieldDesc string `json:"fieldDesc"` // 字段描述 + Options []DictionaryOption `json:"options"` // AI生成的字典选项 + DictName string `json:"dictName"` // 字典名称(可选) + Description string `json:"description"` // 字典描述(可选) +} + +// DictionaryGenerateResponse 字典生成响应 +type DictionaryGenerateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + DictType string `json:"dictType"` + OptionsCount int `json:"optionsCount"` +} + +// New 返回工具注册信息 +func (d *DictionaryOptionsGenerator) New() mcp.Tool { + return mcp.NewTool("generate_dictionary_options", + mcp.WithDescription("智能生成字典选项并自动创建字典和字典详情"), + mcp.WithString("dictType", + mcp.Required(), + mcp.Description("字典类型,用于标识字典的唯一性"), + ), + mcp.WithString("fieldDesc", + mcp.Required(), + mcp.Description("字段描述,用于AI理解字段含义"), + ), + mcp.WithString("options", + mcp.Required(), + mcp.Description("字典选项JSON字符串,格式:[{\"label\":\"显示名\",\"value\":\"值\",\"sort\":1}]"), + ), + mcp.WithString("dictName", + mcp.Description("字典名称,如果不提供将自动生成"), + ), + mcp.WithString("description", + mcp.Description("字典描述"), + ), + ) +} + +// Name 返回工具名称 +func (d *DictionaryOptionsGenerator) Name() string { + return "generate_dictionary_options" +} + +// Description 返回工具描述 +func (d *DictionaryOptionsGenerator) Description() string { + return `字典选项生成工具 - 让AI生成并创建字典选项 + +此工具允许AI根据字典类型和字段描述生成合适的字典选项,并自动创建字典和字典详情。 + +参数说明: +- dictType: 字典类型(必填) +- fieldDesc: 字段描述(必填) +- options: AI生成的字典选项数组(必填) + - label: 选项标签 + - value: 选项值 + - sort: 排序号 +- dictName: 字典名称(可选,默认根据fieldDesc生成) +- description: 字典描述(可选) + +使用场景: +1. 在创建模块时,如果字段需要字典类型,AI可以根据字段描述智能生成合适的选项 +2. 支持各种业务场景的字典选项生成,如状态、类型、等级等 +3. 自动创建字典和字典详情,无需手动配置 + +示例调用: +{ + "dictType": "user_status", + "fieldDesc": "用户状态", + "options": [ + {"label": "正常", "value": "1", "sort": 1}, + {"label": "禁用", "value": "0", "sort": 2} + ], + "dictName": "用户状态字典", + "description": "用于管理用户账户状态的字典" +}` +} + +// InputSchema 返回输入参数的JSON Schema +func (d *DictionaryOptionsGenerator) InputSchema() map[string]interface{} { + return map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "dictType": map[string]interface{}{ + "type": "string", + "description": "字典类型,用于标识字典的唯一性", + }, + "fieldDesc": map[string]interface{}{ + "type": "string", + "description": "字段描述,用于生成字典名称和理解字典用途", + }, + "options": map[string]interface{}{ + "type": "array", + "description": "AI生成的字典选项数组", + "items": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "label": map[string]interface{}{ + "type": "string", + "description": "选项标签,显示给用户的文本", + }, + "value": map[string]interface{}{ + "type": "string", + "description": "选项值,存储在数据库中的值", + }, + "sort": map[string]interface{}{ + "type": "integer", + "description": "排序号,用于控制选项显示顺序", + }, + }, + "required": []string{"label", "value", "sort"}, + }, + }, + "dictName": map[string]interface{}{ + "type": "string", + "description": "字典名称,可选,默认根据fieldDesc生成", + }, + "description": map[string]interface{}{ + "type": "string", + "description": "字典描述,可选", + }, + }, + "required": []string{"dictType", "fieldDesc", "options"}, + } +} + +// Handle 处理工具调用 +func (d *DictionaryOptionsGenerator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 解析请求参数 + args := request.GetArguments() + + dictType, ok := args["dictType"].(string) + if !ok || dictType == "" { + return nil, errors.New("dictType 参数是必需的") + } + + fieldDesc, ok := args["fieldDesc"].(string) + if !ok || fieldDesc == "" { + return nil, errors.New("fieldDesc 参数是必需的") + } + + optionsStr, ok := args["options"].(string) + if !ok || optionsStr == "" { + return nil, errors.New("options 参数是必需的") + } + + // 解析options JSON字符串 + var options []DictionaryOption + if err := json.Unmarshal([]byte(optionsStr), &options); err != nil { + return nil, fmt.Errorf("options 参数格式错误: %v", err) + } + + if len(options) == 0 { + return nil, errors.New("options 不能为空") + } + + // 可选参数 + dictName, _ := args["dictName"].(string) + description, _ := args["description"].(string) + + // 构建请求对象 + req := &DictionaryGenerateRequest{ + DictType: dictType, + FieldDesc: fieldDesc, + Options: options, + DictName: dictName, + Description: description, + } + + // 创建字典 + response, err := d.createDictionaryWithOptions(ctx, req) + if err != nil { + return nil, err + } + + // 构建响应 + resultJSON, err := json.MarshalIndent(response, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("字典选项生成结果:\n\n%s", string(resultJSON)), + }, + }, + }, nil +} + +// createDictionaryWithOptions 创建字典和字典选项 +func (d *DictionaryOptionsGenerator) createDictionaryWithOptions(ctx context.Context, req *DictionaryGenerateRequest) (*DictionaryGenerateResponse, error) { + // 检查字典是否已存在 + exists, err := d.checkDictionaryExists(req.DictType) + if err != nil { + return nil, fmt.Errorf("检查字典是否存在失败: %v", err) + } + + if exists { + return &DictionaryGenerateResponse{ + Success: false, + Message: fmt.Sprintf("字典 %s 已存在,跳过创建", req.DictType), + DictType: req.DictType, + OptionsCount: 0, + }, nil + } + + // 生成字典名称 + dictName := req.DictName + if dictName == "" { + dictName = d.generateDictionaryName(req.DictType, req.FieldDesc) + } + + // 创建字典 + dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService + dictionary := system.SysDictionary{ + Name: dictName, + Type: req.DictType, + Status: &[]bool{true}[0], // 默认启用 + Desc: req.Description, + } + + err = dictionaryService.CreateSysDictionary(dictionary) + if err != nil { + return nil, fmt.Errorf("创建字典失败: %v", err) + } + + // 获取刚创建的字典ID + var createdDict system.SysDictionary + err = global.GVA_DB.Where("type = ?", req.DictType).First(&createdDict).Error + if err != nil { + return nil, fmt.Errorf("获取创建的字典失败: %v", err) + } + + // 创建字典详情项 + dictionaryDetailService := service.ServiceGroupApp.SystemServiceGroup.DictionaryDetailService + successCount := 0 + + for _, option := range req.Options { + dictionaryDetail := system.SysDictionaryDetail{ + Label: option.Label, + Value: option.Value, + Status: &[]bool{true}[0], // 默认启用 + Sort: option.Sort, + SysDictionaryID: int(createdDict.ID), + } + + err = dictionaryDetailService.CreateSysDictionaryDetail(dictionaryDetail) + if err != nil { + global.GVA_LOG.Warn("创建字典详情项失败", zap.Error(err)) + } else { + successCount++ + } + } + + return &DictionaryGenerateResponse{ + Success: true, + Message: fmt.Sprintf("成功创建字典 %s,包含 %d 个选项", req.DictType, successCount), + DictType: req.DictType, + OptionsCount: successCount, + }, nil +} + +// checkDictionaryExists 检查字典是否存在 +func (d *DictionaryOptionsGenerator) checkDictionaryExists(dictType string) (bool, error) { + var dictionary system.SysDictionary + err := global.GVA_DB.Where("type = ?", dictType).First(&dictionary).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil // 字典不存在 + } + return false, err // 其他错误 + } + return true, nil // 字典存在 +} + +// generateDictionaryName 生成字典名称 +func (d *DictionaryOptionsGenerator) generateDictionaryName(dictType, fieldDesc string) string { + if fieldDesc != "" { + return fmt.Sprintf("%s字典", fieldDesc) + } + return fmt.Sprintf("%s字典", dictType) +} diff --git a/server/mcp/dictionary_query.go b/server/mcp/dictionary_query.go new file mode 100644 index 000000000..aea5df48b --- /dev/null +++ b/server/mcp/dictionary_query.go @@ -0,0 +1,234 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "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" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// 注册工具 +func init() { + RegisterTool(&DictionaryQuery{}) +} + +// DictionaryInfo 字典信息结构 +type DictionaryInfo struct { + ID uint `json:"id"` + Name string `json:"name"` // 字典名(中) + Type string `json:"type"` // 字典名(英) + Status *bool `json:"status"` // 状态 + Desc string `json:"desc"` // 描述 + Details []DictionaryDetailInfo `json:"details"` // 字典详情 +} + +// DictionaryDetailInfo 字典详情信息结构 +type DictionaryDetailInfo struct { + ID uint `json:"id"` + Label string `json:"label"` // 展示值 + Value string `json:"value"` // 字典值 + Extend string `json:"extend"` // 扩展值 + Status *bool `json:"status"` // 启用状态 + Sort int `json:"sort"` // 排序标记 +} + +// DictionaryQueryResponse 字典查询响应结构 +type DictionaryQueryResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Total int `json:"total"` + Dictionaries []DictionaryInfo `json:"dictionaries"` +} + +// DictionaryQuery 字典查询工具 +type DictionaryQuery struct{} + +// New 创建字典查询工具 +func (d *DictionaryQuery) New() mcp.Tool { + return mcp.NewTool("query_dictionaries", + mcp.WithDescription("查询系统中所有的字典和字典属性,用于AI生成逻辑时了解可用的字典选项"), + mcp.WithString("dictType", + mcp.Description("可选:指定字典类型进行精确查询,如果不提供则返回所有字典"), + ), + mcp.WithBoolean("includeDisabled", + mcp.Description("是否包含已禁用的字典和字典项,默认为false(只返回启用的)"), + ), + mcp.WithBoolean("detailsOnly", + mcp.Description("是否只返回字典详情信息(不包含字典基本信息),默认为false"), + ), + ) +} + +// Handle 处理字典查询请求 +func (d *DictionaryQuery) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := request.GetArguments() + + // 获取参数 + dictType := "" + if val, ok := args["dictType"].(string); ok { + dictType = val + } + + includeDisabled := false + if val, ok := args["includeDisabled"].(bool); ok { + includeDisabled = val + } + + detailsOnly := false + if val, ok := args["detailsOnly"].(bool); ok { + detailsOnly = val + } + + // 获取字典服务 + dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService + + var dictionaries []DictionaryInfo + var err error + + if dictType != "" { + // 查询指定类型的字典 + var status *bool + if !includeDisabled { + status = &[]bool{true}[0] + } + + sysDictionary, err := dictionaryService.GetSysDictionary(dictType, 0, status) + if err != nil { + global.GVA_LOG.Error("查询字典失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "查询字典失败: %v", "total": 0, "dictionaries": []}`, err.Error())), + }, + }, nil + } + + // 转换为响应格式 + dictInfo := DictionaryInfo{ + ID: sysDictionary.ID, + Name: sysDictionary.Name, + Type: sysDictionary.Type, + Status: sysDictionary.Status, + Desc: sysDictionary.Desc, + } + + // 获取字典详情 + for _, detail := range sysDictionary.SysDictionaryDetails { + if includeDisabled || (detail.Status != nil && *detail.Status) { + dictInfo.Details = append(dictInfo.Details, DictionaryDetailInfo{ + ID: detail.ID, + Label: detail.Label, + Value: detail.Value, + Extend: detail.Extend, + Status: detail.Status, + Sort: detail.Sort, + }) + } + } + + dictionaries = append(dictionaries, dictInfo) + } else { + // 查询所有字典 + var sysDictionaries []system.SysDictionary + db := global.GVA_DB.Model(&system.SysDictionary{}) + + if !includeDisabled { + db = db.Where("status = ?", true) + } + + err = db.Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB { + if includeDisabled { + return db.Order("sort") + } else { + return db.Where("status = ?", true).Order("sort") + } + }).Find(&sysDictionaries).Error + + if err != nil { + global.GVA_LOG.Error("查询字典列表失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "查询字典列表失败: %v", "total": 0, "dictionaries": []}`, err.Error())), + }, + }, nil + } + + // 转换为响应格式 + for _, dict := range sysDictionaries { + dictInfo := DictionaryInfo{ + ID: dict.ID, + Name: dict.Name, + Type: dict.Type, + Status: dict.Status, + Desc: dict.Desc, + } + + // 获取字典详情 + for _, detail := range dict.SysDictionaryDetails { + if includeDisabled || (detail.Status != nil && *detail.Status) { + dictInfo.Details = append(dictInfo.Details, DictionaryDetailInfo{ + ID: detail.ID, + Label: detail.Label, + Value: detail.Value, + Extend: detail.Extend, + Status: detail.Status, + Sort: detail.Sort, + }) + } + } + + dictionaries = append(dictionaries, dictInfo) + } + } + + // 如果只需要详情信息,则提取所有详情 + if detailsOnly { + var allDetails []DictionaryDetailInfo + for _, dict := range dictionaries { + allDetails = append(allDetails, dict.Details...) + } + + response := map[string]interface{}{ + "success": true, + "message": "查询字典详情成功", + "total": len(allDetails), + "details": allDetails, + } + + responseJSON, _ := json.Marshal(response) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(string(responseJSON)), + }, + }, nil + } + + // 构建响应 + response := DictionaryQueryResponse{ + Success: true, + Message: "查询字典成功", + Total: len(dictionaries), + Dictionaries: dictionaries, + } + + responseJSON, err := json.Marshal(response) + if err != nil { + global.GVA_LOG.Error("序列化响应失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf(`{"success": false, "message": "序列化响应失败: %v", "total": 0, "dictionaries": []}`, err.Error())), + }, + }, nil + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(string(responseJSON)), + }, + }, nil +} \ No newline at end of file diff --git a/server/mcp/execution_plan_schema.md b/server/mcp/execution_plan_schema.md new file mode 100644 index 000000000..622e83d09 --- /dev/null +++ b/server/mcp/execution_plan_schema.md @@ -0,0 +1,529 @@ +# ExecutionPlan 结构体格式说明 + +## 概述 +ExecutionPlan 是用于自动化模块创建的执行计划结构体,包含了创建包和模块所需的所有信息。 + +## 完整结构体定义 + +```go +type ExecutionPlan struct { + PackageName string `json:"packageName"` // 包名,如:"user", "order", "product" + PackageType string `json:"packageType"` // "plugin" 或 "package" + NeedCreatedPackage bool `json:"needCreatedPackage"` // 是否需要创建包 + NeedCreatedModules bool `json:"needCreatedModules"` // 是否需要创建模块 + PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` // 包信息(当NeedCreatedPackage=true时必需) + ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 模块信息数组(当NeedCreatedModules=true时必需,支持批量创建) + Paths map[string]string `json:"paths,omitempty"` // 路径信息 +} +``` + +## 子结构体详细说明 + +### 1. SysAutoCodePackageCreate 结构体 + +```go +type SysAutoCodePackageCreate struct { + Desc string `json:"desc"` // 描述,如:"用户管理模块" + Label string `json:"label"` // 展示名,如:"用户管理" + Template string `json:"template"` // 模板类型:"plugin" 或 "package" + PackageName string `json:"packageName"` // 包名,如:"user" + Module string `json:"-"` // 模块名(自动填充,无需设置) +} +``` + +### 2. AutoCode 结构体(核心字段) + +```go +type AutoCode struct { + Package string `json:"package"` // 包名 + TableName string `json:"tableName"` // 数据库表名 + BusinessDB string `json:"businessDB"` // 业务数据库名 + StructName string `json:"structName"` // 结构体名称 + PackageName string `json:"packageName"` // 文件名称 + Description string `json:"description"` // 结构体中文名称 + Abbreviation string `json:"abbreviation"` // 结构体简称 + HumpPackageName string `json:"humpPackageName"` // 驼峰命名的包名 + GvaModel bool `json:"gvaModel"` // 是否使用GVA默认Model + AutoMigrate bool `json:"autoMigrate"` // 是否自动迁移表结构 + AutoCreateResource bool `json:"autoCreateResource"` // 是否自动创建资源标识 + AutoCreateApiToSql bool `json:"autoCreateApiToSql"` // 是否自动创建API + AutoCreateMenuToSql bool `json:"autoCreateMenuToSql"` // 是否自动创建菜单 + AutoCreateBtnAuth bool `json:"autoCreateBtnAuth"` // 是否自动创建按钮权限 + OnlyTemplate bool `json:"onlyTemplate"` // 是否只生成模板 + IsTree bool `json:"isTree"` // 是否树形结构 + TreeJson string `json:"treeJson"` // 树形结构JSON字段 + IsAdd bool `json:"isAdd"` // 是否新增 + Fields []*AutoCodeField `json:"fields"` // 字段列表 + GenerateWeb bool `json:"generateWeb"` // 是否生成前端代码 + GenerateServer bool `json:"generateServer"` // 是否生成后端代码 + Module string `json:"-"` // 模块(自动填充) + DictTypes []string `json:"-"` // 字典类型(自动填充) +} +``` + +### 3. AutoCodeField 结构体(字段定义) + +```go +type AutoCodeField struct { + FieldName string `json:"fieldName"` // 字段名 + FieldDesc string `json:"fieldDesc"` // 字段中文描述 + FieldType string `json:"fieldType"` // 字段类型:string, int, bool, time.Time等 + FieldJson string `json:"fieldJson"` // JSON标签名 + DataTypeLong string `json:"dataTypeLong"` // 数据库字段长度 + Comment string `json:"comment"` // 数据库字段注释 + ColumnName string `json:"columnName"` // 数据库列名 + FieldSearchType string `json:"fieldSearchType"` // 搜索类型:EQ, LIKE, BETWEEN等 + FieldSearchHide bool `json:"fieldSearchHide"` // 是否隐藏查询条件 + DictType string `json:"dictType"` // 字典类型 + Form bool `json:"form"` // 是否在表单中显示 + Table bool `json:"table"` // 是否在表格中显示 + Desc bool `json:"desc"` // 是否在详情中显示 + Excel bool `json:"excel"` // 是否支持导入导出 + Require bool `json:"require"` // 是否必填 + DefaultValue string `json:"defaultValue"` // 默认值 + ErrorText string `json:"errorText"` // 校验失败提示 + Clearable bool `json:"clearable"` // 是否可清空 + Sort bool `json:"sort"` // 是否支持排序 + PrimaryKey bool `json:"primaryKey"` // 是否主键 + DataSource *DataSource `json:"dataSource"` // 数据源配置(用于关联其他表) + CheckDataSource bool `json:"checkDataSource"` // 是否检查数据源 + FieldIndexType string `json:"fieldIndexType"` // 索引类型 +} +``` + +### 4. DataSource 结构体(关联表配置) + +```go +type DataSource struct { + DBName string `json:"dbName"` // 关联的数据库名称 + Table string `json:"table"` // 关联的表名 + Label string `json:"label"` // 用于显示的字段名(如name、title等) + Value string `json:"value"` // 用于存储的值字段名(通常是id) + Association int `json:"association"` // 关联关系:1=一对一,2=一对多 + HasDeletedAt bool `json:"hasDeletedAt"` // 关联表是否有软删除字段 +} +``` + +## 使用示例 + +### 示例1:创建新包和批量创建多个模块 + +```json +{ + "packageName": "user", + "packageType": "package", + "needCreatedPackage": true, + "needCreatedModules": true, + "packageInfo": { + "desc": "用户管理模块", + "label": "用户管理", + "template": "package", + "packageName": "user" + }, + "modulesInfo": [ + { + "package": "user", + "tableName": "sys_users", + "businessDB": "", + "structName": "User", + "packageName": "user", + "description": "用户", + "abbreviation": "user", + "humpPackageName": "user", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "treeJson": "", + "isAdd": true, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "Username", + "fieldDesc": "用户名", + "fieldType": "string", + "fieldJson": "username", + "dataTypeLong": "50", + "comment": "用户名", + "columnName": "username", + "fieldSearchType": "LIKE", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请输入用户名", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": { + "dbName": "gva", + "table": "sys_users", + "label": "username", + "value": "id", + "association": 2, + "hasDeletedAt": true + }, + "checkDataSource": true, + "fieldIndexType": "" + }, + { + "fieldName": "Email", + "fieldDesc": "邮箱", + "fieldType": "string", + "fieldJson": "email", + "dataTypeLong": "100", + "comment": "邮箱地址", + "columnName": "email", + "fieldSearchType": "EQ", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请输入邮箱", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": null, + "checkDataSource": false, + "fieldIndexType": "index" + } + ] + }, + { + "package": "user", + "tableName": "user_profiles", + "businessDB": "", + "structName": "UserProfile", + "packageName": "user", + "description": "用户档案", + "abbreviation": "userProfile", + "humpPackageName": "user", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "treeJson": "", + "isAdd": true, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "UserID", + "fieldDesc": "用户ID", + "fieldType": "int", + "fieldJson": "userId", + "dataTypeLong": "", + "comment": "关联用户ID", + "columnName": "user_id", + "fieldSearchType": "EQ", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请选择用户", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": null, + "checkDataSource": false, + "fieldIndexType": "index" + }, + { + "fieldName": "Avatar", + "fieldDesc": "头像", + "fieldType": "string", + "fieldJson": "avatar", + "dataTypeLong": "255", + "comment": "用户头像URL", + "columnName": "avatar", + "fieldSearchType": "", + "fieldSearchHide": true, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": false, + "require": false, + "defaultValue": "", + "errorText": "", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": null, + "checkDataSource": false, + "fieldIndexType": "" + } + ] + } + ] +} +``` + +### 示例2:仅在现有包中批量创建多个模块 + +```json +{ + "packageName": "system", + "packageType": "package", + "needCreatedPackage": false, + "needCreatedModules": true, + "packageInfo": null, + "modulesInfo": [ + { + "package": "system", + "tableName": "sys_roles", + "businessDB": "", + "structName": "Role", + "packageName": "system", + "description": "角色", + "abbreviation": "role", + "humpPackageName": "system", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "RoleName", + "fieldDesc": "角色名称", + "fieldType": "string", + "fieldJson": "roleName", + "dataTypeLong": "50", + "comment": "角色名称", + "columnName": "role_name", + "fieldSearchType": "LIKE", + "form": true, + "table": true, + "desc": true, + "require": true + } + ] + }, + { + "package": "system", + "tableName": "sys_permissions", + "businessDB": "", + "structName": "Permission", + "packageName": "system", + "description": "权限", + "abbreviation": "permission", + "humpPackageName": "system", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "PermissionName", + "fieldDesc": "权限名称", + "fieldType": "string", + "fieldJson": "permissionName", + "dataTypeLong": "100", + "comment": "权限名称", + "columnName": "permission_name", + "fieldSearchType": "LIKE", + "form": true, + "table": true, + "desc": true, + "require": true + }, + { + "fieldName": "PermissionCode", + "fieldDesc": "权限代码", + "fieldType": "string", + "fieldJson": "permissionCode", + "dataTypeLong": "50", + "comment": "权限代码", + "columnName": "permission_code", + "fieldSearchType": "=", + "form": true, + "table": true, + "desc": true, + "require": true + } + ] + } + ] +} +``` + +### 示例3:模块关联关系配置详解 + +以下示例展示了如何配置不同类型的关联关系: + +```json +{ + "packageName": "order", + "packageType": "package", + "needCreatedPackage": true, + "needCreatedModules": true, + "packageInfo": { + "desc": "订单管理模块", + "label": "订单管理", + "template": "package", + "packageName": "order" + }, + "modulesInfo": [ + { + "package": "order", + "tableName": "orders", + "structName": "Order", + "packageName": "order", + "description": "订单", + "abbreviation": "order", + "humpPackageName": "order", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "UserID", + "fieldDesc": "下单用户", + "fieldType": "uint", + "fieldJson": "userId", + "columnName": "user_id", + "fieldSearchType": "EQ", + "form": true, + "table": true, + "desc": true, + "require": true, + "dataSource": { + "dbName": "gva", + "table": "sys_users", + "label": "username", + "value": "id", + "association": 2, + "hasDeletedAt": true + }, + "checkDataSource": true + }, + { + "fieldName": "ProductID", + "fieldDesc": "商品", + "fieldType": "uint", + "fieldJson": "productId", + "columnName": "product_id", + "fieldSearchType": "EQ", + "form": true, + "table": true, + "desc": true, + "require": true, + "dataSource": { + "dbName": "gva", + "table": "products", + "label": "name", + "value": "id", + "association": 2, + "hasDeletedAt": false + }, + "checkDataSource": true + }, + { + "fieldName": "Status", + "fieldDesc": "订单状态", + "fieldType": "int", + "fieldJson": "status", + "columnName": "status", + "fieldSearchType": "EQ", + "form": true, + "table": true, + "desc": true, + "require": true, + "dictType": "order_status" + } + ] + } + ] +} +``` + +## DataSource 配置说明 + +### 关联关系类型 +- **association: 1** - 一对一关联(如用户与用户档案) +- **association: 2** - 一对多关联(如用户与订单) + +### 配置要点 +1. **dbName**: 通常为 "gva"(默认数据库) +2. **table**: 关联表的实际表名 +3. **label**: 用于前端显示的字段(如用户名、商品名称) +4. **value**: 用于存储关联ID的字段(通常是 "id") +5. **hasDeletedAt**: 关联表是否支持软删除 +6. **checkDataSource**: 建议设为true,会验证关联表是否存在 + +### 常见关联场景 +- 用户关联:`{"table": "sys_users", "label": "username", "value": "id"}` +- 角色关联:`{"table": "sys_authorities", "label": "authorityName", "value": "authorityId"}` +- 部门关联:`{"table": "sys_departments", "label": "name", "value": "id"}` +- 分类关联:`{"table": "categories", "label": "name", "value": "id"}` + +## 重要注意事项 + +1. **PackageType**: 只能是 "plugin" 或 "package" +2. **NeedCreatedPackage**: 当为true时,PackageInfo必须提供 +3. **NeedCreatedModules**: 当为true时,ModulesInfo必须提供 +4. **字段类型**: FieldType支持的类型包括: + - string(字符串) + - richtext(富文本) + - int(整型) + - bool(布尔值) + - float64(浮点型) + - time.Time(时间) + - enum(枚举) + - picture(单图片,字符串) + - pictures(多图片,json字符串) + - video(视频,字符串) + - file(文件,json字符串) + - json(JSON) + - array(数组) +5. **搜索类型**: FieldSearchType支持:EQ, NE, GT, GE, LT, LE, LIKE, BETWEEN等 +6. **索引类型**: FieldIndexType支持:index, unique等 +7. **GvaModel**: 设置为true时会自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段 +8. **关联配置**: 使用dataSource时,确保关联表已存在,建议开启checkDataSource验证 + +## 常见错误避免 + +1. 确保PackageName和ModuleName符合Go语言命名规范 +2. 字段名使用大写开头的驼峰命名 +3. JSON标签使用小写开头的驼峰命名 +4. 数据库列名使用下划线分隔的小写命名 +5. 必填字段不要遗漏 +6. 字段类型要与实际需求匹配 \ No newline at end of file diff --git a/server/mcp/gag_usage_example.md b/server/mcp/gag_usage_example.md new file mode 100644 index 000000000..cfc7fdcb3 --- /dev/null +++ b/server/mcp/gag_usage_example.md @@ -0,0 +1,205 @@ +# GAG工具使用示例 - 带用户确认流程 + +## 新的工作流程 + +现在GAG工具支持三步工作流程: +1. `analyze` - 分析现有模块信息 +2. `confirm` - 请求用户确认创建计划 +3. `execute` - 执行创建操作(需要用户确认) + +## 使用示例 + +### 第一步:分析 +```json +{ + "action": "analyze", + "requirement": "创建一个图书管理功能" +} +``` + +### 第二步:确认(支持批量创建多个模块) +```json +{ + "action": "confirm", + "executionPlan": { + "packageName": "library", + "packageType": "package", + "needCreatedPackage": true, + "needCreatedModules": true, + "packageInfo": { + "desc": "图书管理包", + "label": "图书管理", + "template": "package", + "packageName": "library" + }, + "modulesInfo": [ + { + "package": "library", + "tableName": "library_books", + "businessDB": "", + "structName": "Book", + "packageName": "library", + "description": "图书信息", + "abbreviation": "book", + "humpPackageName": "Library", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "treeJson": "", + "isAdd": false, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "title", + "fieldDesc": "书名", + "fieldType": "string", + "fieldJson": "title", + "dataTypeLong": "255", + "comment": "书名", + "columnName": "title", + "fieldSearchType": "LIKE", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请输入书名", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": {}, + "checkDataSource": false, + "fieldIndexType": "" + }, + { + "fieldName": "AuthorID", + "fieldDesc": "作者", + "fieldType": "uint", + "fieldJson": "authorId", + "dataTypeLong": "", + "comment": "作者ID", + "columnName": "author_id", + "fieldSearchType": "EQ", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请选择作者", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": { + "dbName": "gva", + "table": "library_authors", + "label": "name", + "value": "id", + "association": 2, + "hasDeletedAt": true + }, + "checkDataSource": true, + "fieldIndexType": "" + } + ] + }, + { + "package": "library", + "tableName": "library_authors", + "businessDB": "", + "structName": "Author", + "packageName": "library", + "description": "作者信息", + "abbreviation": "author", + "humpPackageName": "Library", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "treeJson": "", + "isAdd": false, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "name", + "fieldDesc": "作者姓名", + "fieldType": "string", + "fieldJson": "name", + "dataTypeLong": "100", + "comment": "作者姓名", + "columnName": "name", + "fieldSearchType": "LIKE", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请输入作者姓名", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": {}, + "checkDataSource": false, + "fieldIndexType": "" + } + ] + } + ] + } +} +``` + +### 第三步:执行(需要确认参数) +```json +{ + "action": "execute", + "executionPlan": { + // ... 同上面的executionPlan + }, + "packageConfirm": "yes", // 确认创建包 + "modulesConfirm": "yes" // 确认创建模块 +} +``` + +## 确认参数说明 + +- `packageConfirm`: 当`needCreatedPackage`为true时必需 + - "yes": 确认创建包 + - "no": 取消创建包(停止后续处理) + +- `modulesConfirm`: 当`needCreatedModules`为true时必需 + - "yes": 确认创建模块 + - "no": 取消创建模块(停止后续处理) + +## 取消操作的行为 + +1. 如果用户在`packageConfirm`中选择"no",系统将停止所有后续处理 +2. 如果用户在`modulesConfirm`中选择"no",系统将停止模块创建 +3. 任何取消操作都会返回相应的取消消息,不会执行任何创建操作 + +## 注意事项 + +1. 必须先调用`confirm`来获取确认信息 +2. 在`execute`时必须提供相应的确认参数 +3. 确认参数的值必须是"yes"或"no" +4. 如果不需要创建包或模块,则不需要提供对应的确认参数 +5. 字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组) \ No newline at end of file diff --git a/server/mcp/get_nickname.go b/server/mcp/get_nickname.go deleted file mode 100644 index 6ad55f6ab..000000000 --- a/server/mcp/get_nickname.go +++ /dev/null @@ -1,79 +0,0 @@ -package mcpTool - -import ( - "context" - "errors" - "fmt" - "github.com/flipped-aurora/gin-vue-admin/server/global" - "github.com/flipped-aurora/gin-vue-admin/server/model/system" - "github.com/mark3labs/mcp-go/mcp" - "gorm.io/gorm" -) - -func init() { - RegisterTool(&GetNickname{}) -} - -type GetNickname struct{} - -// 根据用户username获取nickname -func (t *GetNickname) New() mcp.Tool { - return mcp.NewTool("getNickname", - mcp.WithDescription("根据用户username获取nickname"), - mcp.WithString("username", - mcp.Required(), - mcp.Description("用户的username"), - )) -} - -// Handle 处理获取昵称的请求 -func (t *GetNickname) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - // 1. 参数验证 - username, ok := request.GetArguments()["username"].(string) - if !ok { - return nil, errors.New("参数错误:username 必须是字符串类型") - } - - if username == "" { - return nil, errors.New("参数错误:username 不能为空") - } - - // 2. 记录操作日志 - global.GVA_LOG.Info("getNickname 工具被调用") - - // 3. 优化查询,只选择需要的字段 - var user struct { - NickName string - } - - err := global.GVA_DB.Model(&system.SysUser{}). - Select("nick_name"). - Where("username = ?", username). - First(&user).Error - - // 4. 优化错误处理 - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf("用户 %s 不存在", username), - }, - }, - }, nil - } - global.GVA_LOG.Error("数据库查询错误") - return nil, errors.New("系统错误,请稍后再试") - } - - // 构造回复信息 - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.TextContent{ - Type: "text", - Text: fmt.Sprintf("用户 %s 的昵称是 %s", username, user.NickName), - }, - }, - }, nil -} diff --git a/server/mcp/gva_auto_generate.go b/server/mcp/gva_auto_generate.go new file mode 100644 index 000000000..8fe4ef85d --- /dev/null +++ b/server/mcp/gva_auto_generate.go @@ -0,0 +1,1755 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + "unicode" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/service" + systemService "github.com/flipped-aurora/gin-vue-admin/server/service/system" + "github.com/mark3labs/mcp-go/mcp" + "gorm.io/gorm" +) + +func init() { + RegisterTool(&AutomationModuleAnalyzer{}) +} + +type AutomationModuleAnalyzer struct{} + +// ModuleInfo 模块信息 +type ModuleInfo struct { + ID uint `json:"id"` + PackageName string `json:"packageName"` + Label string `json:"label"` + Desc string `json:"desc"` + Template string `json:"template"` // "plugin" 或 "package" + Module string `json:"module"` +} + +// HistoryInfo 历史记录信息 +type HistoryInfo struct { + ID uint `json:"id"` + StructName string `json:"structName"` + TableName string `json:"tableName"` + PackageName string `json:"packageName"` + BusinessDB string `json:"businessDB"` + Description string `json:"description"` + Abbreviation string `json:"abbreviation"` + CreatedAt string `json:"createdAt"` +} + +// PredesignedModuleInfo 预设计模块信息 +type PredesignedModuleInfo struct { + PackageName string `json:"packageName"` + PackageType string `json:"packageType"` // "plugin" 或 "package" + ModuleName string `json:"moduleName"` + Path string `json:"path"` + Modules []string `json:"modules"` // 包含的模块列表(如api、model、service等) + Description string `json:"description"` + StructName string `json:"structName,omitempty"` // 主要结构体名称 +} + +// AnalysisResponse 分析响应 +type AnalysisResponse struct { + Packages []ModuleInfo `json:"packages"` + History []HistoryInfo `json:"history"` + PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` + Message string `json:"message"` +} + +// ExecutionPlan 执行计划 - 支持批量创建 +type ExecutionPlan struct { + PackageName string `json:"packageName"` + PackageType string `json:"packageType"` // "plugin" 或 "package" + NeedCreatedPackage bool `json:"needCreatedPackage"` + NeedCreatedModules bool `json:"needCreatedModules"` + PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` + ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 改为数组支持多个模块 + Paths map[string]string `json:"paths,omitempty"` +} + +// ExecutionResult 执行结果 +type ExecutionResult struct { + Success bool `json:"success"` + Message string `json:"message"` + PackageID uint `json:"packageId,omitempty"` + HistoryID uint `json:"historyId,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + NextActions []string `json:"nextActions,omitempty"` +} + +// ConfirmationRequest 确认请求结构 +type ConfirmationRequest struct { + PackageName string `json:"packageName"` + ModuleName string `json:"moduleName"` + NeedCreatedPackage bool `json:"needCreatedPackage"` + NeedCreatedModules bool `json:"needCreatedModules"` + PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` + ModulesInfo *request.AutoCode `json:"modulesInfo,omitempty"` +} + +// ConfirmationResponse 确认响应结构 +type ConfirmationResponse struct { + Message string `json:"message"` + PackageConfirm bool `json:"packageConfirm"` + ModulesConfirm bool `json:"modulesConfirm"` + CanProceed bool `json:"canProceed"` + ConfirmationKey string `json:"confirmationKey"` +} + +// New 返回工具注册信息 +func (t *AutomationModuleAnalyzer) New() mcp.Tool { + return mcp.NewTool("gva_auto_generate", + mcp.WithDescription(`**🔧 核心执行工具:接收requirement_analyzer分析结果,执行具体的模块创建操作** + +**工作流位置:** +- **第二优先级**:在requirement_analyzer之后使用 +- **接收输入**:来自requirement_analyzer的1xxx2xxx格式分析结果 +- **执行操作**:根据分析结果创建完整模块、包、功能模块 + +**批量创建功能:** +- 支持在单个ExecutionPlan中创建多个模块 +- modulesInfo字段为数组,可包含多个模块配置 +- 一次性处理多个模块的创建和字典生成 +- 与requirement_analyzer配合实现完整工作流 + +分步骤分析自动化模块:1) 分析现有模块信息供AI选择 2) 请求用户确认 3) 根据确认结果执行创建操作 + +**新功能:自动字典创建** +- 当结构体字段使用了字典类型(dictType不为空)时,系统会自动检查字典是否存在 +- 如果字典不存在,会自动创建对应的字典及默认的字典详情项 +- 字典创建包括:字典主表记录和默认的选项值(选项1、选项2等) + +**推荐工作流:** +1. 用户提出需求 → requirement_analyzer(最高优先级) +2. AI分析需求为1xxx2xxx格式 → gva_auto_generate(执行创建) +3. 创建完成后,根据需要使用其他辅助工具 + +**重要限制:** +- 当needCreatedModules=true时,模块创建会自动生成API和菜单,因此不应再调用api_creator和menu_creator工具 +- 只有在单独创建API或菜单(不涉及模块创建)时才使用api_creator和menu_creator工具 + +重要:ExecutionPlan结构体格式要求(支持批量创建): +{ + "packageName": "包名(string)", + "packageType": "package或plugin(string)", + "needCreatedPackage": "是否需要创建包(bool)", + "needCreatedModules": "是否需要创建模块(bool)", + "packageInfo": { + "desc": "描述(string)", + "label": "展示名(string)", + "template": "package或plugin(string)", + "packageName": "包名(string)" + }, + "modulesInfo": [{ + "package": "包名(string)", + "tableName": "数据库表名(string)", + "businessDB": "业务数据库(string)", + "structName": "结构体名(string)", + "packageName": "文件名称(string)", + "description": "中文描述(string)", + "abbreviation": "简称(string)", + "humpPackageName": "文件名称 一般是结构体名的小驼峰(string)", + "gvaModel": "是否使用GVA模型(bool) 固定为true 后续不需要创建ID created_at deleted_at updated_at", + "autoMigrate": "是否自动迁移(bool)", + "autoCreateResource": "是否创建资源(bool)", + "autoCreateApiToSql": "是否创建API(bool)", + "autoCreateMenuToSql": "是否创建菜单(bool)", + "autoCreateBtnAuth": "是否创建按钮权限(bool)", + "onlyTemplate": "是否仅模板(bool)", + "isTree": "是否树形结构(bool)", + "treeJson": "树形JSON字段(string)", + "isAdd": "是否新增(bool) 固定为false", + "generateWeb": "是否生成前端(bool)", + "generateServer": "是否生成后端(bool)", + "fields": [{ + "fieldName": "字段名(string)必须大写开头", + "fieldDesc": "字段描述(string)", + "fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)", + "fieldJson": "JSON标签(string 必须是小驼峰命名,例:userName)", + "dataTypeLong": "数据长度(string)", + "comment": "注释(string)", + "columnName": "数据库列名(string)", + "fieldSearchType": "搜索类型:=/>/=/<=/NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN等(string)", + "fieldSearchHide": "是否隐藏搜索(bool)", + "dictType": "字典类型(string)", + "form": "表单显示(bool)", + "table": "表格显示(bool)", + "desc": "详情显示(bool)", + "excel": "导入导出(bool)", + "require": "是否必填(bool)", + "defaultValue": "默认值(string),JSON类型(array,json,file,pictures)请保持为空他们不可以设置默认值", + "errorText": "错误提示(string)", + "clearable": "是否可清空(bool)", + "sort": "是否排序(bool)", + "primaryKey": "是否主键(bool)", + "dataSource": "数据源配置(object) - 用于配置字段的关联表信息,结构:{\"dbName\":\"数据库名\",\"table\":\"关联表名\",\"label\":\"显示字段\",\"value\":\"值字段\",\"association\":1或2(1=一对一,2=一对多),\"hasDeletedAt\":true/false}。\n\n**获取表名提示:**\n- 可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名\n- 例如:SysUser 的表名为 \"sys_users\",ExaFileUploadAndDownload 的表名为 \"exa_file_upload_and_downloads\"\n- 插件模块示例:Info 的表名为 \"gva_announcements_info\"\n\n**获取数据库名提示:**\n- 主数据库:通常使用 \"gva\"(默认数据库标识)\n- 多数据库:可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段\n- 如果用户未提及关联多数据库信息 则使用默认数据库 默认数据库的情况下 dbName此处填写为空", + "checkDataSource": "是否检查数据源(bool) - 启用后会验证关联表的存在性", + "fieldIndexType": "索引类型(string)" + }] + }, { + "package": "包名(string)", + "tableName": "第二个模块的表名(string)", + "structName": "第二个模块的结构体名(string)", + "description": "第二个模块的描述(string)", + "...": "更多模块配置..." + }] +} + +注意: +1. needCreatedPackage=true时packageInfo必需 +2. needCreatedModules=true时modulesInfo必需 +3. packageType只能是"package"或"plugin" +4. 字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组) +5. 搜索类型支持:=,!=,>,>=,<,<=,NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN +6. gvaModel=true时自动包含ID,CreatedAt,UpdatedAt,DeletedAt字段 +7. **重要**:当gvaModel=false时,必须有一个字段的primaryKey=true,否则会导致PrimaryField为nil错误 +8. **重要**:当gvaModel=true时,系统会自动设置ID字段为主键,无需手动设置primaryKey=true +9. 智能字典创建功能:当字段使用字典类型(DictType)时,系统会: + - 自动检查字典是否存在,如果不存在则创建字典 + - 根据字典类型和字段描述智能生成默认选项,支持状态、性别、类型、等级、优先级、审批、角色、布尔值、订单、颜色、尺寸等常见场景 + - 为无法识别的字典类型提供通用默认选项 +10. **模块关联配置**:当需要配置模块间的关联关系时,使用dataSource字段: + - **dbName**: 关联的数据库名称 + - **table**: 关联的表名 + - **label**: 用于显示的字段名(如name、title等) + - **value**: 用于存储的值字段名(通常是id) + - **association**: 关联关系类型(1=一对一关联,2=一对多关联) + - **hasDeletedAt**: 关联表是否有软删除字段 + - **checkDataSource**: 设为true时会验证关联表的存在性 + - 示例:{"dbName":"gva","table":"sys_users","label":"username","value":"id","association":2,"hasDeletedAt":true}`), + mcp.WithString("action", + mcp.Required(), + mcp.Description("执行操作:'analyze' 分析现有模块信息,'confirm' 请求用户确认创建,'execute' 执行创建操作(支持批量创建多个模块)"), + ), + mcp.WithString("requirement", + mcp.Description("用户需求描述(action=analyze时必需)"), + ), + mcp.WithObject("executionPlan", + mcp.Description("执行计划(action=confirm或execute时必需,必须严格按照上述格式提供完整的JSON对象)"), + ), + mcp.WithString("packageConfirm", + mcp.Description("用户对创建包的确认(action=execute时,如果需要创建包则必需):'yes' 或 'no'"), + ), + mcp.WithString("modulesConfirm", + mcp.Description("用户对创建模块的确认(action=execute时,如果需要创建模块则必需):'yes' 或 'no'"), + ), + ) +} + +// scanPredesignedModules 扫描预设计的模块 +func (t *AutomationModuleAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) { + var predesignedModules []PredesignedModuleInfo + + // 获取autocode配置路径 + if global.GVA_CONFIG.AutoCode.Root == "" { + return predesignedModules, nil // 配置不存在时返回空列表,不报错 + } + + serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + + // 扫描plugin目录下的各个插件模块 + pluginPath := filepath.Join(serverPath, "plugin") + if pluginModules, err := t.scanPluginModules(pluginPath); err == nil { + predesignedModules = append(predesignedModules, pluginModules...) + } + + // 扫描model目录下的各个包模块 + modelPath := filepath.Join(serverPath, "model") + if packageModules, err := t.scanPackageModules(modelPath); err == nil { + predesignedModules = append(predesignedModules, packageModules...) + } + + return predesignedModules, nil +} + +// scanPluginModules 扫描plugin目录下的各个插件模块 +func (t *AutomationModuleAnalyzer) scanPluginModules(pluginPath string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + if _, err := os.Stat(pluginPath); os.IsNotExist(err) { + return modules, nil + } + + entries, err := os.ReadDir(pluginPath) + if err != nil { + return modules, err + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + pluginName := entry.Name() + pluginDir := filepath.Join(pluginPath, pluginName) + + // 扫描插件下的model目录,查找具体的模块文件 + modelDir := filepath.Join(pluginDir, "model") + if _, err := os.Stat(modelDir); err == nil { + if pluginModules, err := t.scanModuleFiles(modelDir, pluginName, "plugin"); err == nil { + modules = append(modules, pluginModules...) + } + } + } + + return modules, nil +} + +// scanPackageModules 扫描model目录下的各个包模块 +func (t *AutomationModuleAnalyzer) scanPackageModules(modelPath string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + if _, err := os.Stat(modelPath); os.IsNotExist(err) { + return modules, nil + } + + entries, err := os.ReadDir(modelPath) + if err != nil { + return modules, err + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + packageName := entry.Name() + // 跳过一些系统目录 + if packageName == "common" || packageName == "request" || packageName == "response" { + continue + } + + packageDir := filepath.Join(modelPath, packageName) + + // 扫描包目录下的模块文件 + if packageModules, err := t.scanModuleFiles(packageDir, packageName, "package"); err == nil { + modules = append(modules, packageModules...) + } + } + + return modules, nil +} + +// scanModuleFiles 扫描目录下的Go文件,识别具体的模块 +func (t *AutomationModuleAnalyzer) scanModuleFiles(dir, packageName, packageType string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + entries, err := os.ReadDir(dir) + if err != nil { + return modules, err + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + fileName := entry.Name() + if !strings.HasSuffix(fileName, ".go") { + continue + } + + // 跳过一些非模块文件 + if strings.HasSuffix(fileName, "_test.go") || + fileName == "enter.go" || + fileName == "request.go" || + fileName == "response.go" { + continue + } + + filePath := filepath.Join(dir, fileName) + moduleName := strings.TrimSuffix(fileName, ".go") + + // 分析模块文件,提取结构体信息 + if moduleInfo, err := t.analyzeModuleFile(filePath, packageName, moduleName, packageType); err == nil { + modules = append(modules, *moduleInfo) + } + } + + return modules, nil +} + +// analyzeModuleFile 分析具体的模块文件 +func (t *AutomationModuleAnalyzer) analyzeModuleFile(filePath, packageName, moduleName, packageType string) (*PredesignedModuleInfo, error) { + content, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + fileContent := string(content) + + // 提取结构体名称和描述 + structNames := t.extractStructNames(fileContent) + description := t.extractModuleDescription(fileContent, moduleName) + + // 确定主要结构体名称 + mainStruct := moduleName + if len(structNames) > 0 { + // 优先选择与文件名相关的结构体 + for _, structName := range structNames { + if strings.Contains(strings.ToLower(structName), strings.ToLower(moduleName)) { + mainStruct = structName + break + } + } + if mainStruct == moduleName && len(structNames) > 0 { + mainStruct = structNames[0] // 如果没有匹配的,使用第一个 + } + } + + return &PredesignedModuleInfo{ + PackageName: packageName, + PackageType: packageType, + ModuleName: moduleName, + Path: filePath, + Modules: structNames, + Description: description, + StructName: mainStruct, + }, nil +} + +// extractStructNames 从文件内容中提取结构体名称 +func (t *AutomationModuleAnalyzer) extractStructNames(content string) []string { + var structNames []string + lines := strings.Split(content, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "type ") && strings.Contains(line, " struct") { + // 提取结构体名称 + parts := strings.Fields(line) + if len(parts) >= 3 && parts[2] == "struct" { + structNames = append(structNames, parts[1]) + } + } + } + + return structNames +} + +// extractModuleDescription 从文件内容中提取模块描述 +func (t *AutomationModuleAnalyzer) extractModuleDescription(content, moduleName string) string { + lines := strings.Split(content, "\n") + + // 查找package注释 + for i, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "package ") { + // 向上查找注释 + for j := i - 1; j >= 0; j-- { + commentLine := strings.TrimSpace(lines[j]) + if strings.HasPrefix(commentLine, "//") { + comment := strings.TrimSpace(strings.TrimPrefix(commentLine, "//")) + if comment != "" && len(comment) > 5 { + return comment + } + } else if commentLine != "" { + break + } + } + break + } + } + + // 查找结构体注释 + for i, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "type ") && strings.Contains(line, " struct") { + // 向上查找注释 + for j := i - 1; j >= 0; j-- { + commentLine := strings.TrimSpace(lines[j]) + if strings.HasPrefix(commentLine, "//") { + comment := strings.TrimSpace(strings.TrimPrefix(commentLine, "//")) + if comment != "" && len(comment) > 5 { + return comment + } + } else if commentLine != "" { + break + } + } + break + } + } + + return fmt.Sprintf("预设计的模块:%s", moduleName) +} + +// Handle 处理工具调用 +func (t *AutomationModuleAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + action, ok := request.GetArguments()["action"].(string) + if !ok || action == "" { + return nil, errors.New("参数错误:action 必须是非空字符串") + } + + switch action { + case "analyze": + return t.handleAnalyze(ctx, request) + case "confirm": + return t.handleConfirm(ctx, request) + case "execute": + return t.handleExecute(ctx, request) + default: + return nil, errors.New("无效的操作:action 必须是 'analyze'、'confirm' 或 'execute'") + } +} + +// handleAnalyze 处理分析请求 +func (t *AutomationModuleAnalyzer) handleAnalyze(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + requirement, ok := request.GetArguments()["requirement"].(string) + if !ok || requirement == "" { + return nil, errors.New("参数错误:requirement 必须是非空字符串") + } + + // 检测用户是否想要创建插件 + suggestedType, isPlugin, confidence := t.detectPluginIntent(requirement) + pluginDetectionMsg := "" + if isPlugin { + pluginDetectionMsg = fmt.Sprintf("\n\n🔍 **插件检测结果**:检测到用户想要创建插件(置信度:%s)\n⚠️ **重要提醒**:当用户提到插件时,packageType和template字段都必须设置为 \"plugin\",不能使用 \"package\"!", confidence) + } else { + pluginDetectionMsg = fmt.Sprintf("\n\n🔍 **类型检测结果**:建议使用 %s 类型", suggestedType) + } + + // 从数据库获取所有自动化包信息 + var packages []model.SysAutoCodePackage + if err := global.GVA_DB.Find(&packages).Error; err != nil { + return nil, fmt.Errorf("获取包信息失败: %v", err) + } + + // 从数据库获取所有历史记录 + var histories []model.SysAutoCodeHistory + if err := global.GVA_DB.Find(&histories).Error; err != nil { + return nil, fmt.Errorf("获取历史记录失败: %v", err) + } + + // 转换包信息并检查空文件夹 + var moduleInfos []ModuleInfo + var validPackages []model.SysAutoCodePackage + var emptyPackageIDs []uint + var emptyPackageNames []string + + for _, pkg := range packages { + // 检查包对应的文件夹是否为空 + isEmpty, err := t.isPackageFolderEmpty(pkg.PackageName, pkg.Template) + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 文件夹失败: %v", pkg.PackageName, err)) + // 如果检查失败,仍然保留该包 + validPackages = append(validPackages, pkg) + continue + } + + if isEmpty { + // 记录需要删除的包ID和包名 + emptyPackageIDs = append(emptyPackageIDs, pkg.ID) + emptyPackageNames = append(emptyPackageNames, pkg.PackageName) + global.GVA_LOG.Info(fmt.Sprintf("发现空包文件夹: %s,将删除数据库记录和文件夹", pkg.PackageName)) + + // 删除空文件夹 + if err := t.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err)) + } + } else { + // 文件夹不为空,保留该包 + validPackages = append(validPackages, pkg) + } + } + + // 批量删除空包的数据库记录 + if len(emptyPackageIDs) > 0 { + if err := global.GVA_DB.Where("id IN ?", emptyPackageIDs).Delete(&model.SysAutoCodePackage{}).Error; err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除空包数据库记录失败: %v", err)) + } else { + global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个空包的数据库记录", len(emptyPackageIDs))) + } + } + + // 转换有效的包信息 + for _, pkg := range validPackages { + moduleInfos = append(moduleInfos, ModuleInfo{ + ID: pkg.ID, + PackageName: pkg.PackageName, + Label: pkg.Label, + Desc: pkg.Desc, + Template: pkg.Template, + Module: pkg.Module, + }) + } + + // 删除与空包相关的历史记录 + var emptyHistoryIDs []uint + if len(emptyPackageNames) > 0 { + for _, history := range histories { + for _, emptyPackageName := range emptyPackageNames { + if history.Package == emptyPackageName { + emptyHistoryIDs = append(emptyHistoryIDs, history.ID) + break + } + } + } + + // 清理相关的API和菜单记录 + if len(emptyHistoryIDs) > 0 { + if err := t.cleanupRelatedApiAndMenus(emptyHistoryIDs); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("清理空包相关API和菜单失败: %v", err)) + } + } + + // 批量删除相关历史记录 + if len(emptyHistoryIDs) > 0 { + if err := global.GVA_DB.Where("id IN ?", emptyHistoryIDs).Delete(&model.SysAutoCodeHistory{}).Error; err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除空包相关历史记录失败: %v", err)) + } else { + global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个空包相关的历史记录", len(emptyHistoryIDs))) + } + } + } + + // 创建有效包名的映射,用于快速查找 + validPackageNames := make(map[string]bool) + for _, pkg := range validPackages { + validPackageNames[pkg.PackageName] = true + } + + // 收集需要删除的脏历史记录ID(包名不在有效包列表中的历史记录) + var dirtyHistoryIDs []uint + for _, history := range histories { + if !validPackageNames[history.Package] { + dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID) + } + } + + // 删除脏历史记录 + if len(dirtyHistoryIDs) > 0 { + // 清理相关的API和菜单记录 + if err := t.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("清理脏历史记录相关API和菜单失败: %v", err)) + } + + if err := global.GVA_DB.Where("id IN ?", dirtyHistoryIDs).Delete(&model.SysAutoCodeHistory{}).Error; err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err)) + } else { + global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个脏历史记录(包名不在有效包列表中)", len(dirtyHistoryIDs))) + } + } + + // 转换有效的历史记录(只保留包名存在于有效包列表中的历史记录) + var historyInfos []HistoryInfo + for _, history := range histories { + // 只保留包名存在于有效包列表中的历史记录 + if validPackageNames[history.Package] { + historyInfos = append(historyInfos, HistoryInfo{ + ID: history.ID, + StructName: history.StructName, + TableName: history.TableName(), + PackageName: history.Package, + BusinessDB: history.BusinessDB, + Description: history.Description, + Abbreviation: history.Abbreviation, + CreatedAt: history.CreatedAt.Format("2006-01-02 15:04:05"), + }) + } + } + + // 扫描预设计的模块 + allPredesignedModules, err := t.scanPredesignedModules() + if err != nil { + global.GVA_LOG.Warn("扫描预设计模块失败" + err.Error()) + allPredesignedModules = []PredesignedModuleInfo{} // 确保不为nil + } + + // 过滤掉与已删除包相关的预设计模块 + var predesignedModules []PredesignedModuleInfo + for _, module := range allPredesignedModules { + isDeleted := false + for _, emptyPackageName := range emptyPackageNames { + if module.PackageName == emptyPackageName { + isDeleted = true + break + } + } + + // 只保留未被删除包的预设计模块 + if !isDeleted { + predesignedModules = append(predesignedModules, module) + } + } + + // 构建分析结果消息 + var message string + var deletionDetails []string + + // 收集删除信息 + if len(emptyHistoryIDs) > 0 { + deletionDetails = append(deletionDetails, fmt.Sprintf("%d个空包相关历史记录", len(emptyHistoryIDs))) + } + if len(dirtyHistoryIDs) > 0 { + deletionDetails = append(deletionDetails, fmt.Sprintf("%d个脏历史记录", len(dirtyHistoryIDs))) + } + if len(allPredesignedModules) > len(predesignedModules) { + deletionDetails = append(deletionDetails, fmt.Sprintf("%d个相关预设计模块", len(allPredesignedModules)-len(predesignedModules))) + } + + if len(emptyPackageNames) > 0 || len(deletionDetails) > 0 { + var cleanupInfo string + if len(emptyPackageNames) > 0 { + cleanupInfo = fmt.Sprintf("检测到存在 %s 包但内容为空,我已经删除这些包的文件夹(包括model、api、service、router目录)和数据库记录", strings.Join(emptyPackageNames, "、")) + } + + deletionInfo := "" + if len(deletionDetails) > 0 { + if cleanupInfo != "" { + deletionInfo = fmt.Sprintf(",同时删除了%s", strings.Join(deletionDetails, "、")) + } else { + deletionInfo = fmt.Sprintf("检测到脏数据,已删除%s", strings.Join(deletionDetails, "、")) + } + } + + if cleanupInfo != "" { + message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块。%s%s,如果需要使用这些包名,需要重新创建。请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules), cleanupInfo, deletionInfo) + } else { + message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块。%s。请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules), deletionInfo) + } + } else { + message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块,请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules)) + } + + // 构建分析结果 + analysisResult := AnalysisResponse{ + Packages: moduleInfos, + History: historyInfos, + PredesignedModules: predesignedModules, + Message: message, + } + + resultJSON, err := json.MarshalIndent(analysisResult, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf(`分析结果: + +%s + +请AI根据用户需求:%s%s + +%s + +分析现有的包、历史记录和预设计模块,然后构建ExecutionPlan结构体调用execute操作。 + +**预设计模块说明**: +- 预设计模块是已经存在于autocode路径下的package或plugin +- 这些模块包含了预先设计好的代码结构,可以直接使用或作为参考 +- 如果用户需求与某个预设计模块匹配,可以考虑直接使用该模块或基于它进行扩展 + +**字典选项生成说明**: +- 当字段需要使用字典类型时(dictType不为空),请使用 generate_dictionary_options 工具 +- 该工具允许AI根据字段描述智能生成合适的字典选项 +- 调用示例: + { + "dictType": "user_status", + "fieldDesc": "用户状态", + "options": [ + {"label": "正常", "value": "1", "sort": 1}, + {"label": "禁用", "value": "0", "sort": 2} + ], + "dictName": "用户状态字典", + "description": "用于管理用户账户状态的字典" + } +- 请在创建模块之前先创建所需的字典选项 + +重要提醒:ExecutionPlan必须严格按照以下格式(支持批量创建多个模块): +{ + "packageName": "包名", + "packageType": "package或plugin", // 当用户提到插件时必须是"plugin" + "needCreatedPackage": true/false, + "needCreatedModules": true/false, + "packageInfo": { + "desc": "描述", + "label": "展示名", + "template": "package或plugin", // 必须与packageType保持一致! + "packageName": "包名" + }, + "modulesInfo": [{ + "package": "包名", + "tableName": "数据库表名", + "businessDB": "", + "structName": "结构体名", + "packageName": "文件名称小驼峰模式 一般是结构体名的小驼峰", + "description": "中文描述", + "abbreviation": "简称 package和结构体简称不可同名 小驼峰模式", + "humpPackageName": "一般是结构体名的下划线分割的小驼峰 例如:sys_user", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true/false 用户不特地强调开启资源标识则为false, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": false/true 用户不特地强调创建按钮权限则为false, + "onlyTemplate": false, + "isTree": false, + "treeJson": "", + "isAdd": false, + "generateWeb": true, + "generateServer": true, + "fields": [{ + "fieldName": "字段名(必须大写开头)", + "fieldDesc": "字段描述", + "fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)", + "fieldJson": "json标签(string 必须是小驼峰命名,例:userName)", + "dataTypeLong": "长度", + "comment": "注释", + "columnName": "数据库列名", + "fieldSearchType": "=/!=/>/=/<=/LIKE等 可以为空", + "fieldSearchHide": true/false, + "dictType": "", + "form": true/false 是否前端创建输入, + "table": true/false 是否前端表格展示, + "desc": true/false 是否前端详情展示, + "excel": true/false 是否导出Excel, + "require": true/false 是否必填, + "defaultValue": "", + "errorText": "错误提示", + "clearable": true, + "sort": false, + "primaryKey": "当gvaModel=false时必须有一个字段设为true(bool)", + "dataSource": null, + "checkDataSource": false, + "fieldIndexType": "" + }] + }, { + "package": "包名", + "tableName": "第二个模块的表名", + "structName": "第二个模块的结构体名", + "description": "第二个模块的描述", + "...": "更多模块配置..." + }] +} + +**重要提醒**:ExecutionPlan必须严格按照以下格式和验证规则: + +**插件类型检测规则(最重要)**: +1. 当用户需求中包含"插件"、"plugin"等关键词时,packageType和template都必须设置为"plugin" +2. packageType和template字段必须保持一致,不能一个是"package"另一个是"plugin" +3. 如果检测到插件意图但设置错误,会导致创建失败 + +**字段完整性要求**: +4. 所有字符串字段都不能为空(包括packageName、moduleName、structName、tableName、description等) +5. 所有布尔字段必须明确设置true或false,不能使用默认值 + +**主键设置规则(关键)**: +6. 当gvaModel=false时:fields数组中必须有且仅有一个字段的primaryKey=true +7. 当gvaModel=true时:系统自动创建ID主键,fields中所有字段的primaryKey都应为false +8. 主键设置错误会导致模板执行时PrimaryField为nil的严重错误! + +**包和模块创建逻辑**: +9. 如果存在可用的package,needCreatedPackage应设为false +10. 如果存在可用的modules,needCreatedModules应设为false +11. 如果发现合适的预设计模块,可以考虑基于它进行扩展而不是从零创建 + +**字典创建流程**: +12. 如果字段需要字典类型,请先使用 generate_dictionary_options 工具创建字典 +13. 字典创建成功后,再执行模块创建操作 + +`, string(resultJSON), requirement, pluginDetectionMsg, + func() string { + if len(emptyPackageNames) > 0 { + return fmt.Sprintf("**重要提醒**:检测到 %s 包存在但内容为空,已自动删除相关文件夹和数据库记录。如果用户需求涉及这些包名,请设置 needCreatedPackage=true 重新创建。", strings.Join(emptyPackageNames, "、")) + } + return "" + }()), + }, + }, + }, nil +} + +// handleConfirm 处理确认请求 +func (t *AutomationModuleAnalyzer) handleConfirm(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + executionPlanData, ok := request.GetArguments()["executionPlan"] + if !ok { + return nil, errors.New("参数错误:executionPlan 必须提供") + } + + // 解析执行计划 + planJSON, err := json.Marshal(executionPlanData) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v", err) + } + + var plan ExecutionPlan + err = json.Unmarshal(planJSON, &plan) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err) + } + + // 验证执行计划的完整性 + if err := t.validateExecutionPlan(&plan); err != nil { + return nil, fmt.Errorf("执行计划验证失败: %v", err) + } + + // 构建确认响应 + var moduleNames []string + for _, moduleInfo := range plan.ModulesInfo { + moduleNames = append(moduleNames, moduleInfo.StructName) + } + moduleNamesStr := strings.Join(moduleNames, "_") + + confirmResponse := ConfirmationResponse{ + Message: "请确认以下创建计划:", + PackageConfirm: plan.NeedCreatedPackage, + ModulesConfirm: plan.NeedCreatedModules, + CanProceed: true, + ConfirmationKey: fmt.Sprintf("%s_%s_%d", plan.PackageName, moduleNamesStr, time.Now().Unix()), + } + + // 构建详细的确认信息 + var confirmDetails strings.Builder + confirmDetails.WriteString(fmt.Sprintf("包名: %s\n", plan.PackageName)) + confirmDetails.WriteString(fmt.Sprintf("包类型: %s\n", plan.PackageType)) + + if plan.NeedCreatedPackage && plan.PackageInfo != nil { + confirmDetails.WriteString("\n需要创建包:\n") + confirmDetails.WriteString(fmt.Sprintf(" - 包名: %s\n", plan.PackageInfo.PackageName)) + confirmDetails.WriteString(fmt.Sprintf(" - 标签: %s\n", plan.PackageInfo.Label)) + confirmDetails.WriteString(fmt.Sprintf(" - 描述: %s\n", plan.PackageInfo.Desc)) + confirmDetails.WriteString(fmt.Sprintf(" - 模板: %s\n", plan.PackageInfo.Template)) + } + + if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { + confirmDetails.WriteString(fmt.Sprintf("\n需要创建模块 (共%d个):\n", len(plan.ModulesInfo))) + for i, moduleInfo := range plan.ModulesInfo { + confirmDetails.WriteString(fmt.Sprintf("\n模块 %d:\n", i+1)) + confirmDetails.WriteString(fmt.Sprintf(" - 结构体名: %s\n", moduleInfo.StructName)) + confirmDetails.WriteString(fmt.Sprintf(" - 表名: %s\n", moduleInfo.TableName)) + confirmDetails.WriteString(fmt.Sprintf(" - 描述: %s\n", moduleInfo.Description)) + confirmDetails.WriteString(fmt.Sprintf(" - 字段数量: %d\n", len(moduleInfo.Fields))) + confirmDetails.WriteString(" - 字段列表:\n") + for _, field := range moduleInfo.Fields { + confirmDetails.WriteString(fmt.Sprintf(" * %s (%s): %s\n", field.FieldName, field.FieldType, field.FieldDesc)) + } + } + } + + resultJSON, err := json.MarshalIndent(confirmResponse, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("确认信息:\n\n%s\n\n详细信息:\n%s\n\n请用户确认是否继续执行此计划。如果确认,请使用execute操作并提供相应的确认参数。", string(resultJSON), confirmDetails.String()), + }, + }, + }, nil +} + +// handleExecute 处理执行请求 +func (t *AutomationModuleAnalyzer) handleExecute(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + executionPlanData, ok := request.GetArguments()["executionPlan"] + if !ok { + return nil, errors.New("参数错误:executionPlan 必须提供") + } + + // 解析执行计划 + planJSON, err := json.Marshal(executionPlanData) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v", err) + } + + var plan ExecutionPlan + err = json.Unmarshal(planJSON, &plan) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err) + } + + // 验证执行计划的完整性 + if err := t.validateExecutionPlan(&plan); err != nil { + return nil, fmt.Errorf("执行计划验证失败: %v", err) + } + + // 检查用户确认 + if plan.NeedCreatedPackage { + packageConfirm, ok := request.GetArguments()["packageConfirm"].(string) + if !ok || (packageConfirm != "yes" && packageConfirm != "no") { + return nil, errors.New("参数错误:当需要创建包时,packageConfirm 必须是 'yes' 或 'no'") + } + if packageConfirm == "no" { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: "用户取消了包的创建操作", + }, + }, + }, nil + } + } + + if plan.NeedCreatedModules { + modulesConfirm, ok := request.GetArguments()["modulesConfirm"].(string) + if !ok || (modulesConfirm != "yes" && modulesConfirm != "no") { + return nil, errors.New("参数错误:当需要创建模块时,modulesConfirm 必须是 'yes' 或 'no'") + } + if modulesConfirm == "no" { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: "用户取消了模块的创建操作", + }, + }, + }, nil + } + } + + // 执行创建操作 + result := t.executeCreation(ctx, &plan) + + resultJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + // 添加权限分配提醒 + permissionReminder := "\n\n⚠️ 重要提醒:\n" + + "模块创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的API和菜单权限," + + "以确保用户能够正常访问新功能。\n" + + "具体步骤:\n" + + "1. 进入角色管理页面\n" + + "2. 选择需要授权的角色\n" + + "3. 在API权限中勾选新创建的API接口\n" + + "4. 在菜单权限中勾选新创建的菜单项\n" + + "5. 保存权限配置" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("执行结果:\n\n%s%s", string(resultJSON), permissionReminder), + }, + }, + }, nil +} + +// isSystemFunction 判断是否为系统功能 +func (t *AutomationModuleAnalyzer) isSystemFunction(requirement string) bool { + systemKeywords := []string{ + "用户", "权限", "角色", "菜单", "系统", "配置", "字典", "参数", + "user", "authority", "role", "menu", "system", "config", "dictionary", + "认证", "授权", "登录", "注册", "JWT", "casbin", + } + + requirementLower := strings.ToLower(requirement) + for _, keyword := range systemKeywords { + if strings.Contains(requirementLower, keyword) { + return true + } + } + return false +} + +// buildDirectoryStructure 构建目录结构信息 +func (t *AutomationModuleAnalyzer) buildDirectoryStructure(plan *ExecutionPlan) map[string]string { + paths := make(map[string]string) + + // 获取配置信息 + autoCodeConfig := global.GVA_CONFIG.AutoCode + + // 构建基础路径 + rootPath := autoCodeConfig.Root + serverPath := autoCodeConfig.Server + webPath := autoCodeConfig.Web + moduleName := autoCodeConfig.Module + + // 如果计划中有包名,使用计划中的包名,否则使用默认 + packageName := "example" + if plan.PackageInfo != nil && plan.PackageInfo.PackageName != "" { + packageName = plan.PackageInfo.PackageName + } + + // 如果计划中有模块信息,获取第一个模块的结构名作为默认值 + structName := "ExampleStruct" + if len(plan.ModulesInfo) > 0 && plan.ModulesInfo[0].StructName != "" { + structName = plan.ModulesInfo[0].StructName + } + + // 根据包类型构建不同的路径结构 + packageType := plan.PackageType + if packageType == "" { + packageType = "package" // 默认为package模式 + } + + // 构建服务端路径 + if serverPath != "" { + serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath) + + if packageType == "plugin" { + // Plugin 模式:所有文件都在 /plugin/packageName/ 目录下 + pluginBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName) + + // API 路径 + paths["api"] = fmt.Sprintf("%s/api", pluginBasePath) + + // Service 路径 + paths["service"] = fmt.Sprintf("%s/service", pluginBasePath) + + // Model 路径 + paths["model"] = fmt.Sprintf("%s/model", pluginBasePath) + + // Router 路径 + paths["router"] = fmt.Sprintf("%s/router", pluginBasePath) + + // Request 路径 + paths["request"] = fmt.Sprintf("%s/model/request", pluginBasePath) + + // Response 路径 + paths["response"] = fmt.Sprintf("%s/model/response", pluginBasePath) + + // Plugin 特有文件 + paths["plugin_main"] = fmt.Sprintf("%s/main.go", pluginBasePath) + paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", pluginBasePath) + paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", pluginBasePath) + } else { + // Package 模式:传统的目录结构 + // API 路径 + paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName) + + // Service 路径 + paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName) + + // Model 路径 + paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName) + + // Router 路径 + paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName) + + // Request 路径 + paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName) + + // Response 路径 + paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName) + } + } + + // 构建前端路径(两种模式相同) + if webPath != "" { + webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath) + + // Vue 页面路径 + paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName) + + // API 路径 + paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName) + } + + // 添加模块信息 + paths["module"] = moduleName + paths["package_name"] = packageName + paths["package_type"] = packageType + paths["struct_name"] = structName + paths["root_path"] = rootPath + paths["server_path"] = serverPath + paths["web_path"] = webPath + + return paths +} + +// validateExecutionPlan 验证执行计划的完整性 +func (t *AutomationModuleAnalyzer) validateExecutionPlan(plan *ExecutionPlan) error { + // 验证基本字段 + if plan.PackageName == "" { + return errors.New("packageName 不能为空") + } + if plan.PackageType != "package" && plan.PackageType != "plugin" { + return errors.New("packageType 必须是 'package' 或 'plugin'") + } + + // 验证packageType和template字段的一致性 + if plan.NeedCreatedPackage && plan.PackageInfo != nil { + if plan.PackageType != plan.PackageInfo.Template { + return errors.New("packageType 和 packageInfo.template 必须保持一致") + } + } + + // 验证包信息 + if plan.NeedCreatedPackage { + if plan.PackageInfo == nil { + return errors.New("当 needCreatedPackage=true 时,packageInfo 不能为空") + } + if plan.PackageInfo.PackageName == "" { + return errors.New("packageInfo.packageName 不能为空") + } + if plan.PackageInfo.Template != "package" && plan.PackageInfo.Template != "plugin" { + return errors.New("packageInfo.template 必须是 'package' 或 'plugin'") + } + if plan.PackageInfo.Label == "" { + return errors.New("packageInfo.label 不能为空") + } + if plan.PackageInfo.Desc == "" { + return errors.New("packageInfo.desc 不能为空") + } + } + + // 验证模块信息(批量验证) + if plan.NeedCreatedModules { + if len(plan.ModulesInfo) == 0 { + return errors.New("当 needCreatedModules=true 时,modulesInfo 不能为空") + } + + // 遍历验证每个模块 + for moduleIndex, moduleInfo := range plan.ModulesInfo { + if moduleInfo.Package == "" { + return fmt.Errorf("模块 %d 的 package 不能为空", moduleIndex+1) + } + if moduleInfo.StructName == "" { + return fmt.Errorf("模块 %d 的 structName 不能为空", moduleIndex+1) + } + if moduleInfo.TableName == "" { + return fmt.Errorf("模块 %d 的 tableName 不能为空", moduleIndex+1) + } + if moduleInfo.Description == "" { + return fmt.Errorf("模块 %d 的 description 不能为空", moduleIndex+1) + } + if moduleInfo.Abbreviation == "" { + return fmt.Errorf("模块 %d 的 abbreviation 不能为空", moduleIndex+1) + } + if moduleInfo.PackageName == "" { + return fmt.Errorf("模块 %d 的 packageName 不能为空", moduleIndex+1) + } + if moduleInfo.HumpPackageName == "" { + return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1) + } + + // 验证字段信息 + if len(moduleInfo.Fields) == 0 { + return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1) + } + + for i, field := range moduleInfo.Fields { + if field.FieldName == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1) + } + + if field.FieldDesc == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1) + } + if field.FieldType == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldType 不能为空", moduleIndex+1, i+1) + } + if field.FieldJson == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldJson 不能为空", moduleIndex+1, i+1) + } + if field.ColumnName == "" { + return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1) + } + + // 确保字段名首字母大写 + if len(field.FieldName) > 0 { + firstChar := string(field.FieldName[0]) + if firstChar >= "a" && firstChar <= "z" { + moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:] + } + } + + // 确保FieldJson使用小驼峰命名 + if len(field.FieldJson) > 0 { + // 处理下划线命名转小驼峰 + if strings.Contains(field.FieldJson, "_") { + parts := strings.Split(field.FieldJson, "_") + camelCase := strings.ToLower(parts[0]) + for j := 1; j < len(parts); j++ { + if len(parts[j]) > 0 { + camelCase += strings.ToUpper(string(parts[j][0])) + strings.ToLower(parts[j][1:]) + } + } + moduleInfo.Fields[i].FieldJson = camelCase + } else { + // 处理首字母大写转小写 + firstChar := string(field.FieldJson[0]) + if firstChar >= "A" && firstChar <= "Z" { + moduleInfo.Fields[i].FieldJson = strings.ToLower(firstChar) + field.FieldJson[1:] + } + } + } + + // 确保ColumnName使用下划线命名 + if len(field.ColumnName) > 0 { + // 将驼峰命名转换为下划线命名 + var result strings.Builder + for i, r := range field.ColumnName { + if i > 0 && r >= 'A' && r <= 'Z' { + result.WriteRune('_') + } + result.WriteRune(unicode.ToLower(r)) + } + moduleInfo.Fields[i].ColumnName = result.String() + } + + // 验证字段类型 + validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"} + validType := false + for _, validFieldType := range validFieldTypes { + if field.FieldType == validFieldType { + validType = true + break + } + } + if !validType { + return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldType, validFieldTypes) + } + + // 验证搜索类型(如果设置了) + if field.FieldSearchType != "" { + validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"} + validSearchType := false + for _, validType := range validSearchTypes { + if field.FieldSearchType == validType { + validSearchType = true + break + } + } + if !validSearchType { + return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldSearchType, validSearchTypes) + } + } + } + + // 验证主键设置 + if !moduleInfo.GvaModel { + // 当不使用GVA模型时,必须有且仅有一个字段设置为主键 + primaryKeyCount := 0 + for _, field := range moduleInfo.Fields { + if field.PrimaryKey { + primaryKeyCount++ + } + } + if primaryKeyCount == 0 { + return fmt.Errorf("模块 %d:当 gvaModel=false 时,必须有一个字段的 primaryKey=true", moduleIndex+1) + } + if primaryKeyCount > 1 { + return fmt.Errorf("模块 %d:当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1) + } + } else { + // 当使用GVA模型时,所有字段的primaryKey都应该为false + for i, field := range moduleInfo.Fields { + if field.PrimaryKey { + return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false,系统会自动创建ID主键", moduleIndex+1, i+1) + } + } + } + } + } + + return nil +} + +// executeCreation 执行创建操作 +func (t *AutomationModuleAnalyzer) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecutionResult { + result := &ExecutionResult{ + Success: false, + Paths: make(map[string]string), + } + + // 无论如何都先构建目录结构信息,确保paths始终返回 + result.Paths = t.buildDirectoryStructure(plan) + + if !plan.NeedCreatedModules { + result.Success = true + result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; " + return result + } + + // 创建包(如果需要) + if plan.NeedCreatedPackage && plan.PackageInfo != nil { + packageService := service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage + err := packageService.Create(ctx, plan.PackageInfo) + if err != nil { + result.Message = fmt.Sprintf("创建包失败: %v", err) + // 即使创建包失败,也要返回paths信息 + return result + } + result.Message += "包创建成功; " + } + + // 批量创建字典和模块(如果需要) + if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { + templateService := service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate + + // 先批量创建所有模块需要的字典 + dictResult := t.createRequiredDictionaries(ctx, plan.ModulesInfo) + result.Message += dictResult + + // 遍历所有模块进行创建 + for _, moduleInfo := range plan.ModulesInfo { + + // 创建模块 + err := moduleInfo.Pretreatment() + if err != nil { + result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err) + continue // 继续处理下一个模块 + } + + err = templateService.Create(ctx, *moduleInfo) + if err != nil { + result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err) + continue // 继续处理下一个模块 + } + result.Message += fmt.Sprintf("模块 %s 创建成功; ", moduleInfo.StructName) + } + + result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo)) + + // 添加重要提醒:不要使用其他MCP工具 + result.Message += "\n\n⚠️ 重要提醒:\n" + result.Message += "模块创建已完成,API和菜单已自动生成。请不要再调用以下MCP工具:\n" + result.Message += "- api_creator:API权限已在模块创建时自动生成\n" + result.Message += "- menu_creator:前端菜单已在模块创建时自动生成\n" + result.Message += "如需修改API或菜单,请直接在系统管理界面中进行配置。\n" + } + + result.Message += "已构建目录结构信息; " + result.Success = true + + if result.Message == "" { + result.Message = "执行计划完成" + } + + return result +} + +// createRequiredDictionaries 创建所需的字典(批量处理) +func (t *AutomationModuleAnalyzer) createRequiredDictionaries(ctx context.Context, modulesInfoList []*request.AutoCode) string { + var messages []string + dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService + createdDictTypes := make(map[string]bool) // 用于避免重复创建相同的字典 + + // 遍历所有模块 + for moduleIndex, modulesInfo := range modulesInfoList { + messages = append(messages, fmt.Sprintf("处理模块 %d (%s) 的字典: ", moduleIndex+1, modulesInfo.StructName)) + + // 遍历当前模块的所有字段,查找使用字典的字段 + moduleHasDictFields := false + for _, field := range modulesInfo.Fields { + if field.DictType != "" { + moduleHasDictFields = true + + // 如果这个字典类型已经在之前的模块中创建过,跳过 + if createdDictTypes[field.DictType] { + messages = append(messages, fmt.Sprintf("字典 %s 已在前面的模块中创建,跳过; ", field.DictType)) + continue + } + + // 检查字典是否存在 + exists, err := t.checkDictionaryExists(field.DictType) + if err != nil { + messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", field.DictType, err)) + continue + } + + if !exists { + // 字典不存在,创建字典 + dictionary := model.SysDictionary{ + Name: t.generateDictionaryName(field.DictType, field.FieldDesc), + Type: field.DictType, + Status: &[]bool{true}[0], // 默认启用 + Desc: fmt.Sprintf("自动生成的字典,用于模块 %s 字段: %s (%s)", modulesInfo.StructName, field.FieldName, field.FieldDesc), + } + + err = dictionaryService.CreateSysDictionary(dictionary) + if err != nil { + messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", field.DictType, err)) + } else { + messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", field.DictType, dictionary.Name)) + createdDictTypes[field.DictType] = true // 标记为已创建 + + // 创建默认的字典详情项 + t.createDefaultDictionaryDetails(ctx, field.DictType, field.FieldDesc) + } + } else { + messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", field.DictType)) + createdDictTypes[field.DictType] = true // 标记为已存在 + } + } + } + + if !moduleHasDictFields { + messages = append(messages, "无需创建字典; ") + } + } + + if len(messages) == 0 { + return "未发现需要创建的字典; " + } + + return strings.Join(messages, "") +} + +// checkDictionaryExists 检查字典是否存在 +func (t *AutomationModuleAnalyzer) checkDictionaryExists(dictType string) (bool, error) { + var dictionary model.SysDictionary + err := global.GVA_DB.Where("type = ?", dictType).First(&dictionary).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil // 字典不存在 + } + return false, err // 其他错误 + } + return true, nil // 字典存在 +} + +// generateDictionaryName 生成字典名称 +func (t *AutomationModuleAnalyzer) generateDictionaryName(dictType, fieldDesc string) string { + if fieldDesc != "" { + return fmt.Sprintf("%s字典", fieldDesc) + } + return fmt.Sprintf("%s字典", dictType) +} + +// createDefaultDictionaryDetails 创建默认的字典详情项 +func (t *AutomationModuleAnalyzer) createDefaultDictionaryDetails(ctx context.Context, dictType, fieldDesc string) { + // 字典选项现在通过 generate_dictionary_options MCP工具由AI client传入 + // 这里不再创建默认选项,只是保留方法以保持兼容性 + global.GVA_LOG.Info(fmt.Sprintf("字典 %s 已创建,请使用 generate_dictionary_options 工具添加字典选项", dictType)) +} + +// DictionaryOption 字典选项结构 +type DictionaryOption struct { + Label string `json:"label"` + Value string `json:"value"` + Sort int `json:"sort"` +} + +// generateSmartDictionaryOptions 通过MCP调用让AI生成字典选项 +func (t *AutomationModuleAnalyzer) generateSmartDictionaryOptions(dictType, fieldDesc string) []struct { + label string + value string + sort int +} { + // 返回空切片,不再使用预制选项 + // 字典选项将通过新的MCP工具由AI client传入 + return []struct { + label string + value string + sort int + }{} +} + +// detectPluginIntent 检测用户需求中是否包含插件相关的关键词 +func (t *AutomationModuleAnalyzer) detectPluginIntent(requirement string) (suggestedType string, isPlugin bool, confidence string) { + // 转换为小写进行匹配 + requirementLower := strings.ToLower(requirement) + + // 插件相关关键词 + pluginKeywords := []string{ + "插件", "plugin", "扩展", "extension", "addon", "模块插件", + "功能插件", "业务插件", "第三方插件", "自定义插件", + } + + // 包相关关键词(用于排除误判) + packageKeywords := []string{ + "包", "package", "模块包", "业务包", "功能包", + } + + // 检测插件关键词 + pluginMatches := 0 + for _, keyword := range pluginKeywords { + if strings.Contains(requirementLower, keyword) { + pluginMatches++ + } + } + + // 检测包关键词 + packageMatches := 0 + for _, keyword := range packageKeywords { + if strings.Contains(requirementLower, keyword) { + packageMatches++ + } + } + + // 决策逻辑 + if pluginMatches > 0 { + if packageMatches == 0 || pluginMatches > packageMatches { + return "plugin", true, "高" + } else { + return "plugin", true, "中" + } + } + + // 默认返回package + return "package", false, "低" +} + +// isPackageFolderEmpty 检查包对应的文件夹是否为空 +func (t *AutomationModuleAnalyzer) isPackageFolderEmpty(packageName, template string) (bool, error) { + // 根据模板类型确定基础路径 + var basePath string + if template == "plugin" { + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) + } else { + // package 类型 + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName) + } + + // 检查文件夹是否存在 + if _, err := os.Stat(basePath); os.IsNotExist(err) { + // 文件夹不存在,认为是空的 + return true, nil + } else if err != nil { + return false, fmt.Errorf("检查文件夹状态失败: %v", err) + } + + // 读取文件夹内容 + entries, err := os.ReadDir(basePath) + if err != nil { + return false, fmt.Errorf("读取文件夹内容失败: %v", err) + } + + // 检查目录下是否有 .go 文件 + hasGoFiles := false + for _, entry := range entries { + name := entry.Name() + // 跳过隐藏文件、.DS_Store 等系统文件 + if strings.HasPrefix(name, ".") { + continue + } + // 如果是目录,递归检查子目录中的 .go 文件 + if entry.IsDir() { + subPath := filepath.Join(basePath, name) + subEntries, err := os.ReadDir(subPath) + if err != nil { + continue + } + for _, subEntry := range subEntries { + if !subEntry.IsDir() && strings.HasSuffix(subEntry.Name(), ".go") { + hasGoFiles = true + break + } + } + if hasGoFiles { + break + } + } else if strings.HasSuffix(name, ".go") { + // 如果是 .go 文件 + hasGoFiles = true + break + } + } + + // 如果没有 .go 文件,认为是空包 + return !hasGoFiles, nil +} + +// removeEmptyPackageFolder 删除空的包文件夹 +func (t *AutomationModuleAnalyzer) removeEmptyPackageFolder(packageName, template string) error { + var errors []string + + if template == "plugin" { + // plugin 类型只删除 plugin 目录下的文件夹 + basePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) + if err := t.removeDirectoryIfExists(basePath); err != nil { + errors = append(errors, fmt.Sprintf("删除plugin文件夹失败: %v", err)) + } + } else { + // package 类型需要删除多个目录下的相关文件 + paths := []string{ + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName), + } + + for _, path := range paths { + if err := t.removeDirectoryIfExists(path); err != nil { + errors = append(errors, fmt.Sprintf("删除%s失败: %v", path, err)) + } + } + } + + if len(errors) > 0 { + return fmt.Errorf("删除过程中出现错误: %s", strings.Join(errors, "; ")) + } + + return nil +} + +// removeDirectoryIfExists 删除目录(如果存在) +func (t *AutomationModuleAnalyzer) removeDirectoryIfExists(dirPath string) error { + // 检查文件夹是否存在 + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + // 文件夹不存在,无需删除 + return nil + } else if err != nil { + return fmt.Errorf("检查文件夹状态失败: %v", err) + } + + // 删除文件夹及其所有内容 + if err := os.RemoveAll(dirPath); err != nil { + return fmt.Errorf("删除文件夹失败: %v", err) + } + + global.GVA_LOG.Info(fmt.Sprintf("成功删除目录: %s", dirPath)) + return nil +} + +// cleanupRelatedApiAndMenus 清理与删除的模块相关的API和菜单记录 +func (t *AutomationModuleAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error { + if len(historyIDs) == 0 { + return nil + } + + // 获取要删除的历史记录信息 + var histories []model.SysAutoCodeHistory + if err := global.GVA_DB.Where("id IN ?", historyIDs).Find(&histories).Error; err != nil { + return fmt.Errorf("获取历史记录失败: %v", err) + } + + var deletedApiCount, deletedMenuCount int + + for _, history := range histories { + // 删除相关的API记录(使用存储的API IDs) + if len(history.ApiIDs) > 0 { + ids := make([]int, 0, len(history.ApiIDs)) + for _, id := range history.ApiIDs { + ids = append(ids, int(id)) + } + idsReq := common.IdsReq{Ids: ids} + if err := systemService.ApiServiceApp.DeleteApisByIds(idsReq); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除API记录失败 (模块: %s): %v", history.StructName, err)) + } else { + deletedApiCount += len(ids) + global.GVA_LOG.Info(fmt.Sprintf("成功删除API记录 (模块: %s, 数量: %d)", history.StructName, len(ids))) + } + } + + // 删除相关的菜单记录(使用存储的菜单ID) + if history.MenuID != 0 { + if err := systemService.BaseMenuServiceApp.DeleteBaseMenu(int(history.MenuID)); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除菜单记录失败 (模块: %s, 菜单ID: %d): %v", history.StructName, history.MenuID, err)) + } else { + deletedMenuCount++ + global.GVA_LOG.Info(fmt.Sprintf("成功删除菜单记录 (模块: %s, 菜单ID: %d)", history.StructName, history.MenuID)) + } + } + } + + if deletedApiCount > 0 || deletedMenuCount > 0 { + global.GVA_LOG.Info(fmt.Sprintf("清理完成:删除了 %d 个API记录和 %d 个菜单记录", deletedApiCount, deletedMenuCount)) + } + + return nil +} diff --git a/server/mcp/menu_creator.go b/server/mcp/menu_creator.go new file mode 100644 index 000000000..a5f83545b --- /dev/null +++ b/server/mcp/menu_creator.go @@ -0,0 +1,287 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "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" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + RegisterTool(&MenuCreator{}) +} + +// MenuCreateRequest 菜单创建请求结构 +type MenuCreateRequest struct { + ParentId uint `json:"parentId"` // 父菜单ID,0表示根菜单 + Path string `json:"path"` // 路由path + Name string `json:"name"` // 路由name + Hidden bool `json:"hidden"` // 是否在列表隐藏 + Component string `json:"component"` // 对应前端文件路径 + Sort int `json:"sort"` // 排序标记 + Title string `json:"title"` // 菜单名 + Icon string `json:"icon"` // 菜单图标 + KeepAlive bool `json:"keepAlive"` // 是否缓存 + DefaultMenu bool `json:"defaultMenu"` // 是否是基础路由 + CloseTab bool `json:"closeTab"` // 自动关闭tab + ActiveName string `json:"activeName"` // 高亮菜单 + Parameters []MenuParameterRequest `json:"parameters"` // 路由参数 + MenuBtn []MenuButtonRequest `json:"menuBtn"` // 菜单按钮 +} + +// MenuParameterRequest 菜单参数请求结构 +type MenuParameterRequest struct { + Type string `json:"type"` // 参数类型:params或query + Key string `json:"key"` // 参数key + Value string `json:"value"` // 参数值 +} + +// MenuButtonRequest 菜单按钮请求结构 +type MenuButtonRequest struct { + Name string `json:"name"` // 按钮名称 + Desc string `json:"desc"` // 按钮描述 +} + +// MenuCreateResponse 菜单创建响应结构 +type MenuCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + MenuID uint `json:"menuId"` + Name string `json:"name"` + Path string `json:"path"` +} + +// MenuCreator 菜单创建工具 +type MenuCreator struct{} + +// New 创建菜单创建工具 +func (m *MenuCreator) New() mcp.Tool { + return mcp.NewTool("create_menu", + mcp.WithDescription(`创建前端菜单记录,用于AI编辑器自动添加前端页面时自动创建对应的菜单项。 + +**重要限制:** +- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成菜单项,不应调用此工具 +- 仅在以下情况使用:1) 单独创建菜单(不涉及模块创建);2) AI编辑器自动添加前端页面时`), + mcp.WithNumber("parentId", + mcp.Description("父菜单ID,0表示根菜单"), + mcp.DefaultNumber(0), + ), + mcp.WithString("path", + mcp.Required(), + mcp.Description("路由path,如:userList"), + ), + mcp.WithString("name", + mcp.Required(), + mcp.Description("路由name,用于Vue Router,如:userList"), + ), + mcp.WithBoolean("hidden", + mcp.Description("是否在菜单列表中隐藏"), + ), + mcp.WithString("component", + mcp.Required(), + mcp.Description("对应的前端Vue组件路径,如:view/user/list.vue"), + ), + mcp.WithNumber("sort", + mcp.Description("菜单排序号,数字越小越靠前"), + mcp.DefaultNumber(1), + ), + mcp.WithString("title", + mcp.Required(), + mcp.Description("菜单显示标题"), + ), + mcp.WithString("icon", + mcp.Description("菜单图标名称"), + mcp.DefaultString("menu"), + ), + mcp.WithBoolean("keepAlive", + mcp.Description("是否缓存页面"), + ), + mcp.WithBoolean("defaultMenu", + mcp.Description("是否是基础路由"), + ), + mcp.WithBoolean("closeTab", + mcp.Description("是否自动关闭tab"), + ), + mcp.WithString("activeName", + mcp.Description("高亮菜单名称"), + ), + mcp.WithString("parameters", + mcp.Description("路由参数JSON字符串,格式:[{\"type\":\"params\",\"key\":\"id\",\"value\":\"1\"}]"), + ), + mcp.WithString("menuBtn", + mcp.Description("菜单按钮JSON字符串,格式:[{\"name\":\"add\",\"desc\":\"新增\"}]"), + ), + ) +} + +// Handle 处理菜单创建请求 +func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 解析请求参数 + args := request.GetArguments() + + // 必需参数 + path, ok := args["path"].(string) + if !ok || path == "" { + return nil, errors.New("path 参数是必需的") + } + + name, ok := args["name"].(string) + if !ok || name == "" { + return nil, errors.New("name 参数是必需的") + } + + component, ok := args["component"].(string) + if !ok || component == "" { + return nil, errors.New("component 参数是必需的") + } + + title, ok := args["title"].(string) + if !ok || title == "" { + return nil, errors.New("title 参数是必需的") + } + + // 可选参数 + parentId := uint(0) + if val, ok := args["parentId"].(float64); ok { + parentId = uint(val) + } + + hidden := false + if val, ok := args["hidden"].(bool); ok { + hidden = val + } + + sort := 1 + if val, ok := args["sort"].(float64); ok { + sort = int(val) + } + + icon := "menu" + if val, ok := args["icon"].(string); ok && val != "" { + icon = val + } + + keepAlive := false + if val, ok := args["keepAlive"].(bool); ok { + keepAlive = val + } + + defaultMenu := false + if val, ok := args["defaultMenu"].(bool); ok { + defaultMenu = val + } + + closeTab := false + if val, ok := args["closeTab"].(bool); ok { + closeTab = val + } + + activeName := "" + if val, ok := args["activeName"].(string); ok { + activeName = val + } + + // 解析参数和按钮 + var parameters []system.SysBaseMenuParameter + if parametersStr, ok := args["parameters"].(string); ok && parametersStr != "" { + var paramReqs []MenuParameterRequest + if err := json.Unmarshal([]byte(parametersStr), ¶mReqs); err != nil { + return nil, fmt.Errorf("parameters 参数格式错误: %v", err) + } + for _, param := range paramReqs { + parameters = append(parameters, system.SysBaseMenuParameter{ + Type: param.Type, + Key: param.Key, + Value: param.Value, + }) + } + } + + var menuBtn []system.SysBaseMenuBtn + if menuBtnStr, ok := args["menuBtn"].(string); ok && menuBtnStr != "" { + var btnReqs []MenuButtonRequest + if err := json.Unmarshal([]byte(menuBtnStr), &btnReqs); err != nil { + return nil, fmt.Errorf("menuBtn 参数格式错误: %v", err) + } + for _, btn := range btnReqs { + menuBtn = append(menuBtn, system.SysBaseMenuBtn{ + Name: btn.Name, + Desc: btn.Desc, + }) + } + } + + // 构建菜单对象 + menu := system.SysBaseMenu{ + ParentId: parentId, + Path: path, + Name: name, + Hidden: hidden, + Component: component, + Sort: sort, + Meta: system.Meta{ + Title: title, + Icon: icon, + KeepAlive: keepAlive, + DefaultMenu: defaultMenu, + CloseTab: closeTab, + ActiveName: activeName, + }, + Parameters: parameters, + MenuBtn: menuBtn, + } + + // 创建菜单 + menuService := service.ServiceGroupApp.SystemServiceGroup.MenuService + err := menuService.AddBaseMenu(menu) + if err != nil { + return nil, fmt.Errorf("创建菜单失败: %v", err) + } + + // 获取创建的菜单ID + var createdMenu system.SysBaseMenu + err = global.GVA_DB.Where("name = ? AND path = ?", name, path).First(&createdMenu).Error + if err != nil { + global.GVA_LOG.Warn("获取创建的菜单ID失败", zap.Error(err)) + } + + // 构建响应 + response := &MenuCreateResponse{ + Success: true, + Message: fmt.Sprintf("成功创建菜单 %s", title), + MenuID: createdMenu.ID, + Name: name, + Path: path, + } + + resultJSON, err := json.MarshalIndent(response, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + // 添加权限分配提醒 + permissionReminder := "\n\n⚠️ 重要提醒:\n" + + "菜单创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的菜单权限," + + "以确保用户能够正常访问新菜单。\n" + + "具体步骤:\n" + + "1. 进入角色管理页面\n" + + "2. 选择需要授权的角色\n" + + "3. 在菜单权限中勾选新创建的菜单项\n" + + "4. 保存权限配置" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("菜单创建结果:\n\n%s%s", string(resultJSON), permissionReminder), + }, + }, + }, nil +} diff --git a/server/mcp/menu_lister.go b/server/mcp/menu_lister.go new file mode 100644 index 000000000..c04c83e92 --- /dev/null +++ b/server/mcp/menu_lister.go @@ -0,0 +1,112 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + // 注册工具将在enter.go中统一处理 + RegisterTool(&MenuLister{}) +} + +// MenuListResponse 菜单列表响应结构 +type MenuListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Menus []system.SysBaseMenu `json:"menus"` + TotalCount int `json:"totalCount"` + Description string `json:"description"` +} + +// MenuLister 菜单列表工具 +type MenuLister struct{} + +// New 创建菜单列表工具 +func (m *MenuLister) New() mcp.Tool { + return mcp.NewTool("list_all_menus", + mcp.WithDescription(`获取系统中所有菜单信息,包括菜单树结构、路由信息、组件路径等,用于前端编写vue-router时正确跳转 + +**功能说明:** +- 返回完整的菜单树形结构 +- 包含路由配置信息(path、name、component) +- 包含菜单元数据(title、icon、keepAlive等) +- 包含菜单参数和按钮配置 +- 支持父子菜单关系展示 + +**使用场景:** +- 前端路由配置:获取所有菜单信息用于配置vue-router +- 菜单权限管理:了解系统中所有可用的菜单项 +- 导航组件开发:构建动态导航菜单 +- 系统架构分析:了解系统的菜单结构和页面组织`), + ) +} + +// Handle 处理菜单列表请求 +func (m *MenuLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 获取所有基础菜单 + allMenus, err := m.getAllMenus() + if err != nil { + global.GVA_LOG.Error("获取菜单列表失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("获取菜单列表失败: %v", err), + }, + }, + IsError: true, + }, nil + } + + // 构建返回结果 + response := MenuListResponse{ + Success: true, + Message: "获取菜单列表成功", + Menus: allMenus, + TotalCount: len(allMenus), + Description: "系统中所有菜单信息的标准列表,包含路由配置和组件信息", + } + + // 序列化响应 + responseJSON, err := json.MarshalIndent(response, "", " ") + if err != nil { + global.GVA_LOG.Error("序列化菜单响应失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("序列化响应失败: %v", err), + }, + }, + IsError: true, + }, nil + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(responseJSON), + }, + }, + }, nil +} + +// getAllMenus 获取所有基础菜单 +func (m *MenuLister) getAllMenus() ([]system.SysBaseMenu, error) { + var menus []system.SysBaseMenu + err := global.GVA_DB.Order("sort").Preload("Parameters").Preload("MenuBtn").Find(&menus).Error + if err != nil { + return nil, err + } + return menus, nil +} + diff --git a/server/mcp/requirement_analyzer.go b/server/mcp/requirement_analyzer.go new file mode 100644 index 000000000..541b925ba --- /dev/null +++ b/server/mcp/requirement_analyzer.go @@ -0,0 +1,139 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&RequirementAnalyzer{}) +} + +type RequirementAnalyzer struct{} + +// RequirementAnalysisRequest 需求分析请求 +type RequirementAnalysisRequest struct { + UserRequirement string `json:"userRequirement"` +} + + + +// RequirementAnalysisResponse 需求分析响应 +type RequirementAnalysisResponse struct { + AIPrompt string `json:"aiPrompt"` // 给AI的提示词 +} + +// New 返回工具注册信息 +func (t *RequirementAnalyzer) New() mcp.Tool { + return mcp.NewTool("requirement_analyzer", + mcp.WithDescription(`**🚀 需求分析工具 - 首选入口工具(最高优先级)** + +**⭐ 重要提示:这是所有MCP工具的首选入口,请优先使用!** + +**🎯 核心职责:** +将用户的自然语言需求转换为AI可理解的结构化提示词 + +**📋 工作流程:** +1. 接收用户自然语言需求描述 +2. 生成专业的AI提示词,要求AI将需求梳理为清晰的逻辑步骤: + - **1. 第一步功能描述** + - **2. 第二步功能描述** + - **3. 第三步功能描述** + - **...** +3. 指导后续使用 gva_auto_generate 工具进行代码生成 + +**✅ 适用场景:** +- 用户有新的业务需求需要开发 +- 需要创建新的功能模块 +- 想要快速搭建业务系统 +- 需求描述比较模糊,需要AI帮助梳理 + +**❌ 不负责的事情:** +- 不生成具体的包名和模块名(交给 gva_auto_generate) +- 不进行代码生成(交给 gva_auto_generate) +- 不创建数据库表结构(交给 gva_auto_generate) + +**🔄 推荐工作流:** +requirement_analyzer → gva_auto_generate → 其他辅助工具 + +`), + mcp.WithString("userRequirement", + mcp.Required(), + mcp.Description("用户的需求描述,支持自然语言,如:'我要做一个猫舍管理系统,用来录入猫的信息,并且记录每只猫每天的活动信息'"), + ), + ) +} + +// Handle 处理工具调用 +func (t *RequirementAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + userRequirement, ok := request.GetArguments()["userRequirement"].(string) + if !ok || userRequirement == "" { + return nil, errors.New("参数错误:userRequirement 必须是非空字符串") + } + + // 分析用户需求 + analysisResponse, err := t.analyzeRequirement(userRequirement) + if err != nil { + return nil, fmt.Errorf("需求分析失败: %v", err) + } + + // 序列化响应 + responseData, err := json.Marshal(analysisResponse) + if err != nil { + return nil, fmt.Errorf("序列化响应失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(string(responseData)), + }, + }, nil +} + +// analyzeRequirement 分析用户需求 - 专注于AI需求传递 +func (t *RequirementAnalyzer) analyzeRequirement(userRequirement string) (*RequirementAnalysisResponse, error) { + // 生成AI提示词 - 这是唯一功能 + aiPrompt := t.generateAIPrompt(userRequirement) + + return &RequirementAnalysisResponse{ + AIPrompt: aiPrompt, + }, nil +} + +// generateAIPrompt 生成AI提示词 - 要求AI梳理逻辑为1xxx2xxx格式 +func (t *RequirementAnalyzer) generateAIPrompt(userRequirement string) string { + prompt := fmt.Sprintf(`# 🤖 AI需求逻辑梳理任务 + +## 📝 用户原始需求 +%s + +## 🎯 AI任务要求 +请将上述用户需求梳理成清晰的逻辑步骤,格式要求: + +**1. 第一步功能描述** +**2. 第二步功能描述** +**3. 第三步功能描述** +**...** + +## 📋 梳理要求 +- 将需求拆解为具体的功能步骤 +- 每个步骤用数字编号(1、2、3...) +- 步骤描述要清晰、具体、可执行 +- 按照业务逻辑顺序排列 +- 考虑数据流和用户操作流程 + +## 🔄 后续流程 +梳理完成后,请使用 gva_auto_generate 工具进行代码生成: +- gva_auto_generate 会根据梳理的逻辑步骤自动生成包名、模块名 +- gva_auto_generate 会设计数据表结构和API接口 +- gva_auto_generate 会生成完整的前后端代码 + + +现在请开始梳理用户需求:"%s"`, userRequirement, userRequirement) + + return prompt +} diff --git a/server/middleware/jwt.go b/server/middleware/jwt.go index db71ed6b9..7715ed9c7 100644 --- a/server/middleware/jwt.go +++ b/server/middleware/jwt.go @@ -17,7 +17,7 @@ func JWTAuth() gin.HandlerFunc { // 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录 token := utils.GetToken(c) if token == "" { - response.NoAuth("未登录或非法访问", c) + response.NoAuth("未登录或非法访问,请登录", c) c.Abort() return } @@ -32,7 +32,7 @@ func JWTAuth() gin.HandlerFunc { claims, err := j.ParseToken(token) if err != nil { if errors.Is(err, utils.TokenExpired) { - response.NoAuth("授权已过期", c) + response.NoAuth("登录已过期,请重新登录", c) utils.ClearToken(c) c.Abort() return diff --git a/server/middleware/timeout.go b/server/middleware/timeout.go new file mode 100644 index 000000000..473abf6c8 --- /dev/null +++ b/server/middleware/timeout.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "context" + "github.com/gin-gonic/gin" + "net/http" + "time" +) + +// TimeoutMiddleware 创建超时中间件 +// 入参 timeout 设置超时时间(例如:time.Second * 5) +// 使用示例 xxx.Get("path",middleware.TimeoutMiddleware(30*time.Second),HandleFunc) +func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { + return func(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), timeout) + defer cancel() + + c.Request = c.Request.WithContext(ctx) + + // 使用 buffered channel 避免 goroutine 泄漏 + done := make(chan struct{}, 1) + panicChan := make(chan interface{}, 1) + + go func() { + defer func() { + if p := recover(); p != nil { + select { + case panicChan <- p: + default: + } + } + select { + case done <- struct{}{}: + default: + } + }() + c.Next() + }() + + select { + case p := <-panicChan: + panic(p) + case <-done: + return + case <-ctx.Done(): + // 确保服务器超时设置足够长 + c.Header("Connection", "close") + c.AbortWithStatusJSON(http.StatusGatewayTimeout, gin.H{ + "code": 504, + "msg": "请求超时", + }) + return + } + } +} diff --git a/server/model/common/response/response.go b/server/model/common/response/response.go index 9529e7f9c..aa5a941c6 100644 --- a/server/model/common/response/response.go +++ b/server/model/common/response/response.go @@ -20,7 +20,6 @@ const ( // modified by mohamed hassan to support multilanguage func Result(code int, data interface{}, msg string, c *gin.Context) { - // 开始时间 c.JSON(http.StatusOK, Response{ code, data, diff --git a/server/model/system/request/sys_user.go b/server/model/system/request/sys_user.go index a48c46f2c..2cd4cf9f0 100644 --- a/server/model/system/request/sys_user.go +++ b/server/model/system/request/sys_user.go @@ -56,7 +56,6 @@ type ChangeUserInfo struct { AuthorityIds []uint `json:"authorityIds" gorm:"-"` // 角色ID Email string `json:"email" gorm:"comment:用户邮箱"` // 用户邮箱 HeaderImg string `json:"headerImg" gorm:"default:https://qmplusimg.henrongyi.top/gva_header.jpg;comment:用户头像"` // 用户头像 - SideMode string `json:"sideMode" gorm:"comment:用户侧边主题"` // 用户侧边主题 Enable int `json:"enable" gorm:"comment:冻结用户"` //冻结用户 Authorities []system.SysAuthority `json:"-" gorm:"many2many:sys_user_authority;"` } diff --git a/server/model/system/request/sys_version.go b/server/model/system/request/sys_version.go new file mode 100644 index 000000000..3d6d7ff27 --- /dev/null +++ b/server/model/system/request/sys_version.go @@ -0,0 +1,40 @@ +package request + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "time" +) + +type SysVersionSearch struct { + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` + VersionName *string `json:"versionName" form:"versionName"` + VersionCode *string `json:"versionCode" form:"versionCode"` + request.PageInfo +} + +// ExportVersionRequest 导出版本请求结构体 +type ExportVersionRequest struct { + VersionName string `json:"versionName" binding:"required"` // 版本名称 + VersionCode string `json:"versionCode" binding:"required"` // 版本号 + Description string `json:"description"` // 版本描述 + MenuIds []uint `json:"menuIds"` // 选中的菜单ID列表 + ApiIds []uint `json:"apiIds"` // 选中的API ID列表 + DictIds []uint `json:"dictIds"` // 选中的字典ID列表 +} + +// ImportVersionRequest 导入版本请求结构体 +type ImportVersionRequest struct { + VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息 + ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu + ExportApi []system.SysApi `json:"apis"` // API数据,直接复用SysApi + ExportDictionary []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary +} + +// VersionInfo 版本信息结构体 +type VersionInfo struct { + Name string `json:"name" binding:"required"` // 版本名称 + Code string `json:"code" binding:"required"` // 版本号 + Description string `json:"description"` // 版本描述 + ExportTime string `json:"exportTime"` // 导出时间 +} diff --git a/server/model/system/response/sys_version.go b/server/model/system/response/sys_version.go new file mode 100644 index 000000000..b207ddc88 --- /dev/null +++ b/server/model/system/response/sys_version.go @@ -0,0 +1,14 @@ +package response + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" +) + +// ExportVersionResponse 导出版本响应结构体 +type ExportVersionResponse struct { + Version request.VersionInfo `json:"version"` // 版本信息 + Menus []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu + Apis []system.SysApi `json:"apis"` // API数据,直接复用SysApi + Dictionaries []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary +} diff --git a/server/model/system/sys_version.go b/server/model/system/sys_version.go new file mode 100644 index 000000000..e7c0a82cc --- /dev/null +++ b/server/model/system/sys_version.go @@ -0,0 +1,20 @@ +// 自动生成模板SysVersion +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" +) + +// 版本管理 结构体 SysVersion +type SysVersion struct { + global.GVA_MODEL + VersionName *string `json:"versionName" form:"versionName" gorm:"comment:版本名称;column:version_name;size:255;" binding:"required"` //版本名称 + VersionCode *string `json:"versionCode" form:"versionCode" gorm:"comment:版本号;column:version_code;size:100;" binding:"required"` //版本号 + Description *string `json:"description" form:"description" gorm:"comment:版本描述;column:description;size:500;"` //版本描述 + VersionData *string `json:"versionData" form:"versionData" gorm:"comment:版本数据JSON;column:version_data;type:text;"` //版本数据 +} + +// TableName 版本管理 SysVersion自定义表名 sys_versions +func (SysVersion) TableName() string { + return "sys_versions" +} diff --git a/server/resource/package/server/service/service.go.tpl b/server/resource/package/server/service/service.go.tpl index 8226d189f..acd57a3f3 100644 --- a/server/resource/package/server/service/service.go.tpl +++ b/server/resource/package/server/service/service.go.tpl @@ -161,8 +161,8 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoLis var OrderStr string orderMap := make(map[string]bool) {{- if .GvaModel }} - orderMap["ID"] = true - orderMap["CreatedAt"] = true + orderMap["id"] = true + orderMap["created_at"] = true {{- end }} {{- range .Fields}} {{- if .Sort}} diff --git a/server/resource/package/web/view/table.vue.tpl b/server/resource/package/web/view/table.vue.tpl index d73e97aa2..e1ad9c133 100644 --- a/server/resource/package/web/view/table.vue.tpl +++ b/server/resource/package/web/view/table.vue.tpl @@ -1,7 +1,5 @@ {{- $global := . }} -{{- $top := . -}} {{- $templateID := printf "%s_%s" .Package .StructName }} - {{- if .IsAdd }} // 请在搜索条件中增加如下代码 @@ -35,12 +33,84 @@ {{- end }} {{- end }} +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// setOptions方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构(变量处和关闭表单处)增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateDefaultFormValue . }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + + + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref({}) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} + +{{- else }} + +{{- if not .OnlyTemplate}} + + + -{{- end}} - {{- end }} + +{{- end }} \ No newline at end of file diff --git a/server/resource/plugin/server/service/service.go.tpl b/server/resource/plugin/server/service/service.go.tpl index 4bdcea303..9743602ce 100644 --- a/server/resource/plugin/server/service/service.go.tpl +++ b/server/resource/plugin/server/service/service.go.tpl @@ -168,8 +168,8 @@ func (s *{{.Abbreviation}}) Get{{.StructName}}InfoList(ctx context.Context, info var OrderStr string orderMap := make(map[string]bool) {{- if .GvaModel }} - orderMap["ID"] = true - orderMap["CreatedAt"] = true + orderMap["id"] = true + orderMap["created_at"] = true {{- end }} {{- range .Fields}} {{- if .Sort}} diff --git a/server/resource/plugin/web/view/view.vue.tpl b/server/resource/plugin/web/view/view.vue.tpl index 6880840a7..98b557a55 100644 --- a/server/resource/plugin/web/view/view.vue.tpl +++ b/server/resource/plugin/web/view/view.vue.tpl @@ -108,7 +108,7 @@ getDataSourceFunc() {{ if .GvaModel }} - + {{ end }} @@ -229,7 +229,7 @@ getDataSourceFunc() {{- if .IsTree }} { const sortMap = { - CreatedAt:"CreatedAt", - ID:"ID", + CreatedAt:"created_at", + ID:"id", {{- range .Fields}} {{- if .Table}} {{- if and .Sort}} @@ -624,7 +624,7 @@ const enterDialog = async () => { }) } -const detailFrom = ref({}) +const detailForm = ref({}) // 查看详情控制标记 const detailShow = ref(false) @@ -641,7 +641,7 @@ const getDetails = async (row) => { // 打开弹窗 const res = await find{{.StructName}}({ {{.PrimaryField.FieldJson}}: row.{{.PrimaryField.FieldJson}} }) if (res.code === 0) { - detailFrom.value = res.data + detailForm.value = res.data openDetailShow() } } @@ -650,7 +650,7 @@ const getDetails = async (row) => { // 关闭详情弹窗 const closeDetailShow = () => { detailShow.value = false - detailFrom.value = {} + detailForm.value = {} } diff --git a/server/router/system/enter.go b/server/router/system/enter.go index 7127d9e9f..a0a23f54a 100644 --- a/server/router/system/enter.go +++ b/server/router/system/enter.go @@ -19,6 +19,7 @@ type RouterGroup struct { AuthorityBtnRouter SysExportTemplateRouter SysParamsRouter + SysVersionRouter } var ( @@ -41,4 +42,5 @@ var ( dictionaryDetailApi = api.ApiGroupApp.SystemApiGroup.DictionaryDetailApi autoCodeTemplateApi = api.ApiGroupApp.SystemApiGroup.AutoCodeTemplateApi exportTemplateApi = api.ApiGroupApp.SystemApiGroup.SysExportTemplateApi + sysVersionApi = api.ApiGroupApp.SystemApiGroup.SysVersionApi ) diff --git a/server/router/system/sys_user.go b/server/router/system/sys_user.go index 6c14b8200..0e076f772 100644 --- a/server/router/system/sys_user.go +++ b/server/router/system/sys_user.go @@ -18,7 +18,7 @@ func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) { userRouter.PUT("setUserInfo", baseApi.SetUserInfo) // 设置用户信息 userRouter.PUT("setSelfInfo", baseApi.SetSelfInfo) // 设置自身信息 userRouter.POST("setUserAuthorities", baseApi.SetUserAuthorities) // 设置用户权限组 - userRouter.POST("resetPassword", baseApi.ResetPassword) // 设置用户权限组 + userRouter.POST("resetPassword", baseApi.ResetPassword) // 重置用户密码 userRouter.PUT("setSelfSetting", baseApi.SetSelfSetting) // 用户界面配置 } { diff --git a/server/router/system/sys_version.go b/server/router/system/sys_version.go new file mode 100644 index 000000000..249feb8e6 --- /dev/null +++ b/server/router/system/sys_version.go @@ -0,0 +1,25 @@ +package system + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/middleware" + "github.com/gin-gonic/gin" +) + +type SysVersionRouter struct{} + +// InitSysVersionRouter 初始化 版本管理 路由信息 +func (s *SysVersionRouter) InitSysVersionRouter(Router *gin.RouterGroup) { + sysVersionRouter := Router.Group("sysVersion").Use(middleware.OperationRecord()) + sysVersionRouterWithoutRecord := Router.Group("sysVersion") + { + sysVersionRouter.DELETE("deleteSysVersion", sysVersionApi.DeleteSysVersion) // 删除版本管理 + sysVersionRouter.DELETE("deleteSysVersionByIds", sysVersionApi.DeleteSysVersionByIds) // 批量删除版本管理 + sysVersionRouter.POST("exportVersion", sysVersionApi.ExportVersion) // 导出版本数据 + sysVersionRouter.POST("importVersion", sysVersionApi.ImportVersion) // 导入版本数据 + } + { + sysVersionRouterWithoutRecord.GET("findSysVersion", sysVersionApi.FindSysVersion) // 根据ID获取版本管理 + sysVersionRouterWithoutRecord.GET("getSysVersionList", sysVersionApi.GetSysVersionList) // 获取版本管理列表 + sysVersionRouterWithoutRecord.GET("downloadVersionJson", sysVersionApi.DownloadVersionJson) // 下载版本JSON数据 + } +} diff --git a/server/service/system/auto_code_package.go b/server/service/system/auto_code_package.go index 681904b1d..13f8ac904 100644 --- a/server/service/system/auto_code_package.go +++ b/server/service/system/auto_code_package.go @@ -117,6 +117,20 @@ func (s *autoCodePackage) Delete(ctx context.Context, info common.GetById) error return nil } +// DeleteByNames +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) DeleteByNames(ctx context.Context, names []string) error { + if len(names) == 0 { + return nil + } + err := global.GVA_DB.WithContext(ctx).Where("package_name IN ?", names).Delete(&model.SysAutoCodePackage{}).Error + if err != nil { + return errors.Wrap(err, "删除失败!") + } + return nil +} + // All 获取所有包 // @author: [piexlmax](https://github.com/piexlmax) // @author: [SliverHorn](https://github.com/SliverHorn) diff --git a/server/service/system/auto_code_plugin.go b/server/service/system/auto_code_plugin.go index 618ee0180..b522c1e45 100644 --- a/server/service/system/auto_code_plugin.go +++ b/server/service/system/auto_code_plugin.go @@ -170,7 +170,7 @@ func (s *autoCodePlugin) PubPlug(plugName string) (zipPath string, err error) { // (compression is not required; you could use Tar directly) format := archives.CompressedArchive{ //Compression: archives.Gz{}, - Archival: archives.Zip{}, + Archival: archives.Zip{}, } // create the archive @@ -208,7 +208,8 @@ func (s *autoCodePlugin) InitMenu(menuInfo request.InitMenu) (err error) { }, } - err = global.GVA_DB.Find(&menus, "id in (?)", menuInfo.Menus).Error + // 查询菜单及其关联的参数和按钮 + err = global.GVA_DB.Preload("Parameters").Preload("MenuBtn").Find(&menus, "id in (?)", menuInfo.Menus).Error if err != nil { return err } diff --git a/server/service/system/enter.go b/server/service/system/enter.go index 634cd0017..d91f2796d 100644 --- a/server/service/system/enter.go +++ b/server/service/system/enter.go @@ -17,6 +17,7 @@ type ServiceGroup struct { AuthorityBtnService SysExportTemplateService SysParamsService + SysVersionService AutoCodePlugin autoCodePlugin AutoCodePackage autoCodePackage AutoCodeHistory autoCodeHistory diff --git a/server/service/system/sys_auto_code_oracle.go b/server/service/system/sys_auto_code_oracle.go index 3cdb362e9..fc3c43fb3 100644 --- a/server/service/system/sys_auto_code_oracle.go +++ b/server/service/system/sys_auto_code_oracle.go @@ -64,7 +64,7 @@ WHERE lower(a.table_name) = ? AND lower(a.OWNER) = ? ORDER BY - a.COLUMN_ID; + a.COLUMN_ID ` err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error diff --git a/server/service/system/sys_menu.go b/server/service/system/sys_menu.go index 4fff0a829..a8fba2a96 100644 --- a/server/service/system/sys_menu.go +++ b/server/service/system/sys_menu.go @@ -139,10 +139,52 @@ func (menuService *MenuService) getBaseChildrenList(menu *system.SysBaseMenu, tr //@return: error func (menuService *MenuService) AddBaseMenu(menu system.SysBaseMenu) error { - if !errors.Is(global.GVA_DB.Where("name = ?", menu.Name).First(&system.SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { - return errors.New("存在重复name,请修改name") - } - return global.GVA_DB.Create(&menu).Error + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 检查name是否重复 + if !errors.Is(tx.Where("name = ?", menu.Name).First(&system.SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在重复name,请修改name") + } + + if menu.ParentId != 0 { + // 检查父菜单是否存在 + var parentMenu system.SysBaseMenu + if err := tx.First(&parentMenu, menu.ParentId).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("父菜单不存在") + } + return err + } + + // 检查父菜单下现有子菜单数量 + var existingChildrenCount int64 + err := tx.Model(&system.SysBaseMenu{}).Where("parent_id = ?", menu.ParentId).Count(&existingChildrenCount).Error + if err != nil { + return err + } + + // 如果父菜单原本是叶子菜单(没有子菜单),现在要变成枝干菜单,需要清空其权限分配 + if existingChildrenCount == 0 { + // 检查父菜单是否被其他角色设置为首页 + var defaultRouterCount int64 + err := tx.Model(&system.SysAuthority{}).Where("default_router = ?", parentMenu.Name).Count(&defaultRouterCount).Error + if err != nil { + return err + } + if defaultRouterCount > 0 { + return errors.New("父菜单已被其他角色的首页占用,请先释放父菜单的首页权限") + } + + // 清空父菜单的所有权限分配 + err = tx.Where("sys_base_menu_id = ?", menu.ParentId).Delete(&system.SysAuthorityMenu{}).Error + if err != nil { + return err + } + } + } + + // 创建菜单 + return tx.Create(&menu).Error + }) } //@author: [piexlmax](https://github.com/piexlmax) diff --git a/server/service/system/sys_user.go b/server/service/system/sys_user.go index 8a831921b..66bbe72aa 100644 --- a/server/service/system/sys_user.go +++ b/server/service/system/sys_user.go @@ -3,9 +3,10 @@ package system import ( "errors" "fmt" + "time" + "github.com/flipped-aurora/gin-vue-admin/server/model/common" systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" - "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system" @@ -63,20 +64,20 @@ func (userService *UserService) Login(u *system.SysUser) (userInter *system.SysU //@function: ChangePassword //@description: 修改用户密码 //@param: u *model.SysUser, newPassword string -//@return: userInter *model.SysUser,err error +//@return: err error -func (userService *UserService) ChangePassword(u *system.SysUser, newPassword string) (userInter *system.SysUser, err error) { +func (userService *UserService) ChangePassword(u *system.SysUser, newPassword string) (err error) { var user system.SysUser - if err = global.GVA_DB.Where("id = ?", u.ID).First(&user).Error; err != nil { - return nil, err + err = global.GVA_DB.Select("id, password").Where("id = ?", u.ID).First(&user).Error + if err != nil { + return err } if ok := utils.BcryptCheck(u.Password, user.Password); !ok { - return nil, errors.New(global.Translate("sys_auto_code.originalPasswordError")) + return errors.New(global.Translate("sys_auto_code.originalPasswordError")) } - user.Password = utils.BcryptHash(newPassword) - err = global.GVA_DB.Save(&user).Error - return &user, err - + pwd := utils.BcryptHash(newPassword) + err = global.GVA_DB.Model(&user).Update("password", pwd).Error + return err } //@author: [piexlmax](https://github.com/piexlmax) diff --git a/server/service/system/sys_version.go b/server/service/system/sys_version.go new file mode 100644 index 000000000..68d5ae2dd --- /dev/null +++ b/server/service/system/sys_version.go @@ -0,0 +1,230 @@ +package system + +import ( + "context" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "gorm.io/gorm" +) + +type SysVersionService struct{} + +// CreateSysVersion 创建版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) CreateSysVersion(ctx context.Context, sysVersion *system.SysVersion) (err error) { + err = global.GVA_DB.Create(sysVersion).Error + return err +} + +// DeleteSysVersion 删除版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) DeleteSysVersion(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.Delete(&system.SysVersion{}, "id = ?", ID).Error + return err +} + +// DeleteSysVersionByIds 批量删除版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) DeleteSysVersionByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.Where("id in ?", IDs).Delete(&system.SysVersion{}).Error + return err +} + +// GetSysVersion 根据ID获取版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) GetSysVersion(ctx context.Context, ID string) (sysVersion system.SysVersion, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&sysVersion).Error + return +} + +// GetSysVersionInfoList 分页获取版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) GetSysVersionInfoList(ctx context.Context, info systemReq.SysVersionSearch) (list []system.SysVersion, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysVersion{}) + var sysVersions []system.SysVersion + // 如果有条件搜索 下方会自动创建搜索语句 + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) + } + + if info.VersionName != nil && *info.VersionName != "" { + db = db.Where("version_name LIKE ?", "%"+*info.VersionName+"%") + } + if info.VersionCode != nil && *info.VersionCode != "" { + db = db.Where("version_code = ?", *info.VersionCode) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysVersions).Error + return sysVersions, total, err +} +func (sysVersionService *SysVersionService) GetSysVersionPublic(ctx context.Context) { + // 此方法为获取数据源定义的数据 + // 请自行实现 +} + +// GetMenusByIds 根据ID列表获取菜单数据 +func (sysVersionService *SysVersionService) GetMenusByIds(ctx context.Context, ids []uint) (menus []system.SysBaseMenu, err error) { + err = global.GVA_DB.Where("id in ?", ids).Preload("Parameters").Preload("MenuBtn").Find(&menus).Error + return +} + +// GetApisByIds 根据ID列表获取API数据 +func (sysVersionService *SysVersionService) GetApisByIds(ctx context.Context, ids []uint) (apis []system.SysApi, err error) { + err = global.GVA_DB.Where("id in ?", ids).Find(&apis).Error + return +} + +// GetDictionariesByIds 根据ID列表获取字典数据 +func (sysVersionService *SysVersionService) GetDictionariesByIds(ctx context.Context, ids []uint) (dictionaries []system.SysDictionary, err error) { + err = global.GVA_DB.Where("id in ?", ids).Preload("SysDictionaryDetails").Find(&dictionaries).Error + return +} + +// ImportMenus 导入菜单数据 +func (sysVersionService *SysVersionService) ImportMenus(ctx context.Context, menus []system.SysBaseMenu) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 递归创建菜单 + return sysVersionService.createMenusRecursively(tx, menus, 0) + }) +} + +// createMenusRecursively 递归创建菜单 +func (sysVersionService *SysVersionService) createMenusRecursively(tx *gorm.DB, menus []system.SysBaseMenu, parentId uint) error { + for _, menu := range menus { + // 检查菜单是否已存在 + var existingMenu system.SysBaseMenu + if err := tx.Where("name = ? AND path = ?", menu.Name, menu.Path).First(&existingMenu).Error; err == nil { + // 菜单已存在,使用现有菜单ID继续处理子菜单 + if len(menu.Children) > 0 { + if err := sysVersionService.createMenusRecursively(tx, menu.Children, existingMenu.ID); err != nil { + return err + } + } + continue + } + + // 保存参数和按钮数据,稍后处理 + parameters := menu.Parameters + menuBtns := menu.MenuBtn + children := menu.Children + + // 创建新菜单(不包含关联数据) + newMenu := system.SysBaseMenu{ + ParentId: parentId, + Path: menu.Path, + Name: menu.Name, + Hidden: menu.Hidden, + Component: menu.Component, + Sort: menu.Sort, + Meta: menu.Meta, + } + + if err := tx.Create(&newMenu).Error; err != nil { + return err + } + + // 创建参数 + if len(parameters) > 0 { + for _, param := range parameters { + newParam := system.SysBaseMenuParameter{ + SysBaseMenuID: newMenu.ID, + Type: param.Type, + Key: param.Key, + Value: param.Value, + } + if err := tx.Create(&newParam).Error; err != nil { + return err + } + } + } + + // 创建菜单按钮 + if len(menuBtns) > 0 { + for _, btn := range menuBtns { + newBtn := system.SysBaseMenuBtn{ + SysBaseMenuID: newMenu.ID, + Name: btn.Name, + Desc: btn.Desc, + } + if err := tx.Create(&newBtn).Error; err != nil { + return err + } + } + } + + // 递归处理子菜单 + if len(children) > 0 { + if err := sysVersionService.createMenusRecursively(tx, children, newMenu.ID); err != nil { + return err + } + } + } + return nil +} + +// ImportApis 导入API数据 +func (sysVersionService *SysVersionService) ImportApis(apis []system.SysApi) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, api := range apis { + // 检查API是否已存在 + var existingApi system.SysApi + if err := tx.Where("path = ? AND method = ?", api.Path, api.Method).First(&existingApi).Error; err == nil { + // API已存在,跳过 + continue + } + + // 创建新API + newApi := system.SysApi{ + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + } + + if err := tx.Create(&newApi).Error; err != nil { + return err + } + } + return nil + }) +} + +// ImportDictionaries 导入字典数据 +func (sysVersionService *SysVersionService) ImportDictionaries(dictionaries []system.SysDictionary) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, dict := range dictionaries { + // 检查字典是否已存在 + var existingDict system.SysDictionary + if err := tx.Where("type = ?", dict.Type).First(&existingDict).Error; err == nil { + // 字典已存在,跳过 + continue + } + + // 创建新字典 + newDict := system.SysDictionary{ + Name: dict.Name, + Type: dict.Type, + Status: dict.Status, + Desc: dict.Desc, + SysDictionaryDetails: dict.SysDictionaryDetails, + } + + if err := tx.Create(&newDict).Error; err != nil { + return err + } + } + return nil + }) +} diff --git a/server/source/system/api.go b/server/source/system/api.go index cf20017a4..5a1426f97 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -191,6 +191,14 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "system.api.group.mediaLibraryCategories", Method: "GET", Path: "/attachmentCategory/getCategoryList", Description: "system.api.desc.categoryList"}, {ApiGroup: "system.api.group.mediaLibraryCategories", Method: "POST", Path: "/attachmentCategory/addCategory", Description: "system.api.desc.addEditCategory"}, {ApiGroup: "system.api.group.mediaLibraryCategories", Method: "POST", Path: "/attachmentCategory/deleteCategory", Description: "system.api.desc.deleteCategory"}, + + {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/findSysVersion", Description: "获取单一版本"}, + {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/getSysVersionList", Description: "获取版本列表"}, + {ApiGroup: "版本控制", Method: "GET", Path: "/sysVersion/downloadVersionJson", Description: "下载版本json"}, + {ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/exportVersion", Description: "创建版本"}, + {ApiGroup: "版本控制", Method: "POST", Path: "/sysVersion/importVersion", Description: "同步版本"}, + {ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersion", Description: "删除版本"}, + {ApiGroup: "版本控制", Method: "DELETE", Path: "/sysVersion/deleteSysVersionByIds", Description: "批量删除版本"}, } if err := db.Create(&entities).Error; err != nil { return ctx, errors.Wrap(err, sysModel.SysApi{}.TableName()+" "+global.Translate("general.tabelDataInitFail")) diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go index 4e37c1474..94e1a24c0 100644 --- a/server/source/system/casbin.go +++ b/server/source/system/casbin.go @@ -192,6 +192,14 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/attachmentCategory/addCategory", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/attachmentCategory/deleteCategory", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/findSysVersion", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/getSysVersionList", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/downloadVersionJson", V2: "GET"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/exportVersion", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/importVersion", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersion", V2: "DELETE"}, + {Ptype: "p", V0: "888", V1: "/sysVersion/deleteSysVersionByIds", V2: "DELETE"}, + {Ptype: "p", V0: "8881", V1: "/user/admin_register", V2: "POST"}, {Ptype: "p", V0: "8881", V1: "/api/createApi", V2: "POST"}, {Ptype: "p", V0: "8881", V1: "/api/getApiList", V2: "POST"}, diff --git a/server/source/system/dictionary_detail.go b/server/source/system/dictionary_detail.go index 17494fc3f..60df94160 100644 --- a/server/source/system/dictionary_detail.go +++ b/server/source/system/dictionary_detail.go @@ -67,7 +67,7 @@ func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, e } dicts[2].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ - {Label: "date", Status: &True}, + {Label: "date", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, {Label: "time", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, {Label: "year", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, {Label: "datetime", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, @@ -75,7 +75,7 @@ func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, e {Label: "timestamptz", Value: "6", Status: &True, Extend: "pgsql", Sort: 5}, } dicts[3].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ - {Label: "float", Status: &True}, + {Label: "float", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, {Label: "double", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, {Label: "decimal", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, {Label: "numeric", Value: "3", Status: &True, Extend: "pgsql", Sort: 3}, @@ -83,7 +83,7 @@ func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, e } dicts[4].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ - {Label: "char", Status: &True}, + {Label: "char", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, {Label: "varchar", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, {Label: "tinyblob", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, {Label: "tinytext", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, diff --git a/server/source/system/menu.go b/server/source/system/menu.go index e8f024ed1..f228516d7 100644 --- a/server/source/system/menu.go +++ b/server/source/system/menu.go @@ -102,6 +102,7 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "picture", Name: "picture", Component: "view/systemTools/autoCode/picture.vue", Sort: 6, Meta: Meta{Title: "AI页面绘制", Icon: "picture-filled"}}, {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["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: "system.menu.pluginMarket", Icon: "shop"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "system.menu.pluginInstall", Icon: "box"}}, diff --git a/server/source/system/user.go b/server/source/system/user.go index 567b03766..c73aaa1c4 100644 --- a/server/source/system/user.go +++ b/server/source/system/user.go @@ -72,7 +72,7 @@ func (i *initUser) InitializeData(ctx context.Context) (next context.Context, er Username: "a303176530", Password: password, NickName: "用户1", - HeaderImg: "https:///qmplusimg.henrongyi.top/1572075907logo.png", + HeaderImg: "https://qmplusimg.henrongyi.top/1572075907logo.png", AuthorityId: 9528, Phone: "17611111111", Email: "333333333@qq.com"}, diff --git a/server/utils/ast/ast.go b/server/utils/ast/ast.go index 1cdfa4c1e..a2636d237 100644 --- a/server/utils/ast/ast.go +++ b/server/utils/ast/ast.go @@ -121,6 +121,81 @@ func CreateMenuStructAst(menus []system.SysBaseMenu) *[]ast.Expr { }, }, } + + // 添加菜单参数 + if len(menus[i].Parameters) > 0 { + var paramElts []ast.Expr + for _, param := range menus[i].Parameters { + paramElts = append(paramElts, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuParameter"}, + }, + Elts: []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Type"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Type)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Key"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Key)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Value"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", param.Value)}, + }, + }, + }) + } + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Parameters"}, + Value: &ast.CompositeLit{ + Type: &ast.ArrayType{ + Elt: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuParameter"}, + }, + }, + Elts: paramElts, + }, + }) + } + + // 添加菜单按钮 + if len(menus[i].MenuBtn) > 0 { + var btnElts []ast.Expr + for _, btn := range menus[i].MenuBtn { + btnElts = append(btnElts, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuBtn"}, + }, + Elts: []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Name"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", btn.Name)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Desc"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", btn.Desc)}, + }, + }, + }) + } + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "MenuBtn"}, + Value: &ast.CompositeLit{ + Type: &ast.ArrayType{ + Elt: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysBaseMenuBtn"}, + }, + }, + Elts: btnElts, + }, + }) + } + menuElts = append(menuElts, &ast.CompositeLit{ Type: nil, Elts: elts, diff --git a/server/utils/autocode/template_funcs.go b/server/utils/autocode/template_funcs.go index 28e347d7d..40e561345 100644 --- a/server/utils/autocode/template_funcs.go +++ b/server/utils/autocode/template_funcs.go @@ -46,7 +46,8 @@ func GenerateField(field systemReq.AutoCodeField) string { gormTag += "column:" + field.ColumnName + ";" - if field.DataTypeLong != "" && field.FieldType != "enum" { + // 对于int类型,根据DataTypeLong决定具体的Go类型,不使用size标签 + if field.DataTypeLong != "" && field.FieldType != "enum" && field.FieldType != "int" { gormTag += fmt.Sprintf("size:%s;", field.DataTypeLong) } @@ -70,7 +71,7 @@ func GenerateField(field systemReq.AutoCodeField) string { result = fmt.Sprintf(`%s datatypes.JSON `+"`"+`%s swaggertype:"array,object"`+"`"+``, field.FieldName, tagContent) case "richtext": - tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s`, field.FieldJson, field.FieldJson, gormTag) result = fmt.Sprintf(`%s *string `+"`"+`%stype:text;"`+"`"+``, @@ -85,8 +86,27 @@ func GenerateField(field systemReq.AutoCodeField) string { tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, field.FieldJson, field.FieldJson, gormTag) + // 对于int类型,根据DataTypeLong决定具体的Go类型 + var fieldType string + if field.FieldType == "int" { + switch field.DataTypeLong { + case "1", "2", "3": + fieldType = "int8" + case "4", "5": + fieldType = "int16" + case "6", "7", "8", "9", "10": + fieldType = "int32" + case "11", "12", "13", "14", "15", "16", "17", "18", "19", "20": + fieldType = "int64" + default: + fieldType = "int64" + } + } else { + fieldType = field.FieldType + } + result = fmt.Sprintf(`%s *%s `+"`"+`%s`+"`"+``, - field.FieldName, field.FieldType, tagContent) + field.FieldName, fieldType, tagContent) } if field.Require { @@ -222,11 +242,11 @@ func GenerateSearchFormItem(field systemReq.AutoCodeField) string { ` } else if field.FieldType == "float64" || field.FieldType == "int" { if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { - result += fmt.Sprintf(` + result += fmt.Sprintf(` `, field.FieldName) result += ` — ` - result += fmt.Sprintf(` + result += fmt.Sprintf(` `, field.FieldName) } else { result += fmt.Sprintf(` @@ -250,7 +270,7 @@ func GenerateSearchFormItem(field systemReq.AutoCodeField) string { ` result += ` ` - result += fmt.Sprintf(``, field.FieldJson) + result += fmt.Sprintf(``, field.FieldJson) } else { result += fmt.Sprintf(``, field.FieldJson) } @@ -575,7 +595,7 @@ func GenerateDescriptionItem(field systemReq.AutoCodeField) string { result += ` + + + + diff --git a/web/src/view/layout/setting/components/settingItem.vue b/web/src/view/layout/setting/components/settingItem.vue new file mode 100644 index 000000000..bc5d7d84f --- /dev/null +++ b/web/src/view/layout/setting/components/settingItem.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/web/src/view/layout/setting/components/themeColorPicker.vue b/web/src/view/layout/setting/components/themeColorPicker.vue new file mode 100644 index 000000000..4523c923d --- /dev/null +++ b/web/src/view/layout/setting/components/themeColorPicker.vue @@ -0,0 +1,152 @@ + + + + + diff --git a/web/src/view/layout/setting/components/themeModeSelector.vue b/web/src/view/layout/setting/components/themeModeSelector.vue new file mode 100644 index 000000000..72eb4e8c4 --- /dev/null +++ b/web/src/view/layout/setting/components/themeModeSelector.vue @@ -0,0 +1,70 @@ + + + diff --git a/web/src/view/layout/setting/index.vue b/web/src/view/layout/setting/index.vue index b3d52e329..e8cee321b 100644 --- a/web/src/view/layout/setting/index.vue +++ b/web/src/view/layout/setting/index.vue @@ -5,127 +5,50 @@ direction="rtl" :size="width" :show-close="false" + class="theme-config-drawer" > -
-
- -
- -
-
-
- -
-
- -