Gin-vue-admin has arbitrary file upload vulnerability caused by path traversal
Description
Gin-vue-admin is a backstage management system based on vue and gin. Gin-vue-admin <= v2.8.7 has a path traversal vulnerability in the breakpoint resume upload functionality. Attacker can upload any files on any directory. In the breakpoint_continue.go file, the MakeFile function accepts a fileName parameter through the /fileUploadAndDownload/breakpointContinueFinish API endpoint and directly concatenates it with the base directory path (./fileDir/) using os.OpenFile() without any validation for directory traversal sequences (e.g., ../). An attacker with file upload privileges could exploit this vulnerability.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/flipped-aurora/gin-vue-adminGo | <= 2.8.7 | — |
Affected products
1- Range: V2.4.4, V2.5.0b, v0.9.0, …
Patches
12242f5d6e133public: 发布v2.8.8 (#2167)
57 files changed · +1310 −1094
server/api/v1/system/auto_code_plugin.go+24 −0 modified@@ -117,3 +117,27 @@ func (a *AutoCodePluginApi) InitAPI(c *gin.Context) { } response.OkWithMessage("文件变更成功", c) } + +// InitDictionary +// @Tags AutoCodePlugin +// @Summary 打包插件 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Success 200 {object} response.Response{data=map[string]interface{},msg=string} "打包插件成功" +// @Router /autoCode/initDictionary [post] +func (a *AutoCodePluginApi) InitDictionary(c *gin.Context) { + var dictInfo request.InitDictionary + err := c.ShouldBindJSON(&dictInfo) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + err = autoCodePluginService.InitDictionary(dictInfo) + if err != nil { + global.GVA_LOG.Error("创建初始化Dictionary失败!", zap.Error(err)) + response.FailWithMessage("创建初始化Dictionary失败"+err.Error(), c) + return + } + response.OkWithMessage("文件变更成功", c) +}
server/core/internal/zap_core.go+72 −76 modified@@ -1,18 +1,18 @@ package internal import ( - "context" - "fmt" - "github.com/flipped-aurora/gin-vue-admin/server/global" - "github.com/flipped-aurora/gin-vue-admin/server/model/system" - "github.com/flipped-aurora/gin-vue-admin/server/service" - astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" - "github.com/flipped-aurora/gin-vue-admin/server/utils/stacktrace" - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "os" - "strings" - "time" + "context" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service" + astutil "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + "github.com/flipped-aurora/gin-vue-admin/server/utils/stacktrace" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "strings" + "time" ) type ZapCore struct { @@ -61,75 +61,71 @@ func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapco } func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { - for i := 0; i < len(fields); i++ { - if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" { - syncer := z.WriteSyncer(fields[i].String) - z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level) - } - } - // 先写入原日志目标 - err := z.Core.Write(entry, fields) + for i := 0; i < len(fields); i++ { + if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" { + syncer := z.WriteSyncer(fields[i].String) + z.Core = zapcore.NewCore(global.GVA_CONFIG.Zap.Encoder(), syncer, z.level) + } + } + // 先写入原日志目标 + err := z.Core.Write(entry, fields) - // 捕捉 Error 及以上级别日志并入库,且可提取 zap.Error(err) 的错误内容 - if entry.Level >= zapcore.ErrorLevel { - // 避免与 GORM zap 写入互相递归:跳过由 gorm logger writer 触发的日志 - if strings.Contains(entry.Caller.File, "gorm_logger_writer.go") { - return err - } - // 避免重复记录 panic 恢复日志,panic 由 GinRecovery 单独捕捉入库 - if strings.Contains(entry.Message, "[Recovery from panic]") { - return err - } + // 捕捉 Error 及以上级别日志并入库,且可提取 zap.Error(err) 的错误内容 + if entry.Level >= zapcore.ErrorLevel { + // 避免与 GORM zap 写入互相递归:跳过由 gorm logger writer 触发的日志 + if strings.Contains(entry.Caller.File, "gorm_logger_writer.go") { + return err + } - form := "后端" - level := entry.Level.String() - // 生成基础信息 - info := entry.Message + form := "后端" + level := entry.Level.String() + // 生成基础信息 + info := entry.Message - // 提取 zap.Error(err) 内容 - var errStr string - for i := 0; i < len(fields); i++ { - f := fields[i] - if f.Type == zapcore.ErrorType || f.Key == "error" || f.Key == "err" { - if f.Interface != nil { - errStr = fmt.Sprintf("%v", f.Interface) - } else if f.String != "" { - errStr = f.String - } - break - } - } - if errStr != "" { - info = fmt.Sprintf("%s | 错误: %s", info, errStr) - } + // 提取 zap.Error(err) 内容 + var errStr string + for i := 0; i < len(fields); i++ { + f := fields[i] + if f.Type == zapcore.ErrorType || f.Key == "error" || f.Key == "err" { + if f.Interface != nil { + errStr = fmt.Sprintf("%v", f.Interface) + } else if f.String != "" { + errStr = f.String + } + break + } + } + if errStr != "" { + info = fmt.Sprintf("%s | 错误: %s", info, errStr) + } - // 附加来源与堆栈信息 - if entry.Caller.File != "" { - info = fmt.Sprintf("%s \n 源文件:%s:%d", info, entry.Caller.File, entry.Caller.Line) - } - stack := entry.Stack - if stack != "" { - info = fmt.Sprintf("%s \n 调用栈:%s", info, stack) - // 解析最终业务调用方,并提取其方法源码 - if frame, ok := stacktrace.FindFinalCaller(stack); ok { - fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line) - if exErr == nil { - info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc) - } else { - info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr) - } - } - } + // 附加来源与堆栈信息 + if entry.Caller.File != "" { + info = fmt.Sprintf("%s \n 源文件:%s:%d", info, entry.Caller.File, entry.Caller.Line) + } + stack := entry.Stack + if stack != "" { + info = fmt.Sprintf("%s \n 调用栈:%s", info, stack) + // 解析最终业务调用方,并提取其方法源码 + if frame, ok := stacktrace.FindFinalCaller(stack); ok { + fnName, fnSrc, sLine, eLine, exErr := astutil.ExtractFuncSourceByPosition(frame.File, frame.Line) + if exErr == nil { + info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s lines %d-%d)\n----- 产生日志的方法代码如下 -----\n%s", info, frame.File, frame.Line, fnName, sLine, eLine, fnSrc) + } else { + info = fmt.Sprintf("%s \n 最终调用方法:%s:%d (%s) | extract_err=%v", info, frame.File, frame.Line, fnName, exErr) + } + } + } - // 使用后台上下文,避免依赖 gin.Context - ctx := context.Background() - _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(ctx, &system.SysError{ - Form: &form, - Info: &info, - Level: level, - }) - } - return err + // 使用后台上下文,避免依赖 gin.Context + ctx := context.Background() + _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(ctx, &system.SysError{ + Form: &form, + Info: &info, + Level: level, + }) + } + return err } func (z *ZapCore) Sync() error {
server/core/server.go+1 −1 modified@@ -47,7 +47,7 @@ func RunServer() { --------------------------------------版权声明-------------------------------------- ** 版权所有方:flipped-aurora开源团队 ** ** 版权持有公司:北京翻转极光科技有限责任公司 ** - ** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html ** + ** 剔除授权标识需购买商用授权:https://plugin.gin-vue-admin.com/license ** ** 感谢您对Gin-Vue-Admin的支持与关注 合法授权使用更有利于项目的长久发展** `, global.Version, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath) initServer(address, Router, 10*time.Minute, 10*time.Minute)
server/global/version.go+1 −1 modified@@ -4,7 +4,7 @@ package global // 目前只有Version正式使用 其余为预留 const ( // Version 当前版本号 - Version = "v2.8.7" + Version = "v2.8.8" // AppName 应用名称 AppName = "Gin-Vue-Admin" // Description 应用描述
server/go.mod+0 −12 modified@@ -20,7 +20,6 @@ require ( github.com/gookit/color v1.5.4 github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible - github.com/localrivet/gomcp v1.7.2 github.com/mark3labs/mcp-go v0.41.1 github.com/mholt/archives v0.1.1 github.com/minio/minio-go/v7 v7.0.84 @@ -78,7 +77,6 @@ require ( github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/eclipse/paho.mqtt.golang v1.5.0 // indirect github.com/emirpasic/gods v1.12.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gammazero/toposort v0.1.1 // indirect @@ -93,16 +91,12 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.24.0 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.4.0 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect @@ -121,7 +115,6 @@ require ( github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/localrivet/wilduri v0.0.0-20250504021349-6ce732e97cca // indirect github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect github.com/magiconair/properties v1.8.9 // indirect github.com/mailru/easyjson v0.9.0 // indirect @@ -135,9 +128,6 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/mozillazg/go-httpheader v0.4.0 // indirect - github.com/nats-io/nats.go v1.42.0 // indirect - github.com/nats-io/nkeys v0.4.11 // indirect - github.com/nats-io/nuid v1.0.1 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nwaples/rardecode/v2 v2.1.0 // indirect github.com/otiai10/mint v1.6.3 // indirect @@ -186,8 +176,6 @@ require ( golang.org/x/sys v0.32.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.29.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect - google.golang.org/grpc v1.72.1 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
server/go.sum+0 −42 modified@@ -113,8 +113,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dzwvip/gorm-oracle v0.1.2 h1:811aFDY7oDfKWHc0Z0lHdXzzr89EmKBSwc/jLJ8GU5g= github.com/dzwvip/gorm-oracle v0.1.2/go.mod h1:TbF7idnO9UgGpJ0qJpDZby1/wGquzP5GYof88ScBITE= -github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= -github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -141,10 +139,6 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= @@ -174,12 +168,6 @@ github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= -github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= @@ -210,8 +198,6 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -248,8 +234,6 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -331,10 +315,6 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/localrivet/gomcp v1.7.2 h1:dJtKCvbI8Gr/L0N7cZlo3XOMyCc7GCahdtbI/Y/K9Ig= -github.com/localrivet/gomcp v1.7.2/go.mod h1:7MBYbqypfmEzDuLWdz2FSkAeX19ZX9cSe6qD6mZgOEc= -github.com/localrivet/wilduri v0.0.0-20250504021349-6ce732e97cca h1:q0KYRv+ktfm8KnMROXcRNJEnfXSI3NZ45aMC8T/mg14= -github.com/localrivet/wilduri v0.0.0-20250504021349-6ce732e97cca/go.mod h1:8B25VIq6WUPYAdY3aodQnj/hDNmYTcPgzzc7ZZ1++NI= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= @@ -378,12 +358,6 @@ github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= github.com/mozillazg/go-httpheader v0.4.0 h1:aBn6aRXtFzyDLZ4VIRLsZbbJloagQfMnCiYgOq6hK4w= github.com/mozillazg/go-httpheader v0.4.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA= -github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM= -github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= -github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= -github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= -github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= @@ -537,18 +511,6 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= -go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= -go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= -go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= -go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= -go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= -go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= -go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= -go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= -go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -801,17 +763,13 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA= -google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
server/main.go+1 −1 modified@@ -21,7 +21,7 @@ import ( // @Tag.Description 用户 // @title Gin-Vue-Admin Swagger API接口文档 -// @version v2.8.7 +// @version v2.8.8 // @description 使用gin+vue进行极速开发的全栈开发基础平台 // @securityDefinitions.apikey ApiKeyAuth // @in header
server/mcp/dictionary_generator.go+0 −87 modified@@ -70,93 +70,6 @@ func (d *DictionaryOptionsGenerator) New() mcp.Tool { ) } -// 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) { // 解析请求参数
server/mcp/gva_execute.go+72 −192 modified@@ -61,139 +61,16 @@ func (g *GVAExecutor) New() mcp.Tool { mcp.WithDescription(`**GVA代码生成执行器:直接执行代码生成,无需确认步骤** **核心功能:** -- 根据需求分析和当前的包信息判断是否调用,如果需要调用,则根据入参描述生成json,用于直接生成代码 -- 支持批量创建多个模块 -- 自动创建包、模块、字典等 -- 移除了确认步骤,提高执行效率 +根据需求分析和当前的包信息判断是否调用,直接生成代码。支持批量创建多个模块、自动创建包、模块、字典等。 **使用场景:** -- 在gva_analyze获取了当前的包信息和字典信息之后,如果已经包含了可以使用的包和模块,那就不要调用本mcp -- 根据分析结果直接生成代码 -- 适用于自动化代码生成流程 - -**批量创建功能:** -- 支持在单个ExecutionPlan中创建多个模块 -- modulesInfo字段为数组,可包含多个模块配置 -- 一次性处理多个模块的创建和字典生成 - -**新功能:自动字典创建** -- 当结构体字段使用了字典类型(dictType不为空)时,系统会自动检查字典是否存在 -- 如果字典不存在,会自动创建对应的字典及默认的字典详情项 -- 字典创建包括:字典主表记录和默认的选项值(选项1、选项2等) - -**重要限制:** -- 当needCreatedModules=true时,模块创建会自动生成API和菜单,因此不应再调用api_creator和menu_creator工具 -- 只有在单独创建API或菜单(不涉及模块创建)时才使用api_creator和menu_creator工具 - -重要:ExecutionPlan结构体格式要求(支持批量创建): -{ - "packageName": "包名(string)", - "packageType": "package或plugin(string),如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package。", - "needCreatedPackage": "是否需要创建包(bool)", - "needCreatedModules": "是否需要创建模块(bool)", - "needCreatedDictionaries": "是否需要创建字典(bool)", - "packageInfo": { - "desc": "描述(string)", - "label": "展示名(string)", - "template": "package或plugin(string),如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package。", - "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,默认为false)", - "autoCreateApiToSql": "是否创建API(bool,默认为true)", - "autoCreateMenuToSql": "是否创建菜单(bool,默认为true)", - "autoCreateBtnAuth": "是否创建按钮权限(bool,默认为false)", - "onlyTemplate": "是否仅模板(bool,默认为false)", - "isTree": "是否树形结构(bool,默认为false)", - "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)", - "dataTypeLong": "数据长度(string)", - "comment": "注释(string)", - "columnName": "数据库列名(string)", - "fieldSearchType": "搜索类型:=/>/</>=/<=/NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN等(string)", - "fieldSearchHide": "是否隐藏搜索(bool)", - "dictType": "字典类型(string)", - "form": "表单显示(bool)", - "table": "表格显示(bool)", - "desc": "详情显示(bool)", - "excel": "导入导出(bool)", - "require": "是否必填(bool)", - "defaultValue": "默认值(string)", - "errorText": "错误提示(string)", - "clearable": "是否可清空(bool)", - "sort": "是否排序(bool)", - "primaryKey": "是否主键(bool)", - "dataSource": "数据源配置(object) - 用于配置字段的关联表信息,结构:{\"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)", - "...": "更多模块配置..." - }], - "dictionariesInfo":[{ - "dictType": "字典类型(string) - 用于标识字典的唯一性", - "dictName": "字典名称(string) - 必须生成,字典的中文名称", - "description": "字典描述(string) - 字典的用途说明", - "status": "字典状态(bool) - true启用,false禁用", - "fieldDesc": "字段描述(string) - 用于AI理解字段含义并生成合适的选项", - "options": [{ - "label": "显示名称(string) - 用户看到的选项名", - "value": "选项值(string) - 实际存储的值", - "sort": "排序号(int) - 数字越小越靠前" - }] - }] -} +在gva_analyze获取了当前的包信息和字典信息之后,如果已经包含了可以使用的包和模块,那就不要调用本mcp。根据分析结果直接生成代码,适用于自动化代码生成流程。 -注意: -1. needCreatedPackage=true时packageInfo必需 -2. needCreatedModules=true时modulesInfo必需 -3. needCreatedDictionaries=true时dictionariesInfo必需 -4. dictionariesInfo中的options字段可选,如果不提供将根据fieldDesc自动生成默认选项 -5. 字典创建会在模块创建之前执行,确保模块字段可以正确引用字典类型 -6. packageType只能是"package"或"plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package。" -7. 字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组) -8. 搜索类型支持:=,!=,>,>=,<,<=,NOT BETWEEN/LIKE/BETWEEN/IN/NOT IN -9. gvaModel=true时自动包含ID,CreatedAt,UpdatedAt,DeletedAt字段 -10. **重要**:当gvaModel=false时,必须有一个字段的primaryKey=true,否则会导致PrimaryField为nil错误 -11. **重要**:当gvaModel=true时,系统会自动设置ID字段为主键,无需手动设置primaryKey=true -12. 智能字典创建功能:当字段使用字典类型(DictType)时,系统会: - - 自动检查字典是否存在,如果不存在则创建字典 - - 根据字典类型和字段描述智能生成默认选项,支持状态、性别、类型、等级、优先级、审批、角色、布尔值、订单、颜色、尺寸等常见场景 - - 为无法识别的字典类型提供通用默认选项 -13. **模块关联配置**:当需要配置模块间的关联关系时,使用dataSource字段: - - **dbName**: 关联的数据库名称 - - **table**: 关联的表名 - - **label**: 用于显示的字段名(如name、title等) - - **value**: 用于存储的值字段名(通常是id) - - **association**: 关联关系类型(1=一对一关联,2=一对多关联)一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个,则选用一对一,如果他需要关联多个他的关联实体,则选用一对多。 - - **hasDeletedAt**: 关联表是否有软删除字段 - - **checkDataSource**: 设为true时会验证关联表的存在性 - - 示例:{"dbName":"","table":"sys_users","label":"username","value":"id","association":1,"hasDeletedAt":true} -14. **自动字段类型修正**:系统会自动检查和修正字段类型: - - 当字段配置了dataSource且association=2(一对多关联)时,系统会自动将fieldType修改为'array' - - 这确保了一对多关联数据的正确存储和处理 - - 修正操作会记录在日志中,便于开发者了解变更情况`), +**重要提示:** +- 当needCreatedModules=true时,模块创建会自动生成API和菜单,不应再调用api_creator和menu_creator工具 +- 字段使用字典类型时,系统会自动检查并创建字典 +- 字典创建会在模块创建之前执行 +- 当字段配置了dataSource且association=2(一对多关联)时,系统会自动将fieldType修改为'array'`), mcp.WithObject("executionPlan", mcp.Description("执行计划,包含包信息、模块与字典信息"), mcp.Required(), @@ -204,95 +81,97 @@ func (g *GVAExecutor) New() mcp.Tool { }, "packageType": map[string]interface{}{ "type": "string", - "description": "package 或 plugin", + "description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package", "enum": []string{"package", "plugin"}, }, "needCreatedPackage": map[string]interface{}{ "type": "boolean", - "description": "是否需要创建包", + "description": "是否需要创建包,为true时packageInfo必需", }, "needCreatedModules": map[string]interface{}{ "type": "boolean", - "description": "是否需要创建模块", + "description": "是否需要创建模块,为true时modulesInfo必需", }, "needCreatedDictionaries": map[string]interface{}{ "type": "boolean", - "description": "是否需要创建字典", + "description": "是否需要创建字典,为true时dictionariesInfo必需", }, "packageInfo": map[string]interface{}{ "type": "object", - "description": "包创建信息", + "description": "包创建信息,当needCreatedPackage=true时必需", "properties": map[string]interface{}{ "desc": map[string]interface{}{"type": "string", "description": "包描述"}, "label": map[string]interface{}{"type": "string", "description": "展示名"}, - "template": map[string]interface{}{"type": "string", "description": "package 或 plugin", "enum": []string{"package", "plugin"}}, + "template": map[string]interface{}{"type": "string", "description": "package 或 plugin,如果用户提到了使用插件则创建plugin,如果用户没有特定说明则一律选用package", "enum": []string{"package", "plugin"}}, "packageName": map[string]interface{}{"type": "string", "description": "包名"}, }, }, "modulesInfo": map[string]interface{}{ "type": "array", - "description": "模块配置列表", + "description": "模块配置列表,支持批量创建多个模块", "items": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "package": map[string]interface{}{"type": "string", "description": "包名(小写开头)"}, - "tableName": map[string]interface{}{"type": "string", "description": "数据库表名(蛇形命名)"}, - "businessDB": map[string]interface{}{"type": "string", "description": "业务数据库(可留空表示默认)"}, + "tableName": map[string]interface{}{"type": "string", "description": "数据库表名(蛇形命名法)"}, + "businessDB": map[string]interface{}{"type": "string", "description": "业务数据库(可留空表示默认)"}, "structName": map[string]interface{}{"type": "string", "description": "结构体名(大驼峰)"}, "packageName": map[string]interface{}{"type": "string", "description": "文件名称"}, "description": map[string]interface{}{"type": "string", "description": "中文描述"}, "abbreviation": map[string]interface{}{"type": "string", "description": "简称"}, - "humpPackageName": map[string]interface{}{"type": "string", "description": "文件名称(小驼峰)"}, - "gvaModel": map[string]interface{}{"type": "boolean", "description": "是否使用GVA模型(固定为true)"}, - "autoMigrate": map[string]interface{}{"type": "boolean"}, - "autoCreateResource": map[string]interface{}{"type": "boolean"}, - "autoCreateApiToSql": map[string]interface{}{"type": "boolean"}, - "autoCreateMenuToSql": map[string]interface{}{"type": "boolean"}, - "autoCreateBtnAuth": map[string]interface{}{"type": "boolean"}, - "onlyTemplate": map[string]interface{}{"type": "boolean"}, - "isTree": map[string]interface{}{"type": "boolean"}, - "treeJson": map[string]interface{}{"type": "string"}, - "isAdd": map[string]interface{}{"type": "boolean"}, - "generateWeb": map[string]interface{}{"type": "boolean"}, - "generateServer": map[string]interface{}{"type": "boolean"}, + "humpPackageName": map[string]interface{}{"type": "string", "description": "文件名称(小驼峰),一般是结构体名的小驼峰"}, + "gvaModel": map[string]interface{}{"type": "boolean", "description": "是否使用GVA模型(固定为true),自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段"}, + "autoMigrate": map[string]interface{}{"type": "boolean", "description": "是否自动迁移数据库"}, + "autoCreateResource": map[string]interface{}{"type": "boolean", "description": "是否创建资源(默认为false)"}, + "autoCreateApiToSql": map[string]interface{}{"type": "boolean", "description": "是否创建API(默认为true)"}, + "autoCreateMenuToSql": map[string]interface{}{"type": "boolean", "description": "是否创建菜单(默认为true)"}, + "autoCreateBtnAuth": map[string]interface{}{"type": "boolean", "description": "是否创建按钮权限(默认为false)"}, + "onlyTemplate": map[string]interface{}{"type": "boolean", "description": "是否仅模板(默认为false)"}, + "isTree": map[string]interface{}{"type": "boolean", "description": "是否树形结构(默认为false)"}, + "treeJson": map[string]interface{}{"type": "string", "description": "树形JSON字段"}, + "isAdd": map[string]interface{}{"type": "boolean", "description": "是否新增(固定为false)"}, + "generateWeb": map[string]interface{}{"type": "boolean", "description": "是否生成前端代码"}, + "generateServer": map[string]interface{}{"type": "boolean", "description": "是否生成后端代码"}, "fields": map[string]interface{}{ - "type": "array", + "type": "array", + "description": "字段列表", "items": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ - "fieldName": map[string]interface{}{"type": "string"}, - "fieldDesc": map[string]interface{}{"type": "string"}, - "fieldType": map[string]interface{}{"type": "string"}, - "fieldJson": map[string]interface{}{"type": "string"}, - "dataTypeLong": map[string]interface{}{"type": "string"}, - "comment": map[string]interface{}{"type": "string"}, - "columnName": map[string]interface{}{"type": "string"}, - "fieldSearchType": map[string]interface{}{"type": "string"}, - "fieldSearchHide": map[string]interface{}{"type": "boolean"}, - "dictType": map[string]interface{}{"type": "string"}, - "form": map[string]interface{}{"type": "boolean"}, - "table": map[string]interface{}{"type": "boolean"}, - "desc": map[string]interface{}{"type": "boolean"}, - "excel": map[string]interface{}{"type": "boolean"}, - "require": map[string]interface{}{"type": "boolean"}, - "defaultValue": map[string]interface{}{"type": "string"}, - "errorText": map[string]interface{}{"type": "string"}, - "clearable": map[string]interface{}{"type": "boolean"}, - "sort": map[string]interface{}{"type": "boolean"}, - "primaryKey": map[string]interface{}{"type": "boolean"}, + "fieldName": map[string]interface{}{"type": "string", "description": "字段名(必须大写开头)"}, + "fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述"}, + "fieldType": map[string]interface{}{"type": "string", "description": "字段类型:string(字符串)、richtext(富文本)、int(整型)、bool(布尔值)、float64(浮点型)、time.Time(时间)、enum(枚举)、picture(单图片)、pictures(多图片)、video(视频)、file(文件)、json(JSON)、array(数组)"}, + "fieldJson": map[string]interface{}{"type": "string", "description": "JSON标签"}, + "dataTypeLong": map[string]interface{}{"type": "string", "description": "数据长度"}, + "comment": map[string]interface{}{"type": "string", "description": "注释"}, + "columnName": map[string]interface{}{"type": "string", "description": "数据库列名"}, + "fieldSearchType": map[string]interface{}{"type": "string", "description": "搜索类型:=、!=、>、>=、<、<=、LIKE、BETWEEN、IN、NOT IN、NOT BETWEEN"}, + "fieldSearchHide": map[string]interface{}{"type": "boolean", "description": "是否隐藏搜索"}, + "dictType": map[string]interface{}{"type": "string", "description": "字典类型,使用字典类型时系统会自动检查并创建字典"}, + "form": map[string]interface{}{"type": "boolean", "description": "表单显示"}, + "table": map[string]interface{}{"type": "boolean", "description": "表格显示"}, + "desc": map[string]interface{}{"type": "boolean", "description": "详情显示"}, + "excel": map[string]interface{}{"type": "boolean", "description": "导入导出"}, + "require": map[string]interface{}{"type": "boolean", "description": "是否必填"}, + "defaultValue": map[string]interface{}{"type": "string", "description": "默认值"}, + "errorText": map[string]interface{}{"type": "string", "description": "错误提示"}, + "clearable": map[string]interface{}{"type": "boolean", "description": "是否可清空"}, + "sort": map[string]interface{}{"type": "boolean", "description": "是否排序"}, + "primaryKey": map[string]interface{}{"type": "boolean", "description": "是否主键(gvaModel=false时必须有一个字段为true)"}, "dataSource": map[string]interface{}{ - "type": "object", + "type": "object", + "description": "数据源配置,用于配置字段的关联表信息。获取表名提示:可在 server/model 和 plugin/xxx/model 目录下查看对应模块的 TableName() 接口实现获取实际表名(如 SysUser 的表名为 sys_users)。获取数据库名提示:主数据库通常使用 gva(默认数据库标识),多数据库可在 config.yaml 的 db-list 配置中查看可用数据库的 alias-name 字段,如果用户未提及关联多数据库信息则使用默认数据库,默认数据库的情况下 dbName填写为空", "properties": map[string]interface{}{ - "dbName": map[string]interface{}{"type": "string"}, - "table": map[string]interface{}{"type": "string"}, - "label": map[string]interface{}{"type": "string"}, - "value": map[string]interface{}{"type": "string"}, - "association": map[string]interface{}{"type": "integer"}, - "hasDeletedAt": map[string]interface{}{"type": "boolean"}, + "dbName": map[string]interface{}{"type": "string", "description": "关联的数据库名称(默认数据库留空)"}, + "table": map[string]interface{}{"type": "string", "description": "关联的表名"}, + "label": map[string]interface{}{"type": "string", "description": "用于显示的字段名(如name、title等)"}, + "value": map[string]interface{}{"type": "string", "description": "用于存储的值字段名(通常是id)"}, + "association": map[string]interface{}{"type": "integer", "description": "关联关系类型:1=一对一关联,2=一对多关联。一对一和一对多的前面的一是当前的实体,如果他只能关联另一个实体的一个则选用一对一,如果他需要关联多个他的关联实体则选用一对多"}, + "hasDeletedAt": map[string]interface{}{"type": "boolean", "description": "关联表是否有软删除字段"}, }, }, - "checkDataSource": map[string]interface{}{"type": "boolean"}, - "fieldIndexType": map[string]interface{}{"type": "string"}, + "checkDataSource": map[string]interface{}{"type": "boolean", "description": "是否检查数据源,启用后会验证关联表的存在性"}, + "fieldIndexType": map[string]interface{}{"type": "string", "description": "索引类型"}, }, }, }, @@ -306,23 +185,24 @@ func (g *GVAExecutor) New() mcp.Tool { }, "dictionariesInfo": map[string]interface{}{ "type": "array", - "description": "字典创建信息", + "description": "字典创建信息,字典创建会在模块创建之前执行", "items": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ - "dictType": map[string]interface{}{"type": "string"}, - "dictName": map[string]interface{}{"type": "string"}, - "description": map[string]interface{}{"type": "string"}, - "status": map[string]interface{}{"type": "boolean"}, - "fieldDesc": map[string]interface{}{"type": "string"}, + "dictType": map[string]interface{}{"type": "string", "description": "字典类型,用于标识字典的唯一性"}, + "dictName": map[string]interface{}{"type": "string", "description": "字典名称,必须生成,字典的中文名称"}, + "description": map[string]interface{}{"type": "string", "description": "字典描述,字典的用途说明"}, + "status": map[string]interface{}{"type": "boolean", "description": "字典状态:true启用,false禁用"}, + "fieldDesc": map[string]interface{}{"type": "string", "description": "字段描述,用于AI理解字段含义并生成合适的选项"}, "options": map[string]interface{}{ - "type": "array", + "type": "array", + "description": "字典选项列表(可选,如果不提供将根据fieldDesc自动生成默认选项)", "items": map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ - "label": map[string]interface{}{"type": "string"}, - "value": map[string]interface{}{"type": "string"}, - "sort": map[string]interface{}{"type": "integer"}, + "label": map[string]interface{}{"type": "string", "description": "显示名称,用户看到的选项名"}, + "value": map[string]interface{}{"type": "string", "description": "选项值,实际存储的值"}, + "sort": map[string]interface{}{"type": "integer", "description": "排序号,数字越小越靠前"}, }, }, },
server/middleware/error.go+26 −7 modified@@ -1,14 +1,18 @@ package middleware import ( - "net" - "net/http" - "net/http/httputil" - "os" - "runtime/debug" - "strings" + "context" + "fmt" + "net" + "net/http" + "net/http/httputil" + "os" + "runtime/debug" + "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/service" "github.com/gin-gonic/gin" "go.uber.org/zap" ) @@ -42,12 +46,27 @@ func GinRecovery(stack bool) gin.HandlerFunc { } if stack { + form := "后端" + info := fmt.Sprintf("Panic: %v\nRequest: %s\nStack: %s", err, string(httpRequest), string(debug.Stack())) + level := "error" + _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(context.Background(), &system.SysError{ + Form: &form, + Info: &info, + Level: level, + }) global.GVA_LOG.Error("[Recovery from panic]", zap.Any("error", err), zap.String("request", string(httpRequest)), - zap.String("stack", string(debug.Stack())), ) } else { + form := "后端" + info := fmt.Sprintf("Panic: %v\nRequest: %s", err, string(httpRequest)) + level := "error" + _ = service.ServiceGroupApp.SystemServiceGroup.SysErrorService.CreateSysError(context.Background(), &system.SysError{ + Form: &form, + Info: &info, + Level: level, + }) global.GVA_LOG.Error("[Recovery from panic]", zap.Any("error", err), zap.String("request", string(httpRequest)),
server/model/system/request/sys_auto_code.go+5 −0 modified@@ -280,6 +280,11 @@ type InitApi struct { APIs []uint `json:"apis"` } +type InitDictionary struct { + PlugName string `json:"plugName"` + Dictionaries []uint `json:"dictionaries"` +} + type LLMAutoCode struct { Prompt string `json:"prompt" form:"prompt" gorm:"column:prompt;comment:提示语;type:text;"` //提示语 Mode string `json:"mode" form:"mode" gorm:"column:mode;comment:模式;type:text;"` //模式
server/model/system/sys_export_template.go+7 −5 modified@@ -8,11 +8,13 @@ import ( // 导出模板 结构体 SysExportTemplate type SysExportTemplate struct { global.GVA_MODEL - DBName string `json:"dbName" form:"dbName" gorm:"column:db_name;comment:数据库名称;"` //数据库名称 - Name string `json:"name" form:"name" gorm:"column:name;comment:模板名称;"` //模板名称 - TableName string `json:"tableName" form:"tableName" gorm:"column:table_name;comment:表名称;"` //表名称 - TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识;"` //模板标识 - TemplateInfo string `json:"templateInfo" form:"templateInfo" gorm:"column:template_info;type:text;"` //模板信息 + DBName string `json:"dbName" form:"dbName" gorm:"column:db_name;comment:数据库名称;"` //数据库名称 + Name string `json:"name" form:"name" gorm:"column:name;comment:模板名称;"` //模板名称 + TableName string `json:"tableName" form:"tableName" gorm:"column:table_name;comment:表名称;"` //表名称 + TemplateID string `json:"templateID" form:"templateID" gorm:"column:template_id;comment:模板标识;"` //模板标识 + TemplateInfo string `json:"templateInfo" form:"templateInfo" gorm:"column:template_info;type:text;"` //模板信息 + SQL string `json:"sql" form:"sql" gorm:"column:sql;type:text;comment:自定义导出SQL;"` //自定义导出SQL + ImportSQL string `json:"importSql" form:"importSql" gorm:"column:import_sql;type:text;comment:自定义导入SQL;"` //自定义导入SQL Limit *int `json:"limit" form:"limit" gorm:"column:limit;comment:导出限制"` Order string `json:"order" form:"order" gorm:"column:order;comment:排序"` Conditions []Condition `json:"conditions" form:"conditions" gorm:"foreignKey:TemplateID;references:TemplateID;comment:条件"`
server/plugin/announcement/initialize/dictionary.go+12 −0 added@@ -0,0 +1,12 @@ +package initialize + +import ( + "context" + model "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/plugin-tool/utils" +) + +func Dictionary(ctx context.Context) { + entities := []model.SysDictionary{} + utils.RegisterDictionaries(entities...) +}
server/plugin/announcement/plugin.go+3 −1 modified@@ -19,8 +19,10 @@ func (p *plugin) Register(group *gin.Engine) { // initialize.Viper() // 安装插件时候自动注册的api数据请到下方法.Api方法中实现 initialize.Api(ctx) - // 安装插件时候自动注册的api数据请到下方法.Menu方法中实现 + // 安装插件时候自动注册的Menu数据请到下方法.Menu方法中实现 initialize.Menu(ctx) + // 安装插件时候自动注册的Dictionary数据请到下方法.Dictionary方法中实现 + initialize.Dictionary(ctx) initialize.Gorm(ctx) initialize.Router(group) }
server/plugin/plugin-tool/utils/check.go+32 −2 modified@@ -9,7 +9,7 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/model/system" ) -func RegisterApis( apis ...system.SysApi) { +func RegisterApis(apis ...system.SysApi) { err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { for _, api := range apis { err := tx.Model(system.SysApi{}).Where("path = ? AND method = ? AND api_group = ? ", api.Path, api.Method, api.ApiGroup).FirstOrCreate(&api).Error @@ -25,7 +25,7 @@ func RegisterApis( apis ...system.SysApi) { } } -func RegisterMenus( menus ...system.SysBaseMenu) { +func RegisterMenus(menus ...system.SysBaseMenu) { parentMenu := menus[0] otherMenus := menus[1:] err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { @@ -51,3 +51,33 @@ func RegisterMenus( menus ...system.SysBaseMenu) { } } + +func RegisterDictionaries(dictionaries ...system.SysDictionary) { + err := global.GVA_DB.Transaction(func(tx *gorm.DB) error { + for _, dict := range dictionaries { + details := dict.SysDictionaryDetails + dict.SysDictionaryDetails = nil + err := tx.Model(system.SysDictionary{}).Where("type = ?", dict.Type).FirstOrCreate(&dict).Error + if err != nil { + zap.L().Error("注册字典失败", zap.Error(err), zap.String("type", dict.Type)) + return err + } + for _, detail := range details { + detail.SysDictionaryID = int(dict.ID) + err = tx.Model(system.SysDictionaryDetail{}).Where("sys_dictionary_id = ? AND value = ?", dict.ID, detail.Value).FirstOrCreate(&detail).Error + if err != nil { + zap.L().Error("注册字典详情失败", zap.Error(err), zap.String("value", detail.Value)) + return err + } + } + } + return nil + }) + if err != nil { + zap.L().Error("注册字典失败", zap.Error(err)) + } +} + +func Pointer[T any](in T) *T { + return &in +}
server/resource/plugin/server/initialize/dictionary.go.tpl+12 −0 added@@ -0,0 +1,12 @@ +package initialize + +import ( + "context" + model "{{.Module}}/model/system" + "{{.Module}}/plugin/plugin-tool/utils" +) + +func Dictionary(ctx context.Context) { + entities := []model.SysDictionary{} + utils.RegisterDictionaries(entities...) +}
server/resource/plugin/server/plugin.go.tpl+2 −0 modified@@ -19,6 +19,8 @@ type plugin struct{} // initialize.Api(ctx) // 安装插件时候自动注册的api数据请到下方法.Menu方法中实现并添加如下方法 // initialize.Menu(ctx) +// 安装插件时候自动注册的api数据请到下方法.Dictionary方法中实现并添加如下方法 +// initialize.Dictionary(ctx) func (p *plugin) Register(group *gin.Engine) { ctx := context.Background() initialize.Gorm(ctx)
server/router/system/sys_auto_code.go+3 −2 modified@@ -39,7 +39,8 @@ func (s *AutoCodeRouter) InitAutoCodeRouter(Router *gin.RouterGroup, RouterPubli } { publicAutoCodeRouter.POST("llmAuto", autoCodeApi.LLMAuto) - publicAutoCodeRouter.POST("initMenu", autoCodePluginApi.InitMenu) // 同步插件菜单 - publicAutoCodeRouter.POST("initAPI", autoCodePluginApi.InitAPI) // 同步插件API + publicAutoCodeRouter.POST("initMenu", autoCodePluginApi.InitMenu) // 同步插件菜单 + publicAutoCodeRouter.POST("initAPI", autoCodePluginApi.InitAPI) // 同步插件API + publicAutoCodeRouter.POST("initDictionary", autoCodePluginApi.InitDictionary) // 同步插件字典 } }
server/service/system/auto_code_package.go+3 −2 modified@@ -547,10 +547,11 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod router := strings.Index(threeDirs[k].Name(), "router") hasGorm := strings.Index(threeDirs[k].Name(), "gorm") response := strings.Index(threeDirs[k].Name(), "response") - if gen != -1 && api != -1 && menu != -1 && viper != -1 && plugin != -1 && config != -1 && router != -1 && hasGorm != -1 && response != -1 { + dictionary := strings.Index(threeDirs[k].Name(), "dictionary") + if gen != -1 && api != -1 && menu != -1 && viper != -1 && plugin != -1 && config != -1 && router != -1 && hasGorm != -1 && response != -1 && dictionary != -1 { return nil, nil, nil, errors.Errorf("[filpath:%s]非法模版文件!", four) } - if api != -1 || menu != -1 || viper != -1 || response != -1 || plugin != -1 || config != -1 { + if api != -1 || menu != -1 || viper != -1 || response != -1 || plugin != -1 || config != -1 || dictionary != -1 { creates[four] = filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", entity.PackageName, secondDirs[j].Name(), strings.TrimSuffix(threeDirs[k].Name(), ext)) } if gen != -1 {
server/service/system/auto_code_plugin.go+25 −0 modified@@ -264,3 +264,28 @@ func (s *autoCodePlugin) InitAPI(apiInfo request.InitApi) (err error) { os.WriteFile(apiPath, bf.Bytes(), 0666) return nil } + +func (s *autoCodePlugin) InitDictionary(dictInfo request.InitDictionary) (err error) { + dictPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "plugin", dictInfo.PlugName, "initialize", "dictionary.go") + src, err := os.ReadFile(dictPath) + if err != nil { + fmt.Println(err) + } + fileSet := token.NewFileSet() + astFile, err := parser.ParseFile(fileSet, "", src, 0) + arrayAst := ast.FindArray(astFile, "model", "SysDictionary") + var dictionaries []system.SysDictionary + err = global.GVA_DB.Preload("SysDictionaryDetails").Find(&dictionaries, "id in (?)", dictInfo.Dictionaries).Error + if err != nil { + return err + } + dictExpr := ast.CreateDictionaryStructAst(dictionaries) + arrayAst.Elts = *dictExpr + + var out []byte + bf := bytes.NewBuffer(out) + printer.Fprint(bf, fileSet, astFile) + + os.WriteFile(dictPath, bf.Bytes(), 0666) + return nil +}
server/service/system/sys_error.go+3 −0 modified@@ -14,6 +14,9 @@ type SysErrorService struct{} // CreateSysError 创建错误日志记录 // Author [yourname](https://github.com/yourname) func (sysErrorService *SysErrorService) CreateSysError(ctx context.Context, sysError *system.SysError) (err error) { + if global.GVA_DB == nil { + return nil + } err = global.GVA_DB.Create(sysError).Error return err }
server/service/system/sys_export_template.go+348 −297 modified@@ -173,129 +173,147 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID db = global.MustGetGlobalDBByDBName(template.DBName) } - if len(template.JoinTemplate) > 0 { - for _, join := range template.JoinTemplate { - db = db.Joins(join.JOINS + " " + join.Table + " ON " + join.ON) + // 如果有自定义SQL,则优先使用自定义SQL + if template.SQL != "" { + // 将 url.Values 转换为 map[string]interface{} 以支持 GORM 的命名参数 + sqlParams := make(map[string]interface{}) + for k, v := range paramsValues { + if len(v) > 0 { + sqlParams[k] = v[0] + } } - } - db = db.Select(selects).Table(template.TableName) + // 执行原生 SQL,支持 @key 命名参数 + err = db.Raw(template.SQL, sqlParams).Scan(&tableMap).Error + if err != nil { + return nil, "", err + } + } else { + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + db = db.Joins(join.JOINS + " " + join.Table + " ON " + join.ON) + } + } - filterDeleted := false + db = db.Select(selects).Table(template.TableName) - filterParam := paramsValues.Get("filterDeleted") - if filterParam == "true" { - filterDeleted = true - } + filterDeleted := false - if filterDeleted { - // 自动过滤主表的软删除 - db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", template.TableName)) + filterParam := paramsValues.Get("filterDeleted") + if filterParam == "true" { + filterDeleted = true + } - // 过滤关联表的软删除(如果有) - 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 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 := paramsValues.Get(condition.From) + if len(template.Conditions) > 0 { + for _, condition := range template.Conditions { + sql := fmt.Sprintf("%s %s ?", condition.Column, condition.Operator) + value := paramsValues.Get(condition.From) - if condition.Operator == "IN" || condition.Operator == "NOT IN" { - sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator) - } + if condition.Operator == "IN" || condition.Operator == "NOT IN" { + sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator) + } - if condition.Operator == "BETWEEN" { - sql = fmt.Sprintf("%s BETWEEN ? AND ?", condition.Column) - startValue := paramsValues.Get("start" + condition.From) - endValue := paramsValues.Get("end" + condition.From) - if startValue != "" && endValue != "" { - db = db.Where(sql, startValue, endValue) + if condition.Operator == "BETWEEN" { + sql = fmt.Sprintf("%s BETWEEN ? AND ?", condition.Column) + startValue := paramsValues.Get("start" + condition.From) + endValue := paramsValues.Get("end" + condition.From) + if startValue != "" && endValue != "" { + db = db.Where(sql, startValue, endValue) + } + continue } - continue - } - if value != "" { - if condition.Operator == "LIKE" { - value = "%" + value + "%" + if value != "" { + if condition.Operator == "LIKE" { + value = "%" + value + "%" + } + db = db.Where(sql, value) } - db = db.Where(sql, value) } } - } - // 通过参数传入limit - limit := paramsValues.Get("limit") - if limit != "" { - l, e := strconv.Atoi(limit) - if e == nil { - db = db.Limit(l) + // 通过参数传入limit + limit := paramsValues.Get("limit") + if limit != "" { + l, e := strconv.Atoi(limit) + if e == nil { + db = db.Limit(l) + } + } + // 模板的默认limit + if limit == "" && template.Limit != nil && *template.Limit != 0 { + db = db.Limit(*template.Limit) } - } - // 模板的默认limit - if limit == "" && template.Limit != nil && *template.Limit != 0 { - db = db.Limit(*template.Limit) - } - // 通过参数传入offset - offset := paramsValues.Get("offset") - if offset != "" { - o, e := strconv.Atoi(offset) - if e == nil { - db = db.Offset(o) + // 通过参数传入offset + offset := paramsValues.Get("offset") + if offset != "" { + o, e := strconv.Atoi(offset) + if e == nil { + db = db.Offset(o) + } } - } - // 获取当前表的所有字段 - table := template.TableName - orderColumns, err := db.Migrator().ColumnTypes(table) - if err != nil { - return nil, "", err - } + // 获取当前表的所有字段 + table := template.TableName + orderColumns, err := db.Migrator().ColumnTypes(table) + if err != nil { + return nil, "", err + } - // 创建一个 map 来存储字段名 - fields := make(map[string]bool) + // 创建一个 map 来存储字段名 + fields := make(map[string]bool) - for _, column := range orderColumns { - fields[column.Name()] = true - } + for _, column := range orderColumns { + fields[column.Name()] = true + } - // 通过参数传入order - order := paramsValues.Get("order") + // 通过参数传入order + order := paramsValues.Get("order") - if order == "" && template.Order != "" { - // 如果没有order入参,这里会使用模板的默认排序 - order = template.Order - } + if order == "" && template.Order != "" { + // 如果没有order入参,这里会使用模板的默认排序 + order = template.Order + } - if order != "" { - checkOrderArr := strings.Split(order, " ") - orderStr := "" - // 检查请求的排序字段是否在字段列表中 - if _, ok := fields[checkOrderArr[0]]; !ok { - return nil, "", fmt.Errorf("order by %s is not in the fields", order) - } - orderStr = checkOrderArr[0] - if len(checkOrderArr) > 1 { - if checkOrderArr[1] != "asc" && checkOrderArr[1] != "desc" { - return nil, "", fmt.Errorf("order by %s is not secure", order) + if order != "" { + checkOrderArr := strings.Split(order, " ") + orderStr := "" + // 检查请求的排序字段是否在字段列表中 + if _, ok := fields[checkOrderArr[0]]; !ok { + return nil, "", fmt.Errorf("order by %s is not in the fields", order) } - orderStr = orderStr + " " + checkOrderArr[1] + orderStr = checkOrderArr[0] + if len(checkOrderArr) > 1 { + if checkOrderArr[1] != "asc" && checkOrderArr[1] != "desc" { + return nil, "", fmt.Errorf("order by %s is not secure", order) + } + orderStr = orderStr + " " + checkOrderArr[1] + } + db = db.Order(orderStr) } - db = db.Order(orderStr) - } - err = db.Debug().Find(&tableMap).Error - if err != nil { - return nil, "", err + err = db.Debug().Find(&tableMap).Error + if err != nil { + return nil, "", err + } } + var rows [][]string rows = append(rows, tableTitle) for _, exTable := range tableMap { @@ -353,180 +371,186 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID // PreviewSQL 预览最终生成的 SQL(不执行查询,仅返回 SQL 字符串) // Author [piexlmax](https://github.com/piexlmax) & [trae-ai] func (sysExportTemplateService *SysExportTemplateService) PreviewSQL(templateID string, values url.Values) (sqlPreview string, err error) { - // 解析 params(与导出逻辑保持一致) - var params = values.Get("params") - paramsValues, _ := url.ParseQuery(params) - - // 加载模板 - var template system.SysExportTemplate - err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error - if err != nil { - return "", err - } - - // 解析模板列 - var templateInfoMap = make(map[string]string) - columns, err := utils.GetJSONKeys(template.TemplateInfo) - if err != nil { - return "", err - } - err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) - if err != nil { - return "", err - } - var selectKeyFmt []string - for _, key := range columns { - selectKeyFmt = append(selectKeyFmt, key) - } - selects := strings.Join(selectKeyFmt, ", ") - - // 生成 FROM 与 JOIN 片段 - var sb strings.Builder - sb.WriteString("SELECT ") - sb.WriteString(selects) - sb.WriteString(" FROM ") - sb.WriteString(template.TableName) - - if len(template.JoinTemplate) > 0 { - for _, join := range template.JoinTemplate { - sb.WriteString(" ") - sb.WriteString(join.JOINS) - sb.WriteString(" ") - sb.WriteString(join.Table) - sb.WriteString(" ON ") - sb.WriteString(join.ON) - } - } - - // WHERE 条件 - var wheres []string - - // 软删除过滤 - filterDeleted := false - if paramsValues != nil { - filterParam := paramsValues.Get("filterDeleted") - if filterParam == "true" { - filterDeleted = true - } - } - if filterDeleted { - wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", template.TableName)) - if len(template.JoinTemplate) > 0 { - for _, join := range template.JoinTemplate { - if sysExportTemplateService.hasDeletedAtColumn(join.Table) { - wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table)) - } - } - } - } - - // 模板条件(保留与 ExportExcel 同步的解析规则) - if len(template.Conditions) > 0 { - for _, condition := range template.Conditions { - op := strings.ToUpper(strings.TrimSpace(condition.Operator)) - col := strings.TrimSpace(condition.Column) - - // 预览优先展示传入值,没有则展示占位符 - val := "" - if paramsValues != nil { - val = paramsValues.Get(condition.From) - } - - switch op { - case "BETWEEN": - startValue := "" - endValue := "" - if paramsValues != nil { - startValue = paramsValues.Get("start" + condition.From) - endValue = paramsValues.Get("end" + condition.From) - } - if startValue != "" && endValue != "" { - wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", col, startValue, endValue)) - } else { - wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", col, condition.From, condition.From)) - } - case "IN", "NOT IN": - if val != "" { - // 逗号分隔值做简单展示 - parts := strings.Split(val, ",") - for i := range parts { parts[i] = strings.TrimSpace(parts[i]) } - wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", col, op, strings.Join(parts, "','"))) - } else { - wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", col, op, condition.From)) - } - case "LIKE": - if val != "" { - wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", col, val)) - } else { - wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", col, condition.From)) - } - default: - if val != "" { - wheres = append(wheres, fmt.Sprintf("%s %s '%s'", col, op, val)) - } else { - wheres = append(wheres, fmt.Sprintf("%s %s {%s}", col, op, condition.From)) - } - } - } - } - - if len(wheres) > 0 { - sb.WriteString(" WHERE ") - sb.WriteString(strings.Join(wheres, " AND ")) - } - - // 排序 - order := "" - if paramsValues != nil { - order = paramsValues.Get("order") - } - if order == "" && template.Order != "" { - order = template.Order - } - if order != "" { - sb.WriteString(" ORDER BY ") - sb.WriteString(order) - } - - // limit/offset(如果传入或默认值为0,则不生成) - limitStr := "" - offsetStr := "" - if paramsValues != nil { - limitStr = paramsValues.Get("limit") - offsetStr = paramsValues.Get("offset") - } - - // 处理模板默认limit(仅当非0时) - if limitStr == "" && template.Limit != nil && *template.Limit != 0 { - limitStr = strconv.Itoa(*template.Limit) - } - - // 解析为数值,用于判断是否生成 - limitInt := 0 - offsetInt := 0 - if limitStr != "" { - if v, e := strconv.Atoi(limitStr); e == nil { limitInt = v } - } - if offsetStr != "" { - if v, e := strconv.Atoi(offsetStr); e == nil { offsetInt = v } - } - - if limitInt > 0 { - sb.WriteString(" LIMIT ") - sb.WriteString(strconv.Itoa(limitInt)) - if offsetInt > 0 { - sb.WriteString(" OFFSET ") - sb.WriteString(strconv.Itoa(offsetInt)) - } - } else { - // 当limit未设置或为0时,仅当offset>0才生成OFFSET - if offsetInt > 0 { - sb.WriteString(" OFFSET ") - sb.WriteString(strconv.Itoa(offsetInt)) - } - } - - return sb.String(), nil + // 解析 params(与导出逻辑保持一致) + var params = values.Get("params") + paramsValues, _ := url.ParseQuery(params) + + // 加载模板 + var template system.SysExportTemplate + err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error + if err != nil { + return "", err + } + + // 解析模板列 + var templateInfoMap = make(map[string]string) + columns, err := utils.GetJSONKeys(template.TemplateInfo) + if err != nil { + return "", err + } + err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) + if err != nil { + return "", err + } + var selectKeyFmt []string + for _, key := range columns { + selectKeyFmt = append(selectKeyFmt, key) + } + selects := strings.Join(selectKeyFmt, ", ") + + // 生成 FROM 与 JOIN 片段 + var sb strings.Builder + sb.WriteString("SELECT ") + sb.WriteString(selects) + sb.WriteString(" FROM ") + sb.WriteString(template.TableName) + + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + sb.WriteString(" ") + sb.WriteString(join.JOINS) + sb.WriteString(" ") + sb.WriteString(join.Table) + sb.WriteString(" ON ") + sb.WriteString(join.ON) + } + } + + // WHERE 条件 + var wheres []string + + // 软删除过滤 + filterDeleted := false + if paramsValues != nil { + filterParam := paramsValues.Get("filterDeleted") + if filterParam == "true" { + filterDeleted = true + } + } + if filterDeleted { + wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", template.TableName)) + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + if sysExportTemplateService.hasDeletedAtColumn(join.Table) { + wheres = append(wheres, fmt.Sprintf("%s.deleted_at IS NULL", join.Table)) + } + } + } + } + + // 模板条件(保留与 ExportExcel 同步的解析规则) + if len(template.Conditions) > 0 { + for _, condition := range template.Conditions { + op := strings.ToUpper(strings.TrimSpace(condition.Operator)) + col := strings.TrimSpace(condition.Column) + + // 预览优先展示传入值,没有则展示占位符 + val := "" + if paramsValues != nil { + val = paramsValues.Get(condition.From) + } + + switch op { + case "BETWEEN": + startValue := "" + endValue := "" + if paramsValues != nil { + startValue = paramsValues.Get("start" + condition.From) + endValue = paramsValues.Get("end" + condition.From) + } + if startValue != "" && endValue != "" { + wheres = append(wheres, fmt.Sprintf("%s BETWEEN '%s' AND '%s'", col, startValue, endValue)) + } else { + wheres = append(wheres, fmt.Sprintf("%s BETWEEN {start%s} AND {end%s}", col, condition.From, condition.From)) + } + case "IN", "NOT IN": + if val != "" { + // 逗号分隔值做简单展示 + parts := strings.Split(val, ",") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + wheres = append(wheres, fmt.Sprintf("%s %s ('%s')", col, op, strings.Join(parts, "','"))) + } else { + wheres = append(wheres, fmt.Sprintf("%s %s ({%s})", col, op, condition.From)) + } + case "LIKE": + if val != "" { + wheres = append(wheres, fmt.Sprintf("%s LIKE '%%%s%%'", col, val)) + } else { + wheres = append(wheres, fmt.Sprintf("%s LIKE {%%%s%%}", col, condition.From)) + } + default: + if val != "" { + wheres = append(wheres, fmt.Sprintf("%s %s '%s'", col, op, val)) + } else { + wheres = append(wheres, fmt.Sprintf("%s %s {%s}", col, op, condition.From)) + } + } + } + } + + if len(wheres) > 0 { + sb.WriteString(" WHERE ") + sb.WriteString(strings.Join(wheres, " AND ")) + } + + // 排序 + order := "" + if paramsValues != nil { + order = paramsValues.Get("order") + } + if order == "" && template.Order != "" { + order = template.Order + } + if order != "" { + sb.WriteString(" ORDER BY ") + sb.WriteString(order) + } + + // limit/offset(如果传入或默认值为0,则不生成) + limitStr := "" + offsetStr := "" + if paramsValues != nil { + limitStr = paramsValues.Get("limit") + offsetStr = paramsValues.Get("offset") + } + + // 处理模板默认limit(仅当非0时) + if limitStr == "" && template.Limit != nil && *template.Limit != 0 { + limitStr = strconv.Itoa(*template.Limit) + } + + // 解析为数值,用于判断是否生成 + limitInt := 0 + offsetInt := 0 + if limitStr != "" { + if v, e := strconv.Atoi(limitStr); e == nil { + limitInt = v + } + } + if offsetStr != "" { + if v, e := strconv.Atoi(offsetStr); e == nil { + offsetInt = v + } + } + + if limitInt > 0 { + sb.WriteString(" LIMIT ") + sb.WriteString(strconv.Itoa(limitInt)) + if offsetInt > 0 { + sb.WriteString(" OFFSET ") + sb.WriteString(strconv.Itoa(offsetInt)) + } + } else { + // 当limit未设置或为0时,仅当offset>0才生成OFFSET + if offsetInt > 0 { + sb.WriteString(" OFFSET ") + sb.WriteString(strconv.Itoa(offsetInt)) + } + } + + return sb.String(), nil } // ExportTemplate 导出Excel模板 @@ -618,48 +642,75 @@ func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID return err } - var titleKeyMap = make(map[string]string) - for key, title := range templateInfoMap { - titleKeyMap[title] = key - } - db := global.GVA_DB if template.DBName != "" { db = global.MustGetGlobalDBByDBName(template.DBName) } + items, err := sysExportTemplateService.parseExcelToMap(rows, templateInfoMap) + if err != nil { + return err + } + 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 - } + if template.ImportSQL != "" { + return sysExportTemplateService.importBySQL(tx, template.ImportSQL, items) + } + return sysExportTemplateService.importByGORM(tx, template.TableName, items) + }) +} - needCreated := tx.Migrator().HasColumn(template.TableName, "created_at") - needUpdated := tx.Migrator().HasColumn(template.TableName, "updated_at") +func (sysExportTemplateService *SysExportTemplateService) parseExcelToMap(rows [][]string, templateInfoMap map[string]string) ([]map[string]interface{}, error) { + var titleKeyMap = make(map[string]string) + for key, title := range templateInfoMap { + titleKeyMap[title] = key + } - if item["created_at"] == nil && needCreated { - item["created_at"] = time.Now() + 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 ii >= len(excelTitle) { + continue } - if item["updated_at"] == nil && needUpdated { - item["updated_at"] = time.Now() + if _, ok := titleKeyMap[excelTitle[ii]]; !ok { + continue // excel中多余的标题,在模板信息中没有对应的字段,因此key为空,必须跳过 } + key := titleKeyMap[excelTitle[ii]] + item[key] = value + } + items = append(items, item) + } + return items, nil +} - items = append(items, item) +func (sysExportTemplateService *SysExportTemplateService) importBySQL(tx *gorm.DB, sql string, items []map[string]interface{}) error { + for _, item := range items { + if err := tx.Exec(sql, item).Error; err != nil { + return err } - cErr := tx.Table(template.TableName).CreateInBatches(&items, 1000).Error - return cErr - }) + } + return nil +} + +func (sysExportTemplateService *SysExportTemplateService) importByGORM(tx *gorm.DB, tableName string, items []map[string]interface{}) error { + needCreated := tx.Migrator().HasColumn(tableName, "created_at") + needUpdated := tx.Migrator().HasColumn(tableName, "updated_at") + + for _, item := range items { + if item["created_at"] == nil && needCreated { + item["created_at"] = time.Now() + } + if item["updated_at"] == nil && needUpdated { + item["updated_at"] = time.Now() + } + } + return tx.Table(tableName).CreateInBatches(&items, 1000).Error } func getColumnName(n int) string {
server/utils/ast/ast.go+103 −0 modified@@ -304,3 +304,106 @@ func VariableExistsInBlock(block *ast.BlockStmt, varName string) bool { }) return exists } + +func CreateDictionaryStructAst(dictionaries []system.SysDictionary) *[]ast.Expr { + var dictElts []ast.Expr + for i := range dictionaries { + statusStr := "true" + if dictionaries[i].Status != nil && !*dictionaries[i].Status { + statusStr = "false" + } + + elts := []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Name"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Name)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Type"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Type)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Status"}, + Value: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "utils"}, + Sel: &ast.Ident{Name: "Pointer"}, + }, + Args: []ast.Expr{ + &ast.Ident{Name: statusStr}, + }, + }, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Desc"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", dictionaries[i].Desc)}, + }, + } + + if len(dictionaries[i].SysDictionaryDetails) > 0 { + var detailElts []ast.Expr + for _, detail := range dictionaries[i].SysDictionaryDetails { + detailStatusStr := "true" + if detail.Status != nil && !*detail.Status { + detailStatusStr = "false" + } + + detailElts = append(detailElts, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysDictionaryDetail"}, + }, + Elts: []ast.Expr{ + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Label"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Label)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Value"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Value)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Extend"}, + Value: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"%s\"", detail.Extend)}, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Status"}, + Value: &ast.CallExpr{ + Fun: &ast.SelectorExpr{ + X: &ast.Ident{Name: "utils"}, + Sel: &ast.Ident{Name: "Pointer"}, + }, + Args: []ast.Expr{ + &ast.Ident{Name: detailStatusStr}, + }, + }, + }, + &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "Sort"}, + Value: &ast.BasicLit{Kind: token.INT, Value: fmt.Sprintf("%d", detail.Sort)}, + }, + }, + }) + } + elts = append(elts, &ast.KeyValueExpr{ + Key: &ast.Ident{Name: "SysDictionaryDetails"}, + Value: &ast.CompositeLit{ + Type: &ast.ArrayType{Elt: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysDictionaryDetail"}, + }}, + Elts: detailElts, + }, + }) + } + + dictElts = append(dictElts, &ast.CompositeLit{ + Type: &ast.SelectorExpr{ + X: &ast.Ident{Name: "model"}, + Sel: &ast.Ident{Name: "SysDictionary"}, + }, + Elts: elts, + }) + } + return &dictElts +}
server/utils/breakpoint_continue.go+9 −0 modified@@ -24,6 +24,9 @@ const ( //@return: error, string func BreakPointContinue(content []byte, fileName string, contentNumber int, contentTotal int, fileMd5 string) (string, error) { + if strings.Contains(fileName, "..") || strings.Contains(fileMd5, "..") { + return "", errors.New("文件名或路径不合法") + } path := breakpointDir + fileMd5 + "/" err := os.MkdirAll(path, os.ModePerm) if err != nil { @@ -79,6 +82,9 @@ func makeFileContent(content []byte, fileName string, FileDir string, contentNum //@return: error, string func MakeFile(fileName string, FileMd5 string) (string, error) { + if strings.Contains(fileName, "..") || strings.Contains(FileMd5, "..") { + return "", errors.New("文件名或路径不合法") + } rd, err := os.ReadDir(breakpointDir + FileMd5) if err != nil { return finishDir + fileName, err @@ -107,6 +113,9 @@ func MakeFile(fileName string, FileMd5 string) (string, error) { //@return: error func RemoveChunk(FileMd5 string) error { + if strings.Contains(FileMd5, "..") { + return errors.New("路径不合法") + } err := os.RemoveAll(breakpointDir + FileMd5) return err }
server/utils/upload/aws_s3.go+1 −0 modified@@ -42,6 +42,7 @@ func (*AwsS3) UploadFile(file *multipart.FileHeader) (string, string, error) { Bucket: aws.String(global.GVA_CONFIG.AwsS3.Bucket), Key: aws.String(filename), Body: f, + ContentType: aws.String(file.Header.Get("Content-Type")), }) if err != nil { global.GVA_LOG.Error("function uploader.Upload() failed", zap.Any("err", err.Error()))
web/package.json+1 −1 modified@@ -1,6 +1,6 @@ { "name": "gin-vue-admin", - "version": "2.8.7", + "version": "2.8.8", "private": true, "scripts": { "dev": "node openDocument.js && vite --host --mode development",
web/src/api/autoCode.js+8 −0 modified@@ -207,6 +207,14 @@ export const initAPI = (data) => { }) } +export const initDictionary = (data) => { + return service({ + url: '/autoCode/initDictionary', + method: 'post', + data + }) +} + export const mcp = (data) => { return service({ url: '/autoCode/mcp',
web/src/api/plugin/api.js+10 −0 added@@ -0,0 +1,10 @@ +import service from '@/utils/request' + +export const getShopPluginList = (params) => { + return service({ + baseURL: "plugin", + url: '/shopPlugin/getShopPluginList', + method: 'get', + params + }) +} \ No newline at end of file
web/src/components/charts/index.vue+0 −7 modified@@ -1,10 +1,3 @@ -<!-- - 本组件参考 arco-pro 的实现 - https://github.com/arco-design/arco-design-pro-vue/blob/main/arco-design-pro-vite/src/components/chart/index.vue - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/8 -!--> - <template> <VCharts v-if="renderChart"
web/src/components/errorPreview/index.vue+1 −1 modified@@ -54,7 +54,7 @@ </template> <script setup> -import { defineProps, defineEmits, computed } from 'vue'; +import { computed } from 'vue'; const props = defineProps({ errorData: {
web/src/components/logo/index.vue+2 −2 modified@@ -11,8 +11,8 @@ const props = defineProps({ } }) -import darkLogoPath from "/public/logo.png"; // 系统没有暗黑模式logo,如果需要暗黑模式logo请自行修改文件路径。 -import lightLogoPath from "/public/logo.png"; +const darkLogoPath = "/logo.png"; // 系统没有暗黑模式logo,如果需要暗黑模式logo请自行修改文件路径。 +const lightLogoPath = "/logo.png"; const appStore = useAppStore(); const { isDark } = storeToRefs(appStore);
web/src/core/config.js+1 −1 modified@@ -45,7 +45,7 @@ export const viteLogo = (env) => { console.log(greenText(`** 版权持有公司:北京翻转极光科技有限责任公司 **`)) console.log( greenText( - `** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html **` + `** 剔除授权标识需购买商用授权:https://plugin.gin-vue-admin.com/license **` ) ) console.log('\n')
web/src/core/gin-vue-admin.js+1 −1 modified@@ -22,7 +22,7 @@ export default { --------------------------------------版权声明-------------------------------------- ** 版权所有方:flipped-aurora开源团队 ** ** 版权持有公司:北京翻转极光科技有限责任公司 ** - ** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html ** + ** 剔除授权标识需购买商用授权:https://plugin.gin-vue-admin.com/license ** ** 感谢您对Gin-Vue-Admin的支持与关注 合法授权使用更有利于项目的长久发展** `) }
web/src/style/element_visiable.scss+1 −0 modified@@ -87,6 +87,7 @@ } } .el-menu { + background-color: transparent !important; li { @apply my-1; }
web/src/utils/request.js+1 −1 modified@@ -5,7 +5,6 @@ import { emitter } from '@/utils/bus' import router from '@/router/index' const service = axios.create({ - baseURL: import.meta.env.VITE_BASE_API, timeout: 99999 }) let activeAxios = 0 @@ -102,6 +101,7 @@ service.interceptors.request.use( if (!config.donNotShowLoading) { showLoading(config.loadingOption) } + config.baseURL = config.baseURL || import.meta.env.VITE_BASE_API const userStore = useUserStore() config.headers = { 'Content-Type': 'application/json',
web/src/view/dashboard/components/banner.vue+1 −6 modified@@ -1,8 +1,3 @@ -<!-- - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/8 -!--> - <template> <el-carousel class="-mt-2"> <el-carousel-item @@ -27,7 +22,7 @@ const banners = [ { img: banner, - link: 'https://gin-vue-admin.com/empower/index.html' + link: 'https://plugin.gin-vue-admin.com/license' }, { img: banner2,
web/src/view/dashboard/components/card.vue+7 −9 modified@@ -1,22 +1,20 @@ -<!-- - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/8 -!--> - <template> <div - class="bg-white dark:bg-slate-900 text-gray-800 dark:text-gray-400 rounded shadow" + class="rounded-xl border border-black/10 bg-white text-black/80 dark:text-slate-400 dark:bg-slate-900 dark:text-white/80" :class="[customClass || '', withoutPadding ? 'p-0' : 'p-4']" > <div v-if="title" class="flex justify-between items-center"> - <div class="text-base font-bold"> + <div class="text-sm font-semibold tracking-tight text-black dark:text-white"> {{ title }} </div> - <div v-if="showAction" class="text-sm text-active cursor-pointer"> + <div + v-if="showAction" + class="text-xs text-black/60 dark:text-white/60 hover:text-active cursor-pointer" + > 查看更多 </div> </div> - <div class="mt-2"> + <div :class="title ? 'mt-3' : ''"> <slot /> </div> </div>
web/src/view/dashboard/components/charts-content-numbers.vue+6 −10 modified@@ -1,10 +1,3 @@ -<!-- - 本组件参考 arco-pro 的实现 将 ts 改为 js 写法 - https://github.com/arco-design/arco-design-pro-vue/blob/main/arco-design-pro-vite/src/views/dashboard/workplace/components/content-chart.vue - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/8 -!--> - <template> <Chart :height="height" :option="chartOption" /> </template> @@ -24,8 +17,11 @@ default: '128px' } }) + const axisTextColor = computed(() => { + return appStore.isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.70)' + }) const dotColor = computed(() => { - return appStore.isDark ? '#333' : '#E5E8EF' + return appStore.isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)' }) const graphicFactory = (side) => { return { @@ -35,7 +31,7 @@ style: { text: '', textAlign: 'center', - fill: '#4E5969', + fill: axisTextColor.value, fontSize: 12 } } @@ -69,7 +65,7 @@ data: xAxis.value, boundaryGap: false, axisLabel: { - color: '#4E5969', + color: axisTextColor.value, formatter(value, idx) { if (idx === 0) return '' if (idx === xAxis.value.length - 1) return ''
web/src/view/dashboard/components/charts-people-numbers.vue+1 −9 modified@@ -1,11 +1,3 @@ -<!-- - 本组件参考 arco-pro 的实现 将 ts 改为 js 写法 - https://github.com/arco-design/arco-design-pro-vue/blob/main/arco-design-pro-vite/src/views/dashboard/workplace/components/content-chart.vue - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/8 - @desc: 人数统计图表 -!--> - <template> <Chart :height="height" :option="chartOption" /> </template> @@ -38,7 +30,7 @@ style: { text: '', textAlign: 'center', - fill: '#4E5969', + fill: appStore.isDark ? '#FFFFFF' : '#000000', fontSize: 12 } }
web/src/view/dashboard/components/charts.vue+4 −9 modified@@ -1,23 +1,18 @@ -<!-- - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/8 -!--> - <template> <div class=""> <div class="flex items-center justify-between mb-2"> - <div v-if="title" class="font-bold"> + <div v-if="title" class="text-sm font-semibold tracking-tight text-black dark:text-white"> {{ title }} </div> <slot v-else name="title" /> </div> <div class="w-full relative"> <div v-if="type !== 4"> - <div class="mt-4 text-gray-600 text-3xl font-mono"> + <div class="mt-4 text-3xl font-mono text-black dark:text-white"> <el-statistic :value="268500" /> </div> - <div class="mt-2 text-green-600 text-sm font-bold font-mono"> - +80% <el-icon><TopRight /></el-icon> + <div class="mt-2 text-xs font-mono text-black/60 dark:text-white/60"> + +80% <el-icon class="align-middle"><TopRight /></el-icon> </div> </div> <div class="absolute top-0 right-2 w-[50%] h-20">
web/src/view/dashboard/components/notice.vue+6 −14 modified@@ -1,20 +1,17 @@ -<!-- - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/8 -!--> - <template> <el-scrollbar> <div v-for="(item, index) in notices" :key="index" - class="flex items-center mb-1.5 gap-3" + class="flex items-center gap-3 py-1" > - <el-tag :type="item.type" size="small"> + <div + class="shrink-0 rounded-full border border-black/10 px-2 py-0.5 text-[11px] leading-4 text-black/70 dark:border-white/10 dark:text-white/70" + > {{ item.typeTitle }} - </el-tag> + </div> <el-tooltip effect="light" :content="item.title" placement="top"> - <div class="text-xs text-gray-700 dark:text-gray-300 line-clamp-1"> + <div class="min-w-0 text-xs text-black/70 dark:text-white/70 line-clamp-1"> {{ item.title }} </div> </el-tooltip> @@ -24,11 +21,6 @@ <script setup> const notices = [ - { - type: 'primary', - typeTitle: '公告', - title: '授权费将在从六月一日起结束第一价格梯度,进入第二价格梯度。' - }, { type: 'success', typeTitle: '通知',
web/src/view/dashboard/components/pluginTable.vue+50 −51 modified@@ -1,66 +1,65 @@ -<!-- - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/8 -!--> - <template> <div> <el-table :data="tableData" stripe style="width: 100%"> - <el-table-column prop="ranking" label="排名" width="80" align="center" /> - <el-table-column prop="title" label="插件标题" show-overflow-tooltip> + <el-table-column prop="name" label="插件标题" show-overflow-tooltip width="200"> + <template #default="{ row }"> + <a + class="text-black dark:text-white decoration-black/20 dark:decoration-white/20 hover:text-active" + :href="`https://plugin.gin-vue-admin.com/details/${row.ID}`" + target="_blank" + >{{ row.name }}</a> + </template> + </el-table-column> + <el-table-column prop="resume" label="简介" show-overflow-tooltip></el-table-column> + <el-table-column prop="money" label="价格" width="100"> <template #default="{ row }"> - <a class="text-active" :href="row.link" target="_blank">{{ - row.title - }}</a> + <span v-if="row.money === 0">免费</span> + <span v-else>¥{{ row.money }}</span> </template> </el-table-column> - <el-table-column prop="click_num" label="关注度" width="100" /> - <el-table-column prop="hot" label="热度值" width="100" /> </el-table> + <div class="gva-pagination"> + <el-pagination + :current-page="page" + :page-size="pageSize" + :page-sizes="[5, 10, 20]" + :total="total" + layout="total, prev, pager, next" + size="small" + @current-change="handleCurrentChange" + @size-change="handleSizeChange" + /> + </div> </div> </template> <script setup> - const tableData = [ - { - ranking: 1, - title: '组织管理插件:更方便管理组织,分配资源权限。', - click_num: 523, - hot: 263, - link: 'https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=36' - }, - { - ranking: 2, - title: - 'Kubernetes容器管理:,Kubernetes 原生资源管理,提供炫酷的YAML 编辑,Pod 终端,方便运维兄弟管理k8s资源', - click_num: 416, - hot: 223, - link: 'https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=42' - }, - { - ranking: 3, - title: - '定时任务配置化管理:本插件用于对系统内部的定时任务进行配置化管理,可以配置自定义的函数和HTTP,可以配置cron和remark等等', - click_num: 337, - hot: 176, - link: 'https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=67' - }, - { - ranking: 4, - title: - '官网CMS系统:基于Gin-Vue-Admin 和 插件市场客户端开发基座开发的企业官网类(cms)系统', - click_num: 292, - hot: 145, - link: 'https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=69' - }, - { - ranking: 5, - title: '微信支付插件:提供扫码支付功能(需自行对接业务)', - click_num: 173, - hot: 110, - link: 'https://plugin.gin-vue-admin.com/#/layout/newPluginInfo?id=28' + import { getShopPluginList } from '@/api/plugin/api' + import { ref } from 'vue' + + const tableData = ref([]) + const page = ref(1) + const pageSize = ref(5) + const total = ref(0) + + const handleCurrentChange = (val) => { + page.value = val + getTableData() + } + const handleSizeChange = (val) => { + pageSize.value = val + getTableData() + } + + const getTableData = async() => { + const res = await getShopPluginList({ page: page.value, pageSize: pageSize.value ,updatedAt: 1}) + if (res.code === 0) { + tableData.value = res.data.list + total.value = res.data.total } - ] + } + + getTableData() </script> <style scoped lang="scss"></style>
web/src/view/dashboard/components/quickLinks.vue+13 −14 modified@@ -1,39 +1,38 @@ -<!-- - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/8 -!--> <template> - <div class="mt-8 w-full"> - <div class="grid grid-cols-2 md:grid-cols-3 3xl:grid-cols-4"> + <div class="mt-4 w-full"> + <div class="text-xs tracking-wide text-black/60 dark:text-white/60">快捷入口</div> + <div class="mt-3 grid grid-cols-3 gap-3 sm:grid-cols-4"> <div v-for="(item, index) in shortcuts" :key="index" - class="flex flex-col items-center mb-3 group cursor-pointer" + class="flex flex-col items-center group cursor-pointer" @click="toPath(item)" > <div - class="w-8 h-8 rounded bg-gray-200 dark:bg-slate-500 flex items-center justify-center group-hover:bg-blue-400 group-hover:text-white" + class="w-10 h-10 rounded-lg border border-black/10 dark:border-white/10 flex items-center justify-center text-black/70 dark:text-white/70 group-hover:bg-[var(--el-color-primary)] group-hover:text-white transition-colors" > <el-icon><component :is="item.icon" /></el-icon> </div> - <div class="text-xs mt-2 text-gray-700 dark:text-gray-300"> + <div class="mt-2 text-[11px] text-black/70 dark:text-white/70"> {{ item.title }} </div> </div> </div> - <div class="grid grid-cols-2 md:grid-cols-3 3xl:grid-cols-4 mt-8"> + + <div class="mt-6 text-xs tracking-wide text-black/60 dark:text-white/60">最近访问</div> + <div class="mt-3 grid grid-cols-3 gap-3 sm:grid-cols-4"> <div v-for="(item, index) in recentVisits" :key="index" - class="flex flex-col items-center mb-3 group cursor-pointer" + class="flex flex-col items-center group cursor-pointer" @click="openLink(item)" > <div - class="w-8 h-8 rounded bg-gray-200 dark:bg-slate-500 flex items-center justify-center group-hover:bg-blue-400 group-hover:text-white" + class="w-10 h-10 rounded-lg border border-black/10 dark:border-white/10 flex items-center justify-center text-black/70 dark:text-white/70 group-hover:bg-[var(--el-color-primary)] group-hover:text-white transition-colors" > <el-icon><component :is="item.icon" /></el-icon> </div> - <div class="text-xs mt-2 text-gray-700 dark:text-gray-300"> + <div class="mt-2 text-[11px] text-black/70 dark:text-white/70"> {{ item.title }} </div> </div> @@ -98,7 +97,7 @@ { icon: Reading, title: '授权购买', - path: 'https://gin-vue-admin.com/empower/index.html' + path: 'https://plugin.gin-vue-admin.com/license' }, { icon: Document,
web/src/view/dashboard/components/table.vue+24 −40 modified@@ -1,52 +1,36 @@ -<!-- - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/8 -!--> - <template> <div> <el-table :data="tableData" stripe style="width: 100%"> <el-table-column prop="ranking" label="排名" width="80" align="center" /> - <el-table-column prop="title" label="内容标题" show-overflow-tooltip /> - <el-table-column prop="click_num" label="关注度" width="100" /> - <el-table-column prop="hot" label="热度值" width="100" /> + <el-table-column prop="message" label="更新内容" show-overflow-tooltip /> + <el-table-column prop="author" label="提交人" width="140" /> + <el-table-column prop="date" label="时间" width="180" /> </el-table> </div> </template> <script setup> - const tableData = [ - { - ranking: 1, - title: '更简洁的使用界面,更快速的操作体验', - click_num: 523, - hot: 263 - }, - { - ranking: 2, - title: '更优质的服务,更便捷的使用体验', - click_num: 416, - hot: 223 - }, - { - ranking: 3, - title: '更快速的创意实现,更高效的工作效率', - click_num: 337, - hot: 176 - }, - { - ranking: 4, - title: '更多的创意资源,更多的创意灵感', - click_num: 292, - hot: 145 - }, - { - ranking: 5, - title: '更合理的代码结构,更清晰的代码逻辑', - click_num: 173, - hot: 110 - } - ] + import { Commits } from '@/api/github' + import { formatTimeToStr } from '@/utils/date' + import { ref, onMounted } from 'vue' + + const tableData = ref([]) + + const loadCommits = async () => { + const { data } = await Commits(1) + tableData.value = data.slice(0, 5).map((item, index) => { + return { + ranking: index + 1, + message: item.commit.message, + author: item.commit.author.name, + date: formatTimeToStr(item.commit.author.date, 'yyyy-MM-dd hh:mm:ss') + } + }) + } + + onMounted(() => { + loadCommits() + }) </script> <style scoped lang="scss"></style>
web/src/view/dashboard/components/wiki.vue+1 −6 modified@@ -1,15 +1,10 @@ -<!-- - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/8 -!--> - <template> <div class="grid grid-cols-2 gap-2"> <a v-for="item in wikis" :key="item.url" :href="item.url" - class="text-sm text-gray-700 dark:text-gray-300 no-underline hover:text-active" + class="text-sm text-black/70 dark:text-white/70 no-underline hover:text-[var(--el-color-primary)] dark:hover:text-white" target="_blank" > {{ item.title }}
web/src/view/dashboard/index.vue+63 −51 modified@@ -1,64 +1,63 @@ <template> <div - class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-7 py-2 gap-4 md:gap-2 gva-container2" + class="h-full gva-container2 overflow-auto bg-white text-black dark:bg-slate-800 dark:text-white" > - <gva-card custom-class="col-span-1 lg:col-span-2 "> - <gva-chart :type="1" title="访问人数" /> - </gva-card> - <gva-card custom-class="col-span-1 lg:col-span-2 "> - <gva-chart :type="2" title="新增客户" /> - </gva-card> - <gva-card custom-class="col-span-1 lg:col-span-2 "> - <gva-chart :type="3" title="解决数量" /> - </gva-card> - <gva-card - title="快捷功能" - show-action - custom-class="col-start-1 md:col-start-3 lg:col-start-7 row-span-2 " - > - <gva-quick-link /> - </gva-card> - <gva-card - title="内容数据" - custom-class="col-span-1 md:col-span-2 md:row-start-2 lg:col-span-6 col-start-1 row-span-2" - > - <gva-chart :type="4" /> - </gva-card> - <gva-card - title="文档" - show-action - custom-class="md:row-start-8 md:col-start-3 lg:row-start-3 lg:col-start-7" - > - <gva-wiki /> - </gva-card> + <div class="p-4 lg:p-6"> + <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3"> + <gva-card> + <gva-chart :type="1" title="访问人数" /> + </gva-card> + <gva-card> + <gva-chart :type="2" title="新增客户" /> + </gva-card> + <gva-card> + <gva-chart :type="3" title="解决数量" /> + </gva-card> + </div> - <gva-card - title="最新更新" - custom-class="col-span-1 md:col-span-3 row-span-2" - > - <gva-table /> - </gva-card> - <gva-card - title="最新插件" - custom-class="col-span-1 md:col-span-3 row-span-2" - > - <gva-plugin-table /> - </gva-card> + <div class="mt-4 grid grid-cols-1 gap-4 xl:grid-cols-12 items-start"> + <div class="grid grid-cols-1 gap-4 xl:col-span-8 self-start content-start"> + <gva-card title="内容数据"> + <gva-chart :type="4" /> + </gva-card> - <gva-card title="公告" show-action custom-class="col-span-1 lg:col-start-7"> - <gva-notice /> - </gva-card> + <div class="grid grid-cols-1 gap-4"> + <gva-card title="最新插件"> + <gva-plugin-table /> + </gva-card> + </div> - <gva-card - without-padding - custom-class="overflow-hidden lg:h-40 col-span-1 md:col-start-2 md:col-span-1 lg:col-start-7" - > - <gva-banner /> - </gva-card> + <div class="grid grid-cols-1 gap-4"> + <gva-card title="最新更新"> + <gva-table /> + </gva-card> + </div> + </div> + + <div class="grid grid-cols-1 gap-4 xl:col-span-4 self-start content-start"> + <gva-card title="快捷功能" show-action> + <gva-quick-link /> + </gva-card> + <gva-card title="公告" show-action> + <gva-notice /> + </gva-card> + <gva-card title="文档" show-action> + <gva-wiki /> + </gva-card> + <gva-card + without-padding + custom-class="overflow-hidden" + > + <gva-banner /> + </gva-card> + </div> + </div> + </div> </div> </template> <script setup> + import { computed } from 'vue' import { GvaPluginTable, GvaTable, @@ -69,6 +68,19 @@ GvaCard, GvaBanner } from './components' + + const today = computed(() => { + try { + const d = new Date() + return d.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }) + } catch (e) { + return new Date().toISOString().slice(0, 10) + } + }) defineOptions({ name: 'Dashboard' })
web/src/view/layout/aside/combinationMode.vue+1 −1 modified@@ -23,7 +23,7 @@ </div> <div v-if="mode === 'normal'" - class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700" + class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 shadow dark:shadow-gray-700" :class="isCollapse ? '' : ' px-2'" :style="{ width: layoutSideWidth + 'px'
web/src/view/layout/aside/headMode.vue+1 −1 modified@@ -1,6 +1,6 @@ <template> <div - class="bg-white h-[calc(100%-4px)] text-slate-700 dark:text-slate-300 mx-2 dark:bg-slate-900 flex items-center w-[calc(100vw-600px)] overflow-auto" + class="h-full text-slate-700 dark:text-slate-300 mx-2 flex items-center w-[calc(100vw-600px)] overflow-auto" ref="menuContainer" > <el-menu
web/src/view/layout/aside/normalMode.vue+1 −1 modified@@ -1,6 +1,6 @@ <template> <div - class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700" + class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 shadow dark:shadow-gray-700" :class="isCollapse ? '' : ' px-2'" :style="{ width: layoutSideWidth + 'px'
web/src/view/layout/aside/sidebarMode.vue+2 −2 modified@@ -2,7 +2,7 @@ <div class="flex h-full"> <!-- 一级菜单常驻侧边栏 --> <div - class="relative !h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700" + class="relative !h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 shadow dark:shadow-gray-700" :style="{ width: config.layout_side_collapsed_width + 'px' }" @@ -64,7 +64,7 @@ <!-- 二级菜单并列显示 --> <div - class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700 px-2" + class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 shadow dark:shadow-gray-700 px-2" :style="{ width: layoutSideWidth + 'px' }"
web/src/view/layout/header/index.vue+0 −5 modified@@ -1,8 +1,3 @@ -<!-- - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/7 -!--> - <template> <div class="flex justify-between fixed top-0 left-0 right-0 z-10 h-16 bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 shadow dark:shadow-gray-700 items-center px-2"
web/src/view/layout/header/tools.vue+0 −5 modified@@ -1,8 +1,3 @@ -<!-- - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/7 -!--> - <template> <div class="flex items-center mx-4 gap-4"> <el-tooltip class="" effect="dark" content="视频教程" placement="bottom">
web/src/view/layout/tabs/index.vue+0 −4 modified@@ -1,7 +1,3 @@ -<!-- - @auther: bypanghu<bypanghu@163.com> - @date: 2024/5/7 -!--> <template> <div class="gva-tabs"> <el-tabs
web/src/view/superAdmin/authority/components/menus.vue+106 −30 modified@@ -1,10 +1,27 @@ <template> <div> - <div class="sticky top-0.5 z-10"> - <el-input v-model="filterText" class="w-3/5" placeholder="筛选" /> - <el-button class="float-right" type="primary" @click="relation" - >确 定</el-button - > + <div class="sticky top-0.5 z-10 pb-2"> + <div class="flex gap-2 items-center mb-2"> + <el-input v-model="filterText" class="flex-1" placeholder="筛选" /> + <el-button type="primary" @click="relation">确 定</el-button> + </div> + <div class="flex items-center gap-2"> + <span class="whitespace-nowrap">默认首页:</span> + <el-select + :model-value="row.defaultRouter" + filterable + placeholder="请选择默认首页" + class="flex-1" + @change="handleDefaultRouterChange" + > + <el-option + v-for="item in menuOptions" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-select> + </div> </div> <div class="tree-content clear-both"> <el-scrollbar> @@ -21,27 +38,15 @@ @check="nodeChange" > <template #default="{ node, data }"> - <span class="custom-tree-node"> + <div class="flex items-center gap-2"> <span>{{ node.label }}</span> - <span v-if="node.checked && !data.name?.startsWith('http://') && !data.name?.startsWith('https://')"> - <el-button - type="primary" - link - :style="{ - color: - row.defaultRouter === data.name ? '#E6A23C' : '#85ce61' - }" - @click.stop="() => setDefault(data)" - > - {{ row.defaultRouter === data.name ? '首页' : '设为首页' }} - </el-button> - </span> + <SvgIcon v-if="row.defaultRouter === data.name" icon="ant-design:home-filled" class="inline text-lg text-active" /> <span v-if="data.menuBtn.length"> <el-button type="primary" link @click.stop="() => OpenBtn(data)"> 分配按钮 </el-button> </span> - </span> + </div> </template> </el-tree> </el-scrollbar> @@ -96,16 +101,75 @@ const menuTreeData = ref([]) const menuTreeIds = ref([]) const needConfirm = ref(false) + const menuTree = ref(null) const menuDefaultProps = ref({ children: 'children', label: function (data) { return data.meta.title }, disabled: function (data) { - return props.row.defaultRouter === data.name + if (props.row.defaultRouter !== data.name) return false + // 只在该节点已勾选时禁用,避免出现“默认首页未勾选却无法勾选”的死锁状态 + const checkedKeys = menuTree.value?.getCheckedKeys?.() || menuTreeIds.value + return checkedKeys.includes(Number(data.ID)) } }) + const menuOptions = ref([]) + + const isExternalRoute = (name) => { + if (!name) return false + return name.startsWith('http://') || name.startsWith('https://') + } + + const findMenuByName = (menus, name) => { + for (const item of menus || []) { + if (item?.name === name) return item + if (item?.children?.length) { + const found = findMenuByName(item.children, name) + if (found) return found + } + } + return null + } + + const buildOptionsFromCheckedLeafMenus = () => { + const checkedLeafMenus = menuTree.value + ? menuTree.value.getCheckedNodes(false, true) + : [] + const options = checkedLeafMenus + .filter((item) => item?.name && !isExternalRoute(item.name)) + .map((item) => ({ + label: item?.meta?.title || item.name, + value: item.name + })) + + // 确保当前默认首页能正常显示(即使历史数据不一致) + if (props.row.defaultRouter && !options.some(o => o.value === props.row.defaultRouter)) { + const found = findMenuByName(menuTreeData.value, props.row.defaultRouter) + if (found && !isExternalRoute(found.name)) { + options.push({ + label: found?.meta?.title || found.name, + value: found.name + }) + } + } + + return options + } + + const refreshDefaultRouterOptions = () => { + menuOptions.value = buildOptionsFromCheckedLeafMenus() + } + + const isDefaultRouterAllowed = (routeName) => { + if (!routeName) return false + const checkedLeafMenus = menuTree.value + ? menuTree.value.getCheckedNodes(false, true) + : [] + return checkedLeafMenus.some((item) => item?.name === routeName) + } + const init = async () => { // 获取所有菜单树 const res = await getBaseMenuTree() @@ -120,6 +184,14 @@ } }) menuTreeIds.value = arr + + // 确保异步数据加载后,树的勾选状态与选项同步 + await nextTick() + if (menuTree.value?.setCheckedKeys) { + menuTree.value.setCheckedKeys(menuTreeIds.value) + await nextTick() + } + refreshDefaultRouterOptions() } init() @@ -136,15 +208,25 @@ emit('changeRow', 'defaultRouter', res.data.authority.defaultRouter) } } + + const handleDefaultRouterChange = (val) => { + // 兜底校验:未勾选菜单不允许被设置为默认首页 + if (!isDefaultRouterAllowed(val)) { + ElMessage.warning('未勾选的菜单不可设置为默认首页,请先勾选后再选择') + return + } + setDefault({ name: val }) + } + const nodeChange = () => { needConfirm.value = true + refreshDefaultRouterOptions() } // 暴露给外层使用的切换拦截统一方法 const enterAndNext = () => { relation() } // 关联树 确认方法 - const menuTree = ref(null) const relation = async () => { const checkArr = menuTree.value.getCheckedNodes(false, true) const res = await addMenuAuthority({ @@ -156,6 +238,8 @@ type: 'success', message: '菜单设置成功!' }) + + refreshDefaultRouterOptions() } } @@ -223,11 +307,3 @@ menuTree.value.filter(val) }) </script> - -<style lang="scss" scoped> - .custom-tree-node { - span + span { - @apply ml-3; - } - } -</style>
web/src/view/systemTools/exportTemplate/exportTemplate.vue+138 −77 modified@@ -313,35 +313,107 @@ /> </el-form-item> - <el-form-item label="关联条件:"> - <div - v-for="(join, key) in formData.joinTemplate" - :key="key" - class="flex gap-4 w-full mb-2" - > - <el-select v-model="join.joins" placeholder="请选择关联方式"> - <el-option label="LEFT JOIN" value="LEFT JOIN" /> - <el-option label="INNER JOIN" value="INNER JOIN" /> - <el-option label="RIGHT JOIN" value="RIGHT JOIN" /> - </el-select> - <el-input v-model="join.table" placeholder="请输入关联表" /> - <el-input - v-model="join.on" - placeholder="关联条件 table1.a = table2.b" - /> - <el-button - type="danger" - icon="delete" - @click="() => formData.joinTemplate.splice(key, 1)" - >删除</el-button - > - </div> - <div class="flex justify-end w-full"> - <el-button type="primary" icon="plus" @click="addJoin" - >添加条件</el-button - > - </div> - </el-form-item> + <el-tabs v-model="activeName"> + <el-tab-pane label="自动构建" name="auto" class="pt-2"> + <el-form-item label="关联条件:"> + <div + v-for="(join, key) in formData.joinTemplate" + :key="key" + class="flex gap-4 w-full mb-2" + > + <el-select v-model="join.joins" placeholder="请选择关联方式"> + <el-option label="LEFT JOIN" value="LEFT JOIN" /> + <el-option label="INNER JOIN" value="INNER JOIN" /> + <el-option label="RIGHT JOIN" value="RIGHT JOIN" /> + </el-select> + <el-input v-model="join.table" placeholder="请输入关联表" /> + <el-input + v-model="join.on" + placeholder="关联条件 table1.a = table2.b" + /> + <el-button + type="danger" + icon="delete" + @click="() => formData.joinTemplate.splice(key, 1)" + >删除</el-button + > + </div> + <div class="flex justify-end w-full"> + <el-button type="primary" icon="plus" @click="addJoin" + >添加条件</el-button + > + </div> + </el-form-item> + + <el-form-item label="默认导出条数:"> + <el-input-number + v-model="formData.limit" + :step="1" + :step-strictly="true" + :precision="0" + /> + </el-form-item> + <el-form-item label="默认排序条件:"> + <el-input v-model="formData.order" placeholder="例:id desc" /> + </el-form-item> + <el-form-item label="导出条件:"> + <div + v-for="(condition, key) in formData.conditions" + :key="key" + class="flex gap-4 w-full mb-2" + > + <el-input + v-model="condition.from" + placeholder="需要从查询条件取的json key" + /> + <el-input v-model="condition.column" placeholder="表对应的column" /> + <el-select + v-model="condition.operator" + placeholder="请选择查询条件" + > + <el-option + v-for="item in typeSearchOptions" + :key="item.value" + :label="item.label" + :value="item.value" + /> + </el-select> + <el-button + type="danger" + icon="delete" + @click="() => formData.conditions.splice(key, 1)" + >删除</el-button + > + </div> + <div class="flex justify-end w-full"> + <el-button type="primary" icon="plus" @click="addCondition" + >添加条件</el-button + > + </div> + </el-form-item> + </el-tab-pane> + <el-tab-pane label="自定义SQL" name="sql" class="pt-2"> + <el-form-item label="导出SQL:" prop="sql"> + <el-input + v-model="formData.sql" + type="textarea" + :rows="10" + placeholder="请输入导出SQL语句,支持GORM命名参数模式,例如:SELECT * FROM sys_apis WHERE id = @id" + /> + </el-form-item> + <el-form-item label="导入SQL:" prop="importSql"> + <el-input + v-model="formData.importSql" + type="textarea" + :rows="10" + placeholder="请输入导入SQL语句,支持GORM命名参数模式,例如:INSERT INTO sys_apis (path, description ,api_group, method) VALUES (@path, @description, @api_group, @method)。参数名对应模板信息中的key。" + /> + </el-form-item> + <el-form-item label="导出条件:"> + 此时导出条件的key必然为 condition = {key1:"value1",key2:"value2"},这里需要和你传入sql语句@key占位符的key一致。 + </el-form-item> + </el-tab-pane> + </el-tabs> <el-form-item label="模板信息:" prop="templateInfo"> <el-input @@ -352,52 +424,6 @@ :placeholder="templatePlaceholder" /> </el-form-item> - <el-form-item label="默认导出条数:"> - <el-input-number - v-model="formData.limit" - :step="1" - :step-strictly="true" - :precision="0" - /> - </el-form-item> - <el-form-item label="默认排序条件:"> - <el-input v-model="formData.order" placeholder="例:id desc" /> - </el-form-item> - <el-form-item label="导出条件:"> - <div - v-for="(condition, key) in formData.conditions" - :key="key" - class="flex gap-4 w-full mb-2" - > - <el-input - v-model="condition.from" - placeholder="需要从查询条件取的json key" - /> - <el-input v-model="condition.column" placeholder="表对应的column" /> - <el-select - v-model="condition.operator" - placeholder="请选择查询条件" - > - <el-option - v-for="item in typeSearchOptions" - :key="item.value" - :label="item.label" - :value="item.value" - /> - </el-select> - <el-button - type="danger" - icon="delete" - @click="() => formData.conditions.splice(key, 1)" - >删除</el-button - > - </div> - <div class="flex justify-end w-full"> - <el-button type="primary" icon="plus" @click="addCondition" - >添加条件</el-button - > - </div> - </el-form-item> </el-form> </el-drawer> @@ -513,6 +539,7 @@ "table_column4":"第四列", "\`rows\`":"我属于数据库关键字或函数", } +如果使用是sql模式,您自行构建的sql的key就是需要写在json的key,例如您写了xxx as k1,那么模板信息中就写{"k1":"对应列名称"} 如果增加了JOINS导出key应该列为 {table_name1.table_column1:"第一列",table_name2.table_column2:"第二列"} 如果有重复的列名导出格式应为 {table_name1.table_column1 as key:"第一列",table_name2.table_column2 as key2:"第二列"} JOINS模式下不支持导入 @@ -528,9 +555,13 @@ JOINS模式下不支持导入 limit: 0, order: '', conditions: [], - joinTemplate: [] + joinTemplate: [], + sql: '', + importSql: '' }) + const activeName = ref('auto') + const prompt = ref('') const tables = ref([]) @@ -916,6 +947,12 @@ JOINS模式下不支持导入 if (!copyData.joinTemplate) { copyData.joinTemplate = [] } + if (!copyData.sql) { + copyData.sql = '' + } + if (!copyData.importSql) { + copyData.importSql = '' + } delete copyData.ID delete copyData.CreatedAt delete copyData.UpdatedAt @@ -938,6 +975,17 @@ JOINS模式下不支持导入 if (!formData.value.joinTemplate) { formData.value.joinTemplate = [] } + if (!formData.value.sql) { + formData.value.sql = '' + } + if (!formData.value.importSql) { + formData.value.importSql = '' + } + if (formData.value.sql || formData.value.importSql) { + activeName.value = 'sql' + } else { + activeName.value = 'auto' + } dialogFormVisible.value = true } } @@ -1034,8 +1082,11 @@ JOINS模式下不支持导入 limit: 0, order: '', conditions: [], - joinTemplate: [] + joinTemplate: [], + sql: '', + importSql: '' } + activeName.value = 'auto' } // 弹窗确定 const enterDialog = async () => { @@ -1051,6 +1102,16 @@ JOINS模式下不支持导入 } const reqData = JSON.parse(JSON.stringify(formData.value)) + if (activeName.value === 'sql') { + reqData.conditions = [] + reqData.joinTemplate = [] + reqData.limit = 0 + reqData.order = '' + } else { + reqData.sql = '' + reqData.importSql = '' + } + for (let i = 0; i < reqData.conditions.length; i++) { if ( !reqData.conditions[i].from ||
web/src/view/systemTools/pubPlug/pubPlug.vue+87 −7 modified@@ -61,6 +61,30 @@ </el-button> </div> </el-card> + <el-card class="mt-2 text-center"> + <el-transfer + v-model="dictionaries" + :props="{ + key: 'ID' + }" + class="plugin-transfer" + :data="dictionariesData" + filterable + :filter-method="filterDictionaryMethod" + filter-placeholder="请输入字典名称/Type" + :titles="['可选字典', '使用字典']" + :button-texts="['移除', '选中']" + > + <template #default="{ option }"> + {{ option.name }} {{ option.type }} + </template> + </el-transfer> + <div class="flex justify-end mt-2"> + <el-button type="primary" @click="fmtInitDictionary"> + 定义安装字典 + </el-button> + </div> + </el-card> </div> <div class="flex justify-end"> <el-button type="primary" @click="pubPlugin"> 打包插件 </el-button> @@ -71,17 +95,20 @@ <script setup> import { ref } from 'vue' import WarningBar from '@/components/warningBar/warningBar.vue' - import { pubPlug, initMenu, initAPI } from '@/api/autoCode.js' + import { pubPlug, initMenu, initAPI, initDictionary } from '@/api/autoCode.js' import { ElMessage, ElMessageBox } from 'element-plus' import { getAllApis } from '@/api/api' import { getMenuList } from '@/api/menu' + import { getSysDictionaryList } from '@/api/sysDictionary' const plugName = ref('') const menus = ref([]) const menusData = ref([]) const apis = ref([]) const apisData = ref([]) + const dictionaries = ref([]) + const dictionariesData = ref([]) const parentMenu = ref('') const fmtMenu = (menus) => { @@ -106,6 +133,13 @@ if (apiRes.code === 0) { apisData.value = apiRes.data.apis } + const dictionaryRes = await getSysDictionaryList({ + page: 1, + pageSize: 9999 + }) + if (dictionaryRes.code === 0) { + dictionariesData.value = dictionaryRes.data + } } const filterMenuMethod = (query, item) => { @@ -118,11 +152,16 @@ return item.description.indexOf(query) > -1 || item.path.indexOf(query) > -1 } + const filterDictionaryMethod = (query, item) => { + return item.name.indexOf(query) > -1 || item.type.indexOf(query) > -1 + } + initData() + const pubPlugin = async () => { ElMessageBox.confirm( - `请检查server下的/plugin/${plugName.value}/plugin.go是否已放开需要的 initialize.Api(ctx) 和 initialize.Menu(ctx)?`, + `请检查server下的/plugin/${plugName.value}/plugin.go是否已放开需要的 initialize.Api(ctx), initialize.Menu(ctx) 和 initialize.Dictionary(ctx)?`, '打包', { confirmButtonText: '打包', @@ -166,13 +205,16 @@ type: 'warning' } ) - .then(() => { + .then(async () => { const req = { plugName: plugName.value, parentMenu: parentMenu.value, menus: menus.value } - initMenu(req) + const res = await initMenu(req) + if (res.code === 0) { + ElMessage.success('菜单注入成功') + } }) .catch(() => { ElMessage({ @@ -199,13 +241,15 @@ type: 'warning' } ) - .then(() => { + .then(async () => { const req = { plugName: plugName.value, apis: apis.value } - initAPI(req) - console.log(req) + const res = await initAPI(req) + if (res.code === 0) { + ElMessage.success('API注入成功') + } }) .catch(() => { ElMessage({ @@ -214,6 +258,42 @@ }) }) } + + const fmtInitDictionary = () => { + if (dictionaries.value.length === 0) { + ElMessage.error('请至少选择一个字典') + return + } + if (plugName.value === '') { + ElMessage.error('请填写插件名') + return + } + ElMessageBox.confirm( + `点击后将会覆盖server下的/plugin/${plugName.value}/initialize/dictionary. 是否继续?`, + '生成初始字典', + { + confirmButtonText: '生成', + cancelButtonText: '取消', + type: 'warning' + } + ) + .then(async () => { + const req = { + plugName: plugName.value, + dictionaries: dictionaries.value + } + const res = await initDictionary(req) + if (res.code === 0) { + ElMessage.success('字典注入成功') + } + }) + .catch(() => { + ElMessage({ + type: 'info', + message: '关闭生成字典' + }) + }) + } </script> <style lang="scss">
web/vite.config.js+7 −0 modified@@ -78,6 +78,13 @@ export default ({ mode }) => { changeOrigin: true, rewrite: (path) => path.replace(new RegExp('^' + process.env.VITE_BASE_API), '') + }, + "/plugin": { + // 需要代理的路径 例如 '/api' + target: `https://plugin.gin-vue-admin.com/api/`, // 代理到 目标路径 + changeOrigin: true, + rewrite: (path) => + path.replace(new RegExp("^/plugin"), '') } } },
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-3558-j79f-vvm6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-22786ghsaADVISORY
- github.com/flipped-aurora/gin-vue-admin/commit/2242f5d6e133e96d1b359ac019bf54fa0e975dd5ghsax_refsource_MISCWEB
- github.com/flipped-aurora/gin-vue-admin/security/advisories/GHSA-3558-j79f-vvm6ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.