From 5496bdaa9475b35f13df6258452649c3cbb0bb83 Mon Sep 17 00:00:00 2001 From: Echo <1711788888@qq.com> Date: Wed, 3 Sep 2025 01:45:01 +0800 Subject: [PATCH] =?UTF-8?q?:art:=20=E6=9B=B4=E6=96=B0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/v1/example/exa_file_upload_download.go | 1 - api/v1/system/auto_code_mcp.go | 144 ++ api/v1/system/enter.go | 2 + api/v1/system/sys_menu.go | 2 +- api/v1/system/sys_operation_record.go | 25 - api/v1/system/sys_system.go | 13 +- api/v1/system/sys_user.go | 59 +- api/v1/system/sys_version.go | 486 +++++ config.yaml | 460 ++--- config/captcha.go | 2 +- config/config.go | 4 +- config/db_list.go | 11 +- config/email.go | 15 +- config/gorm_oracle.go | 10 +- config/mcp.go | 9 + core/server.go | 20 +- core/server_run.go | 60 + core/server_win.go | 21 - core/viper.go | 81 +- global/global.go | 2 + global/version.go | 12 + go.mod | 22 +- go.sum | 46 +- initialize/ensure_tables.go | 2 +- initialize/gorm.go | 5 +- initialize/gorm_biz.go | 15 +- initialize/gorm_mssql.go | 8 +- initialize/gorm_mysql.go | 39 +- initialize/gorm_oracle.go | 35 +- initialize/gorm_pgsql.go | 27 +- initialize/gorm_sqlite.go | 22 +- initialize/init.go | 15 + initialize/internal/gorm.go | 25 +- initialize/mcp.go | 25 + initialize/plugin_biz_v1.go | 2 + initialize/reload.go | 45 + initialize/router.go | 45 +- initialize/router_biz.go | 2 + main.go | 20 +- mcp/api_creator.go | 201 ++ mcp/api_lister.go | 165 ++ mcp/client/client.go | 39 + mcp/client/client_test.go | 132 ++ mcp/dictionary_generator.go | 310 +++ mcp/dictionary_query.go | 234 +++ mcp/enter.go | 31 + mcp/execution_plan_schema.md | 529 +++++ mcp/gag_usage_example.md | 205 ++ mcp/gva_auto_generate.go | 1755 +++++++++++++++++ mcp/menu_creator.go | 287 +++ mcp/menu_lister.go | 111 ++ mcp/requirement_analyzer.go | 137 ++ middleware/casbin_rbac.go | 5 +- middleware/cors.go | 16 - middleware/email.go | 8 +- middleware/jwt.go | 22 +- middleware/operation.go | 6 +- middleware/timeout.go | 55 + middleware/user_jwt.go | 2 +- model/system/request/sys_auto_code.go | 2 +- model/system/request/sys_auto_code_mcp.go | 16 + model/system/request/sys_user.go | 6 +- model/system/request/sys_version.go | 40 + model/system/response/sys_version.go | 14 + model/system/sys_base_menu.go | 13 +- model/system/sys_version.go | 20 + plugin/email/README.MD | 17 +- plugin/email/config/email.go | 15 +- plugin/email/main.go | 3 +- plugin/email/utils/email.go | 42 +- resource/mcp/tools.tpl | 56 + resource/package/server/model/model.go.tpl | 40 +- .../server/model/request/request.go.tpl | 27 +- .../package/server/service/service.go.tpl | 56 +- resource/package/web/view/form.vue.tpl | 204 +- resource/package/web/view/table.vue.tpl | 763 +------ resource/plugin/server/api/api.go.tpl | 255 +++ resource/plugin/server/api/enter.go.tpl | 6 + resource/plugin/server/config/config.go.tpl | 4 + resource/plugin/server/gen/gen.go.tpl | 18 + resource/plugin/server/initialize/api.go.tpl | 12 + resource/plugin/server/initialize/gorm.go.tpl | 17 + resource/plugin/server/initialize/menu.go.tpl | 12 + .../plugin/server/initialize/router.go.tpl | 14 + .../plugin/server/initialize/viper.go.tpl | 17 + resource/plugin/server/model/model.go.tpl | 76 + .../server/model/request/request.go.tpl | 38 + resource/plugin/server/plugin.go.tpl | 26 + resource/plugin/server/plugin/plugin.go.tpl | 5 + resource/plugin/server/router/enter.go.tpl | 6 + resource/plugin/server/router/router.go.tpl | 46 + resource/plugin/server/service/enter.go.tpl | 7 + resource/plugin/server/service/service.go.tpl | 211 ++ resource/plugin/web/api/api.js.tpl | 127 ++ resource/plugin/web/form/form.vue.tpl | 464 +++++ resource/plugin/web/view/view.vue.tpl | 689 +++++++ router/system/enter.go | 2 + router/system/sys_auto_code.go | 5 + router/system/sys_operation_record.go | 1 - router/system/sys_user.go | 2 +- router/system/sys_version.go | 25 + service/system/auto_code_mcp.go | 45 + service/system/auto_code_package.go | 43 +- service/system/auto_code_package_test.go | 15 +- service/system/auto_code_plugin.go | 12 +- service/system/auto_code_template.go | 5 +- service/system/enter.go | 1 + service/system/jwt_black_list.go | 32 - service/system/sys_authority.go | 5 +- service/system/sys_auto_code_oracle.go | 2 +- service/system/sys_base_menu.go | 1 + service/system/sys_casbin.go | 60 +- service/system/sys_export_template.go | 70 +- service/system/sys_menu.go | 50 +- service/system/sys_operation_record.go | 5 - service/system/sys_user.go | 25 +- service/system/sys_version.go | 230 +++ source/system/api.go | 12 + source/system/authorities_menus.go | 63 +- source/system/casbin.go | 11 + source/system/dictionary_detail.go | 6 +- source/system/menu.go | 84 +- utils/ast/ast.go | 75 + utils/autocode/template_funcs.go | 723 +++++++ utils/captcha/redis.go | 7 +- utils/casbin_util.go | 52 + utils/fmt_plus.go | 17 + utils/jwt.go | 18 + utils/system_events.go | 34 + utils/verify.go | 2 +- 130 files changed, 9397 insertions(+), 1816 deletions(-) create mode 100644 api/v1/system/auto_code_mcp.go create mode 100644 api/v1/system/sys_version.go create mode 100644 config/mcp.go create mode 100644 core/server_run.go delete mode 100644 core/server_win.go create mode 100644 global/version.go create mode 100644 initialize/init.go create mode 100644 initialize/mcp.go create mode 100644 initialize/reload.go create mode 100644 mcp/api_creator.go create mode 100644 mcp/api_lister.go create mode 100644 mcp/client/client.go create mode 100644 mcp/client/client_test.go create mode 100644 mcp/dictionary_generator.go create mode 100644 mcp/dictionary_query.go create mode 100644 mcp/enter.go create mode 100644 mcp/execution_plan_schema.md create mode 100644 mcp/gag_usage_example.md create mode 100644 mcp/gva_auto_generate.go create mode 100644 mcp/menu_creator.go create mode 100644 mcp/menu_lister.go create mode 100644 mcp/requirement_analyzer.go create mode 100644 middleware/timeout.go create mode 100644 model/system/request/sys_auto_code_mcp.go create mode 100644 model/system/request/sys_version.go create mode 100644 model/system/response/sys_version.go create mode 100644 model/system/sys_version.go create mode 100644 resource/mcp/tools.tpl create mode 100644 resource/plugin/server/api/api.go.tpl create mode 100644 resource/plugin/server/api/enter.go.tpl create mode 100644 resource/plugin/server/config/config.go.tpl create mode 100644 resource/plugin/server/gen/gen.go.tpl create mode 100644 resource/plugin/server/initialize/api.go.tpl create mode 100644 resource/plugin/server/initialize/gorm.go.tpl create mode 100644 resource/plugin/server/initialize/menu.go.tpl create mode 100644 resource/plugin/server/initialize/router.go.tpl create mode 100644 resource/plugin/server/initialize/viper.go.tpl create mode 100644 resource/plugin/server/model/model.go.tpl create mode 100644 resource/plugin/server/model/request/request.go.tpl create mode 100644 resource/plugin/server/plugin.go.tpl create mode 100644 resource/plugin/server/plugin/plugin.go.tpl create mode 100644 resource/plugin/server/router/enter.go.tpl create mode 100644 resource/plugin/server/router/router.go.tpl create mode 100644 resource/plugin/server/service/enter.go.tpl create mode 100644 resource/plugin/server/service/service.go.tpl create mode 100644 resource/plugin/web/api/api.js.tpl create mode 100644 resource/plugin/web/form/form.vue.tpl create mode 100644 resource/plugin/web/view/view.vue.tpl create mode 100644 router/system/sys_version.go create mode 100644 service/system/auto_code_mcp.go create mode 100644 service/system/sys_version.go create mode 100644 utils/autocode/template_funcs.go create mode 100644 utils/casbin_util.go create mode 100644 utils/system_events.go diff --git a/api/v1/example/exa_file_upload_download.go b/api/v1/example/exa_file_upload_download.go index c1ac5f8..dbced61 100644 --- a/api/v1/example/exa_file_upload_download.go +++ b/api/v1/example/exa_file_upload_download.go @@ -32,7 +32,6 @@ func (b *FileUploadAndDownloadApi) UploadFile(c *gin.Context) { response.FailWithMessage("接收文件失败", c) return } - file, err = fileUploadAndDownloadService.UploadFile(header, noSave, classId) // 文件上传后拿到文件路径 if err != nil { global.GVA_LOG.Error("上传文件失败!", zap.Error(err)) diff --git a/api/v1/system/auto_code_mcp.go b/api/v1/system/auto_code_mcp.go new file mode 100644 index 0000000..9e7ffd0 --- /dev/null +++ b/api/v1/system/auto_code_mcp.go @@ -0,0 +1,144 @@ +package system + +import ( + "fmt" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/mcp/client" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system/request" + "github.com/gin-gonic/gin" + "github.com/mark3labs/mcp-go/mcp" +) + +// Create +// @Tags mcp +// @Summary 自动McpTool +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.AutoMcpTool true "创建自动代码" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/mcp [post] +func (a *AutoCodeTemplateApi) MCP(c *gin.Context) { + var info request.AutoMcpTool + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + toolFilePath, err := autoCodeTemplateService.CreateMcp(c.Request.Context(), info) + if err != nil { + response.FailWithMessage("创建失败", c) + global.GVA_LOG.Error(err.Error()) + return + } + response.OkWithMessage("创建成功,MCP Tool路径:"+toolFilePath, c) +} + +// Create +// @Tags mcp +// @Summary 自动McpTool +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.AutoMcpTool true "创建自动代码" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/mcpList [post] +func (a *AutoCodeTemplateApi) MCPList(c *gin.Context) { + + baseUrl := fmt.Sprintf("http://127.0.0.1:%d%s", global.GVA_CONFIG.System.Addr, global.GVA_CONFIG.MCP.SSEPath) + + testClient, err := client.NewClient(baseUrl, "testClient", "v1.0.0", global.GVA_CONFIG.MCP.Name) + defer testClient.Close() + toolsRequest := mcp.ListToolsRequest{} + + list, err := testClient.ListTools(c.Request.Context(), toolsRequest) + + if err != nil { + response.FailWithMessage("创建失败", c) + global.GVA_LOG.Error(err.Error()) + return + } + + mcpServerConfig := map[string]interface{}{ + "mcpServers": map[string]interface{}{ + global.GVA_CONFIG.MCP.Name: map[string]string{ + "url": baseUrl, + }, + }, + } + response.OkWithData(gin.H{ + "mcpServerConfig": mcpServerConfig, + "list": list, + }, c) +} + +// Create +// @Tags mcp +// @Summary 测试McpTool +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body object true "调用MCP Tool的参数" +// @Success 200 {object} response.Response "{"success":true,"data":{},"msg":"测试成功"}" +// @Router /autoCode/mcpTest [post] +func (a *AutoCodeTemplateApi) MCPTest(c *gin.Context) { + // 定义接口请求结构 + var testRequest struct { + Name string `json:"name" binding:"required"` // 工具名称 + Arguments map[string]interface{} `json:"arguments" binding:"required"` // 工具参数 + } + + // 绑定JSON请求体 + if err := c.ShouldBindJSON(&testRequest); err != nil { + response.FailWithMessage("参数解析失败:"+err.Error(), c) + return + } + + // 创建MCP客户端 + baseUrl := fmt.Sprintf("http://127.0.0.1:%d%s", global.GVA_CONFIG.System.Addr, global.GVA_CONFIG.MCP.SSEPath) + testClient, err := client.NewClient(baseUrl, "testClient", "v1.0.0", global.GVA_CONFIG.MCP.Name) + if err != nil { + response.FailWithMessage("创建MCP客户端失败:"+err.Error(), c) + return + } + defer testClient.Close() + + ctx := c.Request.Context() + + // 初始化MCP连接 + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: "testClient", + Version: "v1.0.0", + } + + _, err = testClient.Initialize(ctx, initRequest) + if err != nil { + response.FailWithMessage("初始化MCP连接失败:"+err.Error(), c) + return + } + + // 构建工具调用请求 + request := mcp.CallToolRequest{} + request.Params.Name = testRequest.Name + request.Params.Arguments = testRequest.Arguments + + // 调用工具 + result, err := testClient.CallTool(ctx, request) + if err != nil { + response.FailWithMessage("工具调用失败:"+err.Error(), c) + return + } + + // 处理响应结果 + if len(result.Content) == 0 { + response.FailWithMessage("工具未返回任何内容", c) + return + } + + // 返回结果 + response.OkWithData(result.Content, c) +} diff --git a/api/v1/system/enter.go b/api/v1/system/enter.go index 0906a2c..f4d74e3 100644 --- a/api/v1/system/enter.go +++ b/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/api/v1/system/sys_menu.go b/api/v1/system/sys_menu.go index e08c4d8..a80eb41 100644 --- a/api/v1/system/sys_menu.go +++ b/api/v1/system/sys_menu.go @@ -143,7 +143,7 @@ func (a *AuthorityMenuApi) AddBaseMenu(c *gin.Context) { err = menuService.AddBaseMenu(menu) if err != nil { global.GVA_LOG.Error("添加失败!", zap.Error(err)) - response.FailWithMessage("添加失败", c) + response.FailWithMessage("添加失败:"+err.Error(), c) return } response.OkWithMessage("添加成功", c) diff --git a/api/v1/system/sys_operation_record.go b/api/v1/system/sys_operation_record.go index f90abab..ba90783 100644 --- a/api/v1/system/sys_operation_record.go +++ b/api/v1/system/sys_operation_record.go @@ -13,31 +13,6 @@ import ( type OperationRecordApi struct{} -// CreateSysOperationRecord -// @Tags SysOperationRecord -// @Summary 创建SysOperationRecord -// @Security ApiKeyAuth -// @accept application/json -// @Produce application/json -// @Param data body system.SysOperationRecord true "创建SysOperationRecord" -// @Success 200 {object} response.Response{msg=string} "创建SysOperationRecord" -// @Router /sysOperationRecord/createSysOperationRecord [post] -func (s *OperationRecordApi) CreateSysOperationRecord(c *gin.Context) { - var sysOperationRecord system.SysOperationRecord - err := c.ShouldBindJSON(&sysOperationRecord) - if err != nil { - response.FailWithMessage(err.Error(), c) - return - } - err = operationRecordService.CreateSysOperationRecord(sysOperationRecord) - if err != nil { - global.GVA_LOG.Error("创建失败!", zap.Error(err)) - response.FailWithMessage("创建失败", c) - return - } - response.OkWithMessage("创建成功", c) -} - // DeleteSysOperationRecord // @Tags SysOperationRecord // @Summary 删除SysOperationRecord diff --git a/api/v1/system/sys_system.go b/api/v1/system/sys_system.go index afb8e63..6d4e073 100644 --- a/api/v1/system/sys_system.go +++ b/api/v1/system/sys_system.go @@ -55,19 +55,20 @@ func (s *SystemApi) SetSystemConfig(c *gin.Context) { // ReloadSystem // @Tags System -// @Summary 重启系统 +// @Summary 重载系统 // @Security ApiKeyAuth // @Produce application/json -// @Success 200 {object} response.Response{msg=string} "重启系统" +// @Success 200 {object} response.Response{msg=string} "重载系统" // @Router /system/reloadSystem [post] func (s *SystemApi) ReloadSystem(c *gin.Context) { - err := utils.Reload() + // 触发系统重载事件 + err := utils.GlobalSystemEvents.TriggerReload() if err != nil { - global.GVA_LOG.Error("重启系统失败!", zap.Error(err)) - response.FailWithMessage("重启系统失败", c) + global.GVA_LOG.Error("重载系统失败!", zap.Error(err)) + response.FailWithMessage("重载系统失败:"+err.Error(), c) return } - response.OkWithMessage("重启系统成功", c) + response.OkWithMessage("重载系统成功", c) } // GetServerInfo diff --git a/api/v1/system/sys_user.go b/api/v1/system/sys_user.go index a61b0d0..b709f2a 100644 --- a/api/v1/system/sys_user.go +++ b/api/v1/system/sys_user.go @@ -27,8 +27,6 @@ import ( func (b *BaseApi) Login(c *gin.Context) { var l systemReq.Login err := c.ShouldBindJSON(&l) - key := c.ClientIP() - if err != nil { response.FailWithMessage(err.Error(), c) return @@ -39,6 +37,7 @@ func (b *BaseApi) Login(c *gin.Context) { return } + key := c.ClientIP() // 判断验证码是否开启 openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数 openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间 @@ -48,30 +47,30 @@ func (b *BaseApi) Login(c *gin.Context) { } var oc bool = openCaptcha == 0 || openCaptcha < interfaceToInt(v) - - if !oc || (l.CaptchaId != "" && l.Captcha != "" && store.Verify(l.CaptchaId, l.Captcha, true)) { - u := &system.SysUser{Username: l.Username, Password: l.Password} - user, err := userService.Login(u) - if err != nil { - global.GVA_LOG.Error("登陆失败! 用户名不存在或者密码错误!", zap.Error(err)) - // 验证码次数+1 - global.BlackCache.Increment(key, 1) - response.FailWithMessage("用户名不存在或者密码错误", c) - return - } - if user.Enable != 1 { - global.GVA_LOG.Error("登陆失败! 用户被禁止登录!") - // 验证码次数+1 - global.BlackCache.Increment(key, 1) - response.FailWithMessage("用户被禁止登录", c) - return - } - b.TokenNext(c, *user) + if oc && (l.Captcha == "" || l.CaptchaId == "" || !store.Verify(l.CaptchaId, l.Captcha, true)) { + // 验证码次数+1 + global.BlackCache.Increment(key, 1) + response.FailWithMessage("验证码错误", c) return } - // 验证码次数+1 - global.BlackCache.Increment(key, 1) - response.FailWithMessage("验证码错误", c) + + u := &system.SysUser{Username: l.Username, Password: l.Password} + user, err := userService.Login(u) + if err != nil { + global.GVA_LOG.Error("登陆失败! 用户名不存在或者密码错误!", zap.Error(err)) + // 验证码次数+1 + global.BlackCache.Increment(key, 1) + response.FailWithMessage("用户名不存在或者密码错误", c) + return + } + if user.Enable != 1 { + global.GVA_LOG.Error("登陆失败! 用户被禁止登录!") + // 验证码次数+1 + global.BlackCache.Increment(key, 1) + response.FailWithMessage("用户被禁止登录", c) + return + } + b.TokenNext(c, *user) } // TokenNext 登录以后签发jwt @@ -93,7 +92,7 @@ func (b *BaseApi) TokenNext(c *gin.Context, user system.SysUser) { } if jwtStr, err := jwtService.GetRedisJWT(user.Username); err == redis.Nil { - if err := jwtService.SetRedisJWT(token, user.Username); err != nil { + if err := utils.SetRedisJWT(token, user.Username); err != nil { global.GVA_LOG.Error("设置登录状态失败!", zap.Error(err)) response.FailWithMessage("设置登录状态失败", c) return @@ -114,7 +113,7 @@ func (b *BaseApi) TokenNext(c *gin.Context, user system.SysUser) { response.FailWithMessage("jwt作废失败", c) return } - if err := jwtService.SetRedisJWT(token, user.GetUsername()); err != nil { + if err := utils.SetRedisJWT(token, user.GetUsername()); err != nil { response.FailWithMessage("设置登录状态失败", c) return } @@ -184,7 +183,7 @@ func (b *BaseApi) ChangePassword(c *gin.Context) { } uid := utils.GetUserID(c) u := &system.SysUser{GVA_MODEL: global.GVA_MODEL{ID: uid}, Password: req.Password} - _, err = userService.ChangePassword(u, req.NewPassword) + err = userService.ChangePassword(u, req.NewPassword) if err != nil { global.GVA_LOG.Error("修改失败!", zap.Error(err)) response.FailWithMessage("修改失败,原密码与当前账户不符", c) @@ -467,13 +466,13 @@ func (b *BaseApi) GetUserInfo(c *gin.Context) { // @Success 200 {object} response.Response{msg=string} "重置用户密码" // @Router /user/resetPassword [post] func (b *BaseApi) ResetPassword(c *gin.Context) { - var user system.SysUser - err := c.ShouldBindJSON(&user) + var rps systemReq.ResetPassword + err := c.ShouldBindJSON(&rps) if err != nil { response.FailWithMessage(err.Error(), c) return } - err = userService.ResetPassword(user.ID) + err = userService.ResetPassword(rps.ID, rps.Password) if err != nil { global.GVA_LOG.Error("重置失败!", zap.Error(err)) response.FailWithMessage("重置失败"+err.Error(), c) diff --git a/api/v1/system/sys_version.go b/api/v1/system/sys_version.go new file mode 100644 index 0000000..f7c6ab9 --- /dev/null +++ b/api/v1/system/sys_version.go @@ -0,0 +1,486 @@ +package system + +import ( + "encoding/json" + "fmt" + "net/http" + "sort" + "strconv" + "time" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/common/response" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/model/system/request" + systemRes "git.echol.cn/loser/lckt/model/system/response" + "git.echol.cn/loser/lckt/utils" + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type SysVersionApi struct{} + +// buildMenuTree 构建菜单树结构 +func buildMenuTree(menus []system.SysBaseMenu) []system.SysBaseMenu { + // 创建菜单映射 + menuMap := make(map[uint]*system.SysBaseMenu) + for i := range menus { + menuMap[menus[i].ID] = &menus[i] + } + + // 构建树结构 + var rootMenus []system.SysBaseMenu + for _, menu := range menus { + if menu.ParentId == 0 { + // 根菜单 + menuData := convertMenuToStruct(menu, menuMap) + rootMenus = append(rootMenus, menuData) + } + } + + // 按sort排序根菜单 + sort.Slice(rootMenus, func(i, j int) bool { + return rootMenus[i].Sort < rootMenus[j].Sort + }) + + return rootMenus +} + +// convertMenuToStruct 将菜单转换为结构体并递归处理子菜单 +func convertMenuToStruct(menu system.SysBaseMenu, menuMap map[uint]*system.SysBaseMenu) system.SysBaseMenu { + result := system.SysBaseMenu{ + Path: menu.Path, + Name: menu.Name, + Hidden: menu.Hidden, + Component: menu.Component, + Sort: menu.Sort, + Meta: menu.Meta, + } + + // 清理并复制参数数据 + if len(menu.Parameters) > 0 { + cleanParameters := make([]system.SysBaseMenuParameter, 0, len(menu.Parameters)) + for _, param := range menu.Parameters { + cleanParam := system.SysBaseMenuParameter{ + Type: param.Type, + Key: param.Key, + Value: param.Value, + // 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID + } + cleanParameters = append(cleanParameters, cleanParam) + } + result.Parameters = cleanParameters + } + + // 清理并复制菜单按钮数据 + if len(menu.MenuBtn) > 0 { + cleanMenuBtns := make([]system.SysBaseMenuBtn, 0, len(menu.MenuBtn)) + for _, btn := range menu.MenuBtn { + cleanBtn := system.SysBaseMenuBtn{ + Name: btn.Name, + Desc: btn.Desc, + // 不复制 ID, CreatedAt, UpdatedAt, SysBaseMenuID + } + cleanMenuBtns = append(cleanMenuBtns, cleanBtn) + } + result.MenuBtn = cleanMenuBtns + } + + // 查找并处理子菜单 + var children []system.SysBaseMenu + for _, childMenu := range menuMap { + if childMenu.ParentId == menu.ID { + childData := convertMenuToStruct(*childMenu, menuMap) + children = append(children, childData) + } + } + + // 按sort排序子菜单 + if len(children) > 0 { + sort.Slice(children, func(i, j int) bool { + return children[i].Sort < children[j].Sort + }) + result.Children = children + } + + return result +} + +// DeleteSysVersion 删除版本管理 +// @Tags SysVersion +// @Summary 删除版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body system.SysVersion true "删除版本管理" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /sysVersion/deleteSysVersion [delete] +func (sysVersionApi *SysVersionApi) DeleteSysVersion(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + err := sysVersionService.DeleteSysVersion(ctx, ID) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// DeleteSysVersionByIds 批量删除版本管理 +// @Tags SysVersion +// @Summary 批量删除版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /sysVersion/deleteSysVersionByIds [delete] +func (sysVersionApi *SysVersionApi) DeleteSysVersionByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + IDs := c.QueryArray("IDs[]") + err := sysVersionService.DeleteSysVersionByIds(ctx, IDs) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:"+err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// FindSysVersion 用id查询版本管理 +// @Tags SysVersion +// @Summary 用id查询版本管理 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param ID query uint true "用id查询版本管理" +// @Success 200 {object} response.Response{data=system.SysVersion,msg=string} "查询成功" +// @Router /sysVersion/findSysVersion [get] +func (sysVersionApi *SysVersionApi) FindSysVersion(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + ID := c.Query("ID") + resysVersion, err := sysVersionService.GetSysVersion(ctx, ID) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:"+err.Error(), c) + return + } + response.OkWithData(resysVersion, c) +} + +// GetSysVersionList 分页获取版本管理列表 +// @Tags SysVersion +// @Summary 分页获取版本管理列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query systemReq.SysVersionSearch true "分页获取版本管理列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /sysVersion/getSysVersionList [get] +func (sysVersionApi *SysVersionApi) GetSysVersionList(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo systemReq.SysVersionSearch + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := sysVersionService.GetSysVersionInfoList(ctx, pageInfo) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:"+err.Error(), c) + return + } + response.OkWithDetailed(response.PageResult{ + List: list, + Total: total, + Page: pageInfo.Page, + PageSize: pageInfo.PageSize, + }, "获取成功", c) +} + +// GetSysVersionPublic 不需要鉴权的版本管理接口 +// @Tags SysVersion +// @Summary 不需要鉴权的版本管理接口 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /sysVersion/getSysVersionPublic [get] +func (sysVersionApi *SysVersionApi) GetSysVersionPublic(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 + // 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + sysVersionService.GetSysVersionPublic(ctx) + response.OkWithDetailed(gin.H{ + "info": "不需要鉴权的版本管理接口信息", + }, "获取成功", c) +} + +// ExportVersion 创建发版数据 +// @Tags SysVersion +// @Summary 创建发版数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body systemReq.ExportVersionRequest true "创建发版数据" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /sysVersion/exportVersion [post] +func (sysVersionApi *SysVersionApi) ExportVersion(c *gin.Context) { + ctx := c.Request.Context() + + var req systemReq.ExportVersionRequest + err := c.ShouldBindJSON(&req) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + // 获取选中的菜单数据 + var menuData []system.SysBaseMenu + if len(req.MenuIds) > 0 { + menuData, err = sysVersionService.GetMenusByIds(ctx, req.MenuIds) + if err != nil { + global.GVA_LOG.Error("获取菜单数据失败!", zap.Error(err)) + response.FailWithMessage("获取菜单数据失败:"+err.Error(), c) + return + } + } + + // 获取选中的API数据 + var apiData []system.SysApi + if len(req.ApiIds) > 0 { + apiData, err = sysVersionService.GetApisByIds(ctx, req.ApiIds) + if err != nil { + global.GVA_LOG.Error("获取API数据失败!", zap.Error(err)) + response.FailWithMessage("获取API数据失败:"+err.Error(), c) + return + } + } + + // 获取选中的字典数据 + var dictData []system.SysDictionary + if len(req.DictIds) > 0 { + dictData, err = sysVersionService.GetDictionariesByIds(ctx, req.DictIds) + if err != nil { + global.GVA_LOG.Error("获取字典数据失败!", zap.Error(err)) + response.FailWithMessage("获取字典数据失败:"+err.Error(), c) + return + } + } + + // 处理菜单数据,构建递归的children结构 + processedMenus := buildMenuTree(menuData) + + // 处理API数据,清除ID和时间戳字段 + processedApis := make([]system.SysApi, 0, len(apiData)) + for _, api := range apiData { + cleanApi := system.SysApi{ + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + } + processedApis = append(processedApis, cleanApi) + } + + // 处理字典数据,清除ID和时间戳字段,包含字典详情 + processedDicts := make([]system.SysDictionary, 0, len(dictData)) + for _, dict := range dictData { + cleanDict := system.SysDictionary{ + Name: dict.Name, + Type: dict.Type, + Status: dict.Status, + Desc: dict.Desc, + } + + // 处理字典详情数据,清除ID和时间戳字段 + cleanDetails := make([]system.SysDictionaryDetail, 0, len(dict.SysDictionaryDetails)) + for _, detail := range dict.SysDictionaryDetails { + cleanDetail := system.SysDictionaryDetail{ + Label: detail.Label, + Value: detail.Value, + Extend: detail.Extend, + Status: detail.Status, + Sort: detail.Sort, + // 不复制 ID, CreatedAt, UpdatedAt, SysDictionaryID + } + cleanDetails = append(cleanDetails, cleanDetail) + } + cleanDict.SysDictionaryDetails = cleanDetails + + processedDicts = append(processedDicts, cleanDict) + } + + // 构建导出数据 + exportData := systemRes.ExportVersionResponse{ + Version: systemReq.VersionInfo{ + Name: req.VersionName, + Code: req.VersionCode, + Description: req.Description, + ExportTime: time.Now().Format("2006-01-02 15:04:05"), + }, + Menus: processedMenus, + Apis: processedApis, + Dictionaries: processedDicts, + } + + // 转换为JSON + jsonData, err := json.MarshalIndent(exportData, "", " ") + if err != nil { + global.GVA_LOG.Error("JSON序列化失败!", zap.Error(err)) + response.FailWithMessage("JSON序列化失败:"+err.Error(), c) + return + } + + // 保存版本记录 + version := system.SysVersion{ + VersionName: utils.Pointer(req.VersionName), + VersionCode: utils.Pointer(req.VersionCode), + Description: utils.Pointer(req.Description), + VersionData: utils.Pointer(string(jsonData)), + } + + err = sysVersionService.CreateSysVersion(ctx, &version) + if err != nil { + global.GVA_LOG.Error("保存版本记录失败!", zap.Error(err)) + response.FailWithMessage("保存版本记录失败:"+err.Error(), c) + return + } + + response.OkWithMessage("创建发版成功", c) +} + +// DownloadVersionJson 下载版本JSON数据 +// @Tags SysVersion +// @Summary 下载版本JSON数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param ID query string true "版本ID" +// @Success 200 {object} response.Response{data=object,msg=string} "下载成功" +// @Router /sysVersion/downloadVersionJson [get] +func (sysVersionApi *SysVersionApi) DownloadVersionJson(c *gin.Context) { + ctx := c.Request.Context() + + ID := c.Query("ID") + if ID == "" { + response.FailWithMessage("版本ID不能为空", c) + return + } + + // 获取版本记录 + version, err := sysVersionService.GetSysVersion(ctx, ID) + if err != nil { + global.GVA_LOG.Error("获取版本记录失败!", zap.Error(err)) + response.FailWithMessage("获取版本记录失败:"+err.Error(), c) + return + } + + // 构建JSON数据 + var jsonData []byte + if version.VersionData != nil && *version.VersionData != "" { + jsonData = []byte(*version.VersionData) + } else { + // 如果没有存储的JSON数据,构建一个基本的结构 + basicData := systemRes.ExportVersionResponse{ + Version: systemReq.VersionInfo{ + Name: *version.VersionName, + Code: *version.VersionCode, + Description: *version.Description, + ExportTime: version.CreatedAt.Format("2006-01-02 15:04:05"), + }, + Menus: []system.SysBaseMenu{}, + Apis: []system.SysApi{}, + } + jsonData, _ = json.MarshalIndent(basicData, "", " ") + } + + // 设置下载响应头 + filename := fmt.Sprintf("version_%s_%s.json", *version.VersionCode, time.Now().Format("20060102150405")) + c.Header("Content-Type", "application/json") + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) + c.Header("Content-Length", strconv.Itoa(len(jsonData))) + + c.Data(http.StatusOK, "application/json", jsonData) +} + +// ImportVersion 导入版本数据 +// @Tags SysVersion +// @Summary 导入版本数据 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body systemReq.ImportVersionRequest true "版本JSON数据" +// @Success 200 {object} response.Response{msg=string} "导入成功" +// @Router /sysVersion/importVersion [post] +func (sysVersionApi *SysVersionApi) ImportVersion(c *gin.Context) { + ctx := c.Request.Context() + + // 获取JSON数据 + var importData systemReq.ImportVersionRequest + err := c.ShouldBindJSON(&importData) + if err != nil { + response.FailWithMessage("解析JSON数据失败:"+err.Error(), c) + return + } + + // 验证数据格式 + if importData.VersionInfo.Name == "" || importData.VersionInfo.Code == "" { + response.FailWithMessage("版本信息格式错误", c) + return + } + + // 导入菜单数据 + if len(importData.ExportMenu) > 0 { + if err := sysVersionService.ImportMenus(ctx, importData.ExportMenu); err != nil { + global.GVA_LOG.Error("导入菜单失败!", zap.Error(err)) + response.FailWithMessage("导入菜单失败: "+err.Error(), c) + return + } + } + + // 导入API数据 + if len(importData.ExportApi) > 0 { + if err := sysVersionService.ImportApis(importData.ExportApi); err != nil { + global.GVA_LOG.Error("导入API失败!", zap.Error(err)) + response.FailWithMessage("导入API失败: "+err.Error(), c) + return + } + } + + // 导入字典数据 + if len(importData.ExportDictionary) > 0 { + if err := sysVersionService.ImportDictionaries(importData.ExportDictionary); err != nil { + global.GVA_LOG.Error("导入字典失败!", zap.Error(err)) + response.FailWithMessage("导入字典失败: "+err.Error(), c) + return + } + } + + // 创建导入记录 + jsonData, _ := json.Marshal(importData) + version := system.SysVersion{ + VersionName: utils.Pointer(importData.VersionInfo.Name), + VersionCode: utils.Pointer(fmt.Sprintf("%s_imported_%s", importData.VersionInfo.Code, time.Now().Format("20060102150405"))), + Description: utils.Pointer(fmt.Sprintf("导入版本: %s", importData.VersionInfo.Description)), + VersionData: utils.Pointer(string(jsonData)), + } + + err = sysVersionService.CreateSysVersion(ctx, &version) + if err != nil { + global.GVA_LOG.Error("保存导入记录失败!", zap.Error(err)) + // 这里不返回错误,因为数据已经导入成功 + } + + response.OkWithMessage("导入成功", c) +} diff --git a/config.yaml b/config.yaml index cff649e..2333237 100644 --- a/config.yaml +++ b/config.yaml @@ -1,132 +1,103 @@ -aliyun-oss: - endpoint: oss-cn-hangzhou.aliyuncs.com - access-key-id: LTAI5tB3Mn5Y7mVo8h3zkf46 - access-key-secret: FtuHdFy4NFdVItEiNBnTun3Ddi8BMK - bucket-name: lckt - bucket-url: https://lckt.oss-cn-hangzhou.aliyuncs.com - base-path: lckt -autocode: - web: web/src - root: C:\Users\Administrator\GolandProjects\zb - server: lckt-server - module: git.echol.cn/loser/lckt - ai-path: "https://ai.gin-vue-admin.com/{FUNC}/loser7659/c178e970-6a59-497d-96ed-86fee6b3285a" -aws-s3: - bucket: xxxxx-10005608 - region: ap-shanghai - endpoint: "" - secret-id: your-secret-id - secret-key: your-secret-key - base-url: https://gin.vue.admin - path-prefix: git.echol.cn/loser/lckt - s3-force-path-style: false - disable-ssl: false -captcha: - key-long: 4 - img-width: 240 - img-height: 80 - open-captcha: 0 - open-captcha-timeout: 3600 -cloudflare-r2: - bucket: xxxx0bucket - base-url: https://gin.vue.admin.com - path: uploads - account-id: xxx_account_id - access-key-id: xxx_key_id - secret-access-key: xxx_secret_key -cors: - mode: strict-whitelist - whitelist: - - allow-origin: example1.com - allow-methods: POST, GET - allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id - expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - allow-credentials: true - - allow-origin: example2.com - allow-methods: GET, POST - allow-headers: content-type - expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - allow-credentials: true -db-list: - - type: "" - alias-name: "" - prefix: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - path: "" - engine: "" - log-mode: "" - max-idle-conns: 10 - max-open-conns: 100 - singular: false - log-zap: false - disable: true -disk-list: - - mount-point: / -email: - to: xxx@qq.com - from: xxx@163.com - host: smtp.163.com - secret: xxx - nickname: test - port: 465 - is-ssl: true -excel: - dir: ./resource/excel/ -hua-wei-obs: - path: you-path - bucket: you-bucket - endpoint: you-endpoint - access-key: you-access-key - secret-key: you-secret-key +# git.echol.cn/loser/lckt Global Configuration + +# jwt configuration jwt: - signing-key: f5a4f443-779c-4734-a2f0-c033dc1f12a2 + signing-key: qmPlus expires-time: 7d buffer-time: 1d issuer: qmPlus -local: - path: uploads/file - store-path: uploads/file -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 -mongo: - coll: "" - options: "" - database: "" - username: "" +# zap logger configuration +zap: + level: info + format: console + prefix: "[git.echol.cn/loser/lckt]" + director: log + show-line: true + encode-level: LowercaseColorLevelEncoder + stacktrace-key: stacktrace + log-in-console: true + retention-day: -1 + +# redis configuration +redis: + #是否使用redis集群模式 + useCluster: false + #使用集群模式addr和db默认无效 + addr: 127.0.0.1:6379 password: "" - auth-source: "" + db: 0 + clusterAddrs: + - "172.21.0.3:7000" + - "172.21.0.4:7001" + - "172.21.0.2:7002" + +# redis-list configuration +redis-list: + - name: cache # 数据库的名称,注意: name 需要在 redis-list 中唯一 + useCluster: false # 是否使用redis集群模式 + addr: 127.0.0.1:6379 # 使用集群模式addr和db默认无效 + password: "" + db: 0 + clusterAddrs: + - "172.21.0.3:7000" + - "172.21.0.4:7001" + - "172.21.0.2:7002" + +# mongo configuration +mongo: + coll: '' + options: '' + database: '' + username: '' + password: '' + auth-source: '' min-pool-size: 0 max-pool-size: 100 socket-timeout-ms: 0 connect-timeout-ms: 0 is-zap: false hosts: - - host: "" - port: "" -mssql: - prefix: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - path: "" - engine: "" - log-mode: "" - max-idle-conns: 10 - max-open-conns: 100 - singular: false - log-zap: false + - host: '' + port: '' + +# email configuration +email: + to: xxx@qq.com + port: 465 + from: xxx@163.com + host: smtp.163.com + is-ssl: true + secret: xxx + nickname: test + +# system configuration +system: + env: local # 修改为public可以关闭路由日志输出 + addr: 8888 + db-type: mysql + oss-type: aliyun-oss # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置 + use-redis: false # 使用redis + use-mongo: false # 使用mongo + use-multipoint: false + # IP限制次数 一个小时15000次 + iplimit-count: 15000 + # IP限制一个小时 + iplimit-time: 3600 + # 路由全局前缀 + router-prefix: "" + # 严格角色模式 打开后权限将会存在上下级关系 + use-strict-auth: false + +# captcha configuration +captcha: + key-long: 4 + img-width: 240 + img-height: 80 + open-captcha: 0 # 0代表一直开启,大于0代表限制次数 + open-captcha-timeout: 3600 # open-captcha大于0时才生效 + +# mysql connect configuration +# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) mysql: prefix: "" port: "3366" @@ -141,87 +112,111 @@ mysql: max-open-conns: 100 singular: false log-zap: true -oracle: - prefix: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - path: "" - engine: "" - log-mode: "" - max-idle-conns: 10 - max-open-conns: 100 - singular: false - log-zap: false + +# pgsql connect configuration +# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) pgsql: - prefix: "" + path: "" port: "" config: "" db-name: "" username: "" password: "" - path: "" - engine: "" - log-mode: "" max-idle-conns: 10 max-open-conns: 100 - singular: false + log-mode: "" log-zap: false +oracle: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +mssql: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +sqlite: + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false +db-list: + - disable: true # 是否禁用 + type: "" # 数据库的类型,目前支持mysql、pgsql、mssql、oracle + alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一 + path: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + max-idle-conns: 10 + max-open-conns: 100 + log-mode: "" + log-zap: false + +# local configuration +local: + path: uploads/file + store-path: uploads/file + +# autocode configuration +autocode: + web: web/src + root: "" # root 自动适配项目根目录, 请不要手动配置,他会在项目加载的时候识别出根路径 + server: /lckt-server + module: 'git.echol.cn/loser/lckt' + ai-path: "https://ai.gin-vue-admin.com/{FUNC}/loser7659/c178e970-6a59-497d-96ed-86fee6b3285a" # AI服务路径 + +# qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址) qiniu: zone: ZoneHuaDong bucket: "" img-path: "" + use-https: false access-key: "" secret-key: "" - use-https: false use-cdn-domains: false -redis: - name: "hw" - addr: 120.46.165.63:6379 - password: "loser765911" - db: 0 - useCluster: false - clusterAddrs: - - 172.21.0.3:7000 - - 172.21.0.4:7001 - - 172.21.0.2:7002 -redis-list: - - name: cache - addr: 120.46.165.63:6379 - password: "loser765911" - db: 1 - useCluster: false - clusterAddrs: - - 172.21.0.3:7000 - - 172.21.0.4:7001 - - 172.21.0.2:7002 -sqlite: - prefix: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - path: "" - engine: "" - log-mode: "" - max-idle-conns: 10 - max-open-conns: 100 - singular: false - log-zap: false -system: - db-type: mysql - oss-type: aliyun-oss - router-prefix: "" - addr: 8888 - iplimit-count: 15000 - iplimit-time: 3600 - use-multipoint: false - use-redis: true - use-mongo: false - use-strict-auth: false + +# minio oss configuration +minio: + endpoint: yourEndpoint + access-key-id: yourAccessKeyId + access-key-secret: yourAccessKeySecret + bucket-name: yourBucketName + use-ssl: false + base-path: "" + bucket-url: "http://host:9000/yourBucketName" + +# aliyun oss configuration +aliyun-oss: + endpoint: oss-cn-hangzhou.aliyuncs.com + access-key-id: LTAI5tB3Mn5Y7mVo8h3zkf46 + access-key-secret: FtuHdFy4NFdVItEiNBnTun3Ddi8BMK + bucket-name: lckt + bucket-url: https://lckt.oss-cn-hangzhou.aliyuncs.com + base-path: lckt + +# tencent cos configuration tencent-cos: bucket: xxxxx-10005608 region: ap-shanghai @@ -229,46 +224,63 @@ tencent-cos: secret-key: your-secret-key base-url: https://gin.vue.admin path-prefix: git.echol.cn/loser/lckt -zap: - level: info - prefix: '[git.echol.cn/loser/lckt]' - format: console - director: log - encode-level: LowercaseColorLevelEncoder - stacktrace-key: stacktrace - show-line: true - log-in-console: true - retention-day: -1 -wechat: - app-id: wx3d21df18d7f8f9fc - app-secret: 3ab19e9b6a5e155c25ac6457be650047 - token: kjeldcsdz2phfwfxnevsajnzsboho1ev - aes-key: PiqqlGdEblw5Gv1RJ5qcTnhKUjFw9YNkBMAX6CIw6Me - callback: https://api.gin-vue-admin.com/wechat/callback -pay-list: - - type: wxpay - alias-name: wxpay-1 - app-id: wx3d21df18d7f8f9fc - mch-id: 1646874753 - v3-key: 1a3sd8561d5179Df152D4789aD38wG9s - cert-path: /resource/cert/apiclient_cert.pem - key-path: /resource/cert/apiclient_key.pem - notify-url: http://lckt.hnlc5588.cn/app_order/notify - serial-no: 59A891FB403EC7A1CF2090DB9C8EC704BD43B101 - - type: wxpay2 - alias-name: wxpay-2 - app-id: 2 - mch-id: 2 - v3-key: 2 - cert-path: 2 - key-path: 2 - notify-url: 2 - serial-no: 2 +# aws s3 configuration (minio compatible) +aws-s3: + bucket: xxxxx-10005608 + region: ap-shanghai + endpoint: "" + s3-force-path-style: false + disable-ssl: false + secret-id: your-secret-id + secret-key: your-secret-key + base-url: https://gin.vue.admin + path-prefix: git.echol.cn/loser/lckt -sms: - access-key-id: your-access-key-id - access-key-secret: your-access-key-secret - sign-name: your-sign-name - template-code: your-template-code - expire-time: 5 \ No newline at end of file +# cloudflare r2 configuration +cloudflare-r2: + bucket: xxxx0bucket + base-url: https://gin.vue.admin.com + path: uploads + account-id: xxx_account_id + access-key-id: xxx_key_id + secret-access-key: xxx_secret_key + +# huawei obs configuration +hua-wei-obs: + path: you-path + bucket: you-bucket + endpoint: you-endpoint + access-key: you-access-key + secret-key: you-secret-key + +# excel configuration +excel: + dir: ./resource/excel/ + +# disk usage configuration +disk-list: + - mount-point: "/" + +# 跨域配置 +# 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用 +cors: + mode: strict-whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝 + whitelist: + - allow-origin: example1.com + allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id + allow-methods: POST, GET + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + + allow-credentials: true # 布尔值 + - allow-origin: example2.com + allow-headers: content-type + allow-methods: GET, POST + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + allow-credentials: true # 布尔值 +mcp: + name: GVA_MCP + version: v1.0.0 + sse_path: /sse + message_path: /message + url_prefix: '' \ No newline at end of file diff --git a/config/captcha.go b/config/captcha.go index 074a9bf..d678a41 100644 --- a/config/captcha.go +++ b/config/captcha.go @@ -4,6 +4,6 @@ type Captcha struct { KeyLong int `mapstructure:"key-long" json:"key-long" yaml:"key-long"` // 验证码长度 ImgWidth int `mapstructure:"img-width" json:"img-width" yaml:"img-width"` // 验证码宽度 ImgHeight int `mapstructure:"img-height" json:"img-height" yaml:"img-height"` // 验证码高度 - OpenCaptcha int `mapstructure:"open-captcha" json:"open-captcha" yaml:"open-captcha"` // 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码 + OpenCaptcha int `mapstructure:"open-captcha" json:"open-captcha" yaml:"open-captcha"` // 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码次数,如3代表错误三次后出现验证码 OpenCaptchaTimeOut int `mapstructure:"open-captcha-timeout" json:"open-captcha-timeout" yaml:"open-captcha-timeout"` // 防爆破验证码超时时间,单位:s(秒) } diff --git a/config/config.go b/config/config.go index 4f6e458..b9fad90 100644 --- a/config/config.go +++ b/config/config.go @@ -35,7 +35,9 @@ type Server struct { // 跨域配置 Cors CORS `mapstructure:"cors" json:"cors" yaml:"cors"` - // Wechat + // MCP配置 + MCP MCP `mapstructure:"mcp" json:"mcp" yaml:"mcp"` + Wechat Wechat `mapstructure:"wechat" json:"wechat" yaml:"wechat"` Pays []Pays `mapstructure:"pay-list" json:"pay-list" yaml:"pay-list"` SMS SMS `mapstructure:"sms" json:"sms" yaml:"sms"` diff --git a/config/db_list.go b/config/db_list.go index 39767f5..17674b7 100644 --- a/config/db_list.go +++ b/config/db_list.go @@ -1,8 +1,9 @@ package config import ( - "gorm.io/gorm/logger" "strings" + + "gorm.io/gorm/logger" ) type DsnProvider interface { @@ -31,13 +32,13 @@ type GeneralDB struct { func (c GeneralDB) LogLevel() logger.LogLevel { switch strings.ToLower(c.LogMode) { - case "silent", "Silent": + case "silent": return logger.Silent - case "error", "Error": + case "error": return logger.Error - case "warn", "Warn": + case "warn": return logger.Warn - case "info", "Info": + case "info": return logger.Info default: return logger.Info diff --git a/config/email.go b/config/email.go index 0984616..9fd7642 100644 --- a/config/email.go +++ b/config/email.go @@ -1,11 +1,12 @@ package config type Email struct { - To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 - From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 - Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 - Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 - Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 - Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 - IsSSL bool `mapstructure:"is-ssl" json:"is-ssl" yaml:"is-ssl"` // 是否SSL 是否开启SSL + To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 + Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 + Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool `mapstructure:"is-ssl" json:"is-ssl" yaml:"is-ssl"` // 是否SSL 是否开启SSL + IsLoginAuth bool `mapstructure:"is-loginauth" json:"is-loginauth" yaml:"is-loginauth"` // 是否LoginAuth 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等) } diff --git a/config/gorm_oracle.go b/config/gorm_oracle.go index 1bbeb46..52cf21c 100644 --- a/config/gorm_oracle.go +++ b/config/gorm_oracle.go @@ -1,10 +1,18 @@ package config +import ( + "fmt" + "net" + "net/url" +) + type Oracle struct { GeneralDB `yaml:",inline" mapstructure:",squash"` } func (m *Oracle) Dsn() string { - return "oracle://" + m.Username + ":" + m.Password + "@" + m.Path + ":" + m.Port + "/" + m.Dbname + "?" + m.Config + dsn := fmt.Sprintf("oracle://%s:%s@%s/%s?%s", url.PathEscape(m.Username), url.PathEscape(m.Password), + net.JoinHostPort(m.Path, m.Port), url.PathEscape(m.Dbname), m.Config) + return dsn } diff --git a/config/mcp.go b/config/mcp.go new file mode 100644 index 0000000..81f4bff --- /dev/null +++ b/config/mcp.go @@ -0,0 +1,9 @@ +package config + +type MCP struct { + Name string `mapstructure:"name" json:"name" yaml:"name"` // MCP名称 + Version string `mapstructure:"version" json:"version" yaml:"version"` // MCP版本 + SSEPath string `mapstructure:"sse_path" json:"sse_path" yaml:"sse_path"` // SSE路径 + MessagePath string `mapstructure:"message_path" json:"message_path" yaml:"message_path"` // 消息路径 + UrlPrefix string `mapstructure:"url_prefix" json:"url_prefix" yaml:"url_prefix"` // URL前缀 +} diff --git a/core/server.go b/core/server.go index 998b9a7..272cef6 100644 --- a/core/server.go +++ b/core/server.go @@ -6,17 +6,16 @@ import ( "git.echol.cn/loser/lckt/initialize" "git.echol.cn/loser/lckt/service/system" "go.uber.org/zap" + "time" ) -type server interface { - ListenAndServe() error -} - -func RunWindowsServer() { - if global.GVA_CONFIG.System.UseMultipoint || global.GVA_CONFIG.System.UseRedis { +func RunServer() { + if global.GVA_CONFIG.System.UseRedis { // 初始化redis服务 initialize.Redis() - initialize.RedisList() + if global.GVA_CONFIG.System.UseMultipoint { + initialize.RedisList() + } } if global.GVA_CONFIG.System.UseMongo { @@ -33,11 +32,6 @@ func RunWindowsServer() { Router := initialize.Routers() address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr) - s := initServer(address, Router) - global.GVA_LOG.Info("server run success on ", zap.String("address", address)) - - fmt.Println(global.GVA_CONFIG.Pays) - - global.GVA_LOG.Error(s.ListenAndServe().Error()) + initServer(address, Router, 10*time.Minute, 10*time.Minute) } diff --git a/core/server_run.go b/core/server_run.go new file mode 100644 index 0000000..067ce6b --- /dev/null +++ b/core/server_run.go @@ -0,0 +1,60 @@ +package core + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type server interface { + ListenAndServe() error + Shutdown(context.Context) error +} + +// initServer 启动服务并实现优雅关闭 +func initServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) { + // 创建服务 + srv := &http.Server{ + Addr: address, + Handler: router, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + MaxHeaderBytes: 1 << 20, + } + + // 在goroutine中启动服务 + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("listen: %s\n", err) + zap.L().Error("server启动失败", zap.Error(err)) + os.Exit(1) + } + }() + + // 等待中断信号以优雅地关闭服务器 + quit := make(chan os.Signal, 1) + // kill (无参数) 默认发送 syscall.SIGTERM + // kill -2 发送 syscall.SIGINT + // kill -9 发送 syscall.SIGKILL,但是无法被捕获,所以不需要添加 + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + zap.L().Info("关闭WEB服务...") + + // 设置5秒的超时时间 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + zap.L().Fatal("WEB服务关闭异常", zap.Error(err)) + } + + zap.L().Info("WEB服务已关闭") +} diff --git a/core/server_win.go b/core/server_win.go deleted file mode 100644 index 20cf44b..0000000 --- a/core/server_win.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build windows -// +build windows - -package core - -import ( - "net/http" - "time" - - "github.com/gin-gonic/gin" -) - -func initServer(address string, router *gin.Engine) server { - return &http.Server{ - Addr: address, - Handler: router, - ReadTimeout: 10 * time.Minute, - WriteTimeout: 10 * time.Minute, - MaxHeaderBytes: 1 << 20, - } -} diff --git a/core/viper.go b/core/viper.go index d8ef508..b16d3ed 100644 --- a/core/viper.go +++ b/core/viper.go @@ -3,55 +3,26 @@ package core import ( "flag" "fmt" - "git.echol.cn/loser/lckt/core/internal" - "github.com/gin-gonic/gin" "os" "path/filepath" - "github.com/fsnotify/fsnotify" - "github.com/spf13/viper" - + "git.echol.cn/loser/lckt/core/internal" "git.echol.cn/loser/lckt/global" + "github.com/fsnotify/fsnotify" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" ) -// Viper // -// 优先级: 命令行 > 环境变量 > 默认值 -// Author [SliverHorn](https://github.com/SliverHorn) -func Viper(path ...string) *viper.Viper { - var config string - - if len(path) == 0 { - flag.StringVar(&config, "c", "", "choose config file.") - flag.Parse() - if config == "" { // 判断命令行参数是否为空 - if configEnv := os.Getenv(internal.ConfigEnv); configEnv == "" { // 判断 internal.ConfigEnv 常量存储的环境变量是否为空 - switch gin.Mode() { - case gin.DebugMode: - config = internal.ConfigDefaultFile - case gin.ReleaseMode: - config = internal.ConfigReleaseFile - case gin.TestMode: - config = internal.ConfigTestFile - } - fmt.Printf("您正在使用gin模式的%s环境名称,config的路径为%s\n", gin.Mode(), config) - } else { // internal.ConfigEnv 常量存储的环境变量不为空 将值赋值于config - config = configEnv - fmt.Printf("您正在使用%s环境变量,config的路径为%s\n", internal.ConfigEnv, config) - } - } else { // 命令行参数不为空 将值赋值于config - fmt.Printf("您正在使用命令行的-c参数传递的值,config的路径为%s\n", config) - } - } else { // 函数传递的可变参数的第一个值赋值于config - config = path[0] - fmt.Printf("您正在使用func Viper()传递的值,config的路径为%s\n", config) - } +// Viper 配置 +func Viper() *viper.Viper { + config := getConfigPath() v := viper.New() v.SetConfigFile(config) v.SetConfigType("yaml") err := v.ReadInConfig() if err != nil { - panic(fmt.Errorf("Fatal error config file: %s \n", err)) + panic(fmt.Errorf("fatal error config file: %w", err)) } v.WatchConfig() @@ -62,10 +33,44 @@ func Viper(path ...string) *viper.Viper { } }) if err = v.Unmarshal(&global.GVA_CONFIG); err != nil { - panic(err) + panic(fmt.Errorf("fatal error unmarshal config: %w", err)) } // root 适配性 根据root位置去找到对应迁移位置,保证root路径有效 global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") return v } + +// getConfigPath 获取配置文件路径, 优先级: 命令行 > 环境变量 > 默认值 +func getConfigPath() (config string) { + // `-c` flag parse + flag.StringVar(&config, "c", "", "choose config file.") + flag.Parse() + if config != "" { // 命令行参数不为空 将值赋值于config + fmt.Printf("您正在使用命令行的 '-c' 参数传递的值, config 的路径为 %s\n", config) + return + } + if env := os.Getenv(internal.ConfigEnv); env != "" { // 判断环境变量 GVA_CONFIG + config = env + fmt.Printf("您正在使用 %s 环境变量, config 的路径为 %s\n", internal.ConfigEnv, config) + return + } + + switch gin.Mode() { // 根据 gin 模式文件名 + case gin.DebugMode: + config = internal.ConfigDebugFile + case gin.ReleaseMode: + config = internal.ConfigReleaseFile + case gin.TestMode: + config = internal.ConfigTestFile + } + fmt.Printf("您正在使用 gin 的 %s 模式运行, config 的路径为 %s\n", gin.Mode(), config) + + _, err := os.Stat(config) + if err != nil || os.IsNotExist(err) { + config = internal.ConfigDefaultFile + fmt.Printf("配置文件路径不存在, 使用默认配置文件路径: %s\n", config) + } + + return +} diff --git a/global/global.go b/global/global.go index c829da1..4e4f377 100644 --- a/global/global.go +++ b/global/global.go @@ -2,6 +2,7 @@ package global import ( "fmt" + "github.com/mark3labs/mcp-go/server" "sync" "github.com/gin-gonic/gin" @@ -35,6 +36,7 @@ var ( GVA_Concurrency_Control = &singleflight.Group{} GVA_ROUTERS gin.RoutesInfo GVA_ACTIVE_DBNAME *string + GVA_MCP_SERVER *server.MCPServer BlackCache local_cache.Cache lock sync.RWMutex ) diff --git a/global/version.go b/global/version.go new file mode 100644 index 0000000..280c5fc --- /dev/null +++ b/global/version.go @@ -0,0 +1,12 @@ +package global + +// Version 版本信息 +// 目前只有Version正式使用 其余为预留 +const ( + // Version 当前版本号 + Version = "v2.8.5" + // AppName 应用名称 + AppName = "LCKT" + // Description 应用描述 + Description = "使用gin+vue进行极速开发的全栈开发基础平台" +) diff --git a/go.mod b/go.mod index e4e8631..c1acfe3 100644 --- a/go.mod +++ b/go.mod @@ -16,9 +16,9 @@ require ( github.com/aws/aws-sdk-go v1.55.6 github.com/casbin/casbin/v2 v2.103.0 github.com/casbin/gorm-adapter/v3 v3.32.0 + github.com/dzwvip/gorm-oracle v0.1.2 github.com/fsnotify/fsnotify v1.8.0 github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 - github.com/gin-contrib/cors v1.7.6 github.com/gin-gonic/gin v1.10.1 github.com/glebarez/sqlite v1.11.0 github.com/go-sql-driver/mysql v1.8.1 @@ -30,7 +30,8 @@ require ( github.com/gorilla/websocket v1.5.3 github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible - github.com/mholt/archiver/v4 v4.0.0-alpha.9 + github.com/mark3labs/mcp-go v0.39.0 + github.com/mholt/archives v0.1.3 github.com/minio/minio-go/v7 v7.0.84 github.com/mojocn/base64Captcha v1.3.8 github.com/otiai10/copy v1.14.1 @@ -67,18 +68,20 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/STARRY-S/zip v0.1.0 // indirect + github.com/STARRY-S/zip v0.2.1 // indirect github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect github.com/alibabacloud-go/debug v1.0.1 // indirect github.com/alibabacloud-go/endpoint-util v1.1.0 // indirect github.com/alibabacloud-go/openapi-util v0.1.1 // indirect github.com/aliyun/credentials-go v1.4.5 // indirect - github.com/andybalholm/brotli v1.1.1 // indirect + github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect github.com/bodgit/plumbing v1.3.0 // indirect github.com/bodgit/sevenzip v1.6.0 // indirect github.com/bodgit/windows v1.0.1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/bytedance/sonic v1.13.3 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/casbin/govaluate v1.3.0 // indirect @@ -90,6 +93,7 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emirpasic/gods v1.12.0 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect github.com/gammazero/toposort v0.1.1 // indirect github.com/gin-contrib/sse v1.1.0 // indirect @@ -113,6 +117,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/invopop/jsonschema v0.13.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.7.2 // indirect @@ -131,7 +136,9 @@ require ( github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/microsoft/go-mssqldb v1.8.0 // indirect + github.com/mikelolasagasti/xz v1.0.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minlz v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -139,7 +146,7 @@ require ( github.com/montanaflynn/stats v0.7.1 // indirect github.com/mozillazg/go-httpheader v0.4.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/nwaples/rardecode/v2 v2.0.1 // indirect + github.com/nwaples/rardecode/v2 v2.1.0 // indirect github.com/otiai10/mint v1.6.3 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -153,25 +160,28 @@ require ( github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sijms/go-ora/v2 v2.7.17 // indirect github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.12.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/therootcompany/xz v1.0.1 // indirect + github.com/thoas/go-funk v0.7.0 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.9.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect github.com/ulikunitz/xz v0.5.12 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 // indirect github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/otel v1.29.0 // indirect diff --git a/go.sum b/go.sum index 9046796..0da3e58 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= -github.com/STARRY-S/zip v0.1.0 h1:eUER3jKmHKXjv+iy3BekLa+QnNSo1Lqz4eTzYBcGDqo= -github.com/STARRY-S/zip v0.1.0/go.mod h1:qj/mTZkvb3AvfGQ2e775/3AODRvB4peSw8KNMvrM8/I= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= @@ -108,10 +108,12 @@ github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmP github.com/aliyun/credentials-go v1.3.10/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk= github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ= +github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.8.0 h1:DSXtrypQddoug1459viM9X9D3dp1Z7993fw36I2kNcQ= github.com/bmatcuk/doublestar/v4 v4.8.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= @@ -125,6 +127,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -167,6 +171,10 @@ github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj6 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dzwvip/gorm-oracle v0.1.2 h1:811aFDY7oDfKWHc0Z0lHdXzzr89EmKBSwc/jLJ8GU5g= +github.com/dzwvip/gorm-oracle v0.1.2/go.mod h1:TbF7idnO9UgGpJ0qJpDZby1/wGquzP5GYof88ScBITE= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -181,8 +189,6 @@ github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBv github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= -github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= -github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= @@ -325,6 +331,8 @@ github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible h1:XQVXdk+WAJ github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible/go.mod h1:l7VUhRbTKCzdOacdT4oWCwATKyvZqUOlOqr0Ous3k4s= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -342,6 +350,7 @@ github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJk github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -394,20 +403,26 @@ github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mark3labs/mcp-go v0.39.0 h1:dQwaOADzUJ1ROslEJB8QV+4u/8XQCqH9ylB//x8cCEQ= +github.com/mark3labs/mcp-go v0.39.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mholt/archiver/v4 v4.0.0-alpha.9 h1:EZgAsW6DsuawxDgTtIdjCUBa2TQ6AOe9pnCidofSRtE= -github.com/mholt/archiver/v4 v4.0.0-alpha.9/go.mod h1:5D3uct315OMkMRXKwEuMB+wQi/2m5NQngKDmApqwVlo= +github.com/mholt/archives v0.1.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458= +github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw= github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo= +github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= +github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E= github.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY= +github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= +github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -432,8 +447,8 @@ github.com/mozillazg/go-httpheader v0.4.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK0 github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nwaples/rardecode/v2 v2.0.1 h1:3MN6/R+Y4c7e+21U3yhWuUcf72sYmcmr6jtiuAVSH1A= -github.com/nwaples/rardecode/v2 v2.0.1/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= +github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= +github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= @@ -493,6 +508,8 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= +github.com/sijms/go-ora/v2 v2.7.17 h1:M/pYIqjaMUeBxyzOWp2oj4ntF6fHSBloJWGNH9vbmsU= +github.com/sijms/go-ora/v2 v2.7.17/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -539,8 +556,8 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= github.com/tencentyun/cos-go-sdk-v5 v0.7.60 h1:/e/tmvRmfKexr/QQIBzWhOkZWsmY3EK72NrI6G/Tv0o= github.com/tencentyun/cos-go-sdk-v5 v0.7.60/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0= -github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= -github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y= +github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= @@ -557,6 +574,8 @@ github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -573,6 +592,8 @@ github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 h1:hOh7aVDrvGJRxzXrQbDY8E github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -915,6 +936,7 @@ gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g= gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g= gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY= gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE= +gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= diff --git a/initialize/ensure_tables.go b/initialize/ensure_tables.go index cebe70a..dc22a8e 100644 --- a/initialize/ensure_tables.go +++ b/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/initialize/gorm.go b/initialize/gorm.go index 8efd7a9..edb9e67 100644 --- a/initialize/gorm.go +++ b/initialize/gorm.go @@ -1,7 +1,6 @@ package initialize import ( - "git.echol.cn/loser/lckt/model/app" "os" "git.echol.cn/loser/lckt/global" @@ -57,15 +56,13 @@ func RegisterTables() { system.Condition{}, system.JoinTemplate{}, system.SysParams{}, + system.SysVersion{}, example.ExaFile{}, example.ExaCustomer{}, example.ExaFileChunk{}, example.ExaFileUploadAndDownload{}, example.ExaAttachmentCategory{}, - - app.Banner{}, - app.Order{}, ) if err != nil { global.GVA_LOG.Error("register table failed", zap.Error(err)) diff --git a/initialize/gorm_biz.go b/initialize/gorm_biz.go index e0abc95..2181f37 100644 --- a/initialize/gorm_biz.go +++ b/initialize/gorm_biz.go @@ -2,24 +2,11 @@ package initialize import ( "git.echol.cn/loser/lckt/global" - "git.echol.cn/loser/lckt/model/article" - "git.echol.cn/loser/lckt/model/bot" - "git.echol.cn/loser/lckt/model/category" - "git.echol.cn/loser/lckt/model/notice" - "git.echol.cn/loser/lckt/model/user" - "git.echol.cn/loser/lckt/model/vip" ) func bizModel() error { db := global.GVA_DB - err := db.AutoMigrate( - category.Category{}, - bot.Bot{}, - article.Article{}, - user.User{}, - vip.Vip{}, - notice.Notice{}, - ) + err := db.AutoMigrate() if err != nil { return err } diff --git a/initialize/gorm_mssql.go b/initialize/gorm_mssql.go index f262eb6..478faaf 100644 --- a/initialize/gorm_mssql.go +++ b/initialize/gorm_mssql.go @@ -28,7 +28,9 @@ func GormMssql() *gorm.DB { DSN: m.Dsn(), // DSN data source name DefaultStringSize: 191, // string 类型字段的默认长度 } - if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); err != nil { return nil } else { db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) @@ -48,7 +50,9 @@ func GormMssqlByConfig(m config.Mssql) *gorm.DB { DSN: m.Dsn(), // DSN data source name DefaultStringSize: 191, // string 类型字段的默认长度 } - if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); err != nil { panic(err) } else { db.InstanceSet("gorm:table_options", "ENGINE=InnoDB") diff --git a/initialize/gorm_mysql.go b/initialize/gorm_mysql.go index f9028aa..89b12a0 100644 --- a/initialize/gorm_mysql.go +++ b/initialize/gorm_mysql.go @@ -12,18 +12,32 @@ import ( // GormMysql 初始化Mysql数据库 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) +// Author [ByteZhou-2018](https://github.com/ByteZhou-2018) func GormMysql() *gorm.DB { m := global.GVA_CONFIG.Mysql + return initMysqlDatabase(m) +} + +// GormMysqlByConfig 通过传入配置初始化Mysql数据库 +func GormMysqlByConfig(m config.Mysql) *gorm.DB { + return initMysqlDatabase(m) +} + +// initMysqlDatabase 初始化Mysql数据库的辅助函数 +func initMysqlDatabase(m config.Mysql) *gorm.DB { if m.Dbname == "" { return nil } + mysqlConfig := mysql.Config{ DSN: m.Dsn(), // DSN data source name DefaultStringSize: 191, // string 类型字段的默认长度 SkipInitializeWithVersion: false, // 根据版本自动配置 } - if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { - return nil + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(general)); err != nil { + panic(err) } else { db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) sqlDB, _ := db.DB() @@ -32,24 +46,3 @@ func GormMysql() *gorm.DB { return db } } - -// GormMysqlByConfig 初始化Mysql数据库用过传入配置 -func GormMysqlByConfig(m config.Mysql) *gorm.DB { - if m.Dbname == "" { - return nil - } - mysqlConfig := mysql.Config{ - DSN: m.Dsn(), // DSN data source name - DefaultStringSize: 191, // string 类型字段的默认长度 - SkipInitializeWithVersion: false, // 根据版本自动配置 - } - if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { - panic(err) - } else { - db.InstanceSet("gorm:table_options", "ENGINE=InnoDB") - sqlDB, _ := db.DB() - sqlDB.SetMaxIdleConns(m.MaxIdleConns) - sqlDB.SetMaxOpenConns(m.MaxOpenConns) - return db - } -} diff --git a/initialize/gorm_oracle.go b/initialize/gorm_oracle.go index 9b5ed1e..d3dc641 100644 --- a/initialize/gorm_oracle.go +++ b/initialize/gorm_oracle.go @@ -1,47 +1,32 @@ package initialize import ( - //"github.com/dzwvip/oracle" "git.echol.cn/loser/lckt/config" "git.echol.cn/loser/lckt/global" "git.echol.cn/loser/lckt/initialize/internal" - - //_ "github.com/godror/godror" - "gorm.io/driver/mysql" + oracle "github.com/dzwvip/gorm-oracle" "gorm.io/gorm" ) // GormOracle 初始化oracle数据库 -// 如果需要Oracle库 放开import里的注释 把下方 mysql.Config 改为 oracle.Config ; mysql.New 改为 oracle.New func GormOracle() *gorm.DB { m := global.GVA_CONFIG.Oracle - if m.Dbname == "" { - return nil - } - oracleConfig := mysql.Config{ - DSN: m.Dsn(), // DSN data source name - DefaultStringSize: 191, // string 类型字段的默认长度 - } - if db, err := gorm.Open(mysql.New(oracleConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { - panic(err) - } else { - sqlDB, _ := db.DB() - sqlDB.SetMaxIdleConns(m.MaxIdleConns) - sqlDB.SetMaxOpenConns(m.MaxOpenConns) - return db - } + return initOracleDatabase(m) } // GormOracleByConfig 初始化Oracle数据库用过传入配置 func GormOracleByConfig(m config.Oracle) *gorm.DB { + return initOracleDatabase(m) +} + +// initOracleDatabase 初始化Oracle数据库的辅助函数 +func initOracleDatabase(m config.Oracle) *gorm.DB { if m.Dbname == "" { return nil } - oracleConfig := mysql.Config{ - DSN: m.Dsn(), // DSN data source name - DefaultStringSize: 191, // string 类型字段的默认长度 - } - if db, err := gorm.Open(mysql.New(oracleConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { + // 数据库配置 + general := m.GeneralDB + if db, err := gorm.Open(oracle.Open(m.Dsn()), internal.Gorm.Config(general)); err != nil { panic(err) } else { sqlDB, _ := db.DB() diff --git a/initialize/gorm_pgsql.go b/initialize/gorm_pgsql.go index 2f97917..4d28aeb 100644 --- a/initialize/gorm_pgsql.go +++ b/initialize/gorm_pgsql.go @@ -13,25 +13,16 @@ import ( // Author [SliverHorn](https://github.com/SliverHorn) func GormPgSql() *gorm.DB { p := global.GVA_CONFIG.Pgsql - if p.Dbname == "" { - return nil - } - pgsqlConfig := postgres.Config{ - DSN: p.Dsn(), // DSN data source name - PreferSimpleProtocol: false, - } - if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(p.Prefix, p.Singular)); err != nil { - return nil - } else { - sqlDB, _ := db.DB() - sqlDB.SetMaxIdleConns(p.MaxIdleConns) - sqlDB.SetMaxOpenConns(p.MaxOpenConns) - return db - } + return initPgSqlDatabase(p) } -// GormPgSqlByConfig 初始化 Postgresql 数据库 通过参数 +// GormPgSqlByConfig 初始化 Postgresql 数据库 通过指定参数 func GormPgSqlByConfig(p config.Pgsql) *gorm.DB { + return initPgSqlDatabase(p) +} + +// initPgSqlDatabase 初始化 Postgresql 数据库的辅助函数 +func initPgSqlDatabase(p config.Pgsql) *gorm.DB { if p.Dbname == "" { return nil } @@ -39,7 +30,9 @@ func GormPgSqlByConfig(p config.Pgsql) *gorm.DB { DSN: p.Dsn(), // DSN data source name PreferSimpleProtocol: false, } - if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(p.Prefix, p.Singular)); err != nil { + // 数据库配置 + general := p.GeneralDB + if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(general)); err != nil { panic(err) } else { sqlDB, _ := db.DB() diff --git a/initialize/gorm_sqlite.go b/initialize/gorm_sqlite.go index 4150039..a062b68 100644 --- a/initialize/gorm_sqlite.go +++ b/initialize/gorm_sqlite.go @@ -11,27 +11,23 @@ import ( // GormSqlite 初始化Sqlite数据库 func GormSqlite() *gorm.DB { s := global.GVA_CONFIG.Sqlite - if s.Dbname == "" { - return nil - } - - if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(s.Prefix, s.Singular)); err != nil { - panic(err) - } else { - sqlDB, _ := db.DB() - sqlDB.SetMaxIdleConns(s.MaxIdleConns) - sqlDB.SetMaxOpenConns(s.MaxOpenConns) - return db - } + return initSqliteDatabase(s) } // GormSqliteByConfig 初始化Sqlite数据库用过传入配置 func GormSqliteByConfig(s config.Sqlite) *gorm.DB { + return initSqliteDatabase(s) +} + +// initSqliteDatabase 初始化Sqlite数据库辅助函数 +func initSqliteDatabase(s config.Sqlite) *gorm.DB { if s.Dbname == "" { return nil } - if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(s.Prefix, s.Singular)); err != nil { + // 数据库配置 + general := s.GeneralDB + if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(general)); err != nil { panic(err) } else { sqlDB, _ := db.DB() diff --git a/initialize/init.go b/initialize/init.go new file mode 100644 index 0000000..3c42364 --- /dev/null +++ b/initialize/init.go @@ -0,0 +1,15 @@ +// 假设这是初始化逻辑的一部分 + +package initialize + +import ( + "git.echol.cn/loser/lckt/utils" +) + +// 初始化全局函数 +func SetupHandlers() { + // 注册系统重载处理函数 + utils.GlobalSystemEvents.RegisterReloadHandler(func() error { + return Reload() + }) +} diff --git a/initialize/internal/gorm.go b/initialize/internal/gorm.go index ef1ce29..a447719 100644 --- a/initialize/internal/gorm.go +++ b/initialize/internal/gorm.go @@ -1,12 +1,12 @@ package internal import ( + "time" + "git.echol.cn/loser/lckt/config" - "git.echol.cn/loser/lckt/global" "gorm.io/gorm" "gorm.io/gorm/logger" "gorm.io/gorm/schema" - "time" ) var Gorm = new(_gorm) @@ -15,22 +15,7 @@ type _gorm struct{} // Config gorm 自定义配置 // Author [SliverHorn](https://github.com/SliverHorn) -func (g *_gorm) Config(prefix string, singular bool) *gorm.Config { - var general config.GeneralDB - switch global.GVA_CONFIG.System.DbType { - case "mysql": - general = global.GVA_CONFIG.Mysql.GeneralDB - case "pgsql": - general = global.GVA_CONFIG.Pgsql.GeneralDB - case "oracle": - general = global.GVA_CONFIG.Oracle.GeneralDB - case "sqlite": - general = global.GVA_CONFIG.Sqlite.GeneralDB - case "mssql": - general = global.GVA_CONFIG.Mssql.GeneralDB - default: - general = global.GVA_CONFIG.Mysql.GeneralDB - } +func (g *_gorm) Config(general config.GeneralDB) *gorm.Config { return &gorm.Config{ Logger: logger.New(NewWriter(general), logger.Config{ SlowThreshold: 200 * time.Millisecond, @@ -38,8 +23,8 @@ func (g *_gorm) Config(prefix string, singular bool) *gorm.Config { Colorful: true, }), NamingStrategy: schema.NamingStrategy{ - TablePrefix: prefix, - SingularTable: singular, + TablePrefix: general.Prefix, + SingularTable: general.Singular, }, DisableForeignKeyConstraintWhenMigrating: true, } diff --git a/initialize/mcp.go b/initialize/mcp.go new file mode 100644 index 0000000..c194e12 --- /dev/null +++ b/initialize/mcp.go @@ -0,0 +1,25 @@ +package initialize + +import ( + "git.echol.cn/loser/lckt/global" + mcpTool "git.echol.cn/loser/lckt/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func McpRun() *server.SSEServer { + config := global.GVA_CONFIG.MCP + + s := server.NewMCPServer( + config.Name, + config.Version, + ) + + global.GVA_MCP_SERVER = s + + mcpTool.RegisterAllTools(s) + + return server.NewSSEServer(s, + server.WithSSEEndpoint(config.SSEPath), + server.WithMessageEndpoint(config.MessagePath), + server.WithBaseURL(config.UrlPrefix)) +} diff --git a/initialize/plugin_biz_v1.go b/initialize/plugin_biz_v1.go index d6a3916..f43101b 100644 --- a/initialize/plugin_biz_v1.go +++ b/initialize/plugin_biz_v1.go @@ -2,6 +2,7 @@ package initialize import ( "fmt" + "git.echol.cn/loser/lckt/global" "git.echol.cn/loser/lckt/plugin/email" "git.echol.cn/loser/lckt/utils/plugin" @@ -29,6 +30,7 @@ func bizPluginV1(group ...*gin.RouterGroup) { global.GVA_CONFIG.Email.Nickname, global.GVA_CONFIG.Email.Port, global.GVA_CONFIG.Email.IsSSL, + global.GVA_CONFIG.Email.IsLoginAuth, )) holder(public, private) } diff --git a/initialize/reload.go b/initialize/reload.go new file mode 100644 index 0000000..7f37a57 --- /dev/null +++ b/initialize/reload.go @@ -0,0 +1,45 @@ +package initialize + +import ( + "git.echol.cn/loser/lckt/global" + "go.uber.org/zap" +) + +// Reload 优雅地重新加载系统配置 +func Reload() error { + global.GVA_LOG.Info("正在重新加载系统配置...") + + // 重新加载配置文件 + if err := global.GVA_VP.ReadInConfig(); err != nil { + global.GVA_LOG.Error("重新读取配置文件失败!", zap.Error(err)) + return err + } + + // 重新初始化数据库连接 + if global.GVA_DB != nil { + db, _ := global.GVA_DB.DB() + err := db.Close() + if err != nil { + global.GVA_LOG.Error("关闭原数据库连接失败!", zap.Error(err)) + return err + } + } + + // 重新建立数据库连接 + global.GVA_DB = Gorm() + + // 重新初始化其他配置 + OtherInit() + DBList() + + if global.GVA_DB != nil { + // 确保数据库表结构是最新的 + RegisterTables() + } + + // 重新初始化定时任务 + Timer() + + global.GVA_LOG.Info("系统配置重新加载完成") + return nil +} diff --git a/initialize/router.go b/initialize/router.go index a0efada..30480af 100644 --- a/initialize/router.go +++ b/initialize/router.go @@ -1,19 +1,16 @@ package initialize import ( + "net/http" + "os" + "git.echol.cn/loser/lckt/docs" "git.echol.cn/loser/lckt/global" "git.echol.cn/loser/lckt/middleware" - "git.echol.cn/loser/lckt/plugin/customerservice" - "git.echol.cn/loser/lckt/plugin/picturelibrary" "git.echol.cn/loser/lckt/router" - gc "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" - "net/http" - "os" - "time" ) type justFilesFilesystem struct { @@ -39,21 +36,21 @@ func (fs justFilesFilesystem) Open(name string) (http.File, error) { func Routers() *gin.Engine { Router := gin.New() Router.Use(gin.Recovery()) - Router.Use(middleware.AllCors()) - - Router.Use(gc.New(gc.Config{ - AllowAllOrigins: true, // 允许所有来源 - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"*"}, // 允许所有自定义header - ExposeHeaders: []string{"Content-Length", "Content-Type"}, - AllowCredentials: true, - MaxAge: 12 * time.Hour, - })) - if gin.Mode() == gin.DebugMode { Router.Use(gin.Logger()) } + sseServer := McpRun() + + // 注册mcp服务 + Router.GET(global.GVA_CONFIG.MCP.SSEPath, func(c *gin.Context) { + sseServer.SSEHandler().ServeHTTP(c.Writer, c.Request) + }) + + Router.POST(global.GVA_CONFIG.MCP.MessagePath, func(c *gin.Context) { + sseServer.MessageHandler().ServeHTTP(c.Writer, c.Request) + }) + systemRouter := router.RouterGroupApp.System exampleRouter := router.RouterGroupApp.Example appRouter := router.RouterGroupApp.APP @@ -61,13 +58,13 @@ func Routers() *gin.Engine { // VUE_APP_BASE_API = / // VUE_APP_BASE_PATH = http://localhost // 然后执行打包命令 npm run build。在打开下面3行注释 - // Router.Static("/favicon.ico", "./dist/favicon.ico") + // Router.StaticFile("/favicon.ico", "./dist/favicon.ico") // Router.Static("/assets", "./dist/assets") // dist里面的静态资源 // Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面 Router.StaticFS(global.GVA_CONFIG.Local.StorePath, justFilesFilesystem{http.Dir(global.GVA_CONFIG.Local.StorePath)}) // Router.Use(middleware.LoadTls()) // 如果需要使用https 请打开此中间件 然后前往 core/server.go 将启动模式 更变为 Router.RunTLS("端口","你的cre/pem文件","你的key文件") // 跨域,如需跨域可以打开下面的注释 - // Router.Use(middleware.AllCors()) // 直接放行全部跨域请求 + // Router.Use(middleware.Cors()) // 直接放行全部跨域请求 // Router.Use(middleware.CorsByRules()) // 按照配置的规则放行跨域请求 // global.GVA_LOG.Info("use middleware cors") docs.SwaggerInfo.BasePath = global.GVA_CONFIG.System.RouterPrefix @@ -76,13 +73,13 @@ func Routers() *gin.Engine { // 方便统一添加路由组前缀 多服务器上线使用 PublicGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) - PublicGroup.Use(middleware.AllCors()) // 直接放行全部跨域请求 + PublicGroup.Use(middleware.Cors()) // 直接放行全部跨域请求 PrivateGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) - PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()).Use(middleware.AllCors()) + PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()).Use(middleware.Cors()) AppAuthGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) //AppAuthGroup.Use(middleware.AllCors()) - AppAuthGroup.Use(middleware.UserJWTAuth()).Use(middleware.AllCors()) + AppAuthGroup.Use(middleware.UserJWTAuth()).Use(middleware.Cors()) { // 健康监测 PublicGroup.GET("/health", func(c *gin.Context) { @@ -100,6 +97,7 @@ func Routers() *gin.Engine { systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由 systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由 systemRouter.InitSystemRouter(PrivateGroup) // system相关路由 + systemRouter.InitSysVersionRouter(PrivateGroup) // 发版相关路由 systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由 systemRouter.InitAutoCodeRouter(PrivateGroup, PublicGroup) // 创建自动化代码 systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由 @@ -128,9 +126,6 @@ func Routers() *gin.Engine { // 注册业务路由 initBizRouter(PrivateGroup, PublicGroup, AppAuthGroup) - PluginInit(PublicGroup, customerservice.CreateCustomerServicePlug()) - PluginInit(PrivateGroup, picturelibrary.CreatePictureLibraryPlug()) - global.GVA_ROUTERS = Router.Routes() global.GVA_LOG.Info("router register success") diff --git a/initialize/router_biz.go b/initialize/router_biz.go index 8c1dc6f..33223d7 100644 --- a/initialize/router_biz.go +++ b/initialize/router_biz.go @@ -5,10 +5,12 @@ import ( "github.com/gin-gonic/gin" ) +// 占位方法,保证文件可以正确加载,避免go空变量检测报错,请勿删除。 func holder(routers ...*gin.RouterGroup) { _ = routers _ = router.RouterGroupApp } + func initBizRouter(routers ...*gin.RouterGroup) { privateGroup := routers[0] publicGroup := routers[1] diff --git a/main.go b/main.go index 53a57b4..1a82446 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,6 @@ import ( "git.echol.cn/loser/lckt/core" "git.echol.cn/loser/lckt/global" "git.echol.cn/loser/lckt/initialize" - "git.echol.cn/loser/lckt/utils/wechat" _ "go.uber.org/automaxprocs" "go.uber.org/zap" ) @@ -22,13 +21,22 @@ import ( // @Tag.Description 用户 // @title Gin-Vue-Admin Swagger API接口文档 -// @version v2.8.0 +// @version v2.8.5 // @description 使用gin+vue进行极速开发的全栈开发基础平台 // @securityDefinitions.apikey ApiKeyAuth // @in header // @name x-token // @BasePath / func main() { + // 初始化系统 + initializeSystem() + // 运行服务器 + core.RunServer() +} + +// initializeSystem 初始化系统所有组件 +// 提取为单独函数以便于系统重载时调用 +func initializeSystem() { global.GVA_VP = core.Viper() // 初始化Viper initialize.OtherInit() global.GVA_LOG = core.Zap() // 初始化zap日志库 @@ -36,14 +44,8 @@ func main() { global.GVA_DB = initialize.Gorm() // gorm连接数据库 initialize.Timer() initialize.DBList() + initialize.SetupHandlers() // 注册全局函数 if global.GVA_DB != nil { initialize.RegisterTables() // 初始化表 - // 程序结束前关闭数据库链接 - db, _ := global.GVA_DB.DB() - defer db.Close() } - - wechat.InitWeOfficial() // 初始化微信公众号SDK - wechat.InitWechatPay() // 初始化微信支付SDK - core.RunWindowsServer() } diff --git a/mcp/api_creator.go b/mcp/api_creator.go new file mode 100644 index 0000000..ceb3b06 --- /dev/null +++ b/mcp/api_creator.go @@ -0,0 +1,201 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/service" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + RegisterTool(&ApiCreator{}) +} + +// ApiCreateRequest API创建请求结构 +type ApiCreateRequest struct { + Path string `json:"path"` // API路径 + Description string `json:"description"` // API中文描述 + ApiGroup string `json:"apiGroup"` // API组 + Method string `json:"method"` // HTTP方法 +} + +// ApiCreateResponse API创建响应结构 +type ApiCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + ApiID uint `json:"apiId"` + Path string `json:"path"` + Method string `json:"method"` +} + +// ApiCreator API创建工具 +type ApiCreator struct{} + +// New 创建API创建工具 +func (a *ApiCreator) New() mcp.Tool { + return mcp.NewTool("create_api", + mcp.WithDescription(`创建后端API记录,用于AI编辑器自动添加API接口时自动创建对应的API权限记录。 + +**重要限制:** +- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成API权限,不应调用此工具 +- 仅在以下情况使用:1) 单独创建API(不涉及模块创建);2) AI编辑器自动添加API;3) router下的文件产生路径变化时`), + mcp.WithString("path", + mcp.Required(), + mcp.Description("API路径,如:/user/create"), + ), + mcp.WithString("description", + mcp.Required(), + mcp.Description("API中文描述,如:创建用户"), + ), + mcp.WithString("apiGroup", + mcp.Required(), + mcp.Description("API组名称,用于分类管理,如:用户管理"), + ), + mcp.WithString("method", + mcp.Description("HTTP方法"), + mcp.DefaultString("POST"), + ), + mcp.WithString("apis", + mcp.Description("批量创建API的JSON字符串,格式:[{\"path\":\"/user/create\",\"description\":\"创建用户\",\"apiGroup\":\"用户管理\",\"method\":\"POST\"}]"), + ), + ) +} + +// Handle 处理API创建请求 +func (a *ApiCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + args := request.GetArguments() + + var apis []ApiCreateRequest + + // 检查是否是批量创建 + if apisStr, ok := args["apis"].(string); ok && apisStr != "" { + if err := json.Unmarshal([]byte(apisStr), &apis); err != nil { + return nil, fmt.Errorf("apis 参数格式错误: %v", err) + } + } else { + // 单个API创建 + path, ok := args["path"].(string) + if !ok || path == "" { + return nil, errors.New("path 参数是必需的") + } + + description, ok := args["description"].(string) + if !ok || description == "" { + return nil, errors.New("description 参数是必需的") + } + + apiGroup, ok := args["apiGroup"].(string) + if !ok || apiGroup == "" { + return nil, errors.New("apiGroup 参数是必需的") + } + + method := "POST" + if val, ok := args["method"].(string); ok && val != "" { + method = val + } + + apis = append(apis, ApiCreateRequest{ + Path: path, + Description: description, + ApiGroup: apiGroup, + Method: method, + }) + } + + if len(apis) == 0 { + return nil, errors.New("没有要创建的API") + } + + // 创建API记录 + apiService := service.ServiceGroupApp.SystemServiceGroup.ApiService + var responses []ApiCreateResponse + successCount := 0 + + for _, apiReq := range apis { + api := system.SysApi{ + Path: apiReq.Path, + Description: apiReq.Description, + ApiGroup: apiReq.ApiGroup, + Method: apiReq.Method, + } + + err := apiService.CreateApi(api) + if err != nil { + global.GVA_LOG.Warn("创建API失败", + zap.String("path", apiReq.Path), + zap.String("method", apiReq.Method), + zap.Error(err)) + + responses = append(responses, ApiCreateResponse{ + Success: false, + Message: fmt.Sprintf("创建API失败: %v", err), + Path: apiReq.Path, + Method: apiReq.Method, + }) + } else { + // 获取创建的API ID + var createdApi system.SysApi + err = global.GVA_DB.Where("path = ? AND method = ?", apiReq.Path, apiReq.Method).First(&createdApi).Error + if err != nil { + global.GVA_LOG.Warn("获取创建的API ID失败", zap.Error(err)) + } + + responses = append(responses, ApiCreateResponse{ + Success: true, + Message: fmt.Sprintf("成功创建API %s %s", apiReq.Method, apiReq.Path), + ApiID: createdApi.ID, + Path: apiReq.Path, + Method: apiReq.Method, + }) + successCount++ + } + } + + // 构建总体响应 + var resultMessage string + if len(apis) == 1 { + resultMessage = responses[0].Message + } else { + resultMessage = fmt.Sprintf("批量创建API完成,成功 %d 个,失败 %d 个", successCount, len(apis)-successCount) + } + + result := map[string]interface{}{ + "success": successCount > 0, + "message": resultMessage, + "totalCount": len(apis), + "successCount": successCount, + "failedCount": len(apis) - successCount, + "details": responses, + } + + resultJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + // 添加权限分配提醒 + permissionReminder := "\n\n⚠️ 重要提醒:\n" + + "API创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的API权限," + + "以确保用户能够正常访问新接口。\n" + + "具体步骤:\n" + + "1. 进入角色管理页面\n" + + "2. 选择需要授权的角色\n" + + "3. 在API权限中勾选新创建的API接口\n" + + "4. 保存权限配置" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("API创建结果:\n\n%s%s", string(resultJSON), permissionReminder), + }, + }, + }, nil +} diff --git a/mcp/api_lister.go b/mcp/api_lister.go new file mode 100644 index 0000000..0c39069 --- /dev/null +++ b/mcp/api_lister.go @@ -0,0 +1,165 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "fmt" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + // 注册工具将在enter.go中统一处理 + RegisterTool(&ApiLister{}) +} + +// ApiInfo API信息结构 +type ApiInfo struct { + ID uint `json:"id,omitempty"` // 数据库ID(仅数据库API有) + Path string `json:"path"` // API路径 + Description string `json:"description,omitempty"` // API描述 + ApiGroup string `json:"apiGroup,omitempty"` // API组 + Method string `json:"method"` // HTTP方法 + Source string `json:"source"` // 来源:database 或 gin +} + +// ApiListResponse API列表响应结构 +type ApiListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + DatabaseApis []ApiInfo `json:"databaseApis"` // 数据库中的API + GinApis []ApiInfo `json:"ginApis"` // gin框架中的API + TotalCount int `json:"totalCount"` // 总数量 +} + +// ApiLister API列表工具 +type ApiLister struct{} + +// New 创建API列表工具 +func (a *ApiLister) New() mcp.Tool { + return mcp.NewTool("list_all_apis", + mcp.WithDescription(`获取系统中所有的API接口,分为两组: + +**功能说明:** +- 返回数据库中已注册的API列表 +- 返回gin框架中实际注册的路由API列表 +- 帮助前端判断是使用现有API还是需要创建新的API,如果api在前端未使用且需要前端调用的时候,请到api文件夹下对应模块的js中添加方法并暴露给当前业务调用 + +**返回数据结构:** +- databaseApis: 数据库中的API记录(包含ID、描述、分组等完整信息) +- ginApis: gin路由中的API(仅包含路径和方法),需要AI根据路径自行揣摩路径的业务含义,例如:/api/user/:id 表示根据用户ID获取用户信息`), + ) +} + +// Handle 处理API列表请求 +func (a *ApiLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + + // 获取数据库中的API + databaseApis, err := a.getDatabaseApis() + if err != nil { + global.GVA_LOG.Error("获取数据库API失败", zap.Error(err)) + errorResponse := ApiListResponse{ + Success: false, + Message: "获取数据库API失败: " + err.Error(), + } + resultJSON, _ := json.Marshal(errorResponse) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(resultJSON), + }, + }, + }, nil + } + + // 获取gin路由中的API + ginApis, err := a.getGinApis() + if err != nil { + global.GVA_LOG.Error("获取gin路由API失败", zap.Error(err)) + errorResponse := ApiListResponse{ + Success: false, + Message: "获取gin路由API失败: " + err.Error(), + } + resultJSON, _ := json.Marshal(errorResponse) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(resultJSON), + }, + }, + }, nil + } + + // 构建响应 + response := ApiListResponse{ + Success: true, + Message: "获取API列表成功", + DatabaseApis: databaseApis, + GinApis: ginApis, + TotalCount: len(databaseApis) + len(ginApis), + } + + global.GVA_LOG.Info("API列表获取成功", + zap.Int("数据库API数量", len(databaseApis)), + zap.Int("gin路由API数量", len(ginApis)), + zap.Int("总数量", response.TotalCount)) + + resultJSON, err := json.Marshal(response) + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(resultJSON), + }, + }, + }, nil +} + +// getDatabaseApis 获取数据库中的所有API +func (a *ApiLister) getDatabaseApis() ([]ApiInfo, error) { + var apis []system.SysApi + err := global.GVA_DB.Model(&system.SysApi{}).Order("api_group ASC, path ASC").Find(&apis).Error + if err != nil { + return nil, err + } + + // 转换为ApiInfo格式 + var result []ApiInfo + for _, api := range apis { + result = append(result, ApiInfo{ + ID: api.ID, + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + Source: "database", + }) + } + + return result, nil +} + +// getGinApis 获取gin路由中的所有API(包含被忽略的API) +func (a *ApiLister) getGinApis() ([]ApiInfo, error) { + // 从gin路由信息中获取所有API + var result []ApiInfo + for _, route := range global.GVA_ROUTERS { + result = append(result, ApiInfo{ + Path: route.Path, + Method: route.Method, + Source: "gin", + }) + } + + return result, nil +} diff --git a/mcp/client/client.go b/mcp/client/client.go new file mode 100644 index 0000000..7e5db1e --- /dev/null +++ b/mcp/client/client.go @@ -0,0 +1,39 @@ +package client + +import ( + "context" + "errors" + mcpClient "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" +) + +func NewClient(baseUrl, name, version, serverName string) (*mcpClient.Client, error) { + client, err := mcpClient.NewSSEMCPClient(baseUrl) + if err != nil { + return nil, err + } + + ctx := context.Background() + + // 启动client + if err := client.Start(ctx); err != nil { + return nil, err + } + + // 初始化 + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: name, + Version: version, + } + + result, err := client.Initialize(ctx, initRequest) + if err != nil { + return nil, err + } + if result.ServerInfo.Name != serverName { + return nil, errors.New("server name mismatch") + } + return client, nil +} diff --git a/mcp/client/client_test.go b/mcp/client/client_test.go new file mode 100644 index 0000000..917d22d --- /dev/null +++ b/mcp/client/client_test.go @@ -0,0 +1,132 @@ +package client + +import ( + "context" + "fmt" + "github.com/mark3labs/mcp-go/mcp" + "testing" +) + +// 测试 MCP 客户端连接 +func TestMcpClientConnection(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf(err.Error()) + } +} + +func TestTools(t *testing.T) { + t.Run("currentTime", func(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + request := mcp.CallToolRequest{} + request.Params.Name = "currentTime" + request.Params.Arguments = map[string]interface{}{ + "timezone": "UTC+8", + } + + result, err := c.CallTool(ctx, request) + if err != nil { + t.Fatalf("方法调用错误: %v", err) + } + + if len(result.Content) != 1 { + t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content)) + } + if content, ok := result.Content[0].(mcp.TextContent); ok { + t.Logf("成功返回信息%s", content.Text) + } else { + t.Logf("返回为止类型信息%+v", content) + } + }) + + t.Run("getNickname", func(t *testing.T) { + + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + // Initialize + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: "test-client", + Version: "1.0.0", + } + + _, err = c.Initialize(ctx, initRequest) + if err != nil { + t.Fatalf("初始化失败: %v", err) + } + + request := mcp.CallToolRequest{} + request.Params.Name = "getNickname" + request.Params.Arguments = map[string]interface{}{ + "username": "admin", + } + + result, err := c.CallTool(ctx, request) + if err != nil { + t.Fatalf("方法调用错误: %v", err) + } + + if len(result.Content) != 1 { + t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content)) + } + if content, ok := result.Content[0].(mcp.TextContent); ok { + t.Logf("成功返回信息%s", content.Text) + } else { + t.Logf("返回为止类型信息%+v", content) + } + }) +} + +func TestGetTools(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + toolsRequest := mcp.ListToolsRequest{} + + toolListResult, err := c.ListTools(ctx, toolsRequest) + if err != nil { + t.Fatalf("获取工具列表失败: %v", err) + } + for i := range toolListResult.Tools { + tool := toolListResult.Tools[i] + fmt.Printf("工具名称: %s\n", tool.Name) + fmt.Printf("工具描述: %s\n", tool.Description) + + // 打印参数信息 + if tool.InputSchema.Properties != nil { + fmt.Println("参数列表:") + for paramName, prop := range tool.InputSchema.Properties { + required := "否" + // 检查参数是否在必填列表中 + for _, reqField := range tool.InputSchema.Required { + if reqField == paramName { + required = "是" + break + } + } + fmt.Printf(" - %s (类型: %s, 描述: %s, 必填: %s)\n", + paramName, prop.(map[string]any)["type"], prop.(map[string]any)["description"], required) + } + } else { + fmt.Println("该工具没有参数") + } + fmt.Println("-------------------") + } +} diff --git a/mcp/dictionary_generator.go b/mcp/dictionary_generator.go new file mode 100644 index 0000000..6a04028 --- /dev/null +++ b/mcp/dictionary_generator.go @@ -0,0 +1,310 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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/mcp/dictionary_query.go b/mcp/dictionary_query.go new file mode 100644 index 0000000..73488d0 --- /dev/null +++ b/mcp/dictionary_query.go @@ -0,0 +1,234 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "fmt" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/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 +} diff --git a/mcp/enter.go b/mcp/enter.go new file mode 100644 index 0000000..ca19f54 --- /dev/null +++ b/mcp/enter.go @@ -0,0 +1,31 @@ +package mcpTool + +import ( + "context" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// McpTool 定义了MCP工具必须实现的接口 +type McpTool interface { + // Handle 返回工具调用信息 + Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + // New 返回工具注册信息 + New() mcp.Tool +} + +// 工具注册表 +var toolRegister = make(map[string]McpTool) + +// RegisterTool 供工具在init时调用,将自己注册到工具注册表中 +func RegisterTool(tool McpTool) { + mcpTool := tool.New() + toolRegister[mcpTool.Name] = tool +} + +// RegisterAllTools 将所有注册的工具注册到MCP服务中 +func RegisterAllTools(mcpServer *server.MCPServer) { + for _, tool := range toolRegister { + mcpServer.AddTool(tool.New(), tool.Handle) + } +} diff --git a/mcp/execution_plan_schema.md b/mcp/execution_plan_schema.md new file mode 100644 index 0000000..622e83d --- /dev/null +++ b/mcp/execution_plan_schema.md @@ -0,0 +1,529 @@ +# ExecutionPlan 结构体格式说明 + +## 概述 +ExecutionPlan 是用于自动化模块创建的执行计划结构体,包含了创建包和模块所需的所有信息。 + +## 完整结构体定义 + +```go +type ExecutionPlan struct { + PackageName string `json:"packageName"` // 包名,如:"user", "order", "product" + PackageType string `json:"packageType"` // "plugin" 或 "package" + NeedCreatedPackage bool `json:"needCreatedPackage"` // 是否需要创建包 + NeedCreatedModules bool `json:"needCreatedModules"` // 是否需要创建模块 + PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` // 包信息(当NeedCreatedPackage=true时必需) + ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 模块信息数组(当NeedCreatedModules=true时必需,支持批量创建) + Paths map[string]string `json:"paths,omitempty"` // 路径信息 +} +``` + +## 子结构体详细说明 + +### 1. SysAutoCodePackageCreate 结构体 + +```go +type SysAutoCodePackageCreate struct { + Desc string `json:"desc"` // 描述,如:"用户管理模块" + Label string `json:"label"` // 展示名,如:"用户管理" + Template string `json:"template"` // 模板类型:"plugin" 或 "package" + PackageName string `json:"packageName"` // 包名,如:"user" + Module string `json:"-"` // 模块名(自动填充,无需设置) +} +``` + +### 2. AutoCode 结构体(核心字段) + +```go +type AutoCode struct { + Package string `json:"package"` // 包名 + TableName string `json:"tableName"` // 数据库表名 + BusinessDB string `json:"businessDB"` // 业务数据库名 + StructName string `json:"structName"` // 结构体名称 + PackageName string `json:"packageName"` // 文件名称 + Description string `json:"description"` // 结构体中文名称 + Abbreviation string `json:"abbreviation"` // 结构体简称 + HumpPackageName string `json:"humpPackageName"` // 驼峰命名的包名 + GvaModel bool `json:"gvaModel"` // 是否使用GVA默认Model + AutoMigrate bool `json:"autoMigrate"` // 是否自动迁移表结构 + AutoCreateResource bool `json:"autoCreateResource"` // 是否自动创建资源标识 + AutoCreateApiToSql bool `json:"autoCreateApiToSql"` // 是否自动创建API + AutoCreateMenuToSql bool `json:"autoCreateMenuToSql"` // 是否自动创建菜单 + AutoCreateBtnAuth bool `json:"autoCreateBtnAuth"` // 是否自动创建按钮权限 + OnlyTemplate bool `json:"onlyTemplate"` // 是否只生成模板 + IsTree bool `json:"isTree"` // 是否树形结构 + TreeJson string `json:"treeJson"` // 树形结构JSON字段 + IsAdd bool `json:"isAdd"` // 是否新增 + Fields []*AutoCodeField `json:"fields"` // 字段列表 + GenerateWeb bool `json:"generateWeb"` // 是否生成前端代码 + GenerateServer bool `json:"generateServer"` // 是否生成后端代码 + Module string `json:"-"` // 模块(自动填充) + DictTypes []string `json:"-"` // 字典类型(自动填充) +} +``` + +### 3. AutoCodeField 结构体(字段定义) + +```go +type AutoCodeField struct { + FieldName string `json:"fieldName"` // 字段名 + FieldDesc string `json:"fieldDesc"` // 字段中文描述 + FieldType string `json:"fieldType"` // 字段类型:string, int, bool, time.Time等 + FieldJson string `json:"fieldJson"` // JSON标签名 + DataTypeLong string `json:"dataTypeLong"` // 数据库字段长度 + Comment string `json:"comment"` // 数据库字段注释 + ColumnName string `json:"columnName"` // 数据库列名 + FieldSearchType string `json:"fieldSearchType"` // 搜索类型:EQ, LIKE, BETWEEN等 + FieldSearchHide bool `json:"fieldSearchHide"` // 是否隐藏查询条件 + DictType string `json:"dictType"` // 字典类型 + Form bool `json:"form"` // 是否在表单中显示 + Table bool `json:"table"` // 是否在表格中显示 + Desc bool `json:"desc"` // 是否在详情中显示 + Excel bool `json:"excel"` // 是否支持导入导出 + Require bool `json:"require"` // 是否必填 + DefaultValue string `json:"defaultValue"` // 默认值 + ErrorText string `json:"errorText"` // 校验失败提示 + Clearable bool `json:"clearable"` // 是否可清空 + Sort bool `json:"sort"` // 是否支持排序 + PrimaryKey bool `json:"primaryKey"` // 是否主键 + DataSource *DataSource `json:"dataSource"` // 数据源配置(用于关联其他表) + CheckDataSource bool `json:"checkDataSource"` // 是否检查数据源 + FieldIndexType string `json:"fieldIndexType"` // 索引类型 +} +``` + +### 4. DataSource 结构体(关联表配置) + +```go +type DataSource struct { + DBName string `json:"dbName"` // 关联的数据库名称 + Table string `json:"table"` // 关联的表名 + Label string `json:"label"` // 用于显示的字段名(如name、title等) + Value string `json:"value"` // 用于存储的值字段名(通常是id) + Association int `json:"association"` // 关联关系:1=一对一,2=一对多 + HasDeletedAt bool `json:"hasDeletedAt"` // 关联表是否有软删除字段 +} +``` + +## 使用示例 + +### 示例1:创建新包和批量创建多个模块 + +```json +{ + "packageName": "user", + "packageType": "package", + "needCreatedPackage": true, + "needCreatedModules": true, + "packageInfo": { + "desc": "用户管理模块", + "label": "用户管理", + "template": "package", + "packageName": "user" + }, + "modulesInfo": [ + { + "package": "user", + "tableName": "sys_users", + "businessDB": "", + "structName": "User", + "packageName": "user", + "description": "用户", + "abbreviation": "user", + "humpPackageName": "user", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "treeJson": "", + "isAdd": true, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "Username", + "fieldDesc": "用户名", + "fieldType": "string", + "fieldJson": "username", + "dataTypeLong": "50", + "comment": "用户名", + "columnName": "username", + "fieldSearchType": "LIKE", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请输入用户名", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": { + "dbName": "gva", + "table": "sys_users", + "label": "username", + "value": "id", + "association": 2, + "hasDeletedAt": true + }, + "checkDataSource": true, + "fieldIndexType": "" + }, + { + "fieldName": "Email", + "fieldDesc": "邮箱", + "fieldType": "string", + "fieldJson": "email", + "dataTypeLong": "100", + "comment": "邮箱地址", + "columnName": "email", + "fieldSearchType": "EQ", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请输入邮箱", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": null, + "checkDataSource": false, + "fieldIndexType": "index" + } + ] + }, + { + "package": "user", + "tableName": "user_profiles", + "businessDB": "", + "structName": "UserProfile", + "packageName": "user", + "description": "用户档案", + "abbreviation": "userProfile", + "humpPackageName": "user", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "treeJson": "", + "isAdd": true, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "UserID", + "fieldDesc": "用户ID", + "fieldType": "int", + "fieldJson": "userId", + "dataTypeLong": "", + "comment": "关联用户ID", + "columnName": "user_id", + "fieldSearchType": "EQ", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请选择用户", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": null, + "checkDataSource": false, + "fieldIndexType": "index" + }, + { + "fieldName": "Avatar", + "fieldDesc": "头像", + "fieldType": "string", + "fieldJson": "avatar", + "dataTypeLong": "255", + "comment": "用户头像URL", + "columnName": "avatar", + "fieldSearchType": "", + "fieldSearchHide": true, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": false, + "require": false, + "defaultValue": "", + "errorText": "", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": null, + "checkDataSource": false, + "fieldIndexType": "" + } + ] + } + ] +} +``` + +### 示例2:仅在现有包中批量创建多个模块 + +```json +{ + "packageName": "system", + "packageType": "package", + "needCreatedPackage": false, + "needCreatedModules": true, + "packageInfo": null, + "modulesInfo": [ + { + "package": "system", + "tableName": "sys_roles", + "businessDB": "", + "structName": "Role", + "packageName": "system", + "description": "角色", + "abbreviation": "role", + "humpPackageName": "system", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "RoleName", + "fieldDesc": "角色名称", + "fieldType": "string", + "fieldJson": "roleName", + "dataTypeLong": "50", + "comment": "角色名称", + "columnName": "role_name", + "fieldSearchType": "LIKE", + "form": true, + "table": true, + "desc": true, + "require": true + } + ] + }, + { + "package": "system", + "tableName": "sys_permissions", + "businessDB": "", + "structName": "Permission", + "packageName": "system", + "description": "权限", + "abbreviation": "permission", + "humpPackageName": "system", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "PermissionName", + "fieldDesc": "权限名称", + "fieldType": "string", + "fieldJson": "permissionName", + "dataTypeLong": "100", + "comment": "权限名称", + "columnName": "permission_name", + "fieldSearchType": "LIKE", + "form": true, + "table": true, + "desc": true, + "require": true + }, + { + "fieldName": "PermissionCode", + "fieldDesc": "权限代码", + "fieldType": "string", + "fieldJson": "permissionCode", + "dataTypeLong": "50", + "comment": "权限代码", + "columnName": "permission_code", + "fieldSearchType": "=", + "form": true, + "table": true, + "desc": true, + "require": true + } + ] + } + ] +} +``` + +### 示例3:模块关联关系配置详解 + +以下示例展示了如何配置不同类型的关联关系: + +```json +{ + "packageName": "order", + "packageType": "package", + "needCreatedPackage": true, + "needCreatedModules": true, + "packageInfo": { + "desc": "订单管理模块", + "label": "订单管理", + "template": "package", + "packageName": "order" + }, + "modulesInfo": [ + { + "package": "order", + "tableName": "orders", + "structName": "Order", + "packageName": "order", + "description": "订单", + "abbreviation": "order", + "humpPackageName": "order", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "UserID", + "fieldDesc": "下单用户", + "fieldType": "uint", + "fieldJson": "userId", + "columnName": "user_id", + "fieldSearchType": "EQ", + "form": true, + "table": true, + "desc": true, + "require": true, + "dataSource": { + "dbName": "gva", + "table": "sys_users", + "label": "username", + "value": "id", + "association": 2, + "hasDeletedAt": true + }, + "checkDataSource": true + }, + { + "fieldName": "ProductID", + "fieldDesc": "商品", + "fieldType": "uint", + "fieldJson": "productId", + "columnName": "product_id", + "fieldSearchType": "EQ", + "form": true, + "table": true, + "desc": true, + "require": true, + "dataSource": { + "dbName": "gva", + "table": "products", + "label": "name", + "value": "id", + "association": 2, + "hasDeletedAt": false + }, + "checkDataSource": true + }, + { + "fieldName": "Status", + "fieldDesc": "订单状态", + "fieldType": "int", + "fieldJson": "status", + "columnName": "status", + "fieldSearchType": "EQ", + "form": true, + "table": true, + "desc": true, + "require": true, + "dictType": "order_status" + } + ] + } + ] +} +``` + +## DataSource 配置说明 + +### 关联关系类型 +- **association: 1** - 一对一关联(如用户与用户档案) +- **association: 2** - 一对多关联(如用户与订单) + +### 配置要点 +1. **dbName**: 通常为 "gva"(默认数据库) +2. **table**: 关联表的实际表名 +3. **label**: 用于前端显示的字段(如用户名、商品名称) +4. **value**: 用于存储关联ID的字段(通常是 "id") +5. **hasDeletedAt**: 关联表是否支持软删除 +6. **checkDataSource**: 建议设为true,会验证关联表是否存在 + +### 常见关联场景 +- 用户关联:`{"table": "sys_users", "label": "username", "value": "id"}` +- 角色关联:`{"table": "sys_authorities", "label": "authorityName", "value": "authorityId"}` +- 部门关联:`{"table": "sys_departments", "label": "name", "value": "id"}` +- 分类关联:`{"table": "categories", "label": "name", "value": "id"}` + +## 重要注意事项 + +1. **PackageType**: 只能是 "plugin" 或 "package" +2. **NeedCreatedPackage**: 当为true时,PackageInfo必须提供 +3. **NeedCreatedModules**: 当为true时,ModulesInfo必须提供 +4. **字段类型**: FieldType支持的类型包括: + - string(字符串) + - richtext(富文本) + - int(整型) + - bool(布尔值) + - float64(浮点型) + - time.Time(时间) + - enum(枚举) + - picture(单图片,字符串) + - pictures(多图片,json字符串) + - video(视频,字符串) + - file(文件,json字符串) + - json(JSON) + - array(数组) +5. **搜索类型**: FieldSearchType支持:EQ, NE, GT, GE, LT, LE, LIKE, BETWEEN等 +6. **索引类型**: FieldIndexType支持:index, unique等 +7. **GvaModel**: 设置为true时会自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段 +8. **关联配置**: 使用dataSource时,确保关联表已存在,建议开启checkDataSource验证 + +## 常见错误避免 + +1. 确保PackageName和ModuleName符合Go语言命名规范 +2. 字段名使用大写开头的驼峰命名 +3. JSON标签使用小写开头的驼峰命名 +4. 数据库列名使用下划线分隔的小写命名 +5. 必填字段不要遗漏 +6. 字段类型要与实际需求匹配 \ No newline at end of file diff --git a/mcp/gag_usage_example.md b/mcp/gag_usage_example.md new file mode 100644 index 0000000..cfc7fdc --- /dev/null +++ b/mcp/gag_usage_example.md @@ -0,0 +1,205 @@ +# GAG工具使用示例 - 带用户确认流程 + +## 新的工作流程 + +现在GAG工具支持三步工作流程: +1. `analyze` - 分析现有模块信息 +2. `confirm` - 请求用户确认创建计划 +3. `execute` - 执行创建操作(需要用户确认) + +## 使用示例 + +### 第一步:分析 +```json +{ + "action": "analyze", + "requirement": "创建一个图书管理功能" +} +``` + +### 第二步:确认(支持批量创建多个模块) +```json +{ + "action": "confirm", + "executionPlan": { + "packageName": "library", + "packageType": "package", + "needCreatedPackage": true, + "needCreatedModules": true, + "packageInfo": { + "desc": "图书管理包", + "label": "图书管理", + "template": "package", + "packageName": "library" + }, + "modulesInfo": [ + { + "package": "library", + "tableName": "library_books", + "businessDB": "", + "structName": "Book", + "packageName": "library", + "description": "图书信息", + "abbreviation": "book", + "humpPackageName": "Library", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "treeJson": "", + "isAdd": false, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "title", + "fieldDesc": "书名", + "fieldType": "string", + "fieldJson": "title", + "dataTypeLong": "255", + "comment": "书名", + "columnName": "title", + "fieldSearchType": "LIKE", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请输入书名", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": {}, + "checkDataSource": false, + "fieldIndexType": "" + }, + { + "fieldName": "AuthorID", + "fieldDesc": "作者", + "fieldType": "uint", + "fieldJson": "authorId", + "dataTypeLong": "", + "comment": "作者ID", + "columnName": "author_id", + "fieldSearchType": "EQ", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请选择作者", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": { + "dbName": "gva", + "table": "library_authors", + "label": "name", + "value": "id", + "association": 2, + "hasDeletedAt": true + }, + "checkDataSource": true, + "fieldIndexType": "" + } + ] + }, + { + "package": "library", + "tableName": "library_authors", + "businessDB": "", + "structName": "Author", + "packageName": "library", + "description": "作者信息", + "abbreviation": "author", + "humpPackageName": "Library", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": true, + "onlyTemplate": false, + "isTree": false, + "treeJson": "", + "isAdd": false, + "generateWeb": true, + "generateServer": true, + "fields": [ + { + "fieldName": "name", + "fieldDesc": "作者姓名", + "fieldType": "string", + "fieldJson": "name", + "dataTypeLong": "100", + "comment": "作者姓名", + "columnName": "name", + "fieldSearchType": "LIKE", + "fieldSearchHide": false, + "dictType": "", + "form": true, + "table": true, + "desc": true, + "excel": true, + "require": true, + "defaultValue": "", + "errorText": "请输入作者姓名", + "clearable": true, + "sort": false, + "primaryKey": false, + "dataSource": {}, + "checkDataSource": false, + "fieldIndexType": "" + } + ] + } + ] + } +} +``` + +### 第三步:执行(需要确认参数) +```json +{ + "action": "execute", + "executionPlan": { + // ... 同上面的executionPlan + }, + "packageConfirm": "yes", // 确认创建包 + "modulesConfirm": "yes" // 确认创建模块 +} +``` + +## 确认参数说明 + +- `packageConfirm`: 当`needCreatedPackage`为true时必需 + - "yes": 确认创建包 + - "no": 取消创建包(停止后续处理) + +- `modulesConfirm`: 当`needCreatedModules`为true时必需 + - "yes": 确认创建模块 + - "no": 取消创建模块(停止后续处理) + +## 取消操作的行为 + +1. 如果用户在`packageConfirm`中选择"no",系统将停止所有后续处理 +2. 如果用户在`modulesConfirm`中选择"no",系统将停止模块创建 +3. 任何取消操作都会返回相应的取消消息,不会执行任何创建操作 + +## 注意事项 + +1. 必须先调用`confirm`来获取确认信息 +2. 在`execute`时必须提供相应的确认参数 +3. 确认参数的值必须是"yes"或"no" +4. 如果不需要创建包或模块,则不需要提供对应的确认参数 +5. 字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组) \ No newline at end of file diff --git a/mcp/gva_auto_generate.go b/mcp/gva_auto_generate.go new file mode 100644 index 0000000..627ed29 --- /dev/null +++ b/mcp/gva_auto_generate.go @@ -0,0 +1,1755 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + "time" + "unicode" + + "git.echol.cn/loser/lckt/global" + common "git.echol.cn/loser/lckt/model/common/request" + model "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/service" + systemService "git.echol.cn/loser/lckt/service/system" + "github.com/mark3labs/mcp-go/mcp" + "gorm.io/gorm" +) + +func init() { + RegisterTool(&AutomationModuleAnalyzer{}) +} + +type AutomationModuleAnalyzer struct{} + +// ModuleInfo 模块信息 +type ModuleInfo struct { + ID uint `json:"id"` + PackageName string `json:"packageName"` + Label string `json:"label"` + Desc string `json:"desc"` + Template string `json:"template"` // "plugin" 或 "package" + Module string `json:"module"` +} + +// HistoryInfo 历史记录信息 +type HistoryInfo struct { + ID uint `json:"id"` + StructName string `json:"structName"` + TableName string `json:"tableName"` + PackageName string `json:"packageName"` + BusinessDB string `json:"businessDB"` + Description string `json:"description"` + Abbreviation string `json:"abbreviation"` + CreatedAt string `json:"createdAt"` +} + +// PredesignedModuleInfo 预设计模块信息 +type PredesignedModuleInfo struct { + PackageName string `json:"packageName"` + PackageType string `json:"packageType"` // "plugin" 或 "package" + ModuleName string `json:"moduleName"` + Path string `json:"path"` + Modules []string `json:"modules"` // 包含的模块列表(如api、model、service等) + Description string `json:"description"` + StructName string `json:"structName,omitempty"` // 主要结构体名称 +} + +// AnalysisResponse 分析响应 +type AnalysisResponse struct { + Packages []ModuleInfo `json:"packages"` + History []HistoryInfo `json:"history"` + PredesignedModules []PredesignedModuleInfo `json:"predesignedModules"` + Message string `json:"message"` +} + +// ExecutionPlan 执行计划 - 支持批量创建 +type ExecutionPlan struct { + PackageName string `json:"packageName"` + PackageType string `json:"packageType"` // "plugin" 或 "package" + NeedCreatedPackage bool `json:"needCreatedPackage"` + NeedCreatedModules bool `json:"needCreatedModules"` + PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` + ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 改为数组支持多个模块 + Paths map[string]string `json:"paths,omitempty"` +} + +// ExecutionResult 执行结果 +type ExecutionResult struct { + Success bool `json:"success"` + Message string `json:"message"` + PackageID uint `json:"packageId,omitempty"` + HistoryID uint `json:"historyId,omitempty"` + Paths map[string]string `json:"paths,omitempty"` + NextActions []string `json:"nextActions,omitempty"` +} + +// ConfirmationRequest 确认请求结构 +type ConfirmationRequest struct { + PackageName string `json:"packageName"` + ModuleName string `json:"moduleName"` + NeedCreatedPackage bool `json:"needCreatedPackage"` + NeedCreatedModules bool `json:"needCreatedModules"` + PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` + ModulesInfo *request.AutoCode `json:"modulesInfo,omitempty"` +} + +// ConfirmationResponse 确认响应结构 +type ConfirmationResponse struct { + Message string `json:"message"` + PackageConfirm bool `json:"packageConfirm"` + ModulesConfirm bool `json:"modulesConfirm"` + CanProceed bool `json:"canProceed"` + ConfirmationKey string `json:"confirmationKey"` +} + +// New 返回工具注册信息 +func (t *AutomationModuleAnalyzer) New() mcp.Tool { + return mcp.NewTool("gva_auto_generate", + mcp.WithDescription(`**🔧 核心执行工具:接收requirement_analyzer分析结果,执行具体的模块创建操作** + +**工作流位置:** +- **第二优先级**:在requirement_analyzer之后使用 +- **接收输入**:来自requirement_analyzer的1xxx2xxx格式分析结果 +- **执行操作**:根据分析结果创建完整模块、包、功能模块 + +**批量创建功能:** +- 支持在单个ExecutionPlan中创建多个模块 +- modulesInfo字段为数组,可包含多个模块配置 +- 一次性处理多个模块的创建和字典生成 +- 与requirement_analyzer配合实现完整工作流 + +分步骤分析自动化模块:1) 分析现有模块信息供AI选择 2) 请求用户确认 3) 根据确认结果执行创建操作 + +**新功能:自动字典创建** +- 当结构体字段使用了字典类型(dictType不为空)时,系统会自动检查字典是否存在 +- 如果字典不存在,会自动创建对应的字典及默认的字典详情项 +- 字典创建包括:字典主表记录和默认的选项值(选项1、选项2等) + +**推荐工作流:** +1. 用户提出需求 → requirement_analyzer(最高优先级) +2. AI分析需求为1xxx2xxx格式 → gva_auto_generate(执行创建) +3. 创建完成后,根据需要使用其他辅助工具 + +**重要限制:** +- 当needCreatedModules=true时,模块创建会自动生成API和菜单,因此不应再调用api_creator和menu_creator工具 +- 只有在单独创建API或菜单(不涉及模块创建)时才使用api_creator和menu_creator工具 + +重要:ExecutionPlan结构体格式要求(支持批量创建): +{ + "packageName": "包名(string)", + "packageType": "package或plugin(string)", + "needCreatedPackage": "是否需要创建包(bool)", + "needCreatedModules": "是否需要创建模块(bool)", + "packageInfo": { + "desc": "描述(string)", + "label": "展示名(string)", + "template": "package或plugin(string)", + "packageName": "包名(string)" + }, + "modulesInfo": [{ + "package": "包名(string)", + "tableName": "数据库表名(string)", + "businessDB": "业务数据库(string)", + "structName": "结构体名(string)", + "packageName": "文件名称(string)", + "description": "中文描述(string)", + "abbreviation": "简称(string)", + "humpPackageName": "文件名称 一般是结构体名的小驼峰(string)", + "gvaModel": "是否使用GVA模型(bool) 固定为true 后续不需要创建ID created_at deleted_at updated_at", + "autoMigrate": "是否自动迁移(bool)", + "autoCreateResource": "是否创建资源(bool)", + "autoCreateApiToSql": "是否创建API(bool)", + "autoCreateMenuToSql": "是否创建菜单(bool)", + "autoCreateBtnAuth": "是否创建按钮权限(bool)", + "onlyTemplate": "是否仅模板(bool)", + "isTree": "是否树形结构(bool)", + "treeJson": "树形JSON字段(string)", + "isAdd": "是否新增(bool) 固定为false", + "generateWeb": "是否生成前端(bool)", + "generateServer": "是否生成后端(bool)", + "fields": [{ + "fieldName": "字段名(string)必须大写开头", + "fieldDesc": "字段描述(string)", + "fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)", + "fieldJson": "JSON标签(string 必须是小驼峰命名,例:userName)", + "dataTypeLong": "数据长度(string)", + "comment": "注释(string)", + "columnName": "数据库列名(string)", + "fieldSearchType": "搜索类型:=/>/=/<=/NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN等(string)", + "fieldSearchHide": "是否隐藏搜索(bool)", + "dictType": "字典类型(string)", + "form": "表单显示(bool)", + "table": "表格显示(bool)", + "desc": "详情显示(bool)", + "excel": "导入导出(bool)", + "require": "是否必填(bool)", + "defaultValue": "默认值(string),JSON类型(array,json,file,pictures)请保持为空他们不可以设置默认值", + "errorText": "错误提示(string)", + "clearable": "是否可清空(bool)", + "sort": "是否排序(bool)", + "primaryKey": "是否主键(bool)", + "dataSource": "数据源配置(object) - 用于配置字段的关联表信息,结构:{\"dbName\":\"数据库名\",\"table\":\"关联表名\",\"label\":\"显示字段\",\"value\":\"值字段\",\"association\":1或2(1=一对一,2=一对多),\"hasDeletedAt\":true/false}。\n\n**获取表名提示:**\n- 可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名\n- 例如:SysUser 的表名为 \"sys_users\",ExaFileUploadAndDownload 的表名为 \"exa_file_upload_and_downloads\"\n- 插件模块示例:Info 的表名为 \"gva_announcements_info\"\n\n**获取数据库名提示:**\n- 主数据库:通常使用 \"gva\"(默认数据库标识)\n- 多数据库:可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段\n- 如果用户未提及关联多数据库信息 则使用默认数据库 默认数据库的情况下 dbName此处填写为空", + "checkDataSource": "是否检查数据源(bool) - 启用后会验证关联表的存在性", + "fieldIndexType": "索引类型(string)" + }] + }, { + "package": "包名(string)", + "tableName": "第二个模块的表名(string)", + "structName": "第二个模块的结构体名(string)", + "description": "第二个模块的描述(string)", + "...": "更多模块配置..." + }] +} + +注意: +1. needCreatedPackage=true时packageInfo必需 +2. needCreatedModules=true时modulesInfo必需 +3. packageType只能是"package"或"plugin" +4. 字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组) +5. 搜索类型支持:=,!=,>,>=,<,<=,NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN +6. gvaModel=true时自动包含ID,CreatedAt,UpdatedAt,DeletedAt字段 +7. **重要**:当gvaModel=false时,必须有一个字段的primaryKey=true,否则会导致PrimaryField为nil错误 +8. **重要**:当gvaModel=true时,系统会自动设置ID字段为主键,无需手动设置primaryKey=true +9. 智能字典创建功能:当字段使用字典类型(DictType)时,系统会: + - 自动检查字典是否存在,如果不存在则创建字典 + - 根据字典类型和字段描述智能生成默认选项,支持状态、性别、类型、等级、优先级、审批、角色、布尔值、订单、颜色、尺寸等常见场景 + - 为无法识别的字典类型提供通用默认选项 +10. **模块关联配置**:当需要配置模块间的关联关系时,使用dataSource字段: + - **dbName**: 关联的数据库名称 + - **table**: 关联的表名 + - **label**: 用于显示的字段名(如name、title等) + - **value**: 用于存储的值字段名(通常是id) + - **association**: 关联关系类型(1=一对一关联,2=一对多关联) + - **hasDeletedAt**: 关联表是否有软删除字段 + - **checkDataSource**: 设为true时会验证关联表的存在性 + - 示例:{"dbName":"gva","table":"sys_users","label":"username","value":"id","association":2,"hasDeletedAt":true}`), + mcp.WithString("action", + mcp.Required(), + mcp.Description("执行操作:'analyze' 分析现有模块信息,'confirm' 请求用户确认创建,'execute' 执行创建操作(支持批量创建多个模块)"), + ), + mcp.WithString("requirement", + mcp.Description("用户需求描述(action=analyze时必需)"), + ), + mcp.WithObject("executionPlan", + mcp.Description("执行计划(action=confirm或execute时必需,必须严格按照上述格式提供完整的JSON对象)"), + ), + mcp.WithString("packageConfirm", + mcp.Description("用户对创建包的确认(action=execute时,如果需要创建包则必需):'yes' 或 'no'"), + ), + mcp.WithString("modulesConfirm", + mcp.Description("用户对创建模块的确认(action=execute时,如果需要创建模块则必需):'yes' 或 'no'"), + ), + ) +} + +// scanPredesignedModules 扫描预设计的模块 +func (t *AutomationModuleAnalyzer) scanPredesignedModules() ([]PredesignedModuleInfo, error) { + var predesignedModules []PredesignedModuleInfo + + // 获取autocode配置路径 + if global.GVA_CONFIG.AutoCode.Root == "" { + return predesignedModules, nil // 配置不存在时返回空列表,不报错 + } + + serverPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server) + + // 扫描plugin目录下的各个插件模块 + pluginPath := filepath.Join(serverPath, "plugin") + if pluginModules, err := t.scanPluginModules(pluginPath); err == nil { + predesignedModules = append(predesignedModules, pluginModules...) + } + + // 扫描model目录下的各个包模块 + modelPath := filepath.Join(serverPath, "model") + if packageModules, err := t.scanPackageModules(modelPath); err == nil { + predesignedModules = append(predesignedModules, packageModules...) + } + + return predesignedModules, nil +} + +// scanPluginModules 扫描plugin目录下的各个插件模块 +func (t *AutomationModuleAnalyzer) scanPluginModules(pluginPath string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + if _, err := os.Stat(pluginPath); os.IsNotExist(err) { + return modules, nil + } + + entries, err := os.ReadDir(pluginPath) + if err != nil { + return modules, err + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + pluginName := entry.Name() + pluginDir := filepath.Join(pluginPath, pluginName) + + // 扫描插件下的model目录,查找具体的模块文件 + modelDir := filepath.Join(pluginDir, "model") + if _, err := os.Stat(modelDir); err == nil { + if pluginModules, err := t.scanModuleFiles(modelDir, pluginName, "plugin"); err == nil { + modules = append(modules, pluginModules...) + } + } + } + + return modules, nil +} + +// scanPackageModules 扫描model目录下的各个包模块 +func (t *AutomationModuleAnalyzer) scanPackageModules(modelPath string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + if _, err := os.Stat(modelPath); os.IsNotExist(err) { + return modules, nil + } + + entries, err := os.ReadDir(modelPath) + if err != nil { + return modules, err + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + packageName := entry.Name() + // 跳过一些系统目录 + if packageName == "common" || packageName == "request" || packageName == "response" { + continue + } + + packageDir := filepath.Join(modelPath, packageName) + + // 扫描包目录下的模块文件 + if packageModules, err := t.scanModuleFiles(packageDir, packageName, "package"); err == nil { + modules = append(modules, packageModules...) + } + } + + return modules, nil +} + +// scanModuleFiles 扫描目录下的Go文件,识别具体的模块 +func (t *AutomationModuleAnalyzer) scanModuleFiles(dir, packageName, packageType string) ([]PredesignedModuleInfo, error) { + var modules []PredesignedModuleInfo + + entries, err := os.ReadDir(dir) + if err != nil { + return modules, err + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + fileName := entry.Name() + if !strings.HasSuffix(fileName, ".go") { + continue + } + + // 跳过一些非模块文件 + if strings.HasSuffix(fileName, "_test.go") || + fileName == "enter.go" || + fileName == "request.go" || + fileName == "response.go" { + continue + } + + filePath := filepath.Join(dir, fileName) + moduleName := strings.TrimSuffix(fileName, ".go") + + // 分析模块文件,提取结构体信息 + if moduleInfo, err := t.analyzeModuleFile(filePath, packageName, moduleName, packageType); err == nil { + modules = append(modules, *moduleInfo) + } + } + + return modules, nil +} + +// analyzeModuleFile 分析具体的模块文件 +func (t *AutomationModuleAnalyzer) analyzeModuleFile(filePath, packageName, moduleName, packageType string) (*PredesignedModuleInfo, error) { + content, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + fileContent := string(content) + + // 提取结构体名称和描述 + structNames := t.extractStructNames(fileContent) + description := t.extractModuleDescription(fileContent, moduleName) + + // 确定主要结构体名称 + mainStruct := moduleName + if len(structNames) > 0 { + // 优先选择与文件名相关的结构体 + for _, structName := range structNames { + if strings.Contains(strings.ToLower(structName), strings.ToLower(moduleName)) { + mainStruct = structName + break + } + } + if mainStruct == moduleName && len(structNames) > 0 { + mainStruct = structNames[0] // 如果没有匹配的,使用第一个 + } + } + + return &PredesignedModuleInfo{ + PackageName: packageName, + PackageType: packageType, + ModuleName: moduleName, + Path: filePath, + Modules: structNames, + Description: description, + StructName: mainStruct, + }, nil +} + +// extractStructNames 从文件内容中提取结构体名称 +func (t *AutomationModuleAnalyzer) extractStructNames(content string) []string { + var structNames []string + lines := strings.Split(content, "\n") + + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "type ") && strings.Contains(line, " struct") { + // 提取结构体名称 + parts := strings.Fields(line) + if len(parts) >= 3 && parts[2] == "struct" { + structNames = append(structNames, parts[1]) + } + } + } + + return structNames +} + +// extractModuleDescription 从文件内容中提取模块描述 +func (t *AutomationModuleAnalyzer) extractModuleDescription(content, moduleName string) string { + lines := strings.Split(content, "\n") + + // 查找package注释 + for i, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "package ") { + // 向上查找注释 + for j := i - 1; j >= 0; j-- { + commentLine := strings.TrimSpace(lines[j]) + if strings.HasPrefix(commentLine, "//") { + comment := strings.TrimSpace(strings.TrimPrefix(commentLine, "//")) + if comment != "" && len(comment) > 5 { + return comment + } + } else if commentLine != "" { + break + } + } + break + } + } + + // 查找结构体注释 + for i, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "type ") && strings.Contains(line, " struct") { + // 向上查找注释 + for j := i - 1; j >= 0; j-- { + commentLine := strings.TrimSpace(lines[j]) + if strings.HasPrefix(commentLine, "//") { + comment := strings.TrimSpace(strings.TrimPrefix(commentLine, "//")) + if comment != "" && len(comment) > 5 { + return comment + } + } else if commentLine != "" { + break + } + } + break + } + } + + return fmt.Sprintf("预设计的模块:%s", moduleName) +} + +// Handle 处理工具调用 +func (t *AutomationModuleAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + action, ok := request.GetArguments()["action"].(string) + if !ok || action == "" { + return nil, errors.New("参数错误:action 必须是非空字符串") + } + + switch action { + case "analyze": + return t.handleAnalyze(ctx, request) + case "confirm": + return t.handleConfirm(ctx, request) + case "execute": + return t.handleExecute(ctx, request) + default: + return nil, errors.New("无效的操作:action 必须是 'analyze'、'confirm' 或 'execute'") + } +} + +// handleAnalyze 处理分析请求 +func (t *AutomationModuleAnalyzer) handleAnalyze(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + requirement, ok := request.GetArguments()["requirement"].(string) + if !ok || requirement == "" { + return nil, errors.New("参数错误:requirement 必须是非空字符串") + } + + // 检测用户是否想要创建插件 + suggestedType, isPlugin, confidence := t.detectPluginIntent(requirement) + pluginDetectionMsg := "" + if isPlugin { + pluginDetectionMsg = fmt.Sprintf("\n\n🔍 **插件检测结果**:检测到用户想要创建插件(置信度:%s)\n⚠️ **重要提醒**:当用户提到插件时,packageType和template字段都必须设置为 \"plugin\",不能使用 \"package\"!", confidence) + } else { + pluginDetectionMsg = fmt.Sprintf("\n\n🔍 **类型检测结果**:建议使用 %s 类型", suggestedType) + } + + // 从数据库获取所有自动化包信息 + var packages []model.SysAutoCodePackage + if err := global.GVA_DB.Find(&packages).Error; err != nil { + return nil, fmt.Errorf("获取包信息失败: %v", err) + } + + // 从数据库获取所有历史记录 + var histories []model.SysAutoCodeHistory + if err := global.GVA_DB.Find(&histories).Error; err != nil { + return nil, fmt.Errorf("获取历史记录失败: %v", err) + } + + // 转换包信息并检查空文件夹 + var moduleInfos []ModuleInfo + var validPackages []model.SysAutoCodePackage + var emptyPackageIDs []uint + var emptyPackageNames []string + + for _, pkg := range packages { + // 检查包对应的文件夹是否为空 + isEmpty, err := t.isPackageFolderEmpty(pkg.PackageName, pkg.Template) + if err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("检查包 %s 文件夹失败: %v", pkg.PackageName, err)) + // 如果检查失败,仍然保留该包 + validPackages = append(validPackages, pkg) + continue + } + + if isEmpty { + // 记录需要删除的包ID和包名 + emptyPackageIDs = append(emptyPackageIDs, pkg.ID) + emptyPackageNames = append(emptyPackageNames, pkg.PackageName) + global.GVA_LOG.Info(fmt.Sprintf("发现空包文件夹: %s,将删除数据库记录和文件夹", pkg.PackageName)) + + // 删除空文件夹 + if err := t.removeEmptyPackageFolder(pkg.PackageName, pkg.Template); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除空包文件夹 %s 失败: %v", pkg.PackageName, err)) + } + } else { + // 文件夹不为空,保留该包 + validPackages = append(validPackages, pkg) + } + } + + // 批量删除空包的数据库记录 + if len(emptyPackageIDs) > 0 { + if err := global.GVA_DB.Where("id IN ?", emptyPackageIDs).Delete(&model.SysAutoCodePackage{}).Error; err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除空包数据库记录失败: %v", err)) + } else { + global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个空包的数据库记录", len(emptyPackageIDs))) + } + } + + // 转换有效的包信息 + for _, pkg := range validPackages { + moduleInfos = append(moduleInfos, ModuleInfo{ + ID: pkg.ID, + PackageName: pkg.PackageName, + Label: pkg.Label, + Desc: pkg.Desc, + Template: pkg.Template, + Module: pkg.Module, + }) + } + + // 删除与空包相关的历史记录 + var emptyHistoryIDs []uint + if len(emptyPackageNames) > 0 { + for _, history := range histories { + for _, emptyPackageName := range emptyPackageNames { + if history.Package == emptyPackageName { + emptyHistoryIDs = append(emptyHistoryIDs, history.ID) + break + } + } + } + + // 清理相关的API和菜单记录 + if len(emptyHistoryIDs) > 0 { + if err := t.cleanupRelatedApiAndMenus(emptyHistoryIDs); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("清理空包相关API和菜单失败: %v", err)) + } + } + + // 批量删除相关历史记录 + if len(emptyHistoryIDs) > 0 { + if err := global.GVA_DB.Where("id IN ?", emptyHistoryIDs).Delete(&model.SysAutoCodeHistory{}).Error; err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除空包相关历史记录失败: %v", err)) + } else { + global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个空包相关的历史记录", len(emptyHistoryIDs))) + } + } + } + + // 创建有效包名的映射,用于快速查找 + validPackageNames := make(map[string]bool) + for _, pkg := range validPackages { + validPackageNames[pkg.PackageName] = true + } + + // 收集需要删除的脏历史记录ID(包名不在有效包列表中的历史记录) + var dirtyHistoryIDs []uint + for _, history := range histories { + if !validPackageNames[history.Package] { + dirtyHistoryIDs = append(dirtyHistoryIDs, history.ID) + } + } + + // 删除脏历史记录 + if len(dirtyHistoryIDs) > 0 { + // 清理相关的API和菜单记录 + if err := t.cleanupRelatedApiAndMenus(dirtyHistoryIDs); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("清理脏历史记录相关API和菜单失败: %v", err)) + } + + if err := global.GVA_DB.Where("id IN ?", dirtyHistoryIDs).Delete(&model.SysAutoCodeHistory{}).Error; err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除脏历史记录失败: %v", err)) + } else { + global.GVA_LOG.Info(fmt.Sprintf("成功删除 %d 个脏历史记录(包名不在有效包列表中)", len(dirtyHistoryIDs))) + } + } + + // 转换有效的历史记录(只保留包名存在于有效包列表中的历史记录) + var historyInfos []HistoryInfo + for _, history := range histories { + // 只保留包名存在于有效包列表中的历史记录 + if validPackageNames[history.Package] { + historyInfos = append(historyInfos, HistoryInfo{ + ID: history.ID, + StructName: history.StructName, + TableName: history.TableName(), + PackageName: history.Package, + BusinessDB: history.BusinessDB, + Description: history.Description, + Abbreviation: history.Abbreviation, + CreatedAt: history.CreatedAt.Format("2006-01-02 15:04:05"), + }) + } + } + + // 扫描预设计的模块 + allPredesignedModules, err := t.scanPredesignedModules() + if err != nil { + global.GVA_LOG.Warn("扫描预设计模块失败" + err.Error()) + allPredesignedModules = []PredesignedModuleInfo{} // 确保不为nil + } + + // 过滤掉与已删除包相关的预设计模块 + var predesignedModules []PredesignedModuleInfo + for _, module := range allPredesignedModules { + isDeleted := false + for _, emptyPackageName := range emptyPackageNames { + if module.PackageName == emptyPackageName { + isDeleted = true + break + } + } + + // 只保留未被删除包的预设计模块 + if !isDeleted { + predesignedModules = append(predesignedModules, module) + } + } + + // 构建分析结果消息 + var message string + var deletionDetails []string + + // 收集删除信息 + if len(emptyHistoryIDs) > 0 { + deletionDetails = append(deletionDetails, fmt.Sprintf("%d个空包相关历史记录", len(emptyHistoryIDs))) + } + if len(dirtyHistoryIDs) > 0 { + deletionDetails = append(deletionDetails, fmt.Sprintf("%d个脏历史记录", len(dirtyHistoryIDs))) + } + if len(allPredesignedModules) > len(predesignedModules) { + deletionDetails = append(deletionDetails, fmt.Sprintf("%d个相关预设计模块", len(allPredesignedModules)-len(predesignedModules))) + } + + if len(emptyPackageNames) > 0 || len(deletionDetails) > 0 { + var cleanupInfo string + if len(emptyPackageNames) > 0 { + cleanupInfo = fmt.Sprintf("检测到存在 %s 包但内容为空,我已经删除这些包的文件夹(包括model、api、service、router目录)和数据库记录", strings.Join(emptyPackageNames, "、")) + } + + deletionInfo := "" + if len(deletionDetails) > 0 { + if cleanupInfo != "" { + deletionInfo = fmt.Sprintf(",同时删除了%s", strings.Join(deletionDetails, "、")) + } else { + deletionInfo = fmt.Sprintf("检测到脏数据,已删除%s", strings.Join(deletionDetails, "、")) + } + } + + if cleanupInfo != "" { + message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块。%s%s,如果需要使用这些包名,需要重新创建。请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules), cleanupInfo, deletionInfo) + } else { + message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块。%s。请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules), deletionInfo) + } + } else { + message = fmt.Sprintf("分析完成:获取到 %d 个有效包、%d 个历史记录和 %d 个预设计模块,请AI根据需求选择合适的包和模块", len(validPackages), len(historyInfos), len(predesignedModules)) + } + + // 构建分析结果 + analysisResult := AnalysisResponse{ + Packages: moduleInfos, + History: historyInfos, + PredesignedModules: predesignedModules, + Message: message, + } + + resultJSON, err := json.MarshalIndent(analysisResult, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf(`分析结果: + +%s + +请AI根据用户需求:%s%s + +%s + +分析现有的包、历史记录和预设计模块,然后构建ExecutionPlan结构体调用execute操作。 + +**预设计模块说明**: +- 预设计模块是已经存在于autocode路径下的package或plugin +- 这些模块包含了预先设计好的代码结构,可以直接使用或作为参考 +- 如果用户需求与某个预设计模块匹配,可以考虑直接使用该模块或基于它进行扩展 + +**字典选项生成说明**: +- 当字段需要使用字典类型时(dictType不为空),请使用 generate_dictionary_options 工具 +- 该工具允许AI根据字段描述智能生成合适的字典选项 +- 调用示例: + { + "dictType": "user_status", + "fieldDesc": "用户状态", + "options": [ + {"label": "正常", "value": "1", "sort": 1}, + {"label": "禁用", "value": "0", "sort": 2} + ], + "dictName": "用户状态字典", + "description": "用于管理用户账户状态的字典" + } +- 请在创建模块之前先创建所需的字典选项 + +重要提醒:ExecutionPlan必须严格按照以下格式(支持批量创建多个模块): +{ + "packageName": "包名", + "packageType": "package或plugin", // 当用户提到插件时必须是"plugin" + "needCreatedPackage": true/false, + "needCreatedModules": true/false, + "packageInfo": { + "desc": "描述", + "label": "展示名", + "template": "package或plugin", // 必须与packageType保持一致! + "packageName": "包名" + }, + "modulesInfo": [{ + "package": "包名", + "tableName": "数据库表名", + "businessDB": "", + "structName": "结构体名", + "packageName": "文件名称小驼峰模式 一般是结构体名的小驼峰", + "description": "中文描述", + "abbreviation": "简称 package和结构体简称不可同名 小驼峰模式", + "humpPackageName": "一般是结构体名的下划线分割的小驼峰 例如:sys_user", + "gvaModel": true, + "autoMigrate": true, + "autoCreateResource": true/false 用户不特地强调开启资源标识则为false, + "autoCreateApiToSql": true, + "autoCreateMenuToSql": true, + "autoCreateBtnAuth": false/true 用户不特地强调创建按钮权限则为false, + "onlyTemplate": false, + "isTree": false, + "treeJson": "", + "isAdd": false, + "generateWeb": true, + "generateServer": true, + "fields": [{ + "fieldName": "字段名(必须大写开头)", + "fieldDesc": "字段描述", + "fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)", + "fieldJson": "json标签(string 必须是小驼峰命名,例:userName)", + "dataTypeLong": "长度", + "comment": "注释", + "columnName": "数据库列名", + "fieldSearchType": "=/!=/>/=/<=/LIKE等 可以为空", + "fieldSearchHide": true/false, + "dictType": "", + "form": true/false 是否前端创建输入, + "table": true/false 是否前端表格展示, + "desc": true/false 是否前端详情展示, + "excel": true/false 是否导出Excel, + "require": true/false 是否必填, + "defaultValue": "", + "errorText": "错误提示", + "clearable": true, + "sort": false, + "primaryKey": "当gvaModel=false时必须有一个字段设为true(bool)", + "dataSource": null, + "checkDataSource": false, + "fieldIndexType": "" + }] + }, { + "package": "包名", + "tableName": "第二个模块的表名", + "structName": "第二个模块的结构体名", + "description": "第二个模块的描述", + "...": "更多模块配置..." + }] +} + +**重要提醒**:ExecutionPlan必须严格按照以下格式和验证规则: + +**插件类型检测规则(最重要)**: +1. 当用户需求中包含"插件"、"plugin"等关键词时,packageType和template都必须设置为"plugin" +2. packageType和template字段必须保持一致,不能一个是"package"另一个是"plugin" +3. 如果检测到插件意图但设置错误,会导致创建失败 + +**字段完整性要求**: +4. 所有字符串字段都不能为空(包括packageName、moduleName、structName、tableName、description等) +5. 所有布尔字段必须明确设置true或false,不能使用默认值 + +**主键设置规则(关键)**: +6. 当gvaModel=false时:fields数组中必须有且仅有一个字段的primaryKey=true +7. 当gvaModel=true时:系统自动创建ID主键,fields中所有字段的primaryKey都应为false +8. 主键设置错误会导致模板执行时PrimaryField为nil的严重错误! + +**包和模块创建逻辑**: +9. 如果存在可用的package,needCreatedPackage应设为false +10. 如果存在可用的modules,needCreatedModules应设为false +11. 如果发现合适的预设计模块,可以考虑基于它进行扩展而不是从零创建 + +**字典创建流程**: +12. 如果字段需要字典类型,请先使用 generate_dictionary_options 工具创建字典 +13. 字典创建成功后,再执行模块创建操作 + +`, string(resultJSON), requirement, pluginDetectionMsg, + func() string { + if len(emptyPackageNames) > 0 { + return fmt.Sprintf("**重要提醒**:检测到 %s 包存在但内容为空,已自动删除相关文件夹和数据库记录。如果用户需求涉及这些包名,请设置 needCreatedPackage=true 重新创建。", strings.Join(emptyPackageNames, "、")) + } + return "" + }()), + }, + }, + }, nil +} + +// handleConfirm 处理确认请求 +func (t *AutomationModuleAnalyzer) handleConfirm(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + executionPlanData, ok := request.GetArguments()["executionPlan"] + if !ok { + return nil, errors.New("参数错误:executionPlan 必须提供") + } + + // 解析执行计划 + planJSON, err := json.Marshal(executionPlanData) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v", err) + } + + var plan ExecutionPlan + err = json.Unmarshal(planJSON, &plan) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err) + } + + // 验证执行计划的完整性 + if err := t.validateExecutionPlan(&plan); err != nil { + return nil, fmt.Errorf("执行计划验证失败: %v", err) + } + + // 构建确认响应 + var moduleNames []string + for _, moduleInfo := range plan.ModulesInfo { + moduleNames = append(moduleNames, moduleInfo.StructName) + } + moduleNamesStr := strings.Join(moduleNames, "_") + + confirmResponse := ConfirmationResponse{ + Message: "请确认以下创建计划:", + PackageConfirm: plan.NeedCreatedPackage, + ModulesConfirm: plan.NeedCreatedModules, + CanProceed: true, + ConfirmationKey: fmt.Sprintf("%s_%s_%d", plan.PackageName, moduleNamesStr, time.Now().Unix()), + } + + // 构建详细的确认信息 + var confirmDetails strings.Builder + confirmDetails.WriteString(fmt.Sprintf("包名: %s\n", plan.PackageName)) + confirmDetails.WriteString(fmt.Sprintf("包类型: %s\n", plan.PackageType)) + + if plan.NeedCreatedPackage && plan.PackageInfo != nil { + confirmDetails.WriteString("\n需要创建包:\n") + confirmDetails.WriteString(fmt.Sprintf(" - 包名: %s\n", plan.PackageInfo.PackageName)) + confirmDetails.WriteString(fmt.Sprintf(" - 标签: %s\n", plan.PackageInfo.Label)) + confirmDetails.WriteString(fmt.Sprintf(" - 描述: %s\n", plan.PackageInfo.Desc)) + confirmDetails.WriteString(fmt.Sprintf(" - 模板: %s\n", plan.PackageInfo.Template)) + } + + if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { + confirmDetails.WriteString(fmt.Sprintf("\n需要创建模块 (共%d个):\n", len(plan.ModulesInfo))) + for i, moduleInfo := range plan.ModulesInfo { + confirmDetails.WriteString(fmt.Sprintf("\n模块 %d:\n", i+1)) + confirmDetails.WriteString(fmt.Sprintf(" - 结构体名: %s\n", moduleInfo.StructName)) + confirmDetails.WriteString(fmt.Sprintf(" - 表名: %s\n", moduleInfo.TableName)) + confirmDetails.WriteString(fmt.Sprintf(" - 描述: %s\n", moduleInfo.Description)) + confirmDetails.WriteString(fmt.Sprintf(" - 字段数量: %d\n", len(moduleInfo.Fields))) + confirmDetails.WriteString(" - 字段列表:\n") + for _, field := range moduleInfo.Fields { + confirmDetails.WriteString(fmt.Sprintf(" * %s (%s): %s\n", field.FieldName, field.FieldType, field.FieldDesc)) + } + } + } + + resultJSON, err := json.MarshalIndent(confirmResponse, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("确认信息:\n\n%s\n\n详细信息:\n%s\n\n请用户确认是否继续执行此计划。如果确认,请使用execute操作并提供相应的确认参数。", string(resultJSON), confirmDetails.String()), + }, + }, + }, nil +} + +// handleExecute 处理执行请求 +func (t *AutomationModuleAnalyzer) handleExecute(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + executionPlanData, ok := request.GetArguments()["executionPlan"] + if !ok { + return nil, errors.New("参数错误:executionPlan 必须提供") + } + + // 解析执行计划 + planJSON, err := json.Marshal(executionPlanData) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v", err) + } + + var plan ExecutionPlan + err = json.Unmarshal(planJSON, &plan) + if err != nil { + return nil, fmt.Errorf("解析执行计划失败: %v\n\n请确保ExecutionPlan格式正确,参考工具描述中的结构体格式要求", err) + } + + // 验证执行计划的完整性 + if err := t.validateExecutionPlan(&plan); err != nil { + return nil, fmt.Errorf("执行计划验证失败: %v", err) + } + + // 检查用户确认 + if plan.NeedCreatedPackage { + packageConfirm, ok := request.GetArguments()["packageConfirm"].(string) + if !ok || (packageConfirm != "yes" && packageConfirm != "no") { + return nil, errors.New("参数错误:当需要创建包时,packageConfirm 必须是 'yes' 或 'no'") + } + if packageConfirm == "no" { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: "用户取消了包的创建操作", + }, + }, + }, nil + } + } + + if plan.NeedCreatedModules { + modulesConfirm, ok := request.GetArguments()["modulesConfirm"].(string) + if !ok || (modulesConfirm != "yes" && modulesConfirm != "no") { + return nil, errors.New("参数错误:当需要创建模块时,modulesConfirm 必须是 'yes' 或 'no'") + } + if modulesConfirm == "no" { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: "用户取消了模块的创建操作", + }, + }, + }, nil + } + } + + // 执行创建操作 + result := t.executeCreation(ctx, &plan) + + resultJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + // 添加权限分配提醒 + permissionReminder := "\n\n⚠️ 重要提醒:\n" + + "模块创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的API和菜单权限," + + "以确保用户能够正常访问新功能。\n" + + "具体步骤:\n" + + "1. 进入角色管理页面\n" + + "2. 选择需要授权的角色\n" + + "3. 在API权限中勾选新创建的API接口\n" + + "4. 在菜单权限中勾选新创建的菜单项\n" + + "5. 保存权限配置" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("执行结果:\n\n%s%s", string(resultJSON), permissionReminder), + }, + }, + }, nil +} + +// isSystemFunction 判断是否为系统功能 +func (t *AutomationModuleAnalyzer) isSystemFunction(requirement string) bool { + systemKeywords := []string{ + "用户", "权限", "角色", "菜单", "系统", "配置", "字典", "参数", + "user", "authority", "role", "menu", "system", "config", "dictionary", + "认证", "授权", "登录", "注册", "JWT", "casbin", + } + + requirementLower := strings.ToLower(requirement) + for _, keyword := range systemKeywords { + if strings.Contains(requirementLower, keyword) { + return true + } + } + return false +} + +// buildDirectoryStructure 构建目录结构信息 +func (t *AutomationModuleAnalyzer) buildDirectoryStructure(plan *ExecutionPlan) map[string]string { + paths := make(map[string]string) + + // 获取配置信息 + autoCodeConfig := global.GVA_CONFIG.AutoCode + + // 构建基础路径 + rootPath := autoCodeConfig.Root + serverPath := autoCodeConfig.Server + webPath := autoCodeConfig.Web + moduleName := autoCodeConfig.Module + + // 如果计划中有包名,使用计划中的包名,否则使用默认 + packageName := "example" + if plan.PackageInfo != nil && plan.PackageInfo.PackageName != "" { + packageName = plan.PackageInfo.PackageName + } + + // 如果计划中有模块信息,获取第一个模块的结构名作为默认值 + structName := "ExampleStruct" + if len(plan.ModulesInfo) > 0 && plan.ModulesInfo[0].StructName != "" { + structName = plan.ModulesInfo[0].StructName + } + + // 根据包类型构建不同的路径结构 + packageType := plan.PackageType + if packageType == "" { + packageType = "package" // 默认为package模式 + } + + // 构建服务端路径 + if serverPath != "" { + serverBasePath := fmt.Sprintf("%s/%s", rootPath, serverPath) + + if packageType == "plugin" { + // Plugin 模式:所有文件都在 /plugin/packageName/ 目录下 + pluginBasePath := fmt.Sprintf("%s/plugin/%s", serverBasePath, packageName) + + // API 路径 + paths["api"] = fmt.Sprintf("%s/api", pluginBasePath) + + // Service 路径 + paths["service"] = fmt.Sprintf("%s/service", pluginBasePath) + + // Model 路径 + paths["model"] = fmt.Sprintf("%s/model", pluginBasePath) + + // Router 路径 + paths["router"] = fmt.Sprintf("%s/router", pluginBasePath) + + // Request 路径 + paths["request"] = fmt.Sprintf("%s/model/request", pluginBasePath) + + // Response 路径 + paths["response"] = fmt.Sprintf("%s/model/response", pluginBasePath) + + // Plugin 特有文件 + paths["plugin_main"] = fmt.Sprintf("%s/main.go", pluginBasePath) + paths["plugin_config"] = fmt.Sprintf("%s/plugin.go", pluginBasePath) + paths["plugin_initialize"] = fmt.Sprintf("%s/initialize", pluginBasePath) + } else { + // Package 模式:传统的目录结构 + // API 路径 + paths["api"] = fmt.Sprintf("%s/api/v1/%s", serverBasePath, packageName) + + // Service 路径 + paths["service"] = fmt.Sprintf("%s/service/%s", serverBasePath, packageName) + + // Model 路径 + paths["model"] = fmt.Sprintf("%s/model/%s", serverBasePath, packageName) + + // Router 路径 + paths["router"] = fmt.Sprintf("%s/router/%s", serverBasePath, packageName) + + // Request 路径 + paths["request"] = fmt.Sprintf("%s/model/%s/request", serverBasePath, packageName) + + // Response 路径 + paths["response"] = fmt.Sprintf("%s/model/%s/response", serverBasePath, packageName) + } + } + + // 构建前端路径(两种模式相同) + if webPath != "" { + webBasePath := fmt.Sprintf("%s/%s", rootPath, webPath) + + // Vue 页面路径 + paths["vue_page"] = fmt.Sprintf("%s/view/%s", webBasePath, packageName) + + // API 路径 + paths["vue_api"] = fmt.Sprintf("%s/api/%s", webBasePath, packageName) + } + + // 添加模块信息 + paths["module"] = moduleName + paths["package_name"] = packageName + paths["package_type"] = packageType + paths["struct_name"] = structName + paths["root_path"] = rootPath + paths["server_path"] = serverPath + paths["web_path"] = webPath + + return paths +} + +// validateExecutionPlan 验证执行计划的完整性 +func (t *AutomationModuleAnalyzer) validateExecutionPlan(plan *ExecutionPlan) error { + // 验证基本字段 + if plan.PackageName == "" { + return errors.New("packageName 不能为空") + } + if plan.PackageType != "package" && plan.PackageType != "plugin" { + return errors.New("packageType 必须是 'package' 或 'plugin'") + } + + // 验证packageType和template字段的一致性 + if plan.NeedCreatedPackage && plan.PackageInfo != nil { + if plan.PackageType != plan.PackageInfo.Template { + return errors.New("packageType 和 packageInfo.template 必须保持一致") + } + } + + // 验证包信息 + if plan.NeedCreatedPackage { + if plan.PackageInfo == nil { + return errors.New("当 needCreatedPackage=true 时,packageInfo 不能为空") + } + if plan.PackageInfo.PackageName == "" { + return errors.New("packageInfo.packageName 不能为空") + } + if plan.PackageInfo.Template != "package" && plan.PackageInfo.Template != "plugin" { + return errors.New("packageInfo.template 必须是 'package' 或 'plugin'") + } + if plan.PackageInfo.Label == "" { + return errors.New("packageInfo.label 不能为空") + } + if plan.PackageInfo.Desc == "" { + return errors.New("packageInfo.desc 不能为空") + } + } + + // 验证模块信息(批量验证) + if plan.NeedCreatedModules { + if len(plan.ModulesInfo) == 0 { + return errors.New("当 needCreatedModules=true 时,modulesInfo 不能为空") + } + + // 遍历验证每个模块 + for moduleIndex, moduleInfo := range plan.ModulesInfo { + if moduleInfo.Package == "" { + return fmt.Errorf("模块 %d 的 package 不能为空", moduleIndex+1) + } + if moduleInfo.StructName == "" { + return fmt.Errorf("模块 %d 的 structName 不能为空", moduleIndex+1) + } + if moduleInfo.TableName == "" { + return fmt.Errorf("模块 %d 的 tableName 不能为空", moduleIndex+1) + } + if moduleInfo.Description == "" { + return fmt.Errorf("模块 %d 的 description 不能为空", moduleIndex+1) + } + if moduleInfo.Abbreviation == "" { + return fmt.Errorf("模块 %d 的 abbreviation 不能为空", moduleIndex+1) + } + if moduleInfo.PackageName == "" { + return fmt.Errorf("模块 %d 的 packageName 不能为空", moduleIndex+1) + } + if moduleInfo.HumpPackageName == "" { + return fmt.Errorf("模块 %d 的 humpPackageName 不能为空", moduleIndex+1) + } + + // 验证字段信息 + if len(moduleInfo.Fields) == 0 { + return fmt.Errorf("模块 %d 的 fields 不能为空,至少需要一个字段", moduleIndex+1) + } + + for i, field := range moduleInfo.Fields { + if field.FieldName == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1) + } + + if field.FieldDesc == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1) + } + if field.FieldType == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldType 不能为空", moduleIndex+1, i+1) + } + if field.FieldJson == "" { + return fmt.Errorf("模块 %d 字段 %d 的 fieldJson 不能为空", moduleIndex+1, i+1) + } + if field.ColumnName == "" { + return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1) + } + + // 确保字段名首字母大写 + if len(field.FieldName) > 0 { + firstChar := string(field.FieldName[0]) + if firstChar >= "a" && firstChar <= "z" { + moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:] + } + } + + // 确保FieldJson使用小驼峰命名 + if len(field.FieldJson) > 0 { + // 处理下划线命名转小驼峰 + if strings.Contains(field.FieldJson, "_") { + parts := strings.Split(field.FieldJson, "_") + camelCase := strings.ToLower(parts[0]) + for j := 1; j < len(parts); j++ { + if len(parts[j]) > 0 { + camelCase += strings.ToUpper(string(parts[j][0])) + strings.ToLower(parts[j][1:]) + } + } + moduleInfo.Fields[i].FieldJson = camelCase + } else { + // 处理首字母大写转小写 + firstChar := string(field.FieldJson[0]) + if firstChar >= "A" && firstChar <= "Z" { + moduleInfo.Fields[i].FieldJson = strings.ToLower(firstChar) + field.FieldJson[1:] + } + } + } + + // 确保ColumnName使用下划线命名 + if len(field.ColumnName) > 0 { + // 将驼峰命名转换为下划线命名 + var result strings.Builder + for i, r := range field.ColumnName { + if i > 0 && r >= 'A' && r <= 'Z' { + result.WriteRune('_') + } + result.WriteRune(unicode.ToLower(r)) + } + moduleInfo.Fields[i].ColumnName = result.String() + } + + // 验证字段类型 + validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"} + validType := false + for _, validFieldType := range validFieldTypes { + if field.FieldType == validFieldType { + validType = true + break + } + } + if !validType { + return fmt.Errorf("模块 %d 字段 %d 的 fieldType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldType, validFieldTypes) + } + + // 验证搜索类型(如果设置了) + if field.FieldSearchType != "" { + validSearchTypes := []string{"=", "!=", ">", ">=", "<", "<=", "LIKE", "BETWEEN", "IN", "NOT IN"} + validSearchType := false + for _, validType := range validSearchTypes { + if field.FieldSearchType == validType { + validSearchType = true + break + } + } + if !validSearchType { + return fmt.Errorf("模块 %d 字段 %d 的 fieldSearchType '%s' 不支持,支持的类型:%v", moduleIndex+1, i+1, field.FieldSearchType, validSearchTypes) + } + } + } + + // 验证主键设置 + if !moduleInfo.GvaModel { + // 当不使用GVA模型时,必须有且仅有一个字段设置为主键 + primaryKeyCount := 0 + for _, field := range moduleInfo.Fields { + if field.PrimaryKey { + primaryKeyCount++ + } + } + if primaryKeyCount == 0 { + return fmt.Errorf("模块 %d:当 gvaModel=false 时,必须有一个字段的 primaryKey=true", moduleIndex+1) + } + if primaryKeyCount > 1 { + return fmt.Errorf("模块 %d:当 gvaModel=false 时,只能有一个字段的 primaryKey=true", moduleIndex+1) + } + } else { + // 当使用GVA模型时,所有字段的primaryKey都应该为false + for i, field := range moduleInfo.Fields { + if field.PrimaryKey { + return fmt.Errorf("模块 %d:当 gvaModel=true 时,字段 %d 的 primaryKey 应该为 false,系统会自动创建ID主键", moduleIndex+1, i+1) + } + } + } + } + } + + return nil +} + +// executeCreation 执行创建操作 +func (t *AutomationModuleAnalyzer) executeCreation(ctx context.Context, plan *ExecutionPlan) *ExecutionResult { + result := &ExecutionResult{ + Success: false, + Paths: make(map[string]string), + } + + // 无论如何都先构建目录结构信息,确保paths始终返回 + result.Paths = t.buildDirectoryStructure(plan) + + if !plan.NeedCreatedModules { + result.Success = true + result.Message += "已列出当前功能所涉及的目录结构信息; 请在paths中查看; 并且在对应指定文件中实现相关的业务逻辑; " + return result + } + + // 创建包(如果需要) + if plan.NeedCreatedPackage && plan.PackageInfo != nil { + packageService := service.ServiceGroupApp.SystemServiceGroup.AutoCodePackage + err := packageService.Create(ctx, plan.PackageInfo) + if err != nil { + result.Message = fmt.Sprintf("创建包失败: %v", err) + // 即使创建包失败,也要返回paths信息 + return result + } + result.Message += "包创建成功; " + } + + // 批量创建字典和模块(如果需要) + if plan.NeedCreatedModules && len(plan.ModulesInfo) > 0 { + templateService := service.ServiceGroupApp.SystemServiceGroup.AutoCodeTemplate + + // 先批量创建所有模块需要的字典 + dictResult := t.createRequiredDictionaries(ctx, plan.ModulesInfo) + result.Message += dictResult + + // 遍历所有模块进行创建 + for _, moduleInfo := range plan.ModulesInfo { + + // 创建模块 + err := moduleInfo.Pretreatment() + if err != nil { + result.Message += fmt.Sprintf("模块 %s 信息预处理失败: %v; ", moduleInfo.StructName, err) + continue // 继续处理下一个模块 + } + + err = templateService.Create(ctx, *moduleInfo) + if err != nil { + result.Message += fmt.Sprintf("创建模块 %s 失败: %v; ", moduleInfo.StructName, err) + continue // 继续处理下一个模块 + } + result.Message += fmt.Sprintf("模块 %s 创建成功; ", moduleInfo.StructName) + } + + result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo)) + + // 添加重要提醒:不要使用其他MCP工具 + result.Message += "\n\n⚠️ 重要提醒:\n" + result.Message += "模块创建已完成,API和菜单已自动生成。请不要再调用以下MCP工具:\n" + result.Message += "- api_creator:API权限已在模块创建时自动生成\n" + result.Message += "- menu_creator:前端菜单已在模块创建时自动生成\n" + result.Message += "如需修改API或菜单,请直接在系统管理界面中进行配置。\n" + } + + result.Message += "已构建目录结构信息; " + result.Success = true + + if result.Message == "" { + result.Message = "执行计划完成" + } + + return result +} + +// createRequiredDictionaries 创建所需的字典(批量处理) +func (t *AutomationModuleAnalyzer) createRequiredDictionaries(ctx context.Context, modulesInfoList []*request.AutoCode) string { + var messages []string + dictionaryService := service.ServiceGroupApp.SystemServiceGroup.DictionaryService + createdDictTypes := make(map[string]bool) // 用于避免重复创建相同的字典 + + // 遍历所有模块 + for moduleIndex, modulesInfo := range modulesInfoList { + messages = append(messages, fmt.Sprintf("处理模块 %d (%s) 的字典: ", moduleIndex+1, modulesInfo.StructName)) + + // 遍历当前模块的所有字段,查找使用字典的字段 + moduleHasDictFields := false + for _, field := range modulesInfo.Fields { + if field.DictType != "" { + moduleHasDictFields = true + + // 如果这个字典类型已经在之前的模块中创建过,跳过 + if createdDictTypes[field.DictType] { + messages = append(messages, fmt.Sprintf("字典 %s 已在前面的模块中创建,跳过; ", field.DictType)) + continue + } + + // 检查字典是否存在 + exists, err := t.checkDictionaryExists(field.DictType) + if err != nil { + messages = append(messages, fmt.Sprintf("检查字典 %s 时出错: %v; ", field.DictType, err)) + continue + } + + if !exists { + // 字典不存在,创建字典 + dictionary := model.SysDictionary{ + Name: t.generateDictionaryName(field.DictType, field.FieldDesc), + Type: field.DictType, + Status: &[]bool{true}[0], // 默认启用 + Desc: fmt.Sprintf("自动生成的字典,用于模块 %s 字段: %s (%s)", modulesInfo.StructName, field.FieldName, field.FieldDesc), + } + + err = dictionaryService.CreateSysDictionary(dictionary) + if err != nil { + messages = append(messages, fmt.Sprintf("创建字典 %s 失败: %v; ", field.DictType, err)) + } else { + messages = append(messages, fmt.Sprintf("成功创建字典 %s (%s); ", field.DictType, dictionary.Name)) + createdDictTypes[field.DictType] = true // 标记为已创建 + + // 创建默认的字典详情项 + t.createDefaultDictionaryDetails(ctx, field.DictType, field.FieldDesc) + } + } else { + messages = append(messages, fmt.Sprintf("字典 %s 已存在,跳过创建; ", field.DictType)) + createdDictTypes[field.DictType] = true // 标记为已存在 + } + } + } + + if !moduleHasDictFields { + messages = append(messages, "无需创建字典; ") + } + } + + if len(messages) == 0 { + return "未发现需要创建的字典; " + } + + return strings.Join(messages, "") +} + +// checkDictionaryExists 检查字典是否存在 +func (t *AutomationModuleAnalyzer) checkDictionaryExists(dictType string) (bool, error) { + var dictionary model.SysDictionary + err := global.GVA_DB.Where("type = ?", dictType).First(&dictionary).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return false, nil // 字典不存在 + } + return false, err // 其他错误 + } + return true, nil // 字典存在 +} + +// generateDictionaryName 生成字典名称 +func (t *AutomationModuleAnalyzer) generateDictionaryName(dictType, fieldDesc string) string { + if fieldDesc != "" { + return fmt.Sprintf("%s字典", fieldDesc) + } + return fmt.Sprintf("%s字典", dictType) +} + +// createDefaultDictionaryDetails 创建默认的字典详情项 +func (t *AutomationModuleAnalyzer) createDefaultDictionaryDetails(ctx context.Context, dictType, fieldDesc string) { + // 字典选项现在通过 generate_dictionary_options MCP工具由AI client传入 + // 这里不再创建默认选项,只是保留方法以保持兼容性 + global.GVA_LOG.Info(fmt.Sprintf("字典 %s 已创建,请使用 generate_dictionary_options 工具添加字典选项", dictType)) +} + +// DictionaryOption 字典选项结构 +type DictionaryOption struct { + Label string `json:"label"` + Value string `json:"value"` + Sort int `json:"sort"` +} + +// generateSmartDictionaryOptions 通过MCP调用让AI生成字典选项 +func (t *AutomationModuleAnalyzer) generateSmartDictionaryOptions(dictType, fieldDesc string) []struct { + label string + value string + sort int +} { + // 返回空切片,不再使用预制选项 + // 字典选项将通过新的MCP工具由AI client传入 + return []struct { + label string + value string + sort int + }{} +} + +// detectPluginIntent 检测用户需求中是否包含插件相关的关键词 +func (t *AutomationModuleAnalyzer) detectPluginIntent(requirement string) (suggestedType string, isPlugin bool, confidence string) { + // 转换为小写进行匹配 + requirementLower := strings.ToLower(requirement) + + // 插件相关关键词 + pluginKeywords := []string{ + "插件", "plugin", "扩展", "extension", "addon", "模块插件", + "功能插件", "业务插件", "第三方插件", "自定义插件", + } + + // 包相关关键词(用于排除误判) + packageKeywords := []string{ + "包", "package", "模块包", "业务包", "功能包", + } + + // 检测插件关键词 + pluginMatches := 0 + for _, keyword := range pluginKeywords { + if strings.Contains(requirementLower, keyword) { + pluginMatches++ + } + } + + // 检测包关键词 + packageMatches := 0 + for _, keyword := range packageKeywords { + if strings.Contains(requirementLower, keyword) { + packageMatches++ + } + } + + // 决策逻辑 + if pluginMatches > 0 { + if packageMatches == 0 || pluginMatches > packageMatches { + return "plugin", true, "高" + } else { + return "plugin", true, "中" + } + } + + // 默认返回package + return "package", false, "低" +} + +// isPackageFolderEmpty 检查包对应的文件夹是否为空 +func (t *AutomationModuleAnalyzer) isPackageFolderEmpty(packageName, template string) (bool, error) { + // 根据模板类型确定基础路径 + var basePath string + if template == "plugin" { + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) + } else { + // package 类型 + basePath = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName) + } + + // 检查文件夹是否存在 + if _, err := os.Stat(basePath); os.IsNotExist(err) { + // 文件夹不存在,认为是空的 + return true, nil + } else if err != nil { + return false, fmt.Errorf("检查文件夹状态失败: %v", err) + } + + // 读取文件夹内容 + entries, err := os.ReadDir(basePath) + if err != nil { + return false, fmt.Errorf("读取文件夹内容失败: %v", err) + } + + // 检查目录下是否有 .go 文件 + hasGoFiles := false + for _, entry := range entries { + name := entry.Name() + // 跳过隐藏文件、.DS_Store 等系统文件 + if strings.HasPrefix(name, ".") { + continue + } + // 如果是目录,递归检查子目录中的 .go 文件 + if entry.IsDir() { + subPath := filepath.Join(basePath, name) + subEntries, err := os.ReadDir(subPath) + if err != nil { + continue + } + for _, subEntry := range subEntries { + if !subEntry.IsDir() && strings.HasSuffix(subEntry.Name(), ".go") { + hasGoFiles = true + break + } + } + if hasGoFiles { + break + } + } else if strings.HasSuffix(name, ".go") { + // 如果是 .go 文件 + hasGoFiles = true + break + } + } + + // 如果没有 .go 文件,认为是空包 + return !hasGoFiles, nil +} + +// removeEmptyPackageFolder 删除空的包文件夹 +func (t *AutomationModuleAnalyzer) removeEmptyPackageFolder(packageName, template string) error { + var errors []string + + if template == "plugin" { + // plugin 类型只删除 plugin 目录下的文件夹 + basePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", packageName) + if err := t.removeDirectoryIfExists(basePath); err != nil { + errors = append(errors, fmt.Sprintf("删除plugin文件夹失败: %v", err)) + } + } else { + // package 类型需要删除多个目录下的相关文件 + paths := []string{ + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "model", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "api", "v1", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "service", packageName), + filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "router", packageName), + } + + for _, path := range paths { + if err := t.removeDirectoryIfExists(path); err != nil { + errors = append(errors, fmt.Sprintf("删除%s失败: %v", path, err)) + } + } + } + + if len(errors) > 0 { + return fmt.Errorf("删除过程中出现错误: %s", strings.Join(errors, "; ")) + } + + return nil +} + +// removeDirectoryIfExists 删除目录(如果存在) +func (t *AutomationModuleAnalyzer) removeDirectoryIfExists(dirPath string) error { + // 检查文件夹是否存在 + if _, err := os.Stat(dirPath); os.IsNotExist(err) { + // 文件夹不存在,无需删除 + return nil + } else if err != nil { + return fmt.Errorf("检查文件夹状态失败: %v", err) + } + + // 删除文件夹及其所有内容 + if err := os.RemoveAll(dirPath); err != nil { + return fmt.Errorf("删除文件夹失败: %v", err) + } + + global.GVA_LOG.Info(fmt.Sprintf("成功删除目录: %s", dirPath)) + return nil +} + +// cleanupRelatedApiAndMenus 清理与删除的模块相关的API和菜单记录 +func (t *AutomationModuleAnalyzer) cleanupRelatedApiAndMenus(historyIDs []uint) error { + if len(historyIDs) == 0 { + return nil + } + + // 获取要删除的历史记录信息 + var histories []model.SysAutoCodeHistory + if err := global.GVA_DB.Where("id IN ?", historyIDs).Find(&histories).Error; err != nil { + return fmt.Errorf("获取历史记录失败: %v", err) + } + + var deletedApiCount, deletedMenuCount int + + for _, history := range histories { + // 删除相关的API记录(使用存储的API IDs) + if len(history.ApiIDs) > 0 { + ids := make([]int, 0, len(history.ApiIDs)) + for _, id := range history.ApiIDs { + ids = append(ids, int(id)) + } + idsReq := common.IdsReq{Ids: ids} + if err := systemService.ApiServiceApp.DeleteApisByIds(idsReq); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除API记录失败 (模块: %s): %v", history.StructName, err)) + } else { + deletedApiCount += len(ids) + global.GVA_LOG.Info(fmt.Sprintf("成功删除API记录 (模块: %s, 数量: %d)", history.StructName, len(ids))) + } + } + + // 删除相关的菜单记录(使用存储的菜单ID) + if history.MenuID != 0 { + if err := systemService.BaseMenuServiceApp.DeleteBaseMenu(int(history.MenuID)); err != nil { + global.GVA_LOG.Warn(fmt.Sprintf("删除菜单记录失败 (模块: %s, 菜单ID: %d): %v", history.StructName, history.MenuID, err)) + } else { + deletedMenuCount++ + global.GVA_LOG.Info(fmt.Sprintf("成功删除菜单记录 (模块: %s, 菜单ID: %d)", history.StructName, history.MenuID)) + } + } + } + + if deletedApiCount > 0 || deletedMenuCount > 0 { + global.GVA_LOG.Info(fmt.Sprintf("清理完成:删除了 %d 个API记录和 %d 个菜单记录", deletedApiCount, deletedMenuCount)) + } + + return nil +} diff --git a/mcp/menu_creator.go b/mcp/menu_creator.go new file mode 100644 index 0000000..8ee4af1 --- /dev/null +++ b/mcp/menu_creator.go @@ -0,0 +1,287 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/service" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + RegisterTool(&MenuCreator{}) +} + +// MenuCreateRequest 菜单创建请求结构 +type MenuCreateRequest struct { + ParentId uint `json:"parentId"` // 父菜单ID,0表示根菜单 + Path string `json:"path"` // 路由path + Name string `json:"name"` // 路由name + Hidden bool `json:"hidden"` // 是否在列表隐藏 + Component string `json:"component"` // 对应前端文件路径 + Sort int `json:"sort"` // 排序标记 + Title string `json:"title"` // 菜单名 + Icon string `json:"icon"` // 菜单图标 + KeepAlive bool `json:"keepAlive"` // 是否缓存 + DefaultMenu bool `json:"defaultMenu"` // 是否是基础路由 + CloseTab bool `json:"closeTab"` // 自动关闭tab + ActiveName string `json:"activeName"` // 高亮菜单 + Parameters []MenuParameterRequest `json:"parameters"` // 路由参数 + MenuBtn []MenuButtonRequest `json:"menuBtn"` // 菜单按钮 +} + +// MenuParameterRequest 菜单参数请求结构 +type MenuParameterRequest struct { + Type string `json:"type"` // 参数类型:params或query + Key string `json:"key"` // 参数key + Value string `json:"value"` // 参数值 +} + +// MenuButtonRequest 菜单按钮请求结构 +type MenuButtonRequest struct { + Name string `json:"name"` // 按钮名称 + Desc string `json:"desc"` // 按钮描述 +} + +// MenuCreateResponse 菜单创建响应结构 +type MenuCreateResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + MenuID uint `json:"menuId"` + Name string `json:"name"` + Path string `json:"path"` +} + +// MenuCreator 菜单创建工具 +type MenuCreator struct{} + +// New 创建菜单创建工具 +func (m *MenuCreator) New() mcp.Tool { + return mcp.NewTool("create_menu", + mcp.WithDescription(`创建前端菜单记录,用于AI编辑器自动添加前端页面时自动创建对应的菜单项。 + +**重要限制:** +- 当使用gva_auto_generate工具且needCreatedModules=true时,模块创建会自动生成菜单项,不应调用此工具 +- 仅在以下情况使用:1) 单独创建菜单(不涉及模块创建);2) AI编辑器自动添加前端页面时`), + mcp.WithNumber("parentId", + mcp.Description("父菜单ID,0表示根菜单"), + mcp.DefaultNumber(0), + ), + mcp.WithString("path", + mcp.Required(), + mcp.Description("路由path,如:userList"), + ), + mcp.WithString("name", + mcp.Required(), + mcp.Description("路由name,用于Vue Router,如:userList"), + ), + mcp.WithBoolean("hidden", + mcp.Description("是否在菜单列表中隐藏"), + ), + mcp.WithString("component", + mcp.Required(), + mcp.Description("对应的前端Vue组件路径,如:view/user/list.vue"), + ), + mcp.WithNumber("sort", + mcp.Description("菜单排序号,数字越小越靠前"), + mcp.DefaultNumber(1), + ), + mcp.WithString("title", + mcp.Required(), + mcp.Description("菜单显示标题"), + ), + mcp.WithString("icon", + mcp.Description("菜单图标名称"), + mcp.DefaultString("menu"), + ), + mcp.WithBoolean("keepAlive", + mcp.Description("是否缓存页面"), + ), + mcp.WithBoolean("defaultMenu", + mcp.Description("是否是基础路由"), + ), + mcp.WithBoolean("closeTab", + mcp.Description("是否自动关闭tab"), + ), + mcp.WithString("activeName", + mcp.Description("高亮菜单名称"), + ), + mcp.WithString("parameters", + mcp.Description("路由参数JSON字符串,格式:[{\"type\":\"params\",\"key\":\"id\",\"value\":\"1\"}]"), + ), + mcp.WithString("menuBtn", + mcp.Description("菜单按钮JSON字符串,格式:[{\"name\":\"add\",\"desc\":\"新增\"}]"), + ), + ) +} + +// Handle 处理菜单创建请求 +func (m *MenuCreator) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 解析请求参数 + args := request.GetArguments() + + // 必需参数 + path, ok := args["path"].(string) + if !ok || path == "" { + return nil, errors.New("path 参数是必需的") + } + + name, ok := args["name"].(string) + if !ok || name == "" { + return nil, errors.New("name 参数是必需的") + } + + component, ok := args["component"].(string) + if !ok || component == "" { + return nil, errors.New("component 参数是必需的") + } + + title, ok := args["title"].(string) + if !ok || title == "" { + return nil, errors.New("title 参数是必需的") + } + + // 可选参数 + parentId := uint(0) + if val, ok := args["parentId"].(float64); ok { + parentId = uint(val) + } + + hidden := false + if val, ok := args["hidden"].(bool); ok { + hidden = val + } + + sort := 1 + if val, ok := args["sort"].(float64); ok { + sort = int(val) + } + + icon := "menu" + if val, ok := args["icon"].(string); ok && val != "" { + icon = val + } + + keepAlive := false + if val, ok := args["keepAlive"].(bool); ok { + keepAlive = val + } + + defaultMenu := false + if val, ok := args["defaultMenu"].(bool); ok { + defaultMenu = val + } + + closeTab := false + if val, ok := args["closeTab"].(bool); ok { + closeTab = val + } + + activeName := "" + if val, ok := args["activeName"].(string); ok { + activeName = val + } + + // 解析参数和按钮 + var parameters []system.SysBaseMenuParameter + if parametersStr, ok := args["parameters"].(string); ok && parametersStr != "" { + var paramReqs []MenuParameterRequest + if err := json.Unmarshal([]byte(parametersStr), ¶mReqs); err != nil { + return nil, fmt.Errorf("parameters 参数格式错误: %v", err) + } + for _, param := range paramReqs { + parameters = append(parameters, system.SysBaseMenuParameter{ + Type: param.Type, + Key: param.Key, + Value: param.Value, + }) + } + } + + var menuBtn []system.SysBaseMenuBtn + if menuBtnStr, ok := args["menuBtn"].(string); ok && menuBtnStr != "" { + var btnReqs []MenuButtonRequest + if err := json.Unmarshal([]byte(menuBtnStr), &btnReqs); err != nil { + return nil, fmt.Errorf("menuBtn 参数格式错误: %v", err) + } + for _, btn := range btnReqs { + menuBtn = append(menuBtn, system.SysBaseMenuBtn{ + Name: btn.Name, + Desc: btn.Desc, + }) + } + } + + // 构建菜单对象 + menu := system.SysBaseMenu{ + ParentId: parentId, + Path: path, + Name: name, + Hidden: hidden, + Component: component, + Sort: sort, + Meta: system.Meta{ + Title: title, + Icon: icon, + KeepAlive: keepAlive, + DefaultMenu: defaultMenu, + CloseTab: closeTab, + ActiveName: activeName, + }, + Parameters: parameters, + MenuBtn: menuBtn, + } + + // 创建菜单 + menuService := service.ServiceGroupApp.SystemServiceGroup.MenuService + err := menuService.AddBaseMenu(menu) + if err != nil { + return nil, fmt.Errorf("创建菜单失败: %v", err) + } + + // 获取创建的菜单ID + var createdMenu system.SysBaseMenu + err = global.GVA_DB.Where("name = ? AND path = ?", name, path).First(&createdMenu).Error + if err != nil { + global.GVA_LOG.Warn("获取创建的菜单ID失败", zap.Error(err)) + } + + // 构建响应 + response := &MenuCreateResponse{ + Success: true, + Message: fmt.Sprintf("成功创建菜单 %s", title), + MenuID: createdMenu.ID, + Name: name, + Path: path, + } + + resultJSON, err := json.MarshalIndent(response, "", " ") + if err != nil { + return nil, fmt.Errorf("序列化结果失败: %v", err) + } + + // 添加权限分配提醒 + permissionReminder := "\n\n⚠️ 重要提醒:\n" + + "菜单创建完成后,请前往【系统管理】->【角色管理】中为相关角色分配新创建的菜单权限," + + "以确保用户能够正常访问新菜单。\n" + + "具体步骤:\n" + + "1. 进入角色管理页面\n" + + "2. 选择需要授权的角色\n" + + "3. 在菜单权限中勾选新创建的菜单项\n" + + "4. 保存权限配置" + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("菜单创建结果:\n\n%s%s", string(resultJSON), permissionReminder), + }, + }, + }, nil +} diff --git a/mcp/menu_lister.go b/mcp/menu_lister.go new file mode 100644 index 0000000..6084632 --- /dev/null +++ b/mcp/menu_lister.go @@ -0,0 +1,111 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "fmt" + + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + "github.com/mark3labs/mcp-go/mcp" + "go.uber.org/zap" +) + +// 注册工具 +func init() { + // 注册工具将在enter.go中统一处理 + RegisterTool(&MenuLister{}) +} + +// MenuListResponse 菜单列表响应结构 +type MenuListResponse struct { + Success bool `json:"success"` + Message string `json:"message"` + Menus []system.SysBaseMenu `json:"menus"` + TotalCount int `json:"totalCount"` + Description string `json:"description"` +} + +// MenuLister 菜单列表工具 +type MenuLister struct{} + +// New 创建菜单列表工具 +func (m *MenuLister) New() mcp.Tool { + return mcp.NewTool("list_all_menus", + mcp.WithDescription(`获取系统中所有菜单信息,包括菜单树结构、路由信息、组件路径等,用于前端编写vue-router时正确跳转 + +**功能说明:** +- 返回完整的菜单树形结构 +- 包含路由配置信息(path、name、component) +- 包含菜单元数据(title、icon、keepAlive等) +- 包含菜单参数和按钮配置 +- 支持父子菜单关系展示 + +**使用场景:** +- 前端路由配置:获取所有菜单信息用于配置vue-router +- 菜单权限管理:了解系统中所有可用的菜单项 +- 导航组件开发:构建动态导航菜单 +- 系统架构分析:了解系统的菜单结构和页面组织`), + ) +} + +// Handle 处理菜单列表请求 +func (m *MenuLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 获取所有基础菜单 + allMenus, err := m.getAllMenus() + if err != nil { + global.GVA_LOG.Error("获取菜单列表失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("获取菜单列表失败: %v", err), + }, + }, + IsError: true, + }, nil + } + + // 构建返回结果 + response := MenuListResponse{ + Success: true, + Message: "获取菜单列表成功", + Menus: allMenus, + TotalCount: len(allMenus), + Description: "系统中所有菜单信息的标准列表,包含路由配置和组件信息", + } + + // 序列化响应 + responseJSON, err := json.MarshalIndent(response, "", " ") + if err != nil { + global.GVA_LOG.Error("序列化菜单响应失败", zap.Error(err)) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("序列化响应失败: %v", err), + }, + }, + IsError: true, + }, nil + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: string(responseJSON), + }, + }, + }, nil +} + +// getAllMenus 获取所有基础菜单 +func (m *MenuLister) getAllMenus() ([]system.SysBaseMenu, error) { + var menus []system.SysBaseMenu + err := global.GVA_DB.Order("sort").Preload("Parameters").Preload("MenuBtn").Find(&menus).Error + if err != nil { + return nil, err + } + return menus, nil +} diff --git a/mcp/requirement_analyzer.go b/mcp/requirement_analyzer.go new file mode 100644 index 0000000..e5ddd56 --- /dev/null +++ b/mcp/requirement_analyzer.go @@ -0,0 +1,137 @@ +package mcpTool + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&RequirementAnalyzer{}) +} + +type RequirementAnalyzer struct{} + +// RequirementAnalysisRequest 需求分析请求 +type RequirementAnalysisRequest struct { + UserRequirement string `json:"userRequirement"` +} + +// RequirementAnalysisResponse 需求分析响应 +type RequirementAnalysisResponse struct { + AIPrompt string `json:"aiPrompt"` // 给AI的提示词 +} + +// New 返回工具注册信息 +func (t *RequirementAnalyzer) New() mcp.Tool { + return mcp.NewTool("requirement_analyzer", + mcp.WithDescription(`**🚀 需求分析工具 - 首选入口工具(最高优先级)** + +**⭐ 重要提示:这是所有MCP工具的首选入口,请优先使用!** + +**🎯 核心职责:** +将用户的自然语言需求转换为AI可理解的结构化提示词 + +**📋 工作流程:** +1. 接收用户自然语言需求描述 +2. 生成专业的AI提示词,要求AI将需求梳理为清晰的逻辑步骤: + - **1. 第一步功能描述** + - **2. 第二步功能描述** + - **3. 第三步功能描述** + - **...** +3. 指导后续使用 gva_auto_generate 工具进行代码生成 + +**✅ 适用场景:** +- 用户有新的业务需求需要开发 +- 需要创建新的功能模块 +- 想要快速搭建业务系统 +- 需求描述比较模糊,需要AI帮助梳理 + +**❌ 不负责的事情:** +- 不生成具体的包名和模块名(交给 gva_auto_generate) +- 不进行代码生成(交给 gva_auto_generate) +- 不创建数据库表结构(交给 gva_auto_generate) + +**🔄 推荐工作流:** +requirement_analyzer → gva_auto_generate → 其他辅助工具 + +`), + mcp.WithString("userRequirement", + mcp.Required(), + mcp.Description("用户的需求描述,支持自然语言,如:'我要做一个猫舍管理系统,用来录入猫的信息,并且记录每只猫每天的活动信息'"), + ), + ) +} + +// Handle 处理工具调用 +func (t *RequirementAnalyzer) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + userRequirement, ok := request.GetArguments()["userRequirement"].(string) + if !ok || userRequirement == "" { + return nil, errors.New("参数错误:userRequirement 必须是非空字符串") + } + + // 分析用户需求 + analysisResponse, err := t.analyzeRequirement(userRequirement) + if err != nil { + return nil, fmt.Errorf("需求分析失败: %v", err) + } + + // 序列化响应 + responseData, err := json.Marshal(analysisResponse) + if err != nil { + return nil, fmt.Errorf("序列化响应失败: %v", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(string(responseData)), + }, + }, nil +} + +// analyzeRequirement 分析用户需求 - 专注于AI需求传递 +func (t *RequirementAnalyzer) analyzeRequirement(userRequirement string) (*RequirementAnalysisResponse, error) { + // 生成AI提示词 - 这是唯一功能 + aiPrompt := t.generateAIPrompt(userRequirement) + + return &RequirementAnalysisResponse{ + AIPrompt: aiPrompt, + }, nil +} + +// generateAIPrompt 生成AI提示词 - 要求AI梳理逻辑为1xxx2xxx格式 +func (t *RequirementAnalyzer) generateAIPrompt(userRequirement string) string { + prompt := fmt.Sprintf(`# 🤖 AI需求逻辑梳理任务 + +## 📝 用户原始需求 +%s + +## 🎯 AI任务要求 +请将上述用户需求梳理成清晰的逻辑步骤,格式要求: + +**1. 第一步功能描述** +**2. 第二步功能描述** +**3. 第三步功能描述** +**...** + +## 📋 梳理要求 +- 将需求拆解为具体的功能步骤 +- 每个步骤用数字编号(1、2、3...) +- 步骤描述要清晰、具体、可执行 +- 按照业务逻辑顺序排列 +- 考虑数据流和用户操作流程 + +## 🔄 后续流程 +梳理完成后,请使用 gva_auto_generate 工具进行代码生成: +- gva_auto_generate 会根据梳理的逻辑步骤自动生成包名、模块名 +- gva_auto_generate 会设计数据表结构和API接口 +- gva_auto_generate 会生成完整的前后端代码 + + +现在请开始梳理用户需求:"%s"`, userRequirement, userRequirement) + + return prompt +} diff --git a/middleware/casbin_rbac.go b/middleware/casbin_rbac.go index b9d6ab8..24400a4 100644 --- a/middleware/casbin_rbac.go +++ b/middleware/casbin_rbac.go @@ -6,13 +6,10 @@ import ( "git.echol.cn/loser/lckt/global" "git.echol.cn/loser/lckt/model/common/response" - "git.echol.cn/loser/lckt/service" "git.echol.cn/loser/lckt/utils" "github.com/gin-gonic/gin" ) -var casbinService = service.ServiceGroupApp.SystemServiceGroup.CasbinService - // CasbinHandler 拦截器 func CasbinHandler() gin.HandlerFunc { return func(c *gin.Context) { @@ -24,7 +21,7 @@ func CasbinHandler() gin.HandlerFunc { act := c.Request.Method // 获取用户的角色 sub := strconv.Itoa(int(waitUse.AuthorityId)) - e := casbinService.Casbin() // 判断策略中是否存在 + e := utils.GetCasbin() // 判断策略中是否存在 success, _ := e.Enforce(sub, obj, act) if !success { response.FailWithDetailed(gin.H{}, "权限不足", c) diff --git a/middleware/cors.go b/middleware/cors.go index b391857..41553b6 100644 --- a/middleware/cors.go +++ b/middleware/cors.go @@ -71,19 +71,3 @@ func checkCors(currentOrigin string) *config.CORSWhitelist { } return nil } - -func AllCors() gin.HandlerFunc { - return func(c *gin.Context) { - c.Writer.Header().Set("Access-Control-Allow-Origin", "*") - c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") - c.Writer.Header().Set("Access-Control-Allow-Headers", "*") - c.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Content-Type") - c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - - if c.Request.Method == "OPTIONS" { - c.AbortWithStatus(204) - return - } - c.Next() - } -} diff --git a/middleware/email.go b/middleware/email.go index 2d587ea..2b5907d 100644 --- a/middleware/email.go +++ b/middleware/email.go @@ -11,13 +11,10 @@ import ( "git.echol.cn/loser/lckt/global" "git.echol.cn/loser/lckt/model/system" - "git.echol.cn/loser/lckt/service" "github.com/gin-gonic/gin" "go.uber.org/zap" ) -var userService = service.ServiceGroupApp.SystemServiceGroup.UserService - func ErrorToEmail() gin.HandlerFunc { return func(c *gin.Context) { var username string @@ -26,11 +23,12 @@ func ErrorToEmail() gin.HandlerFunc { username = claims.Username } else { id, _ := strconv.Atoi(c.Request.Header.Get("x-user-id")) - user, err := userService.FindUserById(id) + var u system.SysUser + err := global.GVA_DB.Where("id = ?", id).First(&u).Error if err != nil { username = "Unknown" } - username = user.Username + username = u.Username } body, _ := io.ReadAll(c.Request.Body) // 再重新写回请求体body中,ioutil.ReadAll会清空c.Request.Body中的数据 diff --git a/middleware/jwt.go b/middleware/jwt.go index 09a3aa2..c10fb13 100644 --- a/middleware/jwt.go +++ b/middleware/jwt.go @@ -9,22 +9,19 @@ import ( "time" "git.echol.cn/loser/lckt/model/common/response" - "git.echol.cn/loser/lckt/service" "github.com/gin-gonic/gin" ) -var jwtService = service.ServiceGroupApp.SystemServiceGroup.JwtService - func JWTAuth() gin.HandlerFunc { return func(c *gin.Context) { // 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录 token := utils.GetToken(c) if token == "" { - response.NoAuth("未登录或非法访问", c) + response.NoAuth("未登录或非法访问,请登录", c) c.Abort() return } - if jwtService.IsBlacklist(token) { + if isBlacklist(token) { response.NoAuth("您的帐户异地登陆或令牌失效", c) utils.ClearToken(c) c.Abort() @@ -35,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 @@ -65,7 +62,7 @@ func JWTAuth() gin.HandlerFunc { utils.SetToken(c, newToken, int(dr.Seconds())) if global.GVA_CONFIG.System.UseMultipoint { // 记录新的活跃jwt - _ = jwtService.SetRedisJWT(newToken, newClaims.Username) + _ = utils.SetRedisJWT(newToken, newClaims.Username) } } c.Next() @@ -78,3 +75,14 @@ func JWTAuth() gin.HandlerFunc { } } } + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: IsBlacklist +//@description: 判断JWT是否在黑名单内部 +//@param: jwt string +//@return: bool + +func isBlacklist(jwt string) bool { + _, ok := global.BlackCache.Get(jwt) + return ok +} diff --git a/middleware/operation.go b/middleware/operation.go index da1017f..475284c 100644 --- a/middleware/operation.go +++ b/middleware/operation.go @@ -15,13 +15,10 @@ import ( "git.echol.cn/loser/lckt/global" "git.echol.cn/loser/lckt/model/system" - "git.echol.cn/loser/lckt/service" "github.com/gin-gonic/gin" "go.uber.org/zap" ) -var operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService - var respPool sync.Pool var bufferSize = 1024 @@ -115,8 +112,7 @@ func OperationRecord() gin.HandlerFunc { record.Body = "超出记录长度" } } - - if err := operationRecordService.CreateSysOperationRecord(record); err != nil { + if err := global.GVA_DB.Create(&record).Error; err != nil { global.GVA_LOG.Error("create operation record error:", zap.Error(err)) } } diff --git a/middleware/timeout.go b/middleware/timeout.go new file mode 100644 index 0000000..473abf6 --- /dev/null +++ b/middleware/timeout.go @@ -0,0 +1,55 @@ +package middleware + +import ( + "context" + "github.com/gin-gonic/gin" + "net/http" + "time" +) + +// TimeoutMiddleware 创建超时中间件 +// 入参 timeout 设置超时时间(例如:time.Second * 5) +// 使用示例 xxx.Get("path",middleware.TimeoutMiddleware(30*time.Second),HandleFunc) +func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc { + return func(c *gin.Context) { + ctx, cancel := context.WithTimeout(c.Request.Context(), timeout) + defer cancel() + + c.Request = c.Request.WithContext(ctx) + + // 使用 buffered channel 避免 goroutine 泄漏 + done := make(chan struct{}, 1) + panicChan := make(chan interface{}, 1) + + go func() { + defer func() { + if p := recover(); p != nil { + select { + case panicChan <- p: + default: + } + } + select { + case done <- struct{}{}: + default: + } + }() + c.Next() + }() + + select { + case p := <-panicChan: + panic(p) + case <-done: + return + case <-ctx.Done(): + // 确保服务器超时设置足够长 + c.Header("Connection", "close") + c.AbortWithStatusJSON(http.StatusGatewayTimeout, gin.H{ + "code": 504, + "msg": "请求超时", + }) + return + } + } +} diff --git a/middleware/user_jwt.go b/middleware/user_jwt.go index f2c8c5d..d48b69d 100644 --- a/middleware/user_jwt.go +++ b/middleware/user_jwt.go @@ -50,7 +50,7 @@ func UserJWTAuth() gin.HandlerFunc { user_jwt.SetToken(c, newToken, int(dr.Seconds())) if global.GVA_CONFIG.System.UseMultipoint { // 记录新的活跃jwt - _ = jwtService.SetRedisJWT(newToken, newClaims.Username) + _ = utils.SetRedisJWT(newToken, newClaims.Username) } } c.Next() diff --git a/model/system/request/sys_auto_code.go b/model/system/request/sys_auto_code.go index d398a9c..d9e7260 100644 --- a/model/system/request/sys_auto_code.go +++ b/model/system/request/sys_auto_code.go @@ -158,7 +158,7 @@ func (r *AutoCode) Pretreatment() error { r.NeedJSON = true case "time.Time": r.HasTimer = true - if r.Fields[i].FieldSearchType != "" { + if r.Fields[i].FieldSearchType != "" && r.Fields[i].FieldSearchType != "BETWEEN" && r.Fields[i].FieldSearchType != "NOT BETWEEN" { r.HasSearchTimer = true } } diff --git a/model/system/request/sys_auto_code_mcp.go b/model/system/request/sys_auto_code_mcp.go new file mode 100644 index 0000000..a52ec7c --- /dev/null +++ b/model/system/request/sys_auto_code_mcp.go @@ -0,0 +1,16 @@ +package request + +type AutoMcpTool struct { + Name string `json:"name" form:"name" binding:"required"` + Description string `json:"description" form:"description" binding:"required"` + Params []struct { + Name string `json:"name" form:"name" binding:"required"` + Description string `json:"description" form:"description" binding:"required"` + Type string `json:"type" form:"type" binding:"required"` // string, number, boolean, object, array + Required bool `json:"required" form:"required"` + Default string `json:"default" form:"default"` + } `json:"params" form:"params"` + Response []struct { + Type string `json:"type" form:"type" binding:"required"` // text, image + } `json:"response" form:"response"` +} diff --git a/model/system/request/sys_user.go b/model/system/request/sys_user.go index 70c47ce..0b5328a 100644 --- a/model/system/request/sys_user.go +++ b/model/system/request/sys_user.go @@ -33,6 +33,11 @@ type ChangePasswordReq struct { NewPassword string `json:"newPassword"` // 新密码 } +type ResetPassword struct { + ID uint `json:"ID" form:"ID"` + Password string `json:"password" form:"password" gorm:"comment:用户登录密码"` // 用户登录密码 +} + // SetUserAuth Modify user's auth structure type SetUserAuth struct { AuthorityId uint `json:"authorityId"` // 角色ID @@ -51,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/model/system/request/sys_version.go b/model/system/request/sys_version.go new file mode 100644 index 0000000..c54a37e --- /dev/null +++ b/model/system/request/sys_version.go @@ -0,0 +1,40 @@ +package request + +import ( + "git.echol.cn/loser/lckt/model/common/request" + "git.echol.cn/loser/lckt/model/system" + "time" +) + +type SysVersionSearch struct { + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` + VersionName *string `json:"versionName" form:"versionName"` + VersionCode *string `json:"versionCode" form:"versionCode"` + request.PageInfo +} + +// ExportVersionRequest 导出版本请求结构体 +type ExportVersionRequest struct { + VersionName string `json:"versionName" binding:"required"` // 版本名称 + VersionCode string `json:"versionCode" binding:"required"` // 版本号 + Description string `json:"description"` // 版本描述 + MenuIds []uint `json:"menuIds"` // 选中的菜单ID列表 + ApiIds []uint `json:"apiIds"` // 选中的API ID列表 + DictIds []uint `json:"dictIds"` // 选中的字典ID列表 +} + +// ImportVersionRequest 导入版本请求结构体 +type ImportVersionRequest struct { + VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息 + ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu + ExportApi []system.SysApi `json:"apis"` // API数据,直接复用SysApi + ExportDictionary []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary +} + +// VersionInfo 版本信息结构体 +type VersionInfo struct { + Name string `json:"name" binding:"required"` // 版本名称 + Code string `json:"code" binding:"required"` // 版本号 + Description string `json:"description"` // 版本描述 + ExportTime string `json:"exportTime"` // 导出时间 +} diff --git a/model/system/response/sys_version.go b/model/system/response/sys_version.go new file mode 100644 index 0000000..fcf9713 --- /dev/null +++ b/model/system/response/sys_version.go @@ -0,0 +1,14 @@ +package response + +import ( + "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/model/system/request" +) + +// ExportVersionResponse 导出版本响应结构体 +type ExportVersionResponse struct { + Version request.VersionInfo `json:"version"` // 版本信息 + Menus []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu + Apis []system.SysApi `json:"apis"` // API数据,直接复用SysApi + Dictionaries []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary +} diff --git a/model/system/sys_base_menu.go b/model/system/sys_base_menu.go index b0bf8dc..6241889 100644 --- a/model/system/sys_base_menu.go +++ b/model/system/sys_base_menu.go @@ -21,12 +21,13 @@ type SysBaseMenu struct { } type Meta struct { - ActiveName string `json:"activeName" gorm:"comment:高亮菜单"` - KeepAlive bool `json:"keepAlive" gorm:"comment:是否缓存"` // 是否缓存 - DefaultMenu bool `json:"defaultMenu" gorm:"comment:是否是基础路由(开发中)"` // 是否是基础路由(开发中) - Title string `json:"title" gorm:"comment:菜单名"` // 菜单名 - Icon string `json:"icon" gorm:"comment:菜单图标"` // 菜单图标 - CloseTab bool `json:"closeTab" gorm:"comment:自动关闭tab"` // 自动关闭tab + ActiveName string `json:"activeName" gorm:"comment:高亮菜单"` + KeepAlive bool `json:"keepAlive" gorm:"comment:是否缓存"` // 是否缓存 + DefaultMenu bool `json:"defaultMenu" gorm:"comment:是否是基础路由(开发中)"` // 是否是基础路由(开发中) + Title string `json:"title" gorm:"comment:菜单名"` // 菜单名 + Icon string `json:"icon" gorm:"comment:菜单图标"` // 菜单图标 + CloseTab bool `json:"closeTab" gorm:"comment:自动关闭tab"` // 自动关闭tab + TransitionType string `json:"transitionType" gorm:"comment:路由切换动画"` // 路由切换动画 } type SysBaseMenuParameter struct { diff --git a/model/system/sys_version.go b/model/system/sys_version.go new file mode 100644 index 0000000..1456d70 --- /dev/null +++ b/model/system/sys_version.go @@ -0,0 +1,20 @@ +// 自动生成模板SysVersion +package system + +import ( + "git.echol.cn/loser/lckt/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:longtext;postgres:type:text;sqlite:type:text;"` //版本数据 +} + +// TableName 版本管理 SysVersion自定义表名 sys_versions +func (SysVersion) TableName() string { + return "sys_versions" +} diff --git a/plugin/email/README.MD b/plugin/email/README.MD index 1720283..685cdd6 100644 --- a/plugin/email/README.MD +++ b/plugin/email/README.MD @@ -14,6 +14,7 @@ global.GVA_CONFIG.Email.Nickname, global.GVA_CONFIG.Email.Port, global.GVA_CONFIG.Email.IsSSL, + global.GVA_CONFIG.Email.IsLoginAuth, )) 同样也可以再传入时写死 @@ -26,6 +27,7 @@ "登录密钥", 465, true, + true, )) ### 2. 配置说明 @@ -34,13 +36,14 @@ //其中 Form 和 Secret 通常来说就是用户名和密码 type Email struct { - To string // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 此处配置主要用于发送错误监控邮件 - From string // 发件人 你自己要发邮件的邮箱 - Host string // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 - Secret string // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 - Nickname string // 昵称 发件人昵称 自定义即可 可以不填 - Port int // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 - IsSSL bool // 是否SSL 是否开启SSL + To string // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 此处配置主要用于发送错误监控邮件 + From string // 发件人 你自己要发邮件的邮箱 + Host string // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string // 昵称 发件人昵称 自定义即可 可以不填 + Port int // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool // 是否SSL 是否开启SSL + IsLoginAuth bool // 是否LoginAuth 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等) } #### 2-2 入参结构说明 //其中 Form 和 Secret 通常来说就是用户名和密码 diff --git a/plugin/email/config/email.go b/plugin/email/config/email.go index c535348..412b5a8 100644 --- a/plugin/email/config/email.go +++ b/plugin/email/config/email.go @@ -1,11 +1,12 @@ package config type Email struct { - To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 - From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 - Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 - Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 - Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 - Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 - IsSSL bool `mapstructure:"is-ssl" json:"isSSL" yaml:"is-ssl"` // 是否SSL 是否开启SSL + To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 + Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 + Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool `mapstructure:"is-ssl" json:"isSSL" yaml:"is-ssl"` // 是否SSL 是否开启SSL + IsLoginAuth bool `mapstructure:"is-loginauth" json:"is-loginauth" yaml:"is-loginauth"` // 是否LoginAuth 是否使用LoginAuth认证 } diff --git a/plugin/email/main.go b/plugin/email/main.go index ae5bae5..481537d 100644 --- a/plugin/email/main.go +++ b/plugin/email/main.go @@ -8,7 +8,7 @@ import ( type emailPlugin struct{} -func CreateEmailPlug(To, From, Host, Secret, Nickname string, Port int, IsSSL bool) *emailPlugin { +func CreateEmailPlug(To, From, Host, Secret, Nickname string, Port int, IsSSL bool, IsLoginAuth bool) *emailPlugin { global.GlobalConfig.To = To global.GlobalConfig.From = From global.GlobalConfig.Host = Host @@ -16,6 +16,7 @@ func CreateEmailPlug(To, From, Host, Secret, Nickname string, Port int, IsSSL bo global.GlobalConfig.Nickname = Nickname global.GlobalConfig.Port = Port global.GlobalConfig.IsSSL = IsSSL + global.GlobalConfig.IsLoginAuth = IsLoginAuth return &emailPlugin{} } diff --git a/plugin/email/utils/email.go b/plugin/email/utils/email.go index 5877648..a4611b9 100644 --- a/plugin/email/utils/email.go +++ b/plugin/email/utils/email.go @@ -60,8 +60,14 @@ func send(to []string, subject string, body string) error { host := global.GlobalConfig.Host port := global.GlobalConfig.Port isSSL := global.GlobalConfig.IsSSL + isLoginAuth := global.GlobalConfig.IsLoginAuth - auth := smtp.PlainAuth("", from, secret, host) + var auth smtp.Auth + if isLoginAuth { + auth = LoginAuth(from, secret) + } else { + auth = smtp.PlainAuth("", from, secret, host) + } e := email.NewEmail() if nickname != "" { e.From = fmt.Sprintf("%s <%s>", nickname, from) @@ -80,3 +86,37 @@ func send(to []string, subject string, body string) error { } return err } + +// LoginAuth 用于IBM、微软邮箱服务器的LOGIN认证方式 +type loginAuth struct { + username, password string +} + +func LoginAuth(username, password string) smtp.Auth { + return &loginAuth{username, password} +} + +func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", []byte{}, nil +} + +func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(a.username), nil + case "Password:": + return []byte(a.password), nil + default: + // 邮箱服务器可能发送的其他提示信息 + prompt := strings.ToLower(string(fromServer)) + if strings.Contains(prompt, "username") || strings.Contains(prompt, "user") { + return []byte(a.username), nil + } + if strings.Contains(prompt, "password") || strings.Contains(prompt, "pass") { + return []byte(a.password), nil + } + } + } + return nil, nil +} diff --git a/resource/mcp/tools.tpl b/resource/mcp/tools.tpl new file mode 100644 index 0000000..49bfa20 --- /dev/null +++ b/resource/mcp/tools.tpl @@ -0,0 +1,56 @@ +package mcpTool + +import ( + "context" + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&{{.Name | title}}{}) +} + +type {{.Name | title}} struct { +} + +// {{.Description}} +func (t *{{.Name | title}}) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // TODO: 实现工具逻辑 + // 参数示例: + // {{- range .Params}} + // {{.Name}} := request.GetArguments()["{{.Name}}"] + // {{- end}} + return &mcp.CallToolResult{ + Content: []mcp.Content{ + {{- range .Response}} + mcp.{{.Type | title}}Content{ + Type: "{{.Type}}", + // TODO: 填充{{.Type}}内容 + }, + {{- end}} + }, + }, nil +} + +func (t *{{.Name | title}}) New() mcp.Tool { + return mcp.NewTool("{{.Name}}", + mcp.WithDescription("{{.Description}}"), + {{- range .Params}} + mcp.With{{.Type | title}}("{{.Name}}", + {{- if .Required}}mcp.Required(),{{end}} + mcp.Description("{{.Description}}"), + {{- if .Default}} + {{- if eq .Type "string"}} + mcp.DefaultString("{{.Default}}"), + {{- else if eq .Type "number"}} + mcp.DefaultNumber({{.Default}}), + {{- else if eq .Type "boolean"}} + mcp.DefaultBoolean({{if or (eq .Default "true") (eq .Default "True")}}true{{else}}false{{end}}), + {{- else if eq .Type "array"}} + // 注意:数组默认值需要在后端代码中预处理为正确的格式 + // mcp.DefaultArray({{.Default}}), + {{- end}} + {{- end}} + ), + {{- end}} + ) +} diff --git a/resource/package/server/model/model.go.tpl b/resource/package/server/model/model.go.tpl index 3114b9c..e1603ed 100644 --- a/resource/package/server/model/model.go.tpl +++ b/resource/package/server/model/model.go.tpl @@ -1,25 +1,7 @@ {{- if .IsAdd}} // 在结构体中新增如下字段 {{- range .Fields}} -{{- if eq .FieldType "enum" }} -{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};type:enum({{.DataTypeLong}});comment:{{.Comment}};" {{- if .Require }} binding:"required"{{- end -}}` -{{- else if eq .FieldType "picture" }} -{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` -{{- else if eq .FieldType "video" }} -{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` -{{- else if eq .FieldType "file" }} -{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` -{{- else if eq .FieldType "pictures" }} -{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` -{{- else if eq .FieldType "richtext" }} -{{.FieldName}} *string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}}` -{{- else if eq .FieldType "json" }} -{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end }} swaggertype:"object"` -{{- else if eq .FieldType "array" }} -{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` -{{- else }} -{{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` -{{- end }} {{ if .FieldDesc }}//{{.FieldDesc}}{{ end }} + {{ GenerateField . }} {{- end }} {{ else }} @@ -47,25 +29,7 @@ type {{.StructName}} struct { global.GVA_MODEL {{- end }} {{- range .Fields}} - {{- if eq .FieldType "enum" }} - {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};type:enum({{.DataTypeLong}});comment:{{.Comment}};" {{- if .Require }} binding:"required"{{- end -}}` - {{- else if eq .FieldType "picture" }} - {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` - {{- else if eq .FieldType "video" }} - {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` - {{- else if eq .FieldType "file" }} - {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` - {{- else if eq .FieldType "pictures" }} - {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` - {{- else if eq .FieldType "richtext" }} - {{.FieldName}} *string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}}` - {{- else if eq .FieldType "json" }} - {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end }} swaggertype:"object"` - {{- else if eq .FieldType "array" }} - {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` - {{- else }} - {{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` - {{- end }} {{ if .FieldDesc }}//{{.FieldDesc}}{{ end }} + {{ GenerateField . }} {{- end }} {{- if .AutoCreateResource }} CreatedBy uint `gorm:"column:created_by;comment:创建者"` diff --git a/resource/package/server/model/request/request.go.tpl b/resource/package/server/model/request/request.go.tpl index ee5816d..f8749f3 100644 --- a/resource/package/server/model/request/request.go.tpl +++ b/resource/package/server/model/request/request.go.tpl @@ -2,16 +2,7 @@ // 在结构体中新增如下字段 {{- range .Fields}} {{- if ne .FieldSearchType ""}} - {{- if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} -Start{{.FieldName}} *{{.FieldType}} `json:"start{{.FieldName}}" form:"start{{.FieldName}}"` -End{{.FieldName}} *{{.FieldType}} `json:"end{{.FieldName}}" form:"end{{.FieldName}}"` - {{- else }} - {{- if or (eq .FieldType "enum") (eq .FieldType "picture") (eq .FieldType "pictures") (eq .FieldType "video") (eq .FieldType "json") }} -{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` - {{- else }} -{{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` - {{- end }} - {{- end }} + {{ GenerateSearchField . }} {{- end}} {{- end }} {{- if .NeedSort}} @@ -24,28 +15,18 @@ package request import ( {{- if not .OnlyTemplate }} "{{.Module}}/model/common/request" - {{ if or .HasSearchTimer .GvaModel}}"time"{{ end }} + {{ if or .HasSearchTimer .GvaModel }}"time"{{ end }} {{- end }} ) type {{.StructName}}Search struct{ {{- if not .OnlyTemplate}} {{- if .GvaModel }} - StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` - EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` {{- end }} {{- range .Fields}} {{- if ne .FieldSearchType ""}} - {{- if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} - Start{{.FieldName}} *{{.FieldType}} `json:"start{{.FieldName}}" form:"start{{.FieldName}}"` - End{{.FieldName}} *{{.FieldType}} `json:"end{{.FieldName}}" form:"end{{.FieldName}}"` - {{- else }} - {{- if or (eq .FieldType "enum") (eq .FieldType "picture") (eq .FieldType "pictures") (eq .FieldType "video") (eq .FieldType "json") }} - {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` - {{- else }} - {{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` - {{- end }} - {{- end }} + {{ GenerateSearchField . }} {{- end}} {{- end }} request.PageInfo diff --git a/resource/package/server/service/service.go.tpl b/resource/package/server/service/service.go.tpl index d0cbb4f..acd57a3 100644 --- a/resource/package/server/service/service.go.tpl +++ b/resource/package/server/service/service.go.tpl @@ -8,29 +8,7 @@ {{- if .IsAdd}} // Get{{.StructName}}InfoList 新增搜索语句 - {{- range .Fields}} - {{- if .FieldSearchType}} - {{- if or (eq .FieldType "enum") (eq .FieldType "pictures") (eq .FieldType "picture") (eq .FieldType "video") (eq .FieldType "json") }} -if info.{{.FieldName}} != "" { - {{- if or (eq .FieldType "enum") }} - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+ {{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) - {{- else}} -// 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 - {{- end}} -} - {{- else if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} -if info.Start{{.FieldName}} != nil && info.End{{.FieldName}} != nil { - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ? AND ? ",info.Start{{.FieldName}},info.End{{.FieldName}}) -} - {{- else}} -if info.{{.FieldName}} != nil{{- if eq .FieldType "string" }} && *info.{{.FieldName}} != ""{{- end }} { - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+{{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) -} - {{- end }} - {{- end }} - {{- end }} - - + {{ GenerateSearchConditions .Fields }} // Get{{.StructName}}InfoList 新增排序语句 请自行在搜索语句中添加orderMap内容 {{- range .Fields}} {{- if .Sort}} @@ -170,31 +148,11 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoLis var {{.Abbreviation}}s []{{.Package}}.{{.StructName}} // 如果有条件搜索 下方会自动创建搜索语句 {{- if .GvaModel }} - if info.StartCreatedAt !=nil && info.EndCreatedAt !=nil { - db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) } {{- end }} - {{- range .Fields}} - {{- if .FieldSearchType}} - {{- if or (eq .FieldType "enum") (eq .FieldType "pictures") (eq .FieldType "picture") (eq .FieldType "video") (eq .FieldType "json") }} - if info.{{.FieldName}} != "" { - {{- if or (eq .FieldType "enum")}} - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+ {{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) - {{- else}} - // 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 - {{- end}} - } - {{- else if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} - if info.Start{{.FieldName}} != nil && info.End{{.FieldName}} != nil { - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ? AND ? ",info.Start{{.FieldName}},info.End{{.FieldName}}) - } - {{- else}} - if info.{{.FieldName}} != nil{{- if eq .FieldType "string" }} && *info.{{.FieldName}} != ""{{- end }} { - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+{{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) - } - {{- end }} - {{- end }} - {{- end }} + {{ GenerateSearchConditions .Fields }} err = db.Count(&total).Error if err!=nil { return @@ -202,6 +160,10 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoLis {{- if .NeedSort}} var OrderStr string orderMap := make(map[string]bool) + {{- if .GvaModel }} + orderMap["id"] = true + orderMap["created_at"] = true + {{- end }} {{- range .Fields}} {{- if .Sort}} orderMap["{{.ColumnName}}"] = true @@ -233,7 +195,7 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}DataSou {{$key}} := make([]map[string]any, 0) {{ $dataDB := "" }} {{- if eq $value.DBName "" }} - {{ $dataDB = $db }} + {{ $dataDB = "global.GVA_DB" }} {{- else}} {{ $dataDB = printf "global.MustGetGlobalDBByDBName(\"%s\")" $value.DBName }} {{- end}} diff --git a/resource/package/web/view/form.vue.tpl b/resource/package/web/view/form.vue.tpl index e45b375..28c1f02 100644 --- a/resource/package/web/view/form.vue.tpl +++ b/resource/package/web/view/form.vue.tpl @@ -1,75 +1,10 @@ {{- if .IsAdd }} // 新增表单中增加如下代码 {{- range .Fields}} - {{- if .Form}} - - {{- if .CheckDataSource}} - - - - {{- else }} - {{- if eq .FieldType "bool" }} - - {{- end }} - {{- if eq .FieldType "string" }} - {{- if .DictType}} - - - - {{- else }} - - {{- end }} - {{- end }} - {{- if eq .FieldType "richtext" }} - - {{- end }} - {{- if eq .FieldType "json" }} - // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 - {{"{{"}} formData.{{.FieldJson}} {{"}}"}} - {{- end }} - {{- if eq .FieldType "array" }} - - {{- end }} - {{- if eq .FieldType "int" }} - - {{- end }} - {{- if eq .FieldType "time.Time" }} - - {{- end }} - {{- if eq .FieldType "float64" }} - - {{- end }} - {{- if eq .FieldType "enum" }} - - - - {{- end }} - {{- if eq .FieldType "picture" }} - - {{- end }} - {{- if eq .FieldType "pictures" }} - - {{- end }} - {{- if eq .FieldType "video" }} - - {{- end }} - {{- if eq .FieldType "file" }} - - {{- end }} - {{- end }} - - {{- end }} - {{- end }} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} +{{- end }} // 字典增加如下代码 {{- range $index, $element := .DictTypes}} @@ -85,42 +20,7 @@ const {{ $element }}Options = ref([]) // 基础formData结构增加如下字段 {{- range .Fields}} {{- if .Form}} - {{- if eq .FieldType "bool" }} -{{.FieldJson}}: false, - {{- end }} - {{- if eq .FieldType "string" }} -{{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "richtext" }} -{{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "int" }} -{{.FieldJson}}: {{- if or .DataSource}} undefined{{ else }} 0{{- end }}, - {{- end }} - {{- if eq .FieldType "time.Time" }} -{{.FieldJson}}: new Date(), - {{- end }} - {{- if eq .FieldType "float64" }} -{{.FieldJson}}: 0, - {{- end }} - {{- if eq .FieldType "picture" }} -{{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "video" }} -{{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "pictures" }} -{{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "file" }} -{{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "json" }} -{{.FieldJson}}: {}, - {{- end }} - {{- if eq .FieldType "array" }} -{{.FieldJson}}: [], - {{- end }} + {{ GenerateDefaultFormValue . }} {{- end }} {{- end }} // 验证规则中增加如下字段 @@ -181,62 +81,7 @@ getDataSourceFunc() {{- end }} {{- range .Fields}} {{- if .Form }} - - {{- if .CheckDataSource}} - - - - {{- else }} - {{- if eq .FieldType "bool" }} - - {{- end }} - {{- if eq .FieldType "string" }} - {{- if .DictType}} - - - - {{- else }} - - {{- end }} - {{- end }} - {{- if eq .FieldType "richtext" }} - - {{- end }} - {{- if eq .FieldType "int" }} - - {{- end }} - {{- if eq .FieldType "time.Time" }} - - {{- end }} - {{- if eq .FieldType "float64" }} - - {{- end }} - {{- if eq .FieldType "enum" }} - - - - {{- end }} - {{- if eq .FieldType "picture" }} - - {{- end }} - {{- if eq .FieldType "video" }} - - {{- end }} - {{- if eq .FieldType "pictures" }} - - {{- end }} - {{- if eq .FieldType "file" }} - - {{- end }} - {{- if eq .FieldType "json" }} - // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 - {{"{{"}} formData.{{.FieldJson}} {{"}}"}} - {{- end }} - {{- if eq .FieldType "array" }} - - {{- end }} - {{- end }} - + {{ GenerateFormItem . }} {{- end }} {{- end }} @@ -333,42 +178,7 @@ const formData = ref({ {{- end }} {{- range .Fields}} {{- if .Form }} - {{- if eq .FieldType "bool" }} - {{.FieldJson}}: false, - {{- end }} - {{- if eq .FieldType "string" }} - {{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "richtext" }} - {{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "int" }} - {{.FieldJson}}: {{- if or .DataSource }} undefined{{ else }} 0{{- end }}, - {{- end }} - {{- if eq .FieldType "time.Time" }} - {{.FieldJson}}: new Date(), - {{- end }} - {{- if eq .FieldType "float64" }} - {{.FieldJson}}: 0, - {{- end }} - {{- if eq .FieldType "picture" }} - {{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "video" }} - {{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "pictures" }} - {{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "file" }} - {{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "json" }} - {{.FieldJson}}: {}, - {{- end }} - {{- if eq .FieldType "array" }} - {{.FieldJson}}: [], - {{- end }} + {{ GenerateDefaultFormValue . }} {{- end }} {{- end }} }) diff --git a/resource/package/web/view/table.vue.tpl b/resource/package/web/view/table.vue.tpl index c6d5f55..beadade 100644 --- a/resource/package/web/view/table.vue.tpl +++ b/resource/package/web/view/table.vue.tpl @@ -1,276 +1,35 @@ {{- $global := . }} {{- $templateID := printf "%s_%s" .Package .StructName }} {{- if .IsAdd }} + // 请在搜索条件中增加如下代码 -{{- range .Fields}} {{- if .FieldSearchType}} {{- if eq .FieldType "bool" }} - - - - - - - - - {{- else if .DictType}} - - - - - - {{- else if .CheckDataSource}} - - - - - - {{- else}} - - {{- if eq .FieldType "float64" "int"}} - {{if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} - -— - - {{- else}} - - {{- end}} - {{- else if eq .FieldType "time.Time"}} - {{if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} - - -— - - {{- else}} - - {{- end}} - {{- else}} - - {{- end}} -{{ end }}{{ end }}{{ end }} +{{- range .Fields}} + {{- if .FieldSearchType}} +{{ GenerateSearchFormItem .}} + {{ end }} +{{ end }} // 表格增加如下列代码 {{- range .Fields}} - {{- if .Table}} - {{- if .CheckDataSource }} - - - - {{- else if .DictType}} - - - - {{- else if eq .FieldType "bool" }} - - - - {{- else if eq .FieldType "time.Time" }} - - - - {{- else if eq .FieldType "picture" }} - - - - {{- else if eq .FieldType "pictures" }} - - - - {{- else if eq .FieldType "video" }} - - - - {{- else if eq .FieldType "richtext" }} - - - - {{- else if eq .FieldType "file" }} - - - - {{- else if eq .FieldType "json" }} - - - - {{- else if eq .FieldType "array" }} - - - - {{- else }} - - {{- end }} - {{- end }} - {{- end }} + {{- if .Table}} + {{ GenerateTableColumn . }} + {{- end }} +{{- end }} // 新增表单中增加如下代码 {{- range .Fields}} - {{- if .Form}} - - {{- if .CheckDataSource}} - - - - {{- else }} - {{- if eq .FieldType "bool" }} - - {{- end }} - {{- if eq .FieldType "string" }} - {{- if .DictType}} - - - - {{- else }} - - {{- end }} - {{- end }} - {{- if eq .FieldType "richtext" }} - - {{- end }} - {{- if eq .FieldType "json" }} - // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 - {{"{{"}} formData.{{.FieldJson}} {{"}}"}} - {{- end }} - {{- if eq .FieldType "array" }} - {{- if .DictType}} - - - - {{- else }} - - {{- end }} - {{- end }} - {{- if eq .FieldType "int" }} - - {{- end }} - {{- if eq .FieldType "time.Time" }} - - {{- end }} - {{- if eq .FieldType "float64" }} - - {{- end }} - {{- if eq .FieldType "enum" }} - - - - {{- end }} - {{- if eq .FieldType "picture" }} - - {{- end }} - {{- if eq .FieldType "pictures" }} - - {{- end }} - {{- if eq .FieldType "video" }} - - {{- end }} - {{- if eq .FieldType "file" }} - - {{- end }} - {{- end }} - - {{- end }} - {{- end }} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} +{{- end }} // 查看抽屉中增加如下代码 {{- range .Fields}} {{- if .Desc }} - -{{- if .CheckDataSource }} - - {{- else if and (ne .FieldType "picture" ) (ne .FieldType "pictures" ) (ne .FieldType "file" ) (ne .FieldType "array" ) }} - {{"{{"}} detailFrom.{{.FieldJson}} {{"}}"}} - {{- else }} - {{- if eq .FieldType "picture" }} - - {{- end }} - {{- if eq .FieldType "array" }} - - {{- end }} - {{- if eq .FieldType "pictures" }} - - {{- end }} - {{- if eq .FieldType "richtext" }} - - {{- end }} - {{- if eq .FieldType "file" }} -
- - - {{"{{"}}item.name{{"}}"}} - -
- {{- end }} - {{- end }} -
+ {{ GenerateDescriptionItem . }} {{- end }} {{- end }} @@ -288,42 +47,7 @@ const {{ $element }}Options = ref([]) // 基础formData结构(变量处和关闭表单处)增加如下字段 {{- range .Fields}} {{- if .Form}} - {{- if eq .FieldType "bool" }} -{{.FieldJson}}: false, - {{- end }} - {{- if eq .FieldType "string" }} -{{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "richtext" }} -{{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "int" }} -{{.FieldJson}}: {{- if .DataSource}} undefined{{ else }} 0{{- end }}, - {{- end }} - {{- if eq .FieldType "time.Time" }} -{{.FieldJson}}: new Date(), - {{- end }} - {{- if eq .FieldType "float64" }} -{{.FieldJson}}: 0, - {{- end }} - {{- if eq .FieldType "picture" }} -{{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "video" }} -{{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "pictures" }} -{{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "file" }} -{{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "json" }} -{{.FieldJson}}: {}, - {{- end }} - {{- if eq .FieldType "array" }} -{{.FieldJson}}: [], - {{- end }} + {{ GenerateDefaultFormValue . }} {{- end }} {{- end }} // 验证规则中增加如下字段 @@ -372,9 +96,9 @@ getDataSourceFunc()
{{- if not .IsTree }} @@ -535,97 +161,13 @@ getDataSourceFunc() > {{ if .GvaModel }} - + {{ end }} {{- range .Fields}} {{- if .Table}} - {{- if .CheckDataSource }} - - - - {{- else if .DictType}} - - - - {{- else if eq .FieldType "bool" }} - - - - {{- else if eq .FieldType "time.Time" }} - - - - {{- else if eq .FieldType "picture" }} - - - - {{- else if eq .FieldType "pictures" }} - - - - {{- else if eq .FieldType "video" }} - - - - {{- else if eq .FieldType "richtext" }} - - - - {{- else if eq .FieldType "file" }} - - - - {{- else if eq .FieldType "json" }} - - - - {{- else if eq .FieldType "array" }} - - - - {{- else }} - - {{- end }} + {{ GenerateTableColumn . }} {{- end }} {{- end }} @@ -679,78 +221,7 @@ getDataSourceFunc() {{- end }} {{- range .Fields}} {{- if .Form}} - - {{- if .CheckDataSource}} - - - - {{- else }} - {{- if eq .FieldType "bool" }} - - {{- end }} - {{- if eq .FieldType "string" }} - {{- if .DictType}} - - - - {{- else }} - - {{- end }} - {{- end }} - {{- if eq .FieldType "richtext" }} - - {{- end }} - {{- if eq .FieldType "json" }} - // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 - {{"{{"}} formData.{{.FieldJson}} {{"}}"}} - {{- end }} - {{- if eq .FieldType "array" }} - {{- if .DictType}} - - - - {{- else }} - - {{- end }} - {{- end }} - {{- if eq .FieldType "int" }} - - {{- end }} - {{- if eq .FieldType "time.Time" }} - - {{- end }} - {{- if eq .FieldType "float64" }} - - {{- end }} - {{- if eq .FieldType "enum" }} - - - - {{- end }} - {{- if eq .FieldType "picture" }} - - {{- end }} - {{- if eq .FieldType "pictures" }} - - {{- end }} - {{- if eq .FieldType "video" }} - - {{- end }} - {{- if eq .FieldType "file" }} - - {{- end }} - {{- end }} - + {{ GenerateFormItem . }} {{- end }} {{- end }} @@ -761,7 +232,7 @@ getDataSourceFunc() {{- if .IsTree }} - {{- if .CheckDataSource }} - {{- if eq .DataSource.Association 2}} - - {{ "{{ item }}" }} - - {{- else }} - {{"{{"}} filterDataSource(dataSource.{{.FieldJson}},detailFrom.{{.FieldJson}}) {{"}}"}} - {{- end }} - {{- else if .DictType}} - {{if eq .FieldType "array"}} - {{"{{"}} filterDict(item,{{.DictType}}Options) {{"}}"}} - {{- else }} - {{"{{"}} filterDict(detailFrom.{{.FieldJson}},{{.DictType}}Options) {{"}}"}} - {{end}} - {{- else if and (ne .FieldType "picture" ) (ne .FieldType "richtext" ) (ne .FieldType "pictures" ) (ne .FieldType "file" ) (ne .FieldType "array" ) }} - {{"{{"}} detailFrom.{{.FieldJson}} {{"}}"}} - {{- else }} - {{- if eq .FieldType "picture" }} - - {{- end }} - {{- if eq .FieldType "array" }} - - {{- end }} - {{- if eq .FieldType "pictures" }} - - {{- end }} - {{- if eq .FieldType "richtext" }} - - {{- end }} - {{- if eq .FieldType "file" }} -
- - - {{"{{"}}item.name{{"}}"}} - -
- {{- end }} - {{- end }} -
+ {{ GenerateDescriptionItem . }} {{- end }} {{- end }} @@ -906,42 +338,7 @@ const formData = ref({ {{- end }} {{- range .Fields}} {{- if .Form}} - {{- if eq .FieldType "bool" }} - {{.FieldJson}}: false, - {{- end }} - {{- if eq .FieldType "string" }} - {{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "richtext" }} - {{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "int" }} - {{.FieldJson}}: {{- if .DataSource}} undefined{{ else }} 0{{- end }}, - {{- end }} - {{- if eq .FieldType "time.Time" }} - {{.FieldJson}}: new Date(), - {{- end }} - {{- if eq .FieldType "float64" }} - {{.FieldJson}}: 0, - {{- end }} - {{- if eq .FieldType "picture" }} - {{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "video" }} - {{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "pictures" }} - {{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "file" }} - {{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "json" }} - {{.FieldJson}}: {}, - {{- end }} - {{- if eq .FieldType "array" }} - {{.FieldJson}}: [], - {{- end }} + {{ GenerateDefaultFormValue . }} {{- end }} {{- end }} }) @@ -982,39 +379,6 @@ const rule = reactive({ {{- end }} }) -const searchRule = reactive({ - createdAt: [ - { validator: (rule, value, callback) => { - if (searchInfo.value.startCreatedAt && !searchInfo.value.endCreatedAt) { - callback(new Error('请填写结束日期')) - } else if (!searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt) { - callback(new Error('请填写开始日期')) - } else if (searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt && (searchInfo.value.startCreatedAt.getTime() === searchInfo.value.endCreatedAt.getTime() || searchInfo.value.startCreatedAt.getTime() > searchInfo.value.endCreatedAt.getTime())) { - callback(new Error('开始日期应当早于结束日期')) - } else { - callback() - } - }, trigger: 'change' } - ], - {{- range .Fields }} - {{- if .FieldSearchType}} - {{- if eq .FieldType "time.Time" }} - {{.FieldJson }} : [{ validator: (rule, value, callback) => { - if (searchInfo.value.start{{.FieldName}} && !searchInfo.value.end{{.FieldName}}) { - callback(new Error('请填写结束日期')) - } else if (!searchInfo.value.start{{.FieldName}} && searchInfo.value.end{{.FieldName}}) { - callback(new Error('请填写开始日期')) - } else if (searchInfo.value.start{{.FieldName}} && searchInfo.value.end{{.FieldName}} && (searchInfo.value.start{{.FieldName}}.getTime() === searchInfo.value.end{{.FieldName}}.getTime() || searchInfo.value.start{{.FieldName}}.getTime() > searchInfo.value.end{{.FieldName}}.getTime())) { - callback(new Error('开始日期应当早于结束日期')) - } else { - callback() - } - }, trigger: 'change' }], - {{- end }} - {{- end }} - {{- end }} -}) - const elFormRef = ref() const elSearchFormRef = ref() @@ -1029,6 +393,8 @@ const searchInfo = ref({}) // 排序 const sortChange = ({ prop, order }) => { const sortMap = { + CreatedAt:"created_at", + ID:"id", {{- range .Fields}} {{- if .Table}} {{- if and .Sort}} @@ -1229,42 +595,7 @@ const closeDialog = () => { formData.value = { {{- range .Fields}} {{- if .Form}} - {{- if eq .FieldType "bool" }} - {{.FieldJson}}: false, - {{- end }} - {{- if eq .FieldType "string" }} - {{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "richtext" }} - {{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "int" }} - {{.FieldJson}}: {{- if .DataSource }} undefined{{ else }} 0{{- end }}, - {{- end }} - {{- if eq .FieldType "time.Time" }} - {{.FieldJson}}: new Date(), - {{- end }} - {{- if eq .FieldType "float64" }} - {{.FieldJson}}: 0, - {{- end }} - {{- if eq .FieldType "picture" }} - {{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "video" }} - {{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "pictures" }} - {{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "file" }} - {{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "json" }} - {{.FieldJson}}: {}, - {{- end }} - {{- if eq .FieldType "array" }} - {{.FieldJson}}: [], - {{- end }} + {{ GenerateDefaultFormValue . }} {{- end }} {{- end }} } @@ -1298,7 +629,7 @@ const enterDialog = async () => { }) } -const detailFrom = ref({}) +const detailForm = ref({}) // 查看详情控制标记 const detailShow = ref(false) @@ -1315,7 +646,7 @@ const getDetails = async (row) => { // 打开弹窗 const res = await find{{.StructName}}({ {{.PrimaryField.FieldJson}}: row.{{.PrimaryField.FieldJson}} }) if (res.code === 0) { - detailFrom.value = res.data + detailForm.value = res.data openDetailShow() } } @@ -1324,7 +655,7 @@ const getDetails = async (row) => { // 关闭详情弹窗 const closeDetailShow = () => { detailShow.value = false - detailFrom.value = {} + detailForm.value = {} } diff --git a/resource/plugin/server/api/api.go.tpl b/resource/plugin/server/api/api.go.tpl new file mode 100644 index 0000000..e69ae82 --- /dev/null +++ b/resource/plugin/server/api/api.go.tpl @@ -0,0 +1,255 @@ +package api + +import ( +{{if not .OnlyTemplate}} + "{{.Module}}/global" + "{{.Module}}/model/common/response" + "{{.Module}}/plugin/{{.Package}}/model" + {{- if not .IsTree}} + "{{.Module}}/plugin/{{.Package}}/model/request" + {{- end }} + "github.com/gin-gonic/gin" + "go.uber.org/zap" + {{- if .AutoCreateResource}} + "{{.Module}}/utils" + {{- end }} +{{- else }} + "{{.Module}}/model/common/response" + "github.com/gin-gonic/gin" +{{- end }} +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} +{{if not .OnlyTemplate}} +// Create{{.StructName}} 创建{{.Description}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "创建成功" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +func (a *{{.Abbreviation}}) Create{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var info model.{{.StructName}} + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + {{- if .AutoCreateResource }} + info.CreatedBy = utils.GetUserID(c) + {{- end }} + err = service{{ .StructName }}.Create{{.StructName}}(ctx,&info) + if err != nil { + global.GVA_LOG.Error("创建失败!", zap.Error(err)) + response.FailWithMessage("创建失败:" + err.Error(), c) + return + } + response.OkWithMessage("创建成功", c) +} + +// Delete{{.StructName}} 删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +func (a *{{.Abbreviation}}) Delete{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") +{{- if .AutoCreateResource }} + userID := utils.GetUserID(c) +{{- end }} + err := service{{ .StructName }}.Delete{{.StructName}}(ctx,{{.PrimaryField.FieldJson}} {{- if .AutoCreateResource -}},userID{{- end -}}) + if err != nil { + global.GVA_LOG.Error("删除失败!", zap.Error(err)) + response.FailWithMessage("删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("删除成功", c) +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}} +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{msg=string} "批量删除成功" +// @Router /{{.Abbreviation}}/delete{{.StructName}}ByIds [delete] +func (a *{{.Abbreviation}}) Delete{{.StructName}}ByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}}s := c.QueryArray("{{.PrimaryField.FieldJson}}s[]") +{{- if .AutoCreateResource }} + userID := utils.GetUserID(c) +{{- end }} + err := service{{ .StructName }}.Delete{{.StructName}}ByIds(ctx,{{.PrimaryField.FieldJson}}s{{- if .AutoCreateResource }},userID{{- end }}) + if err != nil { + global.GVA_LOG.Error("批量删除失败!", zap.Error(err)) + response.FailWithMessage("批量删除失败:" + err.Error(), c) + return + } + response.OkWithMessage("批量删除成功", c) +} + +// Update{{.StructName}} 更新{{.Description}} +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {object} response.Response{msg=string} "更新成功" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +func (a *{{.Abbreviation}}) Update{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var info model.{{.StructName}} + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } +{{- if .AutoCreateResource }} + info.UpdatedBy = utils.GetUserID(c) +{{- end }} + err = service{{ .StructName }}.Update{{.StructName}}(ctx,info) + if err != nil { + global.GVA_LOG.Error("更新失败!", zap.Error(err)) + response.FailWithMessage("更新失败:" + err.Error(), c) + return + } + response.OkWithMessage("更新成功", c) +} + +// Find{{.StructName}} 用id查询{{.Description}} +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param {{.PrimaryField.FieldJson}} query {{.PrimaryField.FieldType}} true "用id查询{{.Description}}" +// @Success 200 {object} response.Response{data=model.{{.StructName}},msg=string} "查询成功" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +func (a *{{.Abbreviation}}) Find{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") + re{{.Abbreviation}}, err := service{{ .StructName }}.Get{{.StructName}}(ctx,{{.PrimaryField.FieldJson}}) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(re{{.Abbreviation}}, c) +} + +{{- if .IsTree }} +// Get{{.StructName}}List 分页获取{{.Description}}列表 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}List(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + list, err := service{{ .StructName }}.Get{{.StructName}}InfoList(ctx) + if err != nil { + global.GVA_LOG.Error("获取失败!", zap.Error(err)) + response.FailWithMessage("获取失败:" + err.Error(), c) + return + } + response.OkWithDetailed(list, "获取成功", c) +} +{{- else }} +// Get{{.StructName}}List 分页获取{{.Description}}列表 +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}List(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + var pageInfo request.{{.StructName}}Search + err := c.ShouldBindQuery(&pageInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + list, total, err := service{{ .StructName }}.Get{{.StructName}}InfoList(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) +} +{{- end }} + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource 获取{{.StructName}}的数据源 +// @Tags {{.StructName}} +// @Summary 获取{{.StructName}}的数据源 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "查询成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}DataSource [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}DataSource(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口为获取数据源定义的数据 + dataSource, err := service{{ .StructName }}.Get{{.StructName}}DataSource(ctx) + if err != nil { + global.GVA_LOG.Error("查询失败!", zap.Error(err)) + response.FailWithMessage("查询失败:" + err.Error(), c) + return + } + response.OkWithData(dataSource, c) +} +{{- end }} +{{- end }} +// Get{{.StructName}}Public 不需要鉴权的{{.Description}}接口 +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @Accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +func (a *{{.Abbreviation}}) Get{{.StructName}}Public(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + + // 此接口不需要鉴权 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 + service{{ .StructName }}.Get{{.StructName}}Public(ctx) + response.OkWithDetailed(gin.H{"info": "不需要鉴权的{{.Description}}接口信息"}, "获取成功", c) +} diff --git a/resource/plugin/server/api/enter.go.tpl b/resource/plugin/server/api/enter.go.tpl new file mode 100644 index 0000000..989fb35 --- /dev/null +++ b/resource/plugin/server/api/enter.go.tpl @@ -0,0 +1,6 @@ +package api + +var Api = new(api) + +type api struct { +} diff --git a/resource/plugin/server/config/config.go.tpl b/resource/plugin/server/config/config.go.tpl new file mode 100644 index 0000000..809bc99 --- /dev/null +++ b/resource/plugin/server/config/config.go.tpl @@ -0,0 +1,4 @@ +package config + +type Config struct { +} diff --git a/resource/plugin/server/gen/gen.go.tpl b/resource/plugin/server/gen/gen.go.tpl new file mode 100644 index 0000000..5639d4a --- /dev/null +++ b/resource/plugin/server/gen/gen.go.tpl @@ -0,0 +1,18 @@ +package main + +import ( + "gorm.io/gen" + "path/filepath" +) + +//go:generate go mod tidy +//go:generate go mod download +//go:generate go run gen.go +func main() { + g := gen.NewGenerator(gen.Config{ + OutPath: filepath.Join("..", "..", "..", "{{ .Package }}", "blender", "model", "dao"), + Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, + }) + g.ApplyBasic() + g.Execute() +} diff --git a/resource/plugin/server/initialize/api.go.tpl b/resource/plugin/server/initialize/api.go.tpl new file mode 100644 index 0000000..dfbea23 --- /dev/null +++ b/resource/plugin/server/initialize/api.go.tpl @@ -0,0 +1,12 @@ +package initialize + +import ( + "context" + model "{{.Module}}/model/system" + "{{.Module}}/plugin/plugin-tool/utils" +) + +func Api(ctx context.Context) { + entities := []model.SysApi{} + utils.RegisterApis(entities...) +} diff --git a/resource/plugin/server/initialize/gorm.go.tpl b/resource/plugin/server/initialize/gorm.go.tpl new file mode 100644 index 0000000..52c8183 --- /dev/null +++ b/resource/plugin/server/initialize/gorm.go.tpl @@ -0,0 +1,17 @@ +package initialize + +import ( + "context" + "fmt" + "{{.Module}}/global" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Gorm(ctx context.Context) { + err := global.GVA_DB.WithContext(ctx).AutoMigrate() + if err != nil { + err = errors.Wrap(err, "注册表失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/resource/plugin/server/initialize/menu.go.tpl b/resource/plugin/server/initialize/menu.go.tpl new file mode 100644 index 0000000..8774f35 --- /dev/null +++ b/resource/plugin/server/initialize/menu.go.tpl @@ -0,0 +1,12 @@ +package initialize + +import ( + "context" + model "{{.Module}}/model/system" + "{{.Module}}/plugin/plugin-tool/utils" +) + +func Menu(ctx context.Context) { + entities := []model.SysBaseMenu{} + utils.RegisterMenus(entities...) +} diff --git a/resource/plugin/server/initialize/router.go.tpl b/resource/plugin/server/initialize/router.go.tpl new file mode 100644 index 0000000..fbf03a3 --- /dev/null +++ b/resource/plugin/server/initialize/router.go.tpl @@ -0,0 +1,14 @@ +package initialize + +import ( + "{{.Module}}/global" + "{{.Module}}/middleware" + "github.com/gin-gonic/gin" +) + +func Router(engine *gin.Engine) { + public := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + public.Use() + private := engine.Group(global.GVA_CONFIG.System.RouterPrefix).Group("") + private.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) +} diff --git a/resource/plugin/server/initialize/viper.go.tpl b/resource/plugin/server/initialize/viper.go.tpl new file mode 100644 index 0000000..e759ad6 --- /dev/null +++ b/resource/plugin/server/initialize/viper.go.tpl @@ -0,0 +1,17 @@ +package initialize + +import ( + "fmt" + "{{.Module}}/global" + "{{.Module}}/plugin/{{ .Package }}/plugin" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +func Viper() { + err := global.GVA_VP.UnmarshalKey("{{ .Package }}", &plugin.Config) + if err != nil { + err = errors.Wrap(err, "初始化配置文件失败!") + zap.L().Error(fmt.Sprintf("%+v", err)) + } +} diff --git a/resource/plugin/server/model/model.go.tpl b/resource/plugin/server/model/model.go.tpl new file mode 100644 index 0000000..283841c --- /dev/null +++ b/resource/plugin/server/model/model.go.tpl @@ -0,0 +1,76 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{ GenerateField . }} +{{- end }} + +{{ else }} +package model + +{{- if not .OnlyTemplate}} +import ( + {{- if .GvaModel }} + "{{.Module}}/global" + {{- end }} + {{- if or .HasTimer }} + "time" + {{- end }} + {{- if .NeedJSON }} + "gorm.io/datatypes" + {{- end }} +) +{{- end }} + +// {{.StructName}} {{.Description}} 结构体 +type {{.StructName}} struct { +{{- if not .OnlyTemplate}} +{{- if .GvaModel }} + global.GVA_MODEL +{{- end }} +{{- range .Fields}} + {{ GenerateField . }} +{{- end }} + {{- if .AutoCreateResource }} + CreatedBy uint `gorm:"column:created_by;comment:创建者"` + UpdatedBy uint `gorm:"column:updated_by;comment:更新者"` + DeletedBy uint `gorm:"column:deleted_by;comment:删除者"` + {{- end }} + {{- if .IsTree }} + Children []*{{.StructName}} `json:"children" gorm:"-"` //子节点 + ParentID int `json:"parentID" gorm:"column:parent_id;comment:父节点"` + {{- end }} + {{- end }} +} + +{{ if .TableName }} +// TableName {{.Description}} {{.StructName}}自定义表名 {{.TableName}} +func ({{.StructName}}) TableName() string { + return "{{.TableName}}" +} +{{ end }} + + +{{if .IsTree }} +// GetChildren 实现TreeNode接口 +func (s *{{.StructName}}) GetChildren() []*{{.StructName}} { + return s.Children +} + +// SetChildren 实现TreeNode接口 +func (s *{{.StructName}}) SetChildren(children *{{.StructName}}) { + s.Children = append(s.Children, children) +} + +// GetID 实现TreeNode接口 +func (s *{{.StructName}}) GetID() int { + return int({{if not .GvaModel}}*{{- end }}s.{{.PrimaryField.FieldName}}) +} + +// GetParentID 实现TreeNode接口 +func (s *{{.StructName}}) GetParentID() int { + return s.ParentID +} +{{ end }} + + +{{ end }} diff --git a/resource/plugin/server/model/request/request.go.tpl b/resource/plugin/server/model/request/request.go.tpl new file mode 100644 index 0000000..60cf677 --- /dev/null +++ b/resource/plugin/server/model/request/request.go.tpl @@ -0,0 +1,38 @@ +{{- if .IsAdd}} +// 在结构体中新增如下字段 +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{ GenerateSearchField . }} + {{- end}} +{{- end }} +{{- if .NeedSort}} +Sort string `json:"sort" form:"sort"` +Order string `json:"order" form:"order"` +{{- end}} +{{- else }} +package request +{{- if not .OnlyTemplate}} +import ( + "{{.Module}}/model/common/request" + {{ if or .HasSearchTimer .GvaModel }}"time"{{ end }} +) +{{- end}} +type {{.StructName}}Search struct{ +{{- if not .OnlyTemplate}} + +{{- if .GvaModel }} + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` +{{- end }} +{{- range .Fields}} + {{- if ne .FieldSearchType ""}} + {{ GenerateSearchField . }} + {{- end}} +{{- end }} + request.PageInfo + {{- if .NeedSort}} + Sort string `json:"sort" form:"sort"` + Order string `json:"order" form:"order"` + {{- end}} +{{- end }} +} +{{- end }} diff --git a/resource/plugin/server/plugin.go.tpl b/resource/plugin/server/plugin.go.tpl new file mode 100644 index 0000000..255b7af --- /dev/null +++ b/resource/plugin/server/plugin.go.tpl @@ -0,0 +1,26 @@ +package {{ .Package }} + +import ( + "context" + "{{.Module}}/plugin/{{ .Package }}/initialize" + interfaces "{{.Module}}/utils/plugin/v2" + "github.com/gin-gonic/gin" +) + +var _ interfaces.Plugin = (*plugin)(nil) + +var Plugin = new(plugin) + +type plugin struct{} + +// 如果需要配置文件,请到config.Config中填充配置结构,且到下方发放中填入其在config.yaml中的key并添加如下方法 +// initialize.Viper() +// 安装插件时候自动注册的api数据请到下方法.Api方法中实现并添加如下方法 +// initialize.Api(ctx) +// 安装插件时候自动注册的api数据请到下方法.Menu方法中实现并添加如下方法 +// initialize.Menu(ctx) +func (p *plugin) Register(group *gin.Engine) { + ctx := context.Background() + initialize.Gorm(ctx) + initialize.Router(group) +} diff --git a/resource/plugin/server/plugin/plugin.go.tpl b/resource/plugin/server/plugin/plugin.go.tpl new file mode 100644 index 0000000..7e25e07 --- /dev/null +++ b/resource/plugin/server/plugin/plugin.go.tpl @@ -0,0 +1,5 @@ +package plugin + +import "{{.Module}}/plugin/{{ .Package }}/config" + +var Config config.Config diff --git a/resource/plugin/server/router/enter.go.tpl b/resource/plugin/server/router/enter.go.tpl new file mode 100644 index 0000000..78517b3 --- /dev/null +++ b/resource/plugin/server/router/enter.go.tpl @@ -0,0 +1,6 @@ +package router + +var Router = new(router) + +type router struct { +} diff --git a/resource/plugin/server/router/router.go.tpl b/resource/plugin/server/router/router.go.tpl new file mode 100644 index 0000000..34bf4d8 --- /dev/null +++ b/resource/plugin/server/router/router.go.tpl @@ -0,0 +1,46 @@ +package router + +import ( + {{if .OnlyTemplate }} // {{end}}"{{.Module}}/middleware" + "github.com/gin-gonic/gin" +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} + +// Init 初始化 {{.Description}} 路由信息 +func (r *{{.Abbreviation}}) Init(public *gin.RouterGroup, private *gin.RouterGroup) { +{{- if not .OnlyTemplate }} + { + group := private.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + group.POST("create{{.StructName}}", api{{.StructName}}.Create{{.StructName}}) // 新建{{.Description}} + group.DELETE("delete{{.StructName}}", api{{.StructName}}.Delete{{.StructName}}) // 删除{{.Description}} + group.DELETE("delete{{.StructName}}ByIds", api{{.StructName}}.Delete{{.StructName}}ByIds) // 批量删除{{.Description}} + group.PUT("update{{.StructName}}", api{{.StructName}}.Update{{.StructName}}) // 更新{{.Description}} + } + { + group := private.Group("{{.Abbreviation}}") + group.GET("find{{.StructName}}", api{{.StructName}}.Find{{.StructName}}) // 根据ID获取{{.Description}} + group.GET("get{{.StructName}}List", api{{.StructName}}.Get{{.StructName}}List) // 获取{{.Description}}列表 + } + { + group := public.Group("{{.Abbreviation}}") + {{- if .HasDataSource}} + group.GET("get{{.StructName}}DataSource", api{{.StructName}}.Get{{.StructName}}DataSource) // 获取{{.Description}}数据源 + {{- end}} + group.GET("get{{.StructName}}Public", api{{.StructName}}.Get{{.StructName}}Public) // {{.Description}}开放接口 + } +{{- else}} + // { + // group := private.Group("{{.Abbreviation}}").Use(middleware.OperationRecord()) + // } + // { + // group := private.Group("{{.Abbreviation}}") + // } + { + group := public.Group("{{.Abbreviation}}") + group.GET("get{{.StructName}}Public", api{{.StructName}}.Get{{.StructName}}Public) // {{.Description}}开放接口 + } +{{- end}} +} diff --git a/resource/plugin/server/service/enter.go.tpl b/resource/plugin/server/service/enter.go.tpl new file mode 100644 index 0000000..034facb --- /dev/null +++ b/resource/plugin/server/service/enter.go.tpl @@ -0,0 +1,7 @@ +package service + +var Service = new(service) + +type service struct { +} + diff --git a/resource/plugin/server/service/service.go.tpl b/resource/plugin/server/service/service.go.tpl new file mode 100644 index 0000000..9743602 --- /dev/null +++ b/resource/plugin/server/service/service.go.tpl @@ -0,0 +1,211 @@ +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} + +{{- if .IsAdd}} + +// Get{{.StructName}}InfoList 新增搜索语句 + + {{ GenerateSearchConditions .Fields }} + +// Get{{.StructName}}InfoList 新增排序语句 请自行在搜索语句中添加orderMap内容 + {{- range .Fields}} + {{- if .Sort}} +orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + + +{{- if .HasDataSource }} +// Get{{.StructName}}DataSource()方法新增关联语句 + {{range $key, $value := .DataSourceMap}} +{{$key}} := make([]map[string]any, 0) +{{$db}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) +res["{{$key}}"] = {{$key}} + {{- end }} +{{- end }} +{{- else}} +package service + +import ( +{{- if not .OnlyTemplate }} + "context" + "{{.Module}}/global" + "{{.Module}}/plugin/{{.Package}}/model" + {{- if not .IsTree }} + "{{.Module}}/plugin/{{.Package}}/model/request" + {{- else }} + "errors" + {{- end }} + {{- if .AutoCreateResource }} + "gorm.io/gorm" + {{- end}} +{{- if .IsTree }} + "{{.Module}}/utils" +{{- end }} +{{- end }} +) + +var {{.StructName}} = new({{.Abbreviation}}) + +type {{.Abbreviation}} struct {} + +{{- $db := "" }} +{{- if eq .BusinessDB "" }} + {{- $db = "global.GVA_DB" }} +{{- else}} + {{- $db = printf "global.MustGetGlobalDBByDBName(\"%s\")" .BusinessDB }} +{{- end}} +{{- if not .OnlyTemplate }} +// Create{{.StructName}} 创建{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Create{{.StructName}}(ctx context.Context, {{.Abbreviation}} *model.{{.StructName}}) (err error) { + err = {{$db}}.Create({{.Abbreviation}}).Error + return err +} + +// Delete{{.StructName}} 删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Delete{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string{{- if .AutoCreateResource -}},userID uint{{- end -}}) (err error) { + + {{- if .IsTree }} + var count int64 + err = {{$db}}.Find(&model.{{.StructName}}{},"parent_id = ?",{{.PrimaryField.FieldJson}}).Count(&count).Error + if count > 0 { + return errors.New("此节点存在子节点不允许删除") + } + if err != nil { + return err + } + {{- end }} + + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).Update("deleted_by", userID).Error; err != nil { + return err + } + if err = tx.Delete(&model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error; err != nil { + return err + } + return nil + }) + {{- else }} + err = {{$db}}.Delete(&model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} = ?",{{.PrimaryField.FieldJson}}).Error + {{- end }} + return err +} + +// Delete{{.StructName}}ByIds 批量删除{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Delete{{.StructName}}ByIds(ctx context.Context, {{.PrimaryField.FieldJson}}s []string {{- if .AutoCreateResource }},deleted_by uint{{- end}}) (err error) { + {{- if .AutoCreateResource }} + err = {{$db}}.Transaction(func(tx *gorm.DB) error { + if err := tx.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Update("deleted_by", deleted_by).Error; err != nil { + return err + } + if err := tx.Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Delete(&model.{{.StructName}}{}).Error; err != nil { + return err + } + return nil + }) + {{- else}} + err = {{$db}}.Delete(&[]model.{{.StructName}}{},"{{.PrimaryField.ColumnName}} in ?",{{.PrimaryField.FieldJson}}s).Error + {{- end}} + return err +} + +// Update{{.StructName}} 更新{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Update{{.StructName}}(ctx context.Context, {{.Abbreviation}} model.{{.StructName}}) (err error) { + err = {{$db}}.Model(&model.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?",{{.Abbreviation}}.{{.PrimaryField.FieldName}}).Updates(&{{.Abbreviation}}).Error + return err +} + +// Get{{.StructName}} 根据{{.PrimaryField.FieldJson}}获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string) ({{.Abbreviation}} model.{{.StructName}}, err error) { + err = {{$db}}.Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).First(&{{.Abbreviation}}).Error + return +} + + +{{- if .IsTree }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录,Tree模式下不添加分页和搜索 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}InfoList(ctx context.Context) (list []*model.{{.StructName}},err error) { + // 创建db + db := {{$db}}.Model(&model.{{.StructName}}{}) + var {{.Abbreviation}}s []*model.{{.StructName}} + + err = db.Find(&{{.Abbreviation}}s).Error + + return utils.BuildTree({{.Abbreviation}}s), err +} +{{- else }} +// Get{{.StructName}}InfoList 分页获取{{.Description}}记录 +// Author [yourname](https://github.com/yourname) +func (s *{{.Abbreviation}}) Get{{.StructName}}InfoList(ctx context.Context, info request.{{.StructName}}Search) (list []model.{{.StructName}}, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := {{$db}}.Model(&model.{{.StructName}}{}) + var {{.Abbreviation}}s []model.{{.StructName}} + // 如果有条件搜索 下方会自动创建搜索语句 +{{- if .GvaModel }} + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) + } +{{- end }} + {{ GenerateSearchConditions .Fields }} + err = db.Count(&total).Error + if err!=nil { + return + } + {{- if .NeedSort}} + var OrderStr string + orderMap := make(map[string]bool) + {{- if .GvaModel }} + orderMap["id"] = true + orderMap["created_at"] = true + {{- end }} + {{- range .Fields}} + {{- if .Sort}} + orderMap["{{.ColumnName}}"] = true + {{- end}} + {{- end}} + if orderMap[info.Sort] { + OrderStr = info.Sort + if info.Order == "descending" { + OrderStr = OrderStr + " desc" + } + db = db.Order(OrderStr) + } + {{- end}} + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + err = db.Find(&{{.Abbreviation}}s).Error + return {{.Abbreviation}}s, total, err +} +{{- end }} +{{- if .HasDataSource }} +func (s *{{.Abbreviation}})Get{{.StructName}}DataSource(ctx context.Context) (res map[string][]map[string]any, err error) { + res = make(map[string][]map[string]any) + {{range $key, $value := .DataSourceMap}} + {{$key}} := make([]map[string]any, 0) + {{$db}}.Table("{{$value.Table}}"){{- if $value.HasDeletedAt}}.Where("deleted_at IS NULL"){{ end }}.Select("{{$value.Label}} as label,{{$value.Value}} as value").Scan(&{{$key}}) + res["{{$key}}"] = {{$key}} + {{- end }} + return +} +{{- end }} +{{- end }} + +func (s *{{.Abbreviation}})Get{{.StructName}}Public(ctx context.Context) { + +} +{{- end }} diff --git a/resource/plugin/web/api/api.js.tpl b/resource/plugin/web/api/api.js.tpl new file mode 100644 index 0000000..0462fde --- /dev/null +++ b/resource/plugin/web/api/api.js.tpl @@ -0,0 +1,127 @@ +import service from '@/utils/request' +{{- if not .OnlyTemplate}} +// @Tags {{.StructName}} +// @Summary 创建{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "创建{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /{{.Abbreviation}}/create{{.StructName}} [post] +export const create{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/create{{.StructName}}', + method: 'post', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 批量删除{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body request.IdsReq true "批量删除{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}" +// @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] +export const delete{{.StructName}}ByIds = (params) => { + return service({ + url: '/{{.Abbreviation}}/delete{{.StructName}}ByIds', + method: 'delete', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 更新{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data body model.{{.StructName}} true "更新{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}" +// @Router /{{.Abbreviation}}/update{{.StructName}} [put] +export const update{{.StructName}} = (data) => { + return service({ + url: '/{{.Abbreviation}}/update{{.StructName}}', + method: 'put', + data + }) +} + +// @Tags {{.StructName}} +// @Summary 用id查询{{.Description}} +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query model.{{.StructName}} true "用id查询{{.Description}}" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}} [get] +export const find{{.StructName}} = (params) => { + return service({ + url: '/{{.Abbreviation}}/find{{.StructName}}', + method: 'get', + params + }) +} + +// @Tags {{.StructName}} +// @Summary 分页获取{{.Description}}列表 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Param data query request.PageInfo true "分页获取{{.Description}}列表" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}" +// @Router /{{.Abbreviation}}/get{{.StructName}}List [get] +export const get{{.StructName}}List = (params) => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}List', + method: 'get', + params + }) +} + +{{- if .HasDataSource}} +// @Tags {{.StructName}} +// @Summary 获取数据源 +// @Security ApiKeyAuth +// @Accept application/json +// @Produce application/json +// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}" +// @Router /{{.Abbreviation}}/find{{.StructName}}DataSource [get] +export const get{{.StructName}}DataSource = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}DataSource', + method: 'get', + }) +} +{{- end}} +{{- end}} +// @Tags {{.StructName}} +// @Summary 不需要鉴权的{{.Description}}接口 +// @Accept application/json +// @Produce application/json +// @Param data query request.{{.StructName}}Search true "分页获取{{.Description}}列表" +// @Success 200 {object} response.Response{data=object,msg=string} "获取成功" +// @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] +export const get{{.StructName}}Public = () => { + return service({ + url: '/{{.Abbreviation}}/get{{.StructName}}Public', + method: 'get', + }) +} diff --git a/resource/plugin/web/form/form.vue.tpl b/resource/plugin/web/form/form.vue.tpl new file mode 100644 index 0000000..7d3406a --- /dev/null +++ b/resource/plugin/web/form/form.vue.tpl @@ -0,0 +1,464 @@ +{{- if .IsAdd }} +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + + {{- if .CheckDataSource}} + + + + {{- else }} + {{- if eq .FieldType "bool" }} + + {{- end }} + {{- if eq .FieldType "string" }} + {{- if .DictType}} + + + + {{- else }} + + {{- end }} + {{- end }} + {{- if eq .FieldType "richtext" }} + + {{- end }} + {{- if eq .FieldType "json" }} + // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 + {{"{{"}} formData.{{.FieldJson}} {{"}}"}} + {{- end }} + {{- if eq .FieldType "array" }} + + {{- end }} + {{- if eq .FieldType "int" }} + + {{- end }} + {{- if eq .FieldType "time.Time" }} + + {{- end }} + {{- if eq .FieldType "float64" }} + + {{- end }} + {{- if eq .FieldType "enum" }} + + + + {{- end }} + {{- if eq .FieldType "picture" }} + + {{- end }} + {{- if eq .FieldType "pictures" }} + + {{- end }} + {{- if eq .FieldType "video" }} + + {{- end }} + {{- if eq .FieldType "file" }} + + {{- end }} + {{- end }} + + {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// init方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{- if eq .FieldType "bool" }} +{{.FieldJson}}: false, + {{- end }} + {{- if eq .FieldType "string" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "richtext" }} +{{.FieldJson}}: '', + {{- end }} + {{- if eq .FieldType "int" }} +{{.FieldJson}}: {{- if or .DataSource}} undefined{{ else }} 0{{- end }}, + {{- end }} + {{- if eq .FieldType "time.Time" }} +{{.FieldJson}}: new Date(), + {{- end }} + {{- if eq .FieldType "float64" }} +{{.FieldJson}}: 0, + {{- end }} + {{- if eq .FieldType "picture" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "video" }} +{{.FieldJson}}: "", + {{- end }} + {{- if eq .FieldType "pictures" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "file" }} +{{.FieldJson}}: [], + {{- end }} + {{- if eq .FieldType "json" }} +{{.FieldJson}}: {}, + {{- end }} + {{- if eq .FieldType "array" }} +{{.FieldJson}}: [], + {{- end }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref([]) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data + } +} +getDataSourceFunc() +{{- end }} +{{- else }} +{{- if not .OnlyTemplate }} + + + + + +{{- else }} + + + +{{- end }} +{{- end }} diff --git a/resource/plugin/web/view/view.vue.tpl b/resource/plugin/web/view/view.vue.tpl new file mode 100644 index 0000000..f5b8547 --- /dev/null +++ b/resource/plugin/web/view/view.vue.tpl @@ -0,0 +1,689 @@ +{{- $global := . }} +{{- $templateID := printf "%s_%s" .Package .StructName }} +{{- if .IsAdd }} +// 请在搜索条件中增加如下代码 +{{- range .Fields}} + {{- if .FieldSearchType}} +{{ GenerateSearchFormItem .}} + {{ end }} +{{ end }} + + +// 表格增加如下列代码 + +{{- range .Fields}} + {{- if .Table}} + {{ GenerateTableColumn . }} + {{- end }} +{{- end }} + +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} +{{- end }} + +// 查看抽屉中增加如下代码 + +{{- range .Fields}} + {{- if .Desc }} + {{ GenerateDescriptionItem . }} + {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// setOptions方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构(变量处和关闭表单处)增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateDefaultFormValue . }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + + + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref({}) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data || [] + } +} +getDataSourceFunc() +{{- end }} + +{{- else }} + +{{- if not .OnlyTemplate}} + + + + + +{{- else}} + + + +{{- end }} + +{{- end }} diff --git a/router/system/enter.go b/router/system/enter.go index c652d10..9d65c9c 100644 --- a/router/system/enter.go +++ b/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/router/system/sys_auto_code.go b/router/system/sys_auto_code.go index e25e1ce..ef89245 100644 --- a/router/system/sys_auto_code.go +++ b/router/system/sys_auto_code.go @@ -19,6 +19,11 @@ func (s *AutoCodeRouter) InitAutoCodeRouter(Router *gin.RouterGroup, RouterPubli autoCodeRouter.POST("createTemp", autoCodeTemplateApi.Create) // 创建自动化代码 autoCodeRouter.POST("addFunc", autoCodeTemplateApi.AddFunc) // 为代码插入方法 } + { + autoCodeRouter.POST("mcp", autoCodeTemplateApi.MCP) // 自动创建Mcp Tool模板 + autoCodeRouter.POST("mcpList", autoCodeTemplateApi.MCPList) // 获取MCP ToolList + autoCodeRouter.POST("mcpTest", autoCodeTemplateApi.MCPTest) // MCP 工具测试 + } { autoCodeRouter.POST("getPackage", autoCodePackageApi.All) // 获取package包 autoCodeRouter.POST("delPackage", autoCodePackageApi.Delete) // 删除package包 diff --git a/router/system/sys_operation_record.go b/router/system/sys_operation_record.go index 11b841d..d158d5e 100644 --- a/router/system/sys_operation_record.go +++ b/router/system/sys_operation_record.go @@ -9,7 +9,6 @@ type OperationRecordRouter struct{} func (s *OperationRecordRouter) InitSysOperationRecordRouter(Router *gin.RouterGroup) { operationRecordRouter := Router.Group("sysOperationRecord") { - operationRecordRouter.POST("createSysOperationRecord", operationRecordApi.CreateSysOperationRecord) // 新建SysOperationRecord operationRecordRouter.DELETE("deleteSysOperationRecord", operationRecordApi.DeleteSysOperationRecord) // 删除SysOperationRecord operationRecordRouter.DELETE("deleteSysOperationRecordByIds", operationRecordApi.DeleteSysOperationRecordByIds) // 批量删除SysOperationRecord operationRecordRouter.GET("findSysOperationRecord", operationRecordApi.FindSysOperationRecord) // 根据ID获取SysOperationRecord diff --git a/router/system/sys_user.go b/router/system/sys_user.go index 32e0130..cb3187a 100644 --- a/router/system/sys_user.go +++ b/router/system/sys_user.go @@ -18,7 +18,7 @@ func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) { userRouter.PUT("setUserInfo", baseApi.SetUserInfo) // 设置用户信息 userRouter.PUT("setSelfInfo", baseApi.SetSelfInfo) // 设置自身信息 userRouter.POST("setUserAuthorities", baseApi.SetUserAuthorities) // 设置用户权限组 - userRouter.POST("resetPassword", baseApi.ResetPassword) // 设置用户权限组 + userRouter.POST("resetPassword", baseApi.ResetPassword) // 重置用户密码 userRouter.PUT("setSelfSetting", baseApi.SetSelfSetting) // 用户界面配置 } { diff --git a/router/system/sys_version.go b/router/system/sys_version.go new file mode 100644 index 0000000..69fef7d --- /dev/null +++ b/router/system/sys_version.go @@ -0,0 +1,25 @@ +package system + +import ( + "git.echol.cn/loser/lckt/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/service/system/auto_code_mcp.go b/service/system/auto_code_mcp.go new file mode 100644 index 0000000..cbfbfd8 --- /dev/null +++ b/service/system/auto_code_mcp.go @@ -0,0 +1,45 @@ +package system + +import ( + "context" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system/request" + "git.echol.cn/loser/lckt/utils" + "git.echol.cn/loser/lckt/utils/autocode" + "os" + "path/filepath" + "text/template" +) + +func (s *autoCodeTemplate) CreateMcp(ctx context.Context, info request.AutoMcpTool) (toolFilePath string, err error) { + mcpTemplatePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "mcp", "tools.tpl") + mcpToolPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "mcp") + + var files *template.Template + + templateName := filepath.Base(mcpTemplatePath) + + files, err = template.New(templateName).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(mcpTemplatePath) + if err != nil { + return + } + + fileName := utils.HumpToUnderscore(info.Name) + + toolFilePath = filepath.Join(mcpToolPath, fileName+".go") + + f, err := os.Create(toolFilePath) + if err != nil { + return + } + defer f.Close() + + // 执行模板,将内容写入文件 + err = files.Execute(f, info) + if err != nil { + return + } + + return + +} diff --git a/service/system/auto_code_package.go b/service/system/auto_code_package.go index 8a99fab..477036f 100644 --- a/service/system/auto_code_package.go +++ b/service/system/auto_code_package.go @@ -3,20 +3,20 @@ package system import ( "context" "fmt" - "go/token" - "os" - "path/filepath" - "strings" - "text/template" - "git.echol.cn/loser/lckt/global" common "git.echol.cn/loser/lckt/model/common/request" model "git.echol.cn/loser/lckt/model/system" "git.echol.cn/loser/lckt/model/system/request" "git.echol.cn/loser/lckt/utils" "git.echol.cn/loser/lckt/utils/ast" + "git.echol.cn/loser/lckt/utils/autocode" "github.com/pkg/errors" + "go/token" "gorm.io/gorm" + "os" + "path/filepath" + "strings" + "text/template" ) var AutoCodePackage = new(autoCodePackage) @@ -59,7 +59,7 @@ func (s *autoCodePackage) Create(ctx context.Context, info *request.SysAutoCodeP } for key, value := range creates { // key 为 模版绝对路径 var files *template.Template - files, err = template.ParseFiles(key) + files, err = template.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(key) if err != nil { return errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", key) } @@ -114,6 +114,20 @@ func (s *autoCodePackage) Delete(ctx context.Context, info common.GetById) error return nil } +// DeleteByNames +// @author: [piexlmax](https://github.com/piexlmax) +// @author: [SliverHorn](https://github.com/SliverHorn) +func (s *autoCodePackage) DeleteByNames(ctx context.Context, names []string) error { + if len(names) == 0 { + return nil + } + err := global.GVA_DB.WithContext(ctx).Where("package_name IN ?", names).Delete(&model.SysAutoCodePackage{}).Error + if err != nil { + return errors.Wrap(err, "删除失败!") + } + return nil +} + // All 获取所有包 // @author: [piexlmax](https://github.com/piexlmax) // @author: [SliverHorn](https://github.com/SliverHorn) @@ -233,6 +247,9 @@ func (s *autoCodePackage) Templates(ctx context.Context) ([]string, error) { if entries[i].Name() == "preview" { continue } // preview 为预览代码生成器的代码 + if entries[i].Name() == "mcp" { + continue + } // preview 为mcp生成器的代码 templates = append(templates, entries[i].Name()) } } @@ -267,7 +284,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod three := filepath.Join(second, secondDirs[j].Name()) if !secondDirs[j].IsDir() { ext := filepath.Ext(secondDirs[j].Name()) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", three) } name := strings.TrimSuffix(secondDirs[j].Name(), ext) @@ -301,7 +318,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) } ext := filepath.Ext(four) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) } api := strings.Index(threeDirs[k].Name(), "api") @@ -473,7 +490,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) } ext := filepath.Ext(four) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) } gen := strings.Index(threeDirs[k].Name(), "gen") @@ -557,7 +574,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", five) } ext := filepath.Ext(five) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", five) } hasRequest := strings.Index(fourDirs[l].Name(), "request") @@ -573,7 +590,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod continue } ext := filepath.Ext(threeDirs[k].Name()) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) } hasModel := strings.Index(threeDirs[k].Name(), "model") @@ -634,7 +651,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件夹!", four) } ext := filepath.Ext(four) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版后缀!", four) } api := strings.Index(threeDirs[k].Name(), "api") diff --git a/service/system/auto_code_package_test.go b/service/system/auto_code_package_test.go index 3956b58..57fbe37 100644 --- a/service/system/auto_code_package_test.go +++ b/service/system/auto_code_package_test.go @@ -2,10 +2,11 @@ package system import ( "context" - model "git.echol.cn/loser/lckt/model/system" - "git.echol.cn/loser/lckt/model/system/request" "reflect" "testing" + + model "git.echol.cn/loser/lckt/model/system" + "git.echol.cn/loser/lckt/model/system/request" ) func Test_autoCodePackage_Create(t *testing.T) { @@ -53,9 +54,10 @@ func Test_autoCodePackage_Create(t *testing.T) { func Test_autoCodePackage_templates(t *testing.T) { type args struct { - ctx context.Context - entity model.SysAutoCodePackage - info request.AutoCode + ctx context.Context + entity model.SysAutoCodePackage + info request.AutoCode + isPackage bool } tests := []struct { name string @@ -78,6 +80,7 @@ func Test_autoCodePackage_templates(t *testing.T) { Abbreviation: "user", HumpPackageName: "user", }, + isPackage: false, }, wantErr: false, }, @@ -85,7 +88,7 @@ func Test_autoCodePackage_templates(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &autoCodePackage{} - gotCode, gotEnter, gotCreates, err := s.templates(tt.args.ctx, tt.args.entity, tt.args.info) + gotCode, gotEnter, gotCreates, err := s.templates(tt.args.ctx, tt.args.entity, tt.args.info, tt.args.isPackage) if (err != nil) != tt.wantErr { t.Errorf("templates() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/service/system/auto_code_plugin.go b/service/system/auto_code_plugin.go index 64c5a79..fb5f99d 100644 --- a/service/system/auto_code_plugin.go +++ b/service/system/auto_code_plugin.go @@ -9,7 +9,7 @@ import ( "git.echol.cn/loser/lckt/model/system/request" "git.echol.cn/loser/lckt/utils" "git.echol.cn/loser/lckt/utils/ast" - "github.com/mholt/archiver/v4" + "github.com/mholt/archives" cp "github.com/otiai10/copy" "github.com/pkg/errors" "go.uber.org/zap" @@ -154,7 +154,7 @@ func (s *autoCodePlugin) PubPlug(plugName string) (zipPath string, err error) { fileName := plugName + ".zip" // 创建一个新的zip文件 - files, err := archiver.FilesFromDisk(nil, map[string]string{ + files, err := archives.FilesFromDisk(context.Background(), nil, map[string]string{ webPath: plugName + "/web/plugin/" + plugName, serverPath: plugName + "/server/plugin/" + plugName, }) @@ -168,8 +168,9 @@ func (s *autoCodePlugin) PubPlug(plugName string) (zipPath string, err error) { // we can use the CompressedArchive type to gzip a tarball // (compression is not required; you could use Tar directly) - format := archiver.Archive{ - Archival: archiver.Zip{}, + format := archives.CompressedArchive{ + //Compression: archives.Gz{}, + Archival: archives.Zip{}, } // create the archive @@ -207,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/service/system/auto_code_template.go b/service/system/auto_code_template.go index b02a6b3..711de74 100644 --- a/service/system/auto_code_template.go +++ b/service/system/auto_code_template.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "git.echol.cn/loser/lckt/utils/autocode" "go/ast" "go/format" "go/parser" @@ -224,7 +225,7 @@ func (s *autoCodeTemplate) generate(ctx context.Context, info request.AutoCode, code := make(map[string]strings.Builder) for key, create := range templates { var files *template.Template - files, err = template.ParseFiles(key) + files, err = template.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(key) if err != nil { return nil, nil, nil, errors.Wrapf(err, "[filpath:%s]读取模版文件失败!", key) } @@ -322,7 +323,7 @@ func (s *autoCodeTemplate) GetApiAndServer(info request.AutoFunc) (map[string]st func (s *autoCodeTemplate) getTemplateStr(t string, info request.AutoFunc) (string, error) { tempPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "function", t+".tpl") - files, err := template.ParseFiles(tempPath) + files, err := template.New(filepath.Base(tempPath)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(tempPath) if err != nil { return "", errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", tempPath) } diff --git a/service/system/enter.go b/service/system/enter.go index 634cd00..d91f279 100644 --- a/service/system/enter.go +++ b/service/system/enter.go @@ -17,6 +17,7 @@ type ServiceGroup struct { AuthorityBtnService SysExportTemplateService SysParamsService + SysVersionService AutoCodePlugin autoCodePlugin AutoCodePackage autoCodePackage AutoCodeHistory autoCodeHistory diff --git a/service/system/jwt_black_list.go b/service/system/jwt_black_list.go index 7b0094e..95242f0 100644 --- a/service/system/jwt_black_list.go +++ b/service/system/jwt_black_list.go @@ -7,7 +7,6 @@ import ( "git.echol.cn/loser/lckt/global" "git.echol.cn/loser/lckt/model/system" - "git.echol.cn/loser/lckt/utils" ) type JwtService struct{} @@ -29,20 +28,6 @@ func (jwtService *JwtService) JsonInBlacklist(jwtList system.JwtBlacklist) (err return } -//@author: [piexlmax](https://github.com/piexlmax) -//@function: IsBlacklist -//@description: 判断JWT是否在黑名单内部 -//@param: jwt string -//@return: bool - -func (jwtService *JwtService) IsBlacklist(jwt string) bool { - _, ok := global.BlackCache.Get(jwt) - return ok - // err := global.GVA_DB.Where("jwt = ?", jwt).First(&system.JwtBlacklist{}).Error - // isNotFound := errors.Is(err, gorm.ErrRecordNotFound) - // return !isNotFound -} - //@author: [piexlmax](https://github.com/piexlmax) //@function: GetRedisJWT //@description: 从redis取jwt @@ -54,23 +39,6 @@ func (jwtService *JwtService) GetRedisJWT(userName string) (redisJWT string, err return redisJWT, err } -//@author: [piexlmax](https://github.com/piexlmax) -//@function: SetRedisJWT -//@description: jwt存入redis并设置过期时间 -//@param: jwt string, userName string -//@return: err error - -func (jwtService *JwtService) SetRedisJWT(jwt string, userName string) (err error) { - // 此处过期时间等于jwt过期时间 - dr, err := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) - if err != nil { - return err - } - timer := dr - err = global.GVA_REDIS.Set(context.Background(), userName, jwt, timer).Err() - return err -} - func LoadAll() { var data []string err := global.GVA_DB.Model(&system.JwtBlacklist{}).Select("jwt").Find(&data).Error diff --git a/service/system/sys_authority.go b/service/system/sys_authority.go index 5ef1a0b..d751428 100644 --- a/service/system/sys_authority.go +++ b/service/system/sys_authority.go @@ -326,5 +326,8 @@ func (authorityService *AuthorityService) findChildrenAuthority(authority *syste func (authorityService *AuthorityService) GetParentAuthorityID(authorityID uint) (parentID uint, err error) { var authority system.SysAuthority err = global.GVA_DB.Where("authority_id = ?", authorityID).First(&authority).Error - return *authority.ParentId, err + if err != nil { + return + } + return *authority.ParentId, nil } diff --git a/service/system/sys_auto_code_oracle.go b/service/system/sys_auto_code_oracle.go index a8abb86..1c4254f 100644 --- a/service/system/sys_auto_code_oracle.go +++ b/service/system/sys_auto_code_oracle.go @@ -64,7 +64,7 @@ WHERE lower(a.table_name) = ? AND lower(a.OWNER) = ? ORDER BY - a.COLUMN_ID; + a.COLUMN_ID ` err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error diff --git a/service/system/sys_base_menu.go b/service/system/sys_base_menu.go index f51870e..bbe72db 100644 --- a/service/system/sys_base_menu.go +++ b/service/system/sys_base_menu.go @@ -72,6 +72,7 @@ func (baseMenuService *BaseMenuService) UpdateBaseMenu(menu system.SysBaseMenu) var oldMenu system.SysBaseMenu upDateMap := make(map[string]interface{}) upDateMap["keep_alive"] = menu.KeepAlive + upDateMap["transition_type"] = menu.TransitionType upDateMap["close_tab"] = menu.CloseTab upDateMap["default_menu"] = menu.DefaultMenu upDateMap["parent_id"] = menu.ParentId diff --git a/service/system/sys_casbin.go b/service/system/sys_casbin.go index 7aa8b93..2e3cb67 100644 --- a/service/system/sys_casbin.go +++ b/service/system/sys_casbin.go @@ -3,17 +3,14 @@ package system import ( "errors" "strconv" - "sync" "gorm.io/gorm" "git.echol.cn/loser/lckt/global" "git.echol.cn/loser/lckt/model/system/request" - "github.com/casbin/casbin/v2" - "github.com/casbin/casbin/v2/model" + "git.echol.cn/loser/lckt/utils" gormadapter "github.com/casbin/gorm-adapter/v3" _ "github.com/go-sql-driver/mysql" - "go.uber.org/zap" ) //@author: [piexlmax](https://github.com/piexlmax) @@ -68,7 +65,7 @@ func (casbinService *CasbinService) UpdateCasbin(adminAuthorityID, AuthorityID u if len(rules) == 0 { return nil } // 设置空权限无需调用 AddPolicies 方法 - e := casbinService.Casbin() + e := utils.GetCasbin() success, _ := e.AddPolicies(rules) if !success { return errors.New("存在相同api,添加失败,请联系管理员") @@ -91,7 +88,7 @@ func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath stri return err } - e := casbinService.Casbin() + e := utils.GetCasbin() return e.LoadPolicy() } @@ -102,7 +99,7 @@ func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath stri //@return: pathMaps []request.CasbinInfo func (casbinService *CasbinService) GetPolicyPathByAuthorityId(AuthorityID uint) (pathMaps []request.CasbinInfo) { - e := casbinService.Casbin() + e := utils.GetCasbin() authorityId := strconv.Itoa(int(AuthorityID)) list, _ := e.GetFilteredPolicy(0, authorityId) for _, v := range list { @@ -121,7 +118,7 @@ func (casbinService *CasbinService) GetPolicyPathByAuthorityId(AuthorityID uint) //@return: bool func (casbinService *CasbinService) ClearCasbin(v int, p ...string) bool { - e := casbinService.Casbin() + e := utils.GetCasbin() success, _ := e.RemoveFilteredPolicy(v, p...) return success } @@ -170,52 +167,7 @@ func (casbinService *CasbinService) AddPolicies(db *gorm.DB, rules [][]string) e } func (casbinService *CasbinService) FreshCasbin() (err error) { - e := casbinService.Casbin() + e := utils.GetCasbin() err = e.LoadPolicy() return err } - -//@author: [piexlmax](https://github.com/piexlmax) -//@function: Casbin -//@description: 持久化到数据库 引入自定义规则 -//@return: *casbin.Enforcer - -var ( - syncedCachedEnforcer *casbin.SyncedCachedEnforcer - once sync.Once -) - -func (casbinService *CasbinService) Casbin() *casbin.SyncedCachedEnforcer { - once.Do(func() { - a, err := gormadapter.NewAdapterByDB(global.GVA_DB) - if err != nil { - zap.L().Error("适配数据库失败请检查casbin表是否为InnoDB引擎!", zap.Error(err)) - return - } - text := ` - [request_definition] - r = sub, obj, act - - [policy_definition] - p = sub, obj, act - - [role_definition] - g = _, _ - - [policy_effect] - e = some(where (p.eft == allow)) - - [matchers] - m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act - ` - m, err := model.NewModelFromString(text) - if err != nil { - zap.L().Error("字符串加载模型失败!", zap.Error(err)) - return - } - syncedCachedEnforcer, _ = casbin.NewSyncedCachedEnforcer(m, a) - syncedCachedEnforcer.SetExpireTime(60 * 60) - _ = syncedCachedEnforcer.LoadPolicy() - }) - return syncedCachedEnforcer -} diff --git a/service/system/sys_export_template.go b/service/system/sys_export_template.go index 158c250..886a2d7 100644 --- a/service/system/sys_export_template.go +++ b/service/system/sys_export_template.go @@ -3,6 +3,7 @@ package system import ( "bytes" "encoding/json" + "errors" "fmt" "mime/multipart" "net/url" @@ -127,6 +128,11 @@ func (sysExportTemplateService *SysExportTemplateService) GetSysExportTemplateIn // ExportExcel 导出Excel // Author [piexlmax](https://github.com/piexlmax) func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID string, values url.Values) (file *bytes.Buffer, name string, err error) { + var params = values.Get("params") + paramsValues, err := url.ParseQuery(params) + if err != nil { + return nil, "", fmt.Errorf("解析 params 参数失败: %v", err) + } var template system.SysExportTemplate err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error if err != nil { @@ -175,10 +181,38 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID db = db.Select(selects).Table(template.TableName) + filterDeleted := false + + filterParam := paramsValues.Get("filterDeleted") + if filterParam == "true" { + filterDeleted = true + } + + if filterDeleted { + // 自动过滤主表的软删除 + db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", template.TableName)) + + // 过滤关联表的软删除(如果有) + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + // 检查关联表是否有deleted_at字段 + hasDeletedAt := sysExportTemplateService.hasDeletedAtColumn(join.Table) + if hasDeletedAt { + db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", join.Table)) + } + } + } + } + if len(template.Conditions) > 0 { for _, condition := range template.Conditions { sql := fmt.Sprintf("%s %s ?", condition.Column, condition.Operator) - value := values.Get(condition.From) + value := paramsValues.Get(condition.From) + + if condition.Operator == "IN" || condition.Operator == "NOT IN" { + sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator) + } + if value != "" { if condition.Operator == "LIKE" { value = "%" + value + "%" @@ -188,7 +222,7 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID } } // 通过参数传入limit - limit := values.Get("limit") + limit := paramsValues.Get("limit") if limit != "" { l, e := strconv.Atoi(limit) if e == nil { @@ -201,7 +235,7 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID } // 通过参数传入offset - offset := values.Get("offset") + offset := paramsValues.Get("offset") if offset != "" { o, e := strconv.Atoi(offset) if e == nil { @@ -224,7 +258,7 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID } // 通过参数传入order - order := values.Get("order") + order := paramsValues.Get("order") if order == "" && template.Order != "" { // 如果没有order入参,这里会使用模板的默认排序 @@ -281,7 +315,17 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID } for i, row := range rows { for j, colCell := range row { - sErr := f.SetCellValue("Sheet1", fmt.Sprintf("%s%d", getColumnName(j+1), i+1), colCell) + cell := fmt.Sprintf("%s%d", getColumnName(j+1), i+1) + + var sErr error + if v, err := strconv.ParseFloat(colCell, 64); err == nil { + sErr = f.SetCellValue("Sheet1", cell, v) + } else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil { + sErr = f.SetCellValue("Sheet1", cell, v) + } else { + sErr = f.SetCellValue("Sheet1", cell, colCell) + } + if sErr != nil { return nil, "", sErr } @@ -344,6 +388,13 @@ func (sysExportTemplateService *SysExportTemplateService) ExportTemplate(templat return file, template.Name, nil } +// 辅助函数:检查表是否有deleted_at列 +func (s *SysExportTemplateService) hasDeletedAtColumn(tableName string) bool { + var count int64 + global.GVA_DB.Raw("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = 'deleted_at'", tableName).Count(&count) + return count > 0 +} + // ImportExcel 导入Excel // Author [piexlmax](https://github.com/piexlmax) func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID string, file *multipart.FileHeader) (err error) { @@ -368,6 +419,9 @@ func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID if err != nil { return err } + if len(rows) < 2 { + return errors.New("Excel data is not enough.\nIt should contain title row and data") + } var templateInfoMap = make(map[string]string) err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) @@ -387,11 +441,17 @@ func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID return db.Transaction(func(tx *gorm.DB) error { excelTitle := rows[0] + for i, str := range excelTitle { + excelTitle[i] = strings.TrimSpace(str) + } values := rows[1:] items := make([]map[string]interface{}, 0, len(values)) for _, row := range values { var item = make(map[string]interface{}) for ii, value := range row { + if _, ok := titleKeyMap[excelTitle[ii]]; !ok { + continue // excel中多余的标题,在模板信息中没有对应的字段,因此key为空,必须跳过 + } key := titleKeyMap[excelTitle[ii]] item[key] = value } diff --git a/service/system/sys_menu.go b/service/system/sys_menu.go index cfaaed8..3d86e77 100644 --- a/service/system/sys_menu.go +++ b/service/system/sys_menu.go @@ -134,10 +134,52 @@ func (menuService *MenuService) getBaseChildrenList(menu *system.SysBaseMenu, tr //@return: error func (menuService *MenuService) AddBaseMenu(menu system.SysBaseMenu) error { - if !errors.Is(global.GVA_DB.Where("name = ?", menu.Name).First(&system.SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { - return errors.New("存在重复name,请修改name") - } - return global.GVA_DB.Create(&menu).Error + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 检查name是否重复 + if !errors.Is(tx.Where("name = ?", menu.Name).First(&system.SysBaseMenu{}).Error, gorm.ErrRecordNotFound) { + return errors.New("存在重复name,请修改name") + } + + if menu.ParentId != 0 { + // 检查父菜单是否存在 + var parentMenu system.SysBaseMenu + if err := tx.First(&parentMenu, menu.ParentId).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("父菜单不存在") + } + return err + } + + // 检查父菜单下现有子菜单数量 + var existingChildrenCount int64 + err := tx.Model(&system.SysBaseMenu{}).Where("parent_id = ?", menu.ParentId).Count(&existingChildrenCount).Error + if err != nil { + return err + } + + // 如果父菜单原本是叶子菜单(没有子菜单),现在要变成枝干菜单,需要清空其权限分配 + if existingChildrenCount == 0 { + // 检查父菜单是否被其他角色设置为首页 + var defaultRouterCount int64 + err := tx.Model(&system.SysAuthority{}).Where("default_router = ?", parentMenu.Name).Count(&defaultRouterCount).Error + if err != nil { + return err + } + if defaultRouterCount > 0 { + return errors.New("父菜单已被其他角色的首页占用,请先释放父菜单的首页权限") + } + + // 清空父菜单的所有权限分配 + err = tx.Where("sys_base_menu_id = ?", menu.ParentId).Delete(&system.SysAuthorityMenu{}).Error + if err != nil { + return err + } + } + } + + // 创建菜单 + return tx.Create(&menu).Error + }) } //@author: [piexlmax](https://github.com/piexlmax) diff --git a/service/system/sys_operation_record.go b/service/system/sys_operation_record.go index c96ba68..589711d 100644 --- a/service/system/sys_operation_record.go +++ b/service/system/sys_operation_record.go @@ -17,11 +17,6 @@ type OperationRecordService struct{} var OperationRecordServiceApp = new(OperationRecordService) -func (operationRecordService *OperationRecordService) CreateSysOperationRecord(sysOperationRecord system.SysOperationRecord) (err error) { - err = global.GVA_DB.Create(&sysOperationRecord).Error - return err -} - //@author: [granty1](https://github.com/granty1) //@author: [piexlmax](https://github.com/piexlmax) //@function: DeleteSysOperationRecordByIds diff --git a/service/system/sys_user.go b/service/system/sys_user.go index 0790a03..ab3e0e1 100644 --- a/service/system/sys_user.go +++ b/service/system/sys_user.go @@ -3,9 +3,10 @@ package system import ( "errors" "fmt" + "time" + "git.echol.cn/loser/lckt/model/common" systemReq "git.echol.cn/loser/lckt/model/system/request" - "time" "git.echol.cn/loser/lckt/global" "git.echol.cn/loser/lckt/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) @@ -311,7 +312,7 @@ func (userService *UserService) FindUserByUuid(uuid string) (user *system.SysUse //@param: ID uint //@return: err error -func (userService *UserService) ResetPassword(ID uint) (err error) { - err = global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", ID).Update("password", utils.BcryptHash("123456")).Error +func (userService *UserService) ResetPassword(ID uint, password string) (err error) { + err = global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", ID).Update("password", utils.BcryptHash(password)).Error return err } diff --git a/service/system/sys_version.go b/service/system/sys_version.go new file mode 100644 index 0000000..2cf8d87 --- /dev/null +++ b/service/system/sys_version.go @@ -0,0 +1,230 @@ +package system + +import ( + "context" + "git.echol.cn/loser/lckt/global" + "git.echol.cn/loser/lckt/model/system" + systemReq "git.echol.cn/loser/lckt/model/system/request" + "gorm.io/gorm" +) + +type SysVersionService struct{} + +// CreateSysVersion 创建版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) CreateSysVersion(ctx context.Context, sysVersion *system.SysVersion) (err error) { + err = global.GVA_DB.Create(sysVersion).Error + return err +} + +// DeleteSysVersion 删除版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) DeleteSysVersion(ctx context.Context, ID string) (err error) { + err = global.GVA_DB.Delete(&system.SysVersion{}, "id = ?", ID).Error + return err +} + +// DeleteSysVersionByIds 批量删除版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) DeleteSysVersionByIds(ctx context.Context, IDs []string) (err error) { + err = global.GVA_DB.Where("id in ?", IDs).Delete(&system.SysVersion{}).Error + return err +} + +// GetSysVersion 根据ID获取版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) GetSysVersion(ctx context.Context, ID string) (sysVersion system.SysVersion, err error) { + err = global.GVA_DB.Where("id = ?", ID).First(&sysVersion).Error + return +} + +// GetSysVersionInfoList 分页获取版本管理记录 +// Author [yourname](https://github.com/yourname) +func (sysVersionService *SysVersionService) GetSysVersionInfoList(ctx context.Context, info systemReq.SysVersionSearch) (list []system.SysVersion, total int64, err error) { + limit := info.PageSize + offset := info.PageSize * (info.Page - 1) + // 创建db + db := global.GVA_DB.Model(&system.SysVersion{}) + var sysVersions []system.SysVersion + // 如果有条件搜索 下方会自动创建搜索语句 + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) + } + + if info.VersionName != nil && *info.VersionName != "" { + db = db.Where("version_name LIKE ?", "%"+*info.VersionName+"%") + } + if info.VersionCode != nil && *info.VersionCode != "" { + db = db.Where("version_code = ?", *info.VersionCode) + } + err = db.Count(&total).Error + if err != nil { + return + } + + if limit != 0 { + db = db.Limit(limit).Offset(offset) + } + + err = db.Find(&sysVersions).Error + return sysVersions, total, err +} +func (sysVersionService *SysVersionService) GetSysVersionPublic(ctx context.Context) { + // 此方法为获取数据源定义的数据 + // 请自行实现 +} + +// GetMenusByIds 根据ID列表获取菜单数据 +func (sysVersionService *SysVersionService) GetMenusByIds(ctx context.Context, ids []uint) (menus []system.SysBaseMenu, err error) { + err = global.GVA_DB.Where("id in ?", ids).Preload("Parameters").Preload("MenuBtn").Find(&menus).Error + return +} + +// GetApisByIds 根据ID列表获取API数据 +func (sysVersionService *SysVersionService) GetApisByIds(ctx context.Context, ids []uint) (apis []system.SysApi, err error) { + err = global.GVA_DB.Where("id in ?", ids).Find(&apis).Error + return +} + +// GetDictionariesByIds 根据ID列表获取字典数据 +func (sysVersionService *SysVersionService) GetDictionariesByIds(ctx context.Context, ids []uint) (dictionaries []system.SysDictionary, err error) { + err = global.GVA_DB.Where("id in ?", ids).Preload("SysDictionaryDetails").Find(&dictionaries).Error + return +} + +// ImportMenus 导入菜单数据 +func (sysVersionService *SysVersionService) ImportMenus(ctx context.Context, menus []system.SysBaseMenu) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + // 递归创建菜单 + return sysVersionService.createMenusRecursively(tx, menus, 0) + }) +} + +// createMenusRecursively 递归创建菜单 +func (sysVersionService *SysVersionService) createMenusRecursively(tx *gorm.DB, menus []system.SysBaseMenu, parentId uint) error { + for _, menu := range menus { + // 检查菜单是否已存在 + var existingMenu system.SysBaseMenu + if err := tx.Where("name = ? AND path = ?", menu.Name, menu.Path).First(&existingMenu).Error; err == nil { + // 菜单已存在,使用现有菜单ID继续处理子菜单 + if len(menu.Children) > 0 { + if err := sysVersionService.createMenusRecursively(tx, menu.Children, existingMenu.ID); err != nil { + return err + } + } + continue + } + + // 保存参数和按钮数据,稍后处理 + parameters := menu.Parameters + menuBtns := menu.MenuBtn + children := menu.Children + + // 创建新菜单(不包含关联数据) + newMenu := system.SysBaseMenu{ + ParentId: parentId, + Path: menu.Path, + Name: menu.Name, + Hidden: menu.Hidden, + Component: menu.Component, + Sort: menu.Sort, + Meta: menu.Meta, + } + + if err := tx.Create(&newMenu).Error; err != nil { + return err + } + + // 创建参数 + if len(parameters) > 0 { + for _, param := range parameters { + newParam := system.SysBaseMenuParameter{ + SysBaseMenuID: newMenu.ID, + Type: param.Type, + Key: param.Key, + Value: param.Value, + } + if err := tx.Create(&newParam).Error; err != nil { + return err + } + } + } + + // 创建菜单按钮 + if len(menuBtns) > 0 { + for _, btn := range menuBtns { + newBtn := system.SysBaseMenuBtn{ + SysBaseMenuID: newMenu.ID, + Name: btn.Name, + Desc: btn.Desc, + } + if err := tx.Create(&newBtn).Error; err != nil { + return err + } + } + } + + // 递归处理子菜单 + if len(children) > 0 { + if err := sysVersionService.createMenusRecursively(tx, children, newMenu.ID); err != nil { + return err + } + } + } + return nil +} + +// ImportApis 导入API数据 +func (sysVersionService *SysVersionService) ImportApis(apis []system.SysApi) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, api := range apis { + // 检查API是否已存在 + var existingApi system.SysApi + if err := tx.Where("path = ? AND method = ?", api.Path, api.Method).First(&existingApi).Error; err == nil { + // API已存在,跳过 + continue + } + + // 创建新API + newApi := system.SysApi{ + Path: api.Path, + Description: api.Description, + ApiGroup: api.ApiGroup, + Method: api.Method, + } + + if err := tx.Create(&newApi).Error; err != nil { + return err + } + } + return nil + }) +} + +// ImportDictionaries 导入字典数据 +func (sysVersionService *SysVersionService) ImportDictionaries(dictionaries []system.SysDictionary) error { + return global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, dict := range dictionaries { + // 检查字典是否已存在 + var existingDict system.SysDictionary + if err := tx.Where("type = ?", dict.Type).First(&existingDict).Error; err == nil { + // 字典已存在,跳过 + continue + } + + // 创建新字典 + newDict := system.SysDictionary{ + Name: dict.Name, + Type: dict.Type, + Status: dict.Status, + Desc: dict.Desc, + SysDictionaryDetails: dict.SysDictionaryDetails, + } + + if err := tx.Create(&newDict).Error; err != nil { + return err + } + } + return nil + }) +} diff --git a/source/system/api.go b/source/system/api.go index fe042fe..c2c8c5c 100644 --- a/source/system/api.go +++ b/source/system/api.go @@ -2,6 +2,7 @@ package system import ( "context" + sysModel "git.echol.cn/loser/lckt/model/system" "git.echol.cn/loser/lckt/service/system" "github.com/pkg/errors" @@ -117,6 +118,9 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "代码生成器", Method: "GET", Path: "/autoCode/getColumn", Description: "获取所选table的所有字段"}, {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/installPlugin", Description: "安装插件"}, {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/pubPlug", Description: "打包插件"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcp", Description: "自动生成 MCP Tool 模板"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpTest", Description: "MCP Tool 测试"}, + {ApiGroup: "代码生成器", Method: "POST", Path: "/autoCode/mcpList", Description: "获取 MCP ToolList"}, {ApiGroup: "模板配置", Method: "POST", Path: "/autoCode/createPackage", Description: "配置模板"}, {ApiGroup: "模板配置", Method: "GET", Path: "/autoCode/getTemplates", Description: "获取模板文件"}, @@ -185,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/source/system/authorities_menus.go b/source/system/authorities_menus.go index 93a9eef..9192280 100644 --- a/source/system/authorities_menus.go +++ b/source/system/authorities_menus.go @@ -35,35 +35,72 @@ func (i *initMenuAuthority) InitializeData(ctx context.Context) (next context.Co if !ok { return ctx, system.ErrMissingDBContext } + initAuth := &initAuthority{} authorities, ok := ctx.Value(initAuth.InitializerName()).([]sysModel.SysAuthority) if !ok { return ctx, errors.Wrap(system.ErrMissingDependentContext, "创建 [菜单-权限] 关联失败, 未找到权限表初始化数据") } - menus, ok := ctx.Value(new(initMenu).InitializerName()).([]sysModel.SysBaseMenu) + + allMenus, ok := ctx.Value(new(initMenu).InitializerName()).([]sysModel.SysBaseMenu) if !ok { return next, errors.Wrap(errors.New(""), "创建 [菜单-权限] 关联失败, 未找到菜单表初始化数据") } next = ctx - // 888 - if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(menus); err != nil { - return next, err + + // 构建菜单ID映射,方便快速查找 + menuMap := make(map[uint]sysModel.SysBaseMenu) + for _, menu := range allMenus { + menuMap[menu.ID] = menu + } + + // 为不同角色分配不同权限 + // 1. 超级管理员角色(888) - 拥有所有菜单权限 + if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(allMenus); err != nil { + return next, errors.Wrap(err, "为超级管理员分配菜单失败") + } + + // 2. 普通用户角色(8881) - 仅拥有基础功能菜单 + // 仅选择部分父级菜单及其子菜单 + var menu8881 []sysModel.SysBaseMenu + + // 添加仪表盘、关于我们和个人信息菜单 + for _, menu := range allMenus { + if menu.ParentId == 0 && (menu.Name == "dashboard" || menu.Name == "about" || menu.Name == "person" || menu.Name == "state") { + menu8881 = append(menu8881, menu) + } } - // 8881 - menu8881 := menus[:2] - menu8881 = append(menu8881, menus[7]) if err = db.Model(&authorities[1]).Association("SysBaseMenus").Replace(menu8881); err != nil { - return next, err + return next, errors.Wrap(err, "为普通用户分配菜单失败") } - // 9528 - if err = db.Model(&authorities[2]).Association("SysBaseMenus").Replace(menus[:11]); err != nil { - return next, err + // 3. 测试角色(9528) - 拥有部分菜单权限 + var menu9528 []sysModel.SysBaseMenu + + // 添加所有父级菜单 + for _, menu := range allMenus { + if menu.ParentId == 0 { + menu9528 = append(menu9528, menu) + } } - if err = db.Model(&authorities[2]).Association("SysBaseMenus").Append(menus[12:17]); err != nil { - return next, err + + // 添加部分子菜单 - 系统工具、示例文件等模块的子菜单 + for _, menu := range allMenus { + parentName := "" + if menu.ParentId > 0 && menuMap[menu.ParentId].Name != "" { + parentName = menuMap[menu.ParentId].Name + } + + if menu.ParentId > 0 && (parentName == "systemTools" || parentName == "example") { + menu9528 = append(menu9528, menu) + } } + + if err = db.Model(&authorities[2]).Association("SysBaseMenus").Replace(menu9528); err != nil { + return next, errors.Wrap(err, "为测试角色分配菜单失败") + } + return next, nil } diff --git a/source/system/casbin.go b/source/system/casbin.go index f12b4f5..ea8730a 100644 --- a/source/system/casbin.go +++ b/source/system/casbin.go @@ -130,6 +130,9 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/autoCode/installPlugin", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/autoCode/pubPlug", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/autoCode/addFunc", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcp", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcpTest", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcpList", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/findSysDictionaryDetail", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/updateSysDictionaryDetail", V2: "PUT"}, @@ -189,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/source/system/dictionary_detail.go b/source/system/dictionary_detail.go index ace9bde..98c7dae 100644 --- a/source/system/dictionary_detail.go +++ b/source/system/dictionary_detail.go @@ -66,7 +66,7 @@ func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, e } dicts[2].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ - {Label: "date", Status: &True}, + {Label: "date", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, {Label: "time", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, {Label: "year", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, {Label: "datetime", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, @@ -74,7 +74,7 @@ func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, e {Label: "timestamptz", Value: "6", Status: &True, Extend: "pgsql", Sort: 5}, } dicts[3].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ - {Label: "float", Status: &True}, + {Label: "float", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, {Label: "double", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, {Label: "decimal", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, {Label: "numeric", Value: "3", Status: &True, Extend: "pgsql", Sort: 3}, @@ -82,7 +82,7 @@ func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, e } dicts[4].SysDictionaryDetails = []sysModel.SysDictionaryDetail{ - {Label: "char", Status: &True}, + {Label: "char", Value: "0", Status: &True, Extend: "mysql", Sort: 0}, {Label: "varchar", Value: "1", Status: &True, Extend: "mysql", Sort: 1}, {Label: "tinyblob", Value: "2", Status: &True, Extend: "mysql", Sort: 2}, {Label: "tinytext", Value: "3", Status: &True, Extend: "mysql", Sort: 3}, diff --git a/source/system/menu.go b/source/system/menu.go index cde0895..b982801 100644 --- a/source/system/menu.go +++ b/source/system/menu.go @@ -50,43 +50,75 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er if !ok { return ctx, system.ErrMissingDBContext } - entities := []SysBaseMenu{ + + // 定义所有菜单 + allMenus := []SysBaseMenu{ {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Meta: Meta{Title: "仪表盘", Icon: "odometer"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "about", Name: "about", Component: "view/about/index.vue", Sort: 9, Meta: Meta{Title: "关于我们", Icon: "info-filled"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "admin", Name: "superAdmin", Component: "view/superAdmin/index.vue", Sort: 3, Meta: Meta{Title: "超级管理员", Icon: "user"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "avatar"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "tickets", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "api管理", Icon: "platform", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "coordinate"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "notebook"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "pie-chart"}}, {MenuLevel: 0, Hidden: true, ParentId: 0, Path: "person", Name: "person", Component: "view/person/person.vue", Sort: 4, Meta: Meta{Title: "个人信息", Icon: "message"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "example", Name: "example", Component: "view/example/index.vue", Sort: 7, Meta: Meta{Title: "示例文件", Icon: "management"}}, - {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 5, Meta: Meta{Title: "媒体库(上传下载)", Icon: "upload"}}, - {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "断点续传", Icon: "upload-filled"}}, - {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "客户列表(资源示例)", Icon: "avatar"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "systemTools", Name: "systemTools", Component: "view/systemTools/index.vue", Sort: 5, Meta: Meta{Title: "系统工具", Icon: "tools"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "代码生成器", Icon: "cpu", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "系统配置", Icon: "operation"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "自动化代码管理", Icon: "magic-stick"}}, - {MenuLevel: 0, Hidden: true, ParentId: 15, Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "自动化代码-${id}", Icon: "magic-stick"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "模板配置", Icon: "folder"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "https://www.gin-vue-admin.com", Name: "https://www.gin-vue-admin.com", Component: "/", Sort: 0, Meta: Meta{Title: "官方网站", Icon: "customer-gva"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "state", Name: "state", Component: "view/system/state.vue", Sort: 8, Meta: Meta{Title: "服务器状态", Icon: "cloudy"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "plugin", Name: "plugin", Component: "view/routerHolder.vue", Sort: 6, Meta: Meta{Title: "插件系统", Icon: "cherry"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, 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: 0, Hidden: false, ParentId: 24, Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "插件安装", Icon: "box"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "打包插件", Icon: "files"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "邮件插件", Icon: "message"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "公告管理[示例]", Icon: "scaleToOriginal"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "compass"}}, } - if err = db.Create(&entities).Error; err != nil { - return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"表数据初始化失败!") + + // 先创建父级菜单(ParentId = 0 的菜单) + if err = db.Create(&allMenus).Error; err != nil { + return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"父级菜单初始化失败!") } - next = context.WithValue(ctx, i.InitializerName(), entities) + + // 建立菜单映射 - 通过Name查找已创建的菜单及其ID + menuNameMap := make(map[string]uint) + for _, menu := range allMenus { + menuNameMap[menu.Name] = menu.ID + } + + // 定义子菜单,并设置正确的ParentId + childMenus := []SysBaseMenu{ + // superAdmin子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "角色管理", Icon: "avatar"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "菜单管理", Icon: "tickets", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "api管理", Icon: "platform", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "用户管理", Icon: "coordinate"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "字典管理", Icon: "notebook"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "操作历史", Icon: "pie-chart"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "参数管理", Icon: "compass"}}, + + // example子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 5, Meta: Meta{Title: "媒体库(上传下载)", Icon: "upload"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "断点续传", Icon: "upload-filled"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "客户列表(资源示例)", Icon: "avatar"}}, + + // systemTools子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "代码生成器", Icon: "cpu", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "表单生成器", Icon: "magic-stick", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "系统配置", Icon: "operation"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "自动化代码管理", Icon: "magic-stick"}}, + {MenuLevel: 1, Hidden: true, ParentId: menuNameMap["systemTools"], Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "自动化代码-${id}", Icon: "magic-stick"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "模板配置", Icon: "folder"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "导出模板", Icon: "reading"}}, + {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"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "打包插件", Icon: "files"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "邮件插件", Icon: "message"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "公告管理[示例]", Icon: "scaleToOriginal"}}, + } + + // 创建子菜单 + if err = db.Create(&childMenus).Error; err != nil { + return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"子菜单初始化失败!") + } + + // 组合所有菜单作为返回结果 + allEntities := append(allMenus, childMenus...) + next = context.WithValue(ctx, i.InitializerName(), allEntities) return next, nil } diff --git a/utils/ast/ast.go b/utils/ast/ast.go index 4ed7a30..34cba37 100644 --- a/utils/ast/ast.go +++ b/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/utils/autocode/template_funcs.go b/utils/autocode/template_funcs.go new file mode 100644 index 0000000..8935948 --- /dev/null +++ b/utils/autocode/template_funcs.go @@ -0,0 +1,723 @@ +package autocode + +import ( + "fmt" + systemReq "git.echol.cn/loser/lckt/model/system/request" + "slices" + "strings" + "text/template" +) + +// GetTemplateFuncMap 返回模板函数映射,用于在模板中使用 +func GetTemplateFuncMap() template.FuncMap { + return template.FuncMap{ + "title": strings.Title, + "GenerateField": GenerateField, + "GenerateSearchField": GenerateSearchField, + "GenerateSearchConditions": GenerateSearchConditions, + "GenerateSearchFormItem": GenerateSearchFormItem, + "GenerateTableColumn": GenerateTableColumn, + "GenerateFormItem": GenerateFormItem, + "GenerateDescriptionItem": GenerateDescriptionItem, + "GenerateDefaultFormValue": GenerateDefaultFormValue, + } +} + +// 渲染Model中的字段 +func GenerateField(field systemReq.AutoCodeField) string { + // 构建gorm标签 + gormTag := `` + + if field.FieldIndexType != "" { + gormTag += field.FieldIndexType + ";" + } + + if field.PrimaryKey { + gormTag += "primarykey;" + } + + if field.DefaultValue != "" { + gormTag += fmt.Sprintf("default:%s;", field.DefaultValue) + } + + if field.Comment != "" { + gormTag += fmt.Sprintf("comment:%s;", field.Comment) + } + + gormTag += "column:" + field.ColumnName + ";" + + // 对于int类型,根据DataTypeLong决定具体的Go类型,不使用size标签 + if field.DataTypeLong != "" && field.FieldType != "enum" && field.FieldType != "int" { + gormTag += fmt.Sprintf("size:%s;", field.DataTypeLong) + } + + requireTag := ` binding:"required"` + "`" + + // 根据字段类型构建不同的字段定义 + var result string + switch field.FieldType { + case "enum": + result = fmt.Sprintf(`%s string `+"`"+`json:"%s" form:"%s" gorm:"%stype:enum(%s);"`+"`", + field.FieldName, field.FieldJson, field.FieldJson, gormTag, field.DataTypeLong) + case "picture", "video": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s string `+"`"+`%s`+"`"+``, field.FieldName, tagContent) + case "file", "pictures", "array": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s datatypes.JSON `+"`"+`%s swaggertype:"array,object"`+"`"+``, + field.FieldName, tagContent) + case "richtext": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s *string `+"`"+`%stype:text;"`+"`"+``, + field.FieldName, tagContent) + case "json": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s datatypes.JSON `+"`"+`%s swaggertype:"object"`+"`"+``, + field.FieldName, tagContent) + default: + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + // 对于int类型,根据DataTypeLong决定具体的Go类型 + var fieldType string + if field.FieldType == "int" { + switch field.DataTypeLong { + case "1", "2", "3": + fieldType = "int8" + case "4", "5": + fieldType = "int16" + case "6", "7", "8", "9", "10": + fieldType = "int32" + case "11", "12", "13", "14", "15", "16", "17", "18", "19", "20": + fieldType = "int64" + default: + fieldType = "int64" + } + } else { + fieldType = field.FieldType + } + + result = fmt.Sprintf(`%s *%s `+"`"+`%s`+"`"+``, + field.FieldName, fieldType, tagContent) + } + + if field.Require { + result = result[0:len(result)-1] + requireTag + } + + // 添加字段描述 + if field.FieldDesc != "" { + result += fmt.Sprintf(" //%s", field.FieldDesc) + } + + return result +} + +// 格式化搜索条件语句 +func GenerateSearchConditions(fields []*systemReq.AutoCodeField) string { + var conditions []string + + for _, field := range fields { + if field.FieldSearchType == "" { + continue + } + + var condition string + + if slices.Contains([]string{"enum", "pictures", "picture", "video", "json", "richtext", "array"}, field.FieldType) { + if field.FieldType == "enum" { + if field.FieldSearchType == "LIKE" { + condition = fmt.Sprintf(` + if info.%s != "" { + db = db.Where("%s LIKE ?", "%%"+ info.%s+"%%") + }`, + field.FieldName, field.ColumnName, field.FieldName) + } else { + condition = fmt.Sprintf(` + if info.%s != "" { + db = db.Where("%s %s ?", info.%s) + }`, + field.FieldName, field.ColumnName, field.FieldSearchType, field.FieldName) + } + } else { + condition = fmt.Sprintf(` + if info.%s != "" { + // TODO 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 + }`, field.FieldName) + } + + } else if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + if field.FieldType == "time.Time" { + condition = fmt.Sprintf(` + if len(info.%sRange) == 2 { + db = db.Where("%s %s ? AND ? ", info.%sRange[0], info.%sRange[1]) + }`, + field.FieldName, field.ColumnName, field.FieldSearchType, field.FieldName, field.FieldName) + } else { + condition = fmt.Sprintf(` + if info.Start%s != nil && info.End%s != nil { + db = db.Where("%s %s ? AND ? ", *info.Start%s, *info.End%s) + }`, + field.FieldName, field.FieldName, field.ColumnName, + field.FieldSearchType, field.FieldName, field.FieldName) + } + } else { + nullCheck := "info." + field.FieldName + " != nil" + if field.FieldType == "string" { + condition = fmt.Sprintf(` + if %s && *info.%s != "" {`, nullCheck, field.FieldName) + } else { + condition = fmt.Sprintf(` + if %s {`, nullCheck) + } + + if field.FieldSearchType == "LIKE" { + condition += fmt.Sprintf(` + db = db.Where("%s LIKE ?", "%%"+ *info.%s+"%%") + }`, + field.ColumnName, field.FieldName) + } else { + condition += fmt.Sprintf(` + db = db.Where("%s %s ?", *info.%s) + }`, + field.ColumnName, field.FieldSearchType, field.FieldName) + } + } + + conditions = append(conditions, condition) + } + + return strings.Join(conditions, "") +} + +// 格式化前端搜索条件 +func GenerateSearchFormItem(field systemReq.AutoCodeField) string { + // 开始构建表单项 + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + + // 根据字段属性生成不同的输入类型 + if field.FieldType == "bool" { + result += fmt.Sprintf(` +`, field.FieldJson) + result += ` +` + result += ` +` + result += ` +` + } else if field.DictType != "" { + multipleAttr := "" + if field.FieldType == "array" { + multipleAttr = "multiple " + } + result += fmt.Sprintf(` +`, + multipleAttr, field.FieldJson, field.FieldJson) + result += fmt.Sprintf(` +`, + field.DictType) + result += ` +` + } else if field.CheckDataSource { + multipleAttr := "" + if field.DataSource.Association == 2 { + multipleAttr = "multiple " + } + result += fmt.Sprintf(` +`, + multipleAttr, field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.FieldJson) + result += ` +` + } else if field.FieldType == "float64" || field.FieldType == "int" { + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + result += fmt.Sprintf(` +`, field.FieldName) + result += ` — +` + result += fmt.Sprintf(` +`, field.FieldName) + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + } else if field.FieldType == "time.Time" { + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + result += ` +` + result += fmt.Sprintf(``, field.FieldJson) + } else { + result += fmt.Sprintf(``, field.FieldJson) + } + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + + // 关闭表单项 + result += `` + + return result +} + +// GenerateTableColumn generates HTML for table column based on field properties +func GenerateTableColumn(field systemReq.AutoCodeField) string { + // Add sortable attribute if needed + sortAttr := "" + if field.Sort { + sortAttr = " sortable" + } + + // Handle different field types + if field.CheckDataSource { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += ` +` + result += `
` + return result + } else if field.DictType != "" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += ` +` + result += `
` + return result + } else if field.FieldType == "bool" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += fmt.Sprintf(` +`, field.FieldJson) + result += `
` + return result + } else if field.FieldType == "time.Time" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += fmt.Sprintf(` +`, field.FieldJson) + result += `
` + return result + } else if field.FieldType == "picture" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "pictures" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "video" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "richtext" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "file" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "json" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "array" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else { + return fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + } +} + +func GenerateFormItem(field systemReq.AutoCodeField) string { + // 开始构建表单项 + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + + // 处理不同字段类型 + if field.CheckDataSource { + multipleAttr := "" + if field.DataSource.Association == 2 { + multipleAttr = " multiple" + } + result += fmt.Sprintf(` +`, + multipleAttr, field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.FieldJson) + result += ` +` + } else { + switch field.FieldType { + case "bool": + result += fmt.Sprintf(` +`, + field.FieldJson) + + case "string": + if field.DictType != "" { + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.DictType) + result += ` +` + } else { + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable, field.FieldDesc) + } + + case "richtext": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "json": + result += fmt.Sprintf(` // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.%s 后端会按照json的类型进行存取 +`, field.FieldJson) + result += fmt.Sprintf(` {{ formData.%s }} +`, field.FieldJson) + + case "array": + if field.DictType != "" { + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.DictType) + result += ` +` + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + + case "int": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable, field.FieldDesc) + + case "time.Time": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable) + + case "float64": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable) + + case "enum": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.DataTypeLong) + result += ` +` + + case "picture": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "pictures": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "video": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "file": + result += fmt.Sprintf(` +`, field.FieldJson) + } + } + + // 关闭表单项 + result += `` + + return result +} + +func GenerateDescriptionItem(field systemReq.AutoCodeField) string { + // 开始构建描述项 + result := fmt.Sprintf(` +`, field.FieldDesc) + + if field.CheckDataSource { + result += ` +` + } else if field.FieldType != "picture" && field.FieldType != "pictures" && + field.FieldType != "file" && field.FieldType != "array" && + field.FieldType != "richtext" { + result += fmt.Sprintf(` {{ detailForm.%s }} +`, field.FieldJson) + } else { + switch field.FieldType { + case "picture": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldJson) + case "array": + result += fmt.Sprintf(` +`, field.FieldJson) + case "pictures": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldJson) + case "richtext": + result += fmt.Sprintf(` +`, field.FieldJson) + case "file": + result += fmt.Sprintf(`
+`, field.FieldJson) + result += ` +` + result += ` +` + result += ` {{ item.name }} +` + result += ` +` + result += `
+` + } + } + + // 关闭描述项 + result += `
` + + return result +} + +func GenerateDefaultFormValue(field systemReq.AutoCodeField) string { + // 根据字段类型确定默认值 + var defaultValue string + + switch field.FieldType { + case "bool": + defaultValue = "false" + case "string", "richtext": + defaultValue = "''" + case "int": + if field.DataSource != nil { // 检查数据源是否存在 + defaultValue = "undefined" + } else { + defaultValue = "0" + } + case "time.Time": + defaultValue = "new Date()" + case "float64": + defaultValue = "0" + case "picture", "video": + defaultValue = "\"\"" + case "pictures", "file", "array": + defaultValue = "[]" + case "json": + defaultValue = "{}" + default: + defaultValue = "null" + } + + // 返回格式化后的默认值字符串 + return fmt.Sprintf(`%s: %s,`, field.FieldJson, defaultValue) +} + +// GenerateSearchField 根据字段属性生成搜索结构体中的字段定义 +func GenerateSearchField(field systemReq.AutoCodeField) string { + var result string + + if field.FieldSearchType == "" { + return "" // 如果没有搜索类型,返回空字符串 + } + + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + // 生成范围搜索字段 + // time 的情况 + if field.FieldType == "time.Time" { + result = fmt.Sprintf("%sRange []time.Time `json:\"%sRange\" form:\"%sRange[]\"`", + field.FieldName, field.FieldJson, field.FieldJson) + } else { + startField := fmt.Sprintf("Start%s *%s `json:\"start%s\" form:\"start%s\"`", + field.FieldName, field.FieldType, field.FieldName, field.FieldName) + endField := fmt.Sprintf("End%s *%s `json:\"end%s\" form:\"end%s\"`", + field.FieldName, field.FieldType, field.FieldName, field.FieldName) + result = startField + "\n" + endField + } + } else { + // 生成普通搜索字段 + if field.FieldType == "enum" || field.FieldType == "picture" || + field.FieldType == "pictures" || field.FieldType == "video" || + field.FieldType == "json" || field.FieldType == "richtext" || field.FieldType == "array" { + result = fmt.Sprintf("%s string `json:\"%s\" form:\"%s\"` ", + field.FieldName, field.FieldJson, field.FieldJson) + } else { + result = fmt.Sprintf("%s *%s `json:\"%s\" form:\"%s\"` ", + field.FieldName, field.FieldType, field.FieldJson, field.FieldJson) + } + } + + return result +} diff --git a/utils/captcha/redis.go b/utils/captcha/redis.go index 5802047..5170aae 100644 --- a/utils/captcha/redis.go +++ b/utils/captcha/redis.go @@ -5,7 +5,6 @@ import ( "time" "git.echol.cn/loser/lckt/global" - "github.com/mojocn/base64Captcha" "go.uber.org/zap" ) @@ -23,8 +22,10 @@ type RedisStore struct { Context context.Context } -func (rs *RedisStore) UseWithCtx(ctx context.Context) base64Captcha.Store { - rs.Context = ctx +func (rs *RedisStore) UseWithCtx(ctx context.Context) *RedisStore { + if ctx == nil { + rs.Context = ctx + } return rs } diff --git a/utils/casbin_util.go b/utils/casbin_util.go new file mode 100644 index 0000000..c6a2024 --- /dev/null +++ b/utils/casbin_util.go @@ -0,0 +1,52 @@ +package utils + +import ( + "sync" + + "git.echol.cn/loser/lckt/global" + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + gormadapter "github.com/casbin/gorm-adapter/v3" + "go.uber.org/zap" +) + +var ( + syncedCachedEnforcer *casbin.SyncedCachedEnforcer + once sync.Once +) + +// GetCasbin 获取casbin实例 +func GetCasbin() *casbin.SyncedCachedEnforcer { + once.Do(func() { + a, err := gormadapter.NewAdapterByDB(global.GVA_DB) + if err != nil { + zap.L().Error("适配数据库失败请检查casbin表是否为InnoDB引擎!", zap.Error(err)) + return + } + text := ` + [request_definition] + r = sub, obj, act + + [policy_definition] + p = sub, obj, act + + [role_definition] + g = _, _ + + [policy_effect] + e = some(where (p.eft == allow)) + + [matchers] + m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act + ` + m, err := model.NewModelFromString(text) + if err != nil { + zap.L().Error("字符串加载模型失败!", zap.Error(err)) + return + } + syncedCachedEnforcer, _ = casbin.NewSyncedCachedEnforcer(m, a) + syncedCachedEnforcer.SetExpireTime(60 * 60) + _ = syncedCachedEnforcer.LoadPolicy() + }) + return syncedCachedEnforcer +} diff --git a/utils/fmt_plus.go b/utils/fmt_plus.go index d849d18..4a613fa 100644 --- a/utils/fmt_plus.go +++ b/utils/fmt_plus.go @@ -68,6 +68,23 @@ func MaheHump(s string) string { return strings.Join(words, "") } +// HumpToUnderscore 将驼峰命名转换为下划线分割模式 +func HumpToUnderscore(s string) string { + var result strings.Builder + + for i, char := range s { + if i > 0 && char >= 'A' && char <= 'Z' { + // 在大写字母前添加下划线 + result.WriteRune('_') + result.WriteRune(char - 'A' + 'a') // 转小写 + } else { + result.WriteRune(char) + } + } + + return strings.ToLower(result.String()) +} + // RandomString 随机字符串 func RandomString(n int) string { var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") diff --git a/utils/jwt.go b/utils/jwt.go index 096e2d1..c4e45d5 100644 --- a/utils/jwt.go +++ b/utils/jwt.go @@ -1,6 +1,7 @@ package utils import ( + "context" "errors" "time" @@ -85,3 +86,20 @@ func (j *JWT) ParseToken(tokenString string) (*request.CustomClaims, error) { } return nil, TokenValid } + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetRedisJWT +//@description: jwt存入redis并设置过期时间 +//@param: jwt string, userName string +//@return: err error + +func SetRedisJWT(jwt string, userName string) (err error) { + // 此处过期时间等于jwt过期时间 + dr, err := ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + if err != nil { + return err + } + timer := dr + err = global.GVA_REDIS.Set(context.Background(), userName, jwt, timer).Err() + return err +} diff --git a/utils/system_events.go b/utils/system_events.go new file mode 100644 index 0000000..126d85b --- /dev/null +++ b/utils/system_events.go @@ -0,0 +1,34 @@ +package utils + +import ( + "sync" +) + +// SystemEvents 定义系统级事件处理 +type SystemEvents struct { + reloadHandlers []func() error + mu sync.RWMutex +} + +// 全局事件管理器 +var GlobalSystemEvents = &SystemEvents{} + +// RegisterReloadHandler 注册系统重载处理函数 +func (e *SystemEvents) RegisterReloadHandler(handler func() error) { + e.mu.Lock() + defer e.mu.Unlock() + e.reloadHandlers = append(e.reloadHandlers, handler) +} + +// TriggerReload 触发所有注册的重载处理函数 +func (e *SystemEvents) TriggerReload() error { + e.mu.RLock() + defer e.mu.RUnlock() + + for _, handler := range e.reloadHandlers { + if err := handler(); err != nil { + return err + } + } + return nil +} diff --git a/utils/verify.go b/utils/verify.go index 43a8672..cc2cb78 100644 --- a/utils/verify.go +++ b/utils/verify.go @@ -5,7 +5,7 @@ var ( ApiVerify = Rules{"Path": {NotEmpty()}, "Description": {NotEmpty()}, "ApiGroup": {NotEmpty()}, "Method": {NotEmpty()}} MenuVerify = Rules{"Path": {NotEmpty()}, "Name": {NotEmpty()}, "Component": {NotEmpty()}, "Sort": {Ge("0")}} MenuMetaVerify = Rules{"Title": {NotEmpty()}} - LoginVerify = Rules{"CaptchaId": {NotEmpty()}, "Username": {NotEmpty()}, "Password": {NotEmpty()}} + LoginVerify = Rules{"Username": {NotEmpty()}, "Password": {NotEmpty()}} RegisterVerify = Rules{"Username": {NotEmpty()}, "NickName": {NotEmpty()}, "Password": {NotEmpty()}, "AuthorityId": {NotEmpty()}} PageInfoVerify = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}} CustomerVerify = Rules{"CustomerName": {NotEmpty()}, "CustomerPhoneData": {NotEmpty()}}