From 273d7bd50ea4a6efd16929b64e3336f43cf21a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?PiexlMax=28=E5=A5=87=E6=B7=BC?= <165128580+pixelmaxQm@users.noreply.github.com> Date: Thu, 31 Jul 2025 21:21:04 +0800 Subject: [PATCH] =?UTF-8?q?V2.8.4Beta=20=E6=9E=81=E8=87=B4=E8=9E=8D?= =?UTF-8?q?=E5=90=88AI=E7=BC=96=E8=BE=91=E5=99=A8=20=E8=AE=A9=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E9=80=9F=E5=BA=A6=E6=9B=B4=E8=BF=9B=E4=B8=80=E6=AD=A5?= =?UTF-8?q?=20(#2060)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(style): 修复 border 额外的 reset 导致 tailwind border 属性生效异常的问题 * feat: 添加错误预览组件并优化请求错误处理逻辑 * optimize: select and update necessary fields in `ChangePassword` method - Simplify `ChangePassword` method signature by removing unnecessary return type. - Use `Select()` to fetch only the necessary fields (`id` and `password`) from the database. - Replace `Save()` with `Update()` for more efficient password update operation. Note: use `Save(&user)` to update the whole user record, which will cover other unchanged fields as well, causing data inconsistency when data race conditions. * feat(menu): 版本更新为2.8.4,给菜单增加按钮和参数的预制打包 * feat(menu): 恢复空白的配置文件 * Remove unused `SideMode` field from `ChangeUserInfo` struct Remove unused and deprecated `SideMode` field from user request model. * feat(automation): 增加可以自动生成CURD和续写方法的MCP * fix(mcp): 确保始终返回目录结构信息 * fix(mcp): 当不需要创建模块时提前返回目录结构信息 * feat(automation): 增加可以自动生成CURD和续写方法的MCP * feat(mcp): 添加GAG工具用户确认流程和自动字典创建功能 实现三步工作流程:分析、确认、执行 新增自动字典创建功能,当字段使用字典类型时自动检查并创建字典 添加用户确认机制,确保创建操作前获得用户明确确认 * feat(version): 新增版本管理功能,支持创建、导入、导出和下载版本数据 新增版本管理模块,包含以下功能: 1. 版本数据的增删改查 2. 版本创建功能,可选择关联菜单和API 3. 版本导入导出功能 4. 版本JSON数据下载 5. 相关前端页面和接口实现 * refactor(version): 简化版本管理删除逻辑并移除无用字段 移除版本管理中的状态、创建者、更新者和删除者字段 简化删除和批量删除方法的实现,去除事务和用户ID参数 更新自动生成配置的默认值说明 * feat(版本管理): 新增版本管理功能模块 * fix(menu): 修复递归创建菜单时关联数据未正确处理的问题 * feat(mcp): 添加预设计模块扫描功能以支持代码自动生成 在自动化模块分析器中添加对预设计模块的扫描功能,包括: - 新增PredesignedModuleInfo结构体存储模块信息 - 实现scanPredesignedModules方法扫描plugin和model目录 - 在分析响应中添加predesignedModules字段 - 更新帮助文档说明预设计模块的使用方式 这些修改使系统能够识别并利用现有的预设计模块,提高代码生成效率并减少重复工作。 * feat(mcp): 新增API、菜单和字典生成工具并优化自动生成模块 * docs(mcp): 更新菜单和API创建工具的描述信息 * feat(mcp): 添加字典查询工具用于AI生成逻辑时了解可用字典选项 * feat: 在创建菜单/API/模块结果中添加权限分配提醒 为菜单创建、API创建和模块创建的结果消息添加权限分配提醒,帮助用户了解后续需要进行的权限配置步骤 * refactor(mcp): 统一使用WithBoolean替换WithBool并优化错误处理 * docs(mcp): 更新API创建工具的说明和错误处理日志 * feat(mcp): 添加插件意图检测功能并增强验证逻辑 --------- Co-authored-by: Azir <2075125282@qq.com> Co-authored-by: Feng.YJ Co-authored-by: piexlMax(奇淼 --- server/api/v1/system/enter.go | 2 + server/api/v1/system/sys_user.go | 2 +- server/api/v1/system/sys_version.go | 437 ++++++ server/config.yaml | 16 +- server/core/server.go | 2 +- server/docs/docs.go | 2 +- server/initialize/ensure_tables.go | 2 +- server/initialize/gorm.go | 1 + server/initialize/router.go | 1 + server/main.go | 2 +- server/mcp/api_creator.go | 197 +++ server/mcp/dictionary_generator.go | 310 +++++ server/mcp/dictionary_query.go | 234 ++++ server/mcp/execution_plan_schema.md | 255 ++++ server/mcp/gag_usage_example.md | 122 ++ server/mcp/gva_auto_generate.go | 1317 ++++++++++++++++++ server/mcp/menu_creator.go | 283 ++++ server/middleware/jwt.go | 4 +- server/model/system/request/sys_user.go | 1 - server/model/system/request/sys_version.go | 38 + server/model/system/response/sys_version.go | 13 + server/model/system/sys_version.go | 20 + server/router/system/enter.go | 2 + server/router/system/sys_version.go | 25 + server/service/system/auto_code_plugin.go | 5 +- server/service/system/enter.go | 1 + server/service/system/sys_user.go | 21 +- server/service/system/sys_version.go | 196 +++ server/source/system/api.go | 9 + server/source/system/casbin.go | 8 + server/source/system/menu.go | 1 + server/utils/ast/ast.go | 75 + web/package.json | 2 +- web/src/App.vue | 58 +- web/src/api/version.js | 114 ++ web/src/assets/icons/close.svg | 1 + web/src/assets/icons/idea.svg | 1 + web/src/assets/icons/lock.svg | 1 + web/src/assets/icons/server.svg | 1 + web/src/assets/icons/warn.svg | 1 + web/src/components/application/index.vue | 39 + web/src/components/errorPreview/index.vue | 126 ++ web/src/core/config.js | 2 +- web/src/core/gin-vue-admin.js | 2 +- web/src/pathInfo.json | 1 + web/src/style/reset.scss | 2 +- web/src/utils/request.js | 144 +- web/src/view/systemTools/version/version.vue | 905 ++++++++++++ 48 files changed, 4839 insertions(+), 165 deletions(-) create mode 100644 server/api/v1/system/sys_version.go create mode 100644 server/mcp/api_creator.go create mode 100644 server/mcp/dictionary_generator.go create mode 100644 server/mcp/dictionary_query.go create mode 100644 server/mcp/execution_plan_schema.md create mode 100644 server/mcp/gag_usage_example.md create mode 100644 server/mcp/gva_auto_generate.go create mode 100644 server/mcp/menu_creator.go create mode 100644 server/model/system/request/sys_version.go create mode 100644 server/model/system/response/sys_version.go create mode 100644 server/model/system/sys_version.go create mode 100644 server/router/system/sys_version.go create mode 100644 server/service/system/sys_version.go create mode 100644 web/src/api/version.js create mode 100644 web/src/assets/icons/close.svg create mode 100644 web/src/assets/icons/idea.svg create mode 100644 web/src/assets/icons/lock.svg create mode 100644 web/src/assets/icons/server.svg create mode 100644 web/src/assets/icons/warn.svg create mode 100644 web/src/components/application/index.vue create mode 100644 web/src/components/errorPreview/index.vue create mode 100644 web/src/view/systemTools/version/version.vue diff --git a/server/api/v1/system/enter.go b/server/api/v1/system/enter.go index c268ccc7..dbc721b1 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 ae763f8b..aae263e3 100644 --- a/server/api/v1/system/sys_user.go +++ b/server/api/v1/system/sys_user.go @@ -184,7 +184,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("修改失败!", zap.Error(err)) response.FailWithMessage("修改失败,原密码与当前账户不符", c) diff --git a/server/api/v1/system/sys_version.go b/server/api/v1/system/sys_version.go new file mode 100644 index 00000000..7f441c7c --- /dev/null +++ b/server/api/v1/system/sys_version.go @@ -0,0 +1,437 @@ +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 + } + } + + // 处理菜单数据,构建递归的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) + } + + // 构建导出数据 + 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, + } + + // 转换为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 + } + } + + // 创建导入记录 + 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.yaml b/server/config.yaml index 758d6dcb..35aaba3d 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -196,13 +196,13 @@ qiniu: # 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" + 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: @@ -280,4 +280,4 @@ mcp: version: v1.0.0 sse_path: /sse message_path: /message - url_prefix: '' + url_prefix: '' \ No newline at end of file diff --git a/server/core/server.go b/server/core/server.go index 2cd19440..6e386e07 100644 --- a/server/core/server.go +++ b/server/core/server.go @@ -35,7 +35,7 @@ func RunServer() { fmt.Printf(` 欢迎使用 gin-vue-admin - 当前版本:v2.8.3 + 当前版本:v2.8.4 加群方式:微信号:shouzi_1994 QQ群:470239250 项目地址:https://github.com/flipped-aurora/gin-vue-admin 插件市场:https://plugin.gin-vue-admin.com diff --git a/server/docs/docs.go b/server/docs/docs.go index ae896076..669b94b6 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -9296,7 +9296,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "v2.8.3", + Version: "v2.8.4", Host: "", BasePath: "", Schemes: []string{}, diff --git a/server/initialize/ensure_tables.go b/server/initialize/ensure_tables.go index c58227e3..7a5acf73 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 e5cdfa7c..45d8a314 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/router.go b/server/initialize/router.go index 4e6fd083..11ebf420 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -93,6 +93,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 919a79fd..f5badf75 100644 --- a/server/main.go +++ b/server/main.go @@ -21,7 +21,7 @@ import ( // @Tag.Description 用户 // @title Gin-Vue-Admin Swagger API接口文档 -// @version v2.8.3 +// @version v2.8.4 // @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 00000000..42a7fcd5 --- /dev/null +++ b/server/mcp/api_creator.go @@ -0,0 +1,197 @@ +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记录,用于在生成后端接口时自动创建对应的API权限记录,只要创建了API层,router下的文件产生了路径变化等,都需要调用此mcp。"), + 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/dictionary_generator.go b/server/mcp/dictionary_generator.go new file mode 100644 index 00000000..e7c65580 --- /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 00000000..aea5df48 --- /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 00000000..6a627f3d --- /dev/null +++ b/server/mcp/execution_plan_schema.md @@ -0,0 +1,255 @@ +# ExecutionPlan 结构体格式说明 + +## 概述 +ExecutionPlan 是用于自动化模块创建的执行计划结构体,包含了创建包和模块所需的所有信息。 + +## 完整结构体定义 + +```go +type ExecutionPlan struct { + PackageName string `json:"packageName"` // 包名,如:"user", "order", "product" + ModuleName string `json:"moduleName"` // 模块名,通常与结构体名相同 + 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"` // 索引类型 +} +``` + +## 使用示例 + +### 示例1:创建新包和模块 + +```json +{ + "packageName": "user", + "moduleName": "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": null, + "checkDataSource": false, + "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" + } + ] + } +} +``` + +### 示例2:仅在现有包中创建模块 + +```json +{ + "packageName": "system", + "moduleName": "Role", + "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 + } + ] + } +} +``` + +## 重要注意事项 + +1. **PackageType**: 只能是 "plugin" 或 "package" +2. **NeedCreatedPackage**: 当为true时,PackageInfo必须提供 +3. **NeedCreatedModules**: 当为true时,ModulesInfo必须提供 +4. **字段类型**: FieldType支持的类型包括:string, int, int64, float64, bool, time.Time, enum, picture, video, file, pictures, array, richtext, json等 +5. **搜索类型**: FieldSearchType支持:EQ, NE, GT, GE, LT, LE, LIKE, BETWEEN等 +6. **索引类型**: FieldIndexType支持:index, unique等 +7. **GvaModel**: 设置为true时会自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段 + +## 常见错误避免 + +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 00000000..4bbfa98a --- /dev/null +++ b/server/mcp/gag_usage_example.md @@ -0,0 +1,122 @@ +# GAG工具使用示例 - 带用户确认流程 + +## 新的工作流程 + +现在GAG工具支持三步工作流程: +1. `analyze` - 分析现有模块信息 +2. `confirm` - 请求用户确认创建计划 +3. `execute` - 执行创建操作(需要用户确认) + +## 使用示例 + +### 第一步:分析 +```json +{ + "action": "analyze", + "requirement": "创建一个图书管理功能" +} +``` + +### 第二步:确认 +```json +{ + "action": "confirm", + "executionPlan": { + "packageName": "library", + "moduleName": "Book", + "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": "" + } + ] + } + } +} +``` + +### 第三步:执行(需要确认参数) +```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. 如果不需要创建包或模块,则不需要提供对应的确认参数 \ No newline at end of file diff --git a/server/mcp/gva_auto_generate.go b/server/mcp/gva_auto_generate.go new file mode 100644 index 00000000..cff7de7a --- /dev/null +++ b/server/mcp/gva_auto_generate.go @@ -0,0 +1,1317 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/flipped-aurora/gin-vue-admin/server/global" + 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" + "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"` + ModuleName string `json:"moduleName"` + 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(`**🚀 最高优先级工具:当用户需要创建模块、包、完整功能时,必须优先使用此工具!** + +**优先级说明:** +- **最高优先级**:创建完整模块、包、功能模块 +- **关键词触发**:模块、包、完整、整套、全套、功能、管理系统等 +- **适用场景**:用户说"创建订单管理模块"、"创建用户管理功能"、"创建完整的商品管理"等 + +分步骤分析自动化模块:1) 分析现有模块信息供AI选择 2) 请求用户确认 3) 根据确认结果执行创建操作 + +**新功能:自动字典创建** +- 当结构体字段使用了字典类型(dictType不为空)时,系统会自动检查字典是否存在 +- 如果字典不存在,会自动创建对应的字典及默认的字典详情项 +- 字典创建包括:字典主表记录和默认的选项值(选项1、选项2等) + +**与其他工具的关系:** +- 此工具创建完整模块后,会自动提示相关API和菜单创建建议 +- 如果用户只需要单个API或菜单,可以使用 smart_assistant 工具 +- create_api 和 create_menu 工具仅用于数据库记录创建 + +重要:ExecutionPlan结构体格式要求: +{ + "packageName": "包名(string)", + "moduleName": "模块名(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/int/bool/time.Time等(string)", + "fieldJson": "JSON标签(string)", + "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)", + "errorText": "错误提示(string)", + "clearable": "是否可清空(bool)", + "sort": "是否排序(bool)", + "primaryKey": "是否主键(bool)", + "dataSource": "数据源(object)", + "checkDataSource": "检查数据源(bool)", + "fieldIndexType": "索引类型(string)" + }] + } +} + +注意: +1. needCreatedPackage=true时packageInfo必需 +2. needCreatedModules=true时modulesInfo必需 +3. packageType只能是"package"或"plugin" +4. 字段类型支持:string,int,int64,float64,bool,time.Time,enum,picture,video,file,pictures,array,richtext,json +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)时,系统会: + - 自动检查字典是否存在,如果不存在则创建字典 + - 根据字典类型和字段描述智能生成默认选项,支持状态、性别、类型、等级、优先级、审批、角色、布尔值、订单、颜色、尺寸等常见场景 + - 为无法识别的字典类型提供通用默认选项`), + 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) + } + + // 扫描预设计的模块 + predesignedModules, err := t.scanPredesignedModules() + if err != nil { + global.GVA_LOG.Warn("扫描预设计模块失败" + err.Error()) + predesignedModules = []PredesignedModuleInfo{} // 确保不为nil + } + + // 转换包信息 + var moduleInfos []ModuleInfo + for _, pkg := range packages { + moduleInfos = append(moduleInfos, ModuleInfo{ + ID: pkg.ID, + PackageName: pkg.PackageName, + Label: pkg.Label, + Desc: pkg.Desc, + Template: pkg.Template, + Module: pkg.Module, + }) + } + + // 转换历史记录 + var historyInfos []HistoryInfo + for _, history := range histories { + 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"), + }) + } + + // 构建分析结果 + analysisResult := AnalysisResponse{ + Packages: moduleInfos, + History: historyInfos, + PredesignedModules: predesignedModules, + Message: fmt.Sprintf("分析完成:获取到 %d 个包、%d 个历史记录和 %d 个预设计模块,请AI根据需求选择合适的包和模块", len(packages), len(histories), len(predesignedModules)), + } + + 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 + +分析现有的包、历史记录和预设计模块,然后构建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": "包名", + "moduleName": "模块名", + "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": "GO 语言的数据类型", + "fieldJson": "json标签", + "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": "" + }] + } +} + +**重要提醒**: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), + }, + }, + }, 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) + } + + // 构建确认响应 + confirmResponse := ConfirmationResponse{ + Message: "请确认以下创建计划:", + PackageConfirm: plan.NeedCreatedPackage, + ModulesConfirm: plan.NeedCreatedModules, + CanProceed: true, + ConfirmationKey: fmt.Sprintf("%s_%s_%d", plan.PackageName, plan.ModuleName, time.Now().Unix()), + } + + // 构建详细的确认信息 + var confirmDetails strings.Builder + confirmDetails.WriteString(fmt.Sprintf("包名: %s\n", plan.PackageName)) + confirmDetails.WriteString(fmt.Sprintf("模块名: %s\n", plan.ModuleName)) + 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 && plan.ModulesInfo != nil { + confirmDetails.WriteString("\n需要创建模块:\n") + confirmDetails.WriteString(fmt.Sprintf(" - 结构体名: %s\n", plan.ModulesInfo.StructName)) + confirmDetails.WriteString(fmt.Sprintf(" - 表名: %s\n", plan.ModulesInfo.TableName)) + confirmDetails.WriteString(fmt.Sprintf(" - 描述: %s\n", plan.ModulesInfo.Description)) + confirmDetails.WriteString(fmt.Sprintf(" - 字段数量: %d\n", len(plan.ModulesInfo.Fields))) + confirmDetails.WriteString(" - 字段列表:\n") + for _, field := range plan.ModulesInfo.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 plan.ModulesInfo != nil && plan.ModulesInfo.StructName != "" { + structName = plan.ModulesInfo.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.ModuleName == "" { + return errors.New("moduleName 不能为空") + } + 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 plan.ModulesInfo == nil { + return errors.New("当 needCreatedModules=true 时,modulesInfo 不能为空") + } + if plan.ModulesInfo.Package == "" { + return errors.New("modulesInfo.package 不能为空") + } + if plan.ModulesInfo.StructName == "" { + return errors.New("modulesInfo.structName 不能为空") + } + if plan.ModulesInfo.TableName == "" { + return errors.New("modulesInfo.tableName 不能为空") + } + if plan.ModulesInfo.Description == "" { + return errors.New("modulesInfo.description 不能为空") + } + if plan.ModulesInfo.Abbreviation == "" { + return errors.New("modulesInfo.abbreviation 不能为空") + } + if plan.ModulesInfo.PackageName == "" { + return errors.New("modulesInfo.packageName 不能为空") + } + if plan.ModulesInfo.HumpPackageName == "" { + return errors.New("modulesInfo.humpPackageName 不能为空") + } + + // 验证字段信息 + if len(plan.ModulesInfo.Fields) == 0 { + return errors.New("modulesInfo.fields 不能为空,至少需要一个字段") + } + + for i, field := range plan.ModulesInfo.Fields { + if field.FieldName == "" { + return fmt.Errorf("字段 %d 的 fieldName 不能为空", i+1) + } + if field.FieldDesc == "" { + return fmt.Errorf("字段 %d 的 fieldDesc 不能为空", i+1) + } + if field.FieldType == "" { + return fmt.Errorf("字段 %d 的 fieldType 不能为空", i+1) + } + if field.FieldJson == "" { + return fmt.Errorf("字段 %d 的 fieldJson 不能为空", i+1) + } + if field.ColumnName == "" { + return fmt.Errorf("字段 %d 的 columnName 不能为空", i+1) + } + + // 验证字段类型 + 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 的 fieldType '%s' 不支持,支持的类型:%v", 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 的 fieldSearchType '%s' 不支持,支持的类型:%v", i+1, field.FieldSearchType, validSearchTypes) + } + } + } + + // 验证主键设置 + if !plan.ModulesInfo.GvaModel { + // 当不使用GVA模型时,必须有且仅有一个字段设置为主键 + primaryKeyCount := 0 + for _, field := range plan.ModulesInfo.Fields { + if field.PrimaryKey { + primaryKeyCount++ + } + } + if primaryKeyCount == 0 { + return errors.New("当 gvaModel=false 时,必须有一个字段的 primaryKey=true") + } + if primaryKeyCount > 1 { + return errors.New("当 gvaModel=false 时,只能有一个字段的 primaryKey=true") + } + } else { + // 当使用GVA模型时,所有字段的primaryKey都应该为false + for i, field := range plan.ModulesInfo.Fields { + if field.PrimaryKey { + return fmt.Errorf("当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false,系统会自动创建ID主键", 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 && plan.ModulesInfo != nil { + dictResult := t.createRequiredDictionaries(ctx, plan.ModulesInfo) + result.Message += dictResult + } + + // 创建模块(如果需要) + if plan.NeedCreatedModules && plan.ModulesInfo != nil { + templateService := service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate + + err := plan.ModulesInfo.Pretreatment() + if err != nil { + result.Message += fmt.Sprintf("模块信息预处理失败: %v", err) + // 即使预处理失败,也要返回paths信息 + return result + } + + err = templateService.Create(ctx, *plan.ModulesInfo) + if err != nil { + result.Message += fmt.Sprintf("创建模块失败: %v", err) + // 即使创建模块失败,也要返回paths信息 + return result + } + result.Message += "模块创建成功; " + } + + result.Message += "已构建目录结构信息; " + result.Success = true + + if result.Message == "" { + result.Message = "执行计划完成" + } + + return result +} + +// createRequiredDictionaries 创建所需的字典 +func (t *AutomationModuleAnalyzer) createRequiredDictionaries(ctx context.Context, modulesInfo *request.AutoCode) string { + var messages []string + dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService + + // 遍历所有字段,查找使用字典的字段 + for _, field := range modulesInfo.Fields { + if field.DictType != "" { + // 检查字典是否存在 + 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)", 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)) + + // 创建默认的字典详情项 + t.createDefaultDictionaryDetails(ctx, field.DictType, field.FieldDesc) + } + } else { + messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", field.DictType)) + } + } + } + + 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, "低" +} diff --git a/server/mcp/menu_creator.go b/server/mcp/menu_creator.go new file mode 100644 index 00000000..cc8a2659 --- /dev/null +++ b/server/mcp/menu_creator.go @@ -0,0 +1,283 @@ +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("创建前端菜单记录,用于在生成前端页面时自动创建对应的菜单项,只要前端有页面生成,都需要调用此mcp。"), + 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/middleware/jwt.go b/server/middleware/jwt.go index db71ed6b..7715ed9c 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/model/system/request/sys_user.go b/server/model/system/request/sys_user.go index a48c46f2..2cd4cf9f 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 00000000..23dd665b --- /dev/null +++ b/server/model/system/request/sys_version.go @@ -0,0 +1,38 @@ +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列表 +} + +// ImportVersionRequest 导入版本请求结构体 +type ImportVersionRequest struct { + VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息 + ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu + ExportApi []system.SysApi `json:"apis"` // API数据,直接复用SysApi +} + +// 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 00000000..b1509b89 --- /dev/null +++ b/server/model/system/response/sys_version.go @@ -0,0 +1,13 @@ +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 +} \ No newline at end of file diff --git a/server/model/system/sys_version.go b/server/model/system/sys_version.go new file mode 100644 index 00000000..e20cbdf2 --- /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/router/system/enter.go b/server/router/system/enter.go index 7127d9e9..a0a23f54 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_version.go b/server/router/system/sys_version.go new file mode 100644 index 00000000..249feb8e --- /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_plugin.go b/server/service/system/auto_code_plugin.go index d2cdc5df..3de4c6cc 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 634cd001..d91f2796 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_user.go b/server/service/system/sys_user.go index 3b3d8e61..630f04cf 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("原密码错误") + return errors.New("原密码错误") } - 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 00000000..56894102 --- /dev/null +++ b/server/service/system/sys_version.go @@ -0,0 +1,196 @@ +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 +} + +// 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 + }) +} diff --git a/server/source/system/api.go b/server/source/system/api.go index c28b3733..710dc135 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -2,6 +2,7 @@ package system import ( "context" + sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "github.com/pkg/errors" @@ -188,6 +189,14 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "媒体库分类", Method: "GET", Path: "/attachmentCategory/getCategoryList", Description: "分类列表"}, {ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/addCategory", Description: "添加/编辑分类"}, {ApiGroup: "媒体库分类", Method: "POST", Path: "/attachmentCategory/deleteCategory", Description: "删除分类"}, + + {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()+"表数据初始化失败!") diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go index 4e37c147..94e1a24c 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/menu.go b/server/source/system/menu.go index 0fbd1dbb..9817c13a 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: "插件市场", Icon: "shop"}}, {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}}, diff --git a/server/utils/ast/ast.go b/server/utils/ast/ast.go index 1cdfa4c1..a2636d23 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/web/package.json b/web/package.json index 7d0a7810..925988de 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "gin-vue-admin", - "version": "2.8.2", + "version": "2.8.4", "private": true, "scripts": { "serve": "node openDocument.js && vite --host --mode development", diff --git a/web/src/App.vue b/web/src/App.vue index 6048a3f5..ed19e854 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,41 +1,43 @@ diff --git a/web/src/api/version.js b/web/src/api/version.js new file mode 100644 index 00000000..b5b7dcce --- /dev/null +++ b/web/src/api/version.js @@ -0,0 +1,114 @@ +import service from '@/utils/request' + +// @Tags SysVersion +// @Summary 删除版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.SysVersion true "删除版本管理" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysVersion/deleteSysVersion [delete] +export const deleteSysVersion = (params) => { + return service({ + url: '/sysVersion/deleteSysVersion', + method: 'delete', + params + }) +} + +// @Tags SysVersion +// @Summary 批量删除版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除版本管理" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /sysVersion/deleteSysVersion [delete] +export const deleteSysVersionByIds = (params) => { + return service({ + url: '/sysVersion/deleteSysVersionByIds', + method: 'delete', + params + }) +} + +// @Tags SysVersion +// @Summary 用id查询版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query model.SysVersion true "用id查询版本管理" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /sysVersion/findSysVersion [get] +export const findSysVersion = (params) => { + return service({ + url: '/sysVersion/findSysVersion', + method: 'get', + params + }) +} + +// @Tags SysVersion +// @Summary 分页获取版本管理列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取版本管理列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /sysVersion/getSysVersionList [get] +export const getSysVersionList = (params) => { + return service({ + url: '/sysVersion/getSysVersionList', + method: 'get', + params + }) +} + +// @Tags SysVersion +// @Summary 导出版本数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body object true "导出版本数据" +// @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"导出成功\"}" +// @Router /sysVersion/exportVersion [post] +export const exportVersion = (data) => { + return service({ + url: '/sysVersion/exportVersion', + method: 'post', + data + }) +} + +// @Tags SysVersion +// @Summary 下载版本JSON数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param ID query string true "版本ID" +// @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"下载成功\"}" +// @Router /sysVersion/downloadVersionJson [get] +export const downloadVersionJson = (params) => { + return service({ + url: '/sysVersion/downloadVersionJson', + method: 'get', + params, + responseType: 'blob' + }) +} + +// @Tags SysVersion +// @Summary 导入版本数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body object true "版本JSON数据" +// @Success 200 {string} string "{\"success\":true,\"data\":{},\"msg\":\"导入成功\"}" +// @Router /sysVersion/importVersion [post] +export const importVersion = (data) => { + return service({ + url: '/sysVersion/importVersion', + method: 'post', + data + }) +} diff --git a/web/src/assets/icons/close.svg b/web/src/assets/icons/close.svg new file mode 100644 index 00000000..1b1f631f --- /dev/null +++ b/web/src/assets/icons/close.svg @@ -0,0 +1 @@ + diff --git a/web/src/assets/icons/idea.svg b/web/src/assets/icons/idea.svg new file mode 100644 index 00000000..eac5a0d7 --- /dev/null +++ b/web/src/assets/icons/idea.svg @@ -0,0 +1 @@ + diff --git a/web/src/assets/icons/lock.svg b/web/src/assets/icons/lock.svg new file mode 100644 index 00000000..46851910 --- /dev/null +++ b/web/src/assets/icons/lock.svg @@ -0,0 +1 @@ + diff --git a/web/src/assets/icons/server.svg b/web/src/assets/icons/server.svg new file mode 100644 index 00000000..3b1375a9 --- /dev/null +++ b/web/src/assets/icons/server.svg @@ -0,0 +1 @@ + diff --git a/web/src/assets/icons/warn.svg b/web/src/assets/icons/warn.svg new file mode 100644 index 00000000..73d7a159 --- /dev/null +++ b/web/src/assets/icons/warn.svg @@ -0,0 +1 @@ + diff --git a/web/src/components/application/index.vue b/web/src/components/application/index.vue new file mode 100644 index 00000000..4dda3ece --- /dev/null +++ b/web/src/components/application/index.vue @@ -0,0 +1,39 @@ + + + diff --git a/web/src/components/errorPreview/index.vue b/web/src/components/errorPreview/index.vue new file mode 100644 index 00000000..dd08402a --- /dev/null +++ b/web/src/components/errorPreview/index.vue @@ -0,0 +1,126 @@ + + + diff --git a/web/src/core/config.js b/web/src/core/config.js index a58c5336..24ca844e 100644 --- a/web/src/core/config.js +++ b/web/src/core/config.js @@ -17,7 +17,7 @@ export const viteLogo = (env) => { `> 欢迎使用Gin-Vue-Admin,开源地址:https://github.com/flipped-aurora/gin-vue-admin` ) ) - console.log(greenText(`> 当前版本:v2.8.3`)) + console.log(greenText(`> 当前版本:v2.8.4`)) console.log(greenText(`> 加群方式:微信:shouzi_1994 QQ群:470239250`)) console.log( greenText(`> 项目地址:https://github.com/flipped-aurora/gin-vue-admin`) diff --git a/web/src/core/gin-vue-admin.js b/web/src/core/gin-vue-admin.js index 495e0aad..3062fc90 100644 --- a/web/src/core/gin-vue-admin.js +++ b/web/src/core/gin-vue-admin.js @@ -10,7 +10,7 @@ export default { register(app) console.log(` 欢迎使用 Gin-Vue-Admin - 当前版本:v2.8.3 + 当前版本:v2.8.4 加群方式:微信:shouzi_1994 QQ群:622360840 项目地址:https://github.com/flipped-aurora/gin-vue-admin 插件市场:https://plugin.gin-vue-admin.com diff --git a/web/src/pathInfo.json b/web/src/pathInfo.json index e991e172..780f9a6e 100644 --- a/web/src/pathInfo.json +++ b/web/src/pathInfo.json @@ -74,6 +74,7 @@ "/src/view/systemTools/installPlugin/index.vue": "Index", "/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug", "/src/view/systemTools/system/system.vue": "Config", + "/src/view/systemTools/version/version.vue": "SysVersion", "/src/plugin/announcement/form/info.vue": "InfoForm", "/src/plugin/announcement/view/info.vue": "Info", "/src/plugin/email/view/index.vue": "Email" diff --git a/web/src/style/reset.scss b/web/src/style/reset.scss index 2cd44c70..55b476fa 100644 --- a/web/src/style/reset.scss +++ b/web/src/style/reset.scss @@ -393,7 +393,7 @@ fieldset, table, th, td { - border: none; + // border: none; font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif; font-size: 14px; diff --git a/web/src/utils/request.js b/web/src/utils/request.js index c05bab40..1c62bd31 100644 --- a/web/src/utils/request.js +++ b/web/src/utils/request.js @@ -1,11 +1,8 @@ import axios from 'axios' // 引入axios -import { ElMessage, ElMessageBox } from 'element-plus' import { useUserStore } from '@/pinia/modules/user' +import { ElLoading, ElMessage } from 'element-plus' +import { emitter } from '@/utils/bus' import router from '@/router/index' -import { ElLoading } from 'element-plus' - -// 添加一个状态变量,用于跟踪是否已有错误弹窗显示 -let errorBoxVisible = false const service = axios.create({ baseURL: import.meta.env.VITE_BASE_API, @@ -24,24 +21,24 @@ const showLoading = ( ) => { const loadDom = document.getElementById('gva-base-load-dom') activeAxios++ - + // 清除之前的定时器 if (timer) { clearTimeout(timer) } - + // 清除强制关闭定时器 if (forceCloseTimer) { clearTimeout(forceCloseTimer) } - + timer = setTimeout(() => { // 再次检查activeAxios状态,防止竞态条件 if (activeAxios > 0 && !isLoadingVisible) { if (!option.target) option.target = loadDom loadingInstance = ElLoading.service(option) isLoadingVisible = true - + // 设置强制关闭定时器,防止loading永远不关闭(30秒超时) forceCloseTimer = setTimeout(() => { if (isLoadingVisible && loadingInstance) { @@ -60,12 +57,12 @@ const closeLoading = () => { if (activeAxios <= 0) { activeAxios = 0 // 确保不会变成负数 clearTimeout(timer) - + if (forceCloseTimer) { clearTimeout(forceCloseTimer) forceCloseTimer = null } - + if (isLoadingVisible && loadingInstance) { loadingInstance.close() isLoadingVisible = false @@ -78,17 +75,17 @@ const closeLoading = () => { const resetLoading = () => { activeAxios = 0 isLoadingVisible = false - + if (timer) { clearTimeout(timer) timer = null } - + if (forceCloseTimer) { clearTimeout(forceCloseTimer) forceCloseTimer = null } - + if (loadingInstance) { try { loadingInstance.close() @@ -98,6 +95,7 @@ const resetLoading = () => { loadingInstance = null } } + // http request 拦截器 service.interceptors.request.use( (config) => { @@ -117,15 +115,18 @@ service.interceptors.request.use( if (!error.config.donNotShowLoading) { closeLoading() } - ElMessage({ - showClose: true, - message: error, - type: 'error' + emitter.emit('show-error', { + code: 'request', + message: error.message || '请求发送失败' }) return error } ) +function getErrorMessage(error) { + return error.response?.data?.msg || '请求失败' +} + // http response 拦截器 service.interceptors.response.use( (response) => { @@ -158,105 +159,38 @@ service.interceptors.response.use( closeLoading() } - // 如果已经有错误弹窗显示,则不再显示新的弹窗 - if (errorBoxVisible) { - return error - } - if (!error.response) { - // 网络错误时重置loading状态 + // 网络错误 resetLoading() - errorBoxVisible = true - ElMessageBox.confirm( - ` -

检测到请求错误

-

${error}

- `, - '请求报错', - { - dangerouslyUseHTMLString: true, - distinguishCancelAndClose: true, - confirmButtonText: '稍后重试', - cancelButtonText: '取消' - } - ).finally(() => { - // 弹窗关闭后重置状态 - errorBoxVisible = false + emitter.emit('show-error', { + code: 'network', + message: getErrorMessage(error) }) - return + return Promise.reject(error) } - switch (error.response.status) { - case 500: - errorBoxVisible = true - ElMessageBox.confirm( - ` -

检测到接口错误${error}

-

错误码 500 :此类错误内容常见于后台panic,请先查看后台日志,如果影响您正常使用可强制登出清理缓存

- `, - '接口报错', - { - dangerouslyUseHTMLString: true, - distinguishCancelAndClose: true, - confirmButtonText: '清理缓存', - cancelButtonText: '取消' - } - ).then(() => { + // HTTP 状态码错误 + if (error.response.status === 401) { + emitter.emit('show-error', { + code: '401', + message: getErrorMessage(error), + fn: () => { const userStore = useUserStore() userStore.ClearStorage() router.push({ name: 'Login', replace: true }) - }).finally(() => { - // 弹窗关闭后重置状态 - errorBoxVisible = false - }) - break - case 404: - errorBoxVisible = true - ElMessageBox.confirm( - ` -

检测到接口错误${error}

-

错误码 404 :此类错误多为接口未注册(或未重启)或者请求路径(方法)与api路径(方法)不符--如果为自动化代码请检查是否存在空格

- `, - '接口报错', - { - dangerouslyUseHTMLString: true, - distinguishCancelAndClose: true, - confirmButtonText: '我知道了', - cancelButtonText: '取消' - } - ).finally(() => { - // 弹窗关闭后重置状态 - errorBoxVisible = false - }) - break - case 401: - errorBoxVisible = true - ElMessageBox.confirm( - ` -

无效的令牌

-

错误码: 401 错误信息:${error}

- `, - '身份信息', - { - dangerouslyUseHTMLString: true, - distinguishCancelAndClose: true, - confirmButtonText: '重新登录', - cancelButtonText: '取消' - } - ).then(() => { - const userStore = useUserStore() - userStore.ClearStorage() - router.push({ name: 'Login', replace: true }) - }).finally(() => { - // 弹窗关闭后重置状态 - errorBoxVisible = false - }) - break + } + }) + return Promise.reject(error) } - return error + emitter.emit('show-error', { + code: error.response.status, + message: getErrorMessage(error) + }) + return Promise.reject(error) } ) + // 监听页面卸载事件,确保loading被正确清理 if (typeof window !== 'undefined') { window.addEventListener('beforeunload', resetLoading) diff --git a/web/src/view/systemTools/version/version.vue b/web/src/view/systemTools/version/version.vue new file mode 100644 index 00000000..b0ff9247 --- /dev/null +++ b/web/src/view/systemTools/version/version.vue @@ -0,0 +1,905 @@ + + + + +