diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 134a4168f..5a890d349 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -3,7 +3,7 @@ description: Report a bug to help us improve Gin-Vue-Admin title: "[Bug]: " labels: [bug] assignees: - - piexlmax + - pixelmaxQm - songzhibin97 - SliverHorn - bypanghu diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 99a2603f6..06566b6a9 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -3,7 +3,7 @@ description: Suggest an idea for Gin-Vue-Admin title: "[Feature]: " labels: [feature] assignees: - - piexlmax + - pixelmaxQm body: - type: textarea id: desc diff --git a/README-en.md b/README-en.md index 1f2a59ca1..3c5a8178a 100644 --- a/README-en.md +++ b/README-en.md @@ -299,6 +299,10 @@ swag init Thank you for considering your contribution to gin-vue-admin! + + Contribution Leaderboard + + diff --git a/README.md b/README.md index d4084919f..6dc2e4eb5 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ [插件市场](https://plugin.gin-vue-admin.com/) +[软件著作权证书](https://www.gin-vue-admin.com/copyright.pdf) + # 重要提示 1.本项目从起步到开发到部署均有文档和详细视频教程 @@ -99,7 +101,7 @@ Gin-vue-admin 的成长离不开大家的支持,如果你愿意为 gin-vue-adm ## 2. 使用说明 ``` -- node版本 > v16.8.3 +- node版本 > v18.16.0 - golang版本 >= v1.22 - IDE推荐:Goland ``` @@ -346,7 +348,7 @@ swag init ### 7.1 技术群 -### QQ交流群:470239250 +### QQ交流群:971857775 ### 微信交流群 | 微信 | @@ -367,9 +369,9 @@ fmt.Println(decodeBytes, err) 感谢您对gin-vue-admin的贡献! - - - + + Contribution Leaderboard + ## 9. 捐赠 @@ -377,4 +379,5 @@ fmt.Println(decodeBytes, err) ## 10. 商用注意事项 -如果您将此项目用于商业用途,请遵守Apache2.0协议并保留作者技术支持声明。 +请严格遵守Apache 2.0协议并保留作品声明,商业用途请务必[获取授权](https://www.gin-vue-admin.com/empower/) +未授权商用将依法追究法律责任 diff --git a/server/api/v1/system/auto_code_mcp.go b/server/api/v1/system/auto_code_mcp.go new file mode 100644 index 000000000..3549aeee4 --- /dev/null +++ b/server/api/v1/system/auto_code_mcp.go @@ -0,0 +1,144 @@ +package system + +import ( + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/mcp/client" + "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/gin-gonic/gin" + "github.com/mark3labs/mcp-go/mcp" +) + +// Create +// @Tags mcp +// @Summary 自动McpTool +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.AutoMcpTool true "创建自动代码" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/mcp [post] +func (a *AutoCodeTemplateApi) MCP(c *gin.Context) { + var info request.AutoMcpTool + err := c.ShouldBindJSON(&info) + if err != nil { + response.FailWithMessage(err.Error(), c) + return + } + + toolFilePath, err := autoCodeTemplateService.CreateMcp(c.Request.Context(), info) + if err != nil { + response.FailWithMessage("创建失败", c) + global.GVA_LOG.Error(err.Error()) + return + } + response.OkWithMessage("创建成功,MCP Tool路径:"+toolFilePath, c) +} + +// Create +// @Tags mcp +// @Summary 自动McpTool +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body request.AutoMcpTool true "创建自动代码" +// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}" +// @Router /autoCode/mcpList [post] +func (a *AutoCodeTemplateApi) MCPList(c *gin.Context) { + + baseUrl := fmt.Sprintf("http://127.0.0.1:%d%s", global.GVA_CONFIG.System.Addr, global.GVA_CONFIG.MCP.SSEPath) + + testClient, err := client.NewClient(baseUrl, "testClient", "v1.0.0", global.GVA_CONFIG.MCP.Name) + defer testClient.Close() + toolsRequest := mcp.ListToolsRequest{} + + list, err := testClient.ListTools(c.Request.Context(), toolsRequest) + + if err != nil { + response.FailWithMessage("创建失败", c) + global.GVA_LOG.Error(err.Error()) + return + } + + mcpServerConfig := map[string]interface{}{ + "mcpServers": map[string]interface{}{ + global.GVA_CONFIG.MCP.Name: map[string]string{ + "url": baseUrl, + }, + }, + } + response.OkWithData(gin.H{ + "mcpServerConfig": mcpServerConfig, + "list": list, + }, c) +} + +// Create +// @Tags mcp +// @Summary 测试McpTool +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Param data body object true "调用MCP Tool的参数" +// @Success 200 {object} response.Response "{"success":true,"data":{},"msg":"测试成功"}" +// @Router /autoCode/mcpTest [post] +func (a *AutoCodeTemplateApi) MCPTest(c *gin.Context) { + // 定义接口请求结构 + var testRequest struct { + Name string `json:"name" binding:"required"` // 工具名称 + Arguments map[string]interface{} `json:"arguments" binding:"required"` // 工具参数 + } + + // 绑定JSON请求体 + if err := c.ShouldBindJSON(&testRequest); err != nil { + response.FailWithMessage("参数解析失败:"+err.Error(), c) + return + } + + // 创建MCP客户端 + baseUrl := fmt.Sprintf("http://127.0.0.1:%d%s", global.GVA_CONFIG.System.Addr, global.GVA_CONFIG.MCP.SSEPath) + testClient, err := client.NewClient(baseUrl, "testClient", "v1.0.0", global.GVA_CONFIG.MCP.Name) + if err != nil { + response.FailWithMessage("创建MCP客户端失败:"+err.Error(), c) + return + } + defer testClient.Close() + + ctx := c.Request.Context() + + // 初始化MCP连接 + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: "testClient", + Version: "v1.0.0", + } + + _, err = testClient.Initialize(ctx, initRequest) + if err != nil { + response.FailWithMessage("初始化MCP连接失败:"+err.Error(), c) + return + } + + // 构建工具调用请求 + request := mcp.CallToolRequest{} + request.Params.Name = testRequest.Name + request.Params.Arguments = testRequest.Arguments + + // 调用工具 + result, err := testClient.CallTool(ctx, request) + if err != nil { + response.FailWithMessage("工具调用失败:"+err.Error(), c) + return + } + + // 处理响应结果 + if len(result.Content) == 0 { + response.FailWithMessage("工具未返回任何内容", c) + return + } + + // 返回结果 + response.OkWithData(result.Content, c) +} diff --git a/server/api/v1/system/sys_export_template.go b/server/api/v1/system/sys_export_template.go index 7eec37883..19419508d 100644 --- a/server/api/v1/system/sys_export_template.go +++ b/server/api/v1/system/sys_export_template.go @@ -3,6 +3,9 @@ package system import ( "fmt" "net/http" + "net/url" + "sync" + "time" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" @@ -15,6 +18,33 @@ import ( "go.uber.org/zap" ) +// 用于token一次性存储 +var ( + exportTokenCache = make(map[string]interface{}) + exportTokenExpiration = make(map[string]time.Time) + tokenMutex sync.RWMutex +) + +// 五分钟检测窗口过期 +func cleanupExpiredTokens() { + for { + time.Sleep(5 * time.Minute) + tokenMutex.Lock() + now := time.Now() + for token, expiry := range exportTokenExpiration { + if now.After(expiry) { + delete(exportTokenCache, token) + delete(exportTokenExpiration, token) + } + } + tokenMutex.Unlock() + } +} + +func init() { + go cleanupExpiredTokens() +} + type SysExportTemplateApi struct { } @@ -183,7 +213,7 @@ func (sysExportTemplateApi *SysExportTemplateApi) GetSysExportTemplateList(c *gi } } -// ExportExcel 导出表格 +// ExportExcel 导出表格token // @Tags SysExportTemplate // @Summary 导出表格 // @Security ApiKeyAuth @@ -192,16 +222,83 @@ func (sysExportTemplateApi *SysExportTemplateApi) GetSysExportTemplateList(c *gi // @Router /sysExportTemplate/exportExcel [get] func (sysExportTemplateApi *SysExportTemplateApi) ExportExcel(c *gin.Context) { templateID := c.Query("templateID") - queryParams := c.Request.URL.Query() if templateID == "" { response.FailWithMessage(global.Translate("sys_export.templateIDEmpty"), c) return } + + queryParams := c.Request.URL.Query() + + //创造一次性token + token := utils.RandomString(32) // 随机32位 + + // 记录本次请求参数 + exportParams := map[string]interface{}{ + "templateID": templateID, + "queryParams": queryParams, + } + + // 参数保留记录完成鉴权 + tokenMutex.Lock() + exportTokenCache[token] = exportParams + exportTokenExpiration[token] = time.Now().Add(30 * time.Minute) + tokenMutex.Unlock() + + // 生成一次性链接 + exportUrl := fmt.Sprintf("/sysExportTemplate/exportExcelByToken?token=%s", token) + response.OkWithData(exportUrl, c) +} + +// ExportExcelByToken 导出表格 +// @Tags ExportExcelByToken +// @Summary 导出表格 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/exportExcelByToken [get] +func (sysExportTemplateApi *SysExportTemplateApi) ExportExcelByToken(c *gin.Context) { + token := c.Query("token") + if token == "" { + response.FailWithMessage("导出token不能为空", c) + return + } + + // 获取token并且从缓存中剔除 + tokenMutex.RLock() + exportParamsRaw, exists := exportTokenCache[token] + expiry, _ := exportTokenExpiration[token] + tokenMutex.RUnlock() + + if !exists || time.Now().After(expiry) { + global.GVA_LOG.Error("导出token无效或已过期!") + response.FailWithMessage("导出token无效或已过期", c) + return + } + + // 从token获取参数 + exportParams, ok := exportParamsRaw.(map[string]interface{}) + if !ok { + global.GVA_LOG.Error("解析导出参数失败!") + response.FailWithMessage("解析导出参数失败", c) + return + } + + // 获取导出参数 + templateID := exportParams["templateID"].(string) + queryParams := exportParams["queryParams"].(url.Values) + + // 清理一次性token + tokenMutex.Lock() + delete(exportTokenCache, token) + delete(exportTokenExpiration, token) + tokenMutex.Unlock() + + // 导出 if file, name, err := sysExportTemplateService.ExportExcel(templateID, queryParams); err != nil { global.GVA_LOG.Error(global.Translate("general.getDataFail"), zap.Error(err)) response.FailWithMessage(global.Translate("general.getDataFail"), c) } else { - c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name+utils.RandomString(6)+".xlsx")) // 对下载的文件重命名 + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name+utils.RandomString(6)+".xlsx")) c.Header("success", "true") c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", file.Bytes()) } @@ -213,18 +310,91 @@ func (sysExportTemplateApi *SysExportTemplateApi) ExportExcel(c *gin.Context) { // @Security ApiKeyAuth // @accept application/json // @Produce application/json -// @Router /sysExportTemplate/ExportTemplate [get] +// @Router /sysExportTemplate/exportTemplate [get] func (sysExportTemplateApi *SysExportTemplateApi) ExportTemplate(c *gin.Context) { templateID := c.Query("templateID") if templateID == "" { response.FailWithMessage(global.Translate("sys_export.templateIDEmpty"), c) return } + + // 创造一次性token + token := utils.RandomString(32) // 随机32位 + + // 记录本次请求参数 + exportParams := map[string]interface{}{ + "templateID": templateID, + "isTemplate": true, + } + + // 参数保留记录完成鉴权 + tokenMutex.Lock() + exportTokenCache[token] = exportParams + exportTokenExpiration[token] = time.Now().Add(30 * time.Minute) + tokenMutex.Unlock() + + // 生成一次性链接 + exportUrl := fmt.Sprintf("/sysExportTemplate/exportTemplateByToken?token=%s", token) + response.OkWithData(exportUrl, c) +} + +// ExportTemplateByToken 通过token导出表格模板 +// @Tags ExportTemplateByToken +// @Summary 通过token导出表格模板 +// @Security ApiKeyAuth +// @accept application/json +// @Produce application/json +// @Router /sysExportTemplate/exportTemplateByToken [get] +func (sysExportTemplateApi *SysExportTemplateApi) ExportTemplateByToken(c *gin.Context) { + token := c.Query("token") + if token == "" { + response.FailWithMessage("导出token不能为空", c) + return + } + + // 获取token并且从缓存中剔除 + tokenMutex.RLock() + exportParamsRaw, exists := exportTokenCache[token] + expiry, _ := exportTokenExpiration[token] + tokenMutex.RUnlock() + + if !exists || time.Now().After(expiry) { + global.GVA_LOG.Error("导出token无效或已过期!") + response.FailWithMessage("导出token无效或已过期", c) + return + } + + // 从token获取参数 + exportParams, ok := exportParamsRaw.(map[string]interface{}) + if !ok { + global.GVA_LOG.Error("解析导出参数失败!") + response.FailWithMessage("解析导出参数失败", c) + return + } + + // 检查是否为模板导出 + isTemplate, _ := exportParams["isTemplate"].(bool) + if !isTemplate { + global.GVA_LOG.Error("token类型错误!") + response.FailWithMessage("token类型错误", c) + return + } + + // 获取导出参数 + templateID := exportParams["templateID"].(string) + + // 清理一次性token + tokenMutex.Lock() + delete(exportTokenCache, token) + delete(exportTokenExpiration, token) + tokenMutex.Unlock() + + // 导出模板 if file, name, err := sysExportTemplateService.ExportTemplate(templateID); err != nil { global.GVA_LOG.Error(global.Translate("general.getDataFail"), zap.Error(err)) response.FailWithMessage(global.Translate("general.getDataFail"), c) } else { - c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name+"模板.xlsx")) // 对下载的文件重命名 + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", name+"模板.xlsx")) c.Header("success", "true") c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", file.Bytes()) } diff --git a/server/api/v1/system/sys_operation_record.go b/server/api/v1/system/sys_operation_record.go index 02e93e3d0..dad681879 100644 --- a/server/api/v1/system/sys_operation_record.go +++ b/server/api/v1/system/sys_operation_record.go @@ -13,31 +13,6 @@ import ( type OperationRecordApi struct{} -// CreateSysOperationRecord -// @Tags SysOperationRecord -// @Summary 创建SysOperationRecord -// @Security ApiKeyAuth -// @accept application/json -// @Produce application/json -// @Param data body system.SysOperationRecord true "创建SysOperationRecord" -// @Success 200 {object} response.Response{msg=string} "创建SysOperationRecord" -// @Router /sysOperationRecord/createSysOperationRecord [post] -func (s *OperationRecordApi) CreateSysOperationRecord(c *gin.Context) { - var sysOperationRecord system.SysOperationRecord - err := c.ShouldBindJSON(&sysOperationRecord) - if err != nil { - response.FailWithMessage(err.Error(), c) - return - } - err = operationRecordService.CreateSysOperationRecord(sysOperationRecord) - if err != nil { - global.GVA_LOG.Error(global.Translate("general.creationFail"), zap.Error(err)) - response.FailWithMessage(global.Translate("general.creationFailErr"), c) - return - } - response.OkWithMessage(global.Translate("general.createSuccess"), c) -} - // DeleteSysOperationRecord // @Tags SysOperationRecord // @Summary 删除SysOperationRecord diff --git a/server/api/v1/system/sys_system.go b/server/api/v1/system/sys_system.go index a1936f51d..27bf8da65 100644 --- a/server/api/v1/system/sys_system.go +++ b/server/api/v1/system/sys_system.go @@ -55,16 +55,17 @@ func (s *SystemApi) SetSystemConfig(c *gin.Context) { // ReloadSystem // @Tags System -// @Summary 重启系统 +// @Summary 重载系统 // @Security ApiKeyAuth // @Produce application/json -// @Success 200 {object} response.Response{msg=string} "重启系统" +// @Success 200 {object} response.Response{msg=string} "重载系统" // @Router /system/reloadSystem [post] func (s *SystemApi) ReloadSystem(c *gin.Context) { - err := utils.Reload() + // 触发系统重载事件 + err := utils.GlobalSystemEvents.TriggerReload() if err != nil { global.GVA_LOG.Error(global.Translate("sys_system.rebootFail"), zap.Error(err)) - response.FailWithMessage(global.Translate("sys_system.rebootFailErr"), c) + response.FailWithMessage("重载系统失败:"+err.Error(), c) return } response.OkWithMessage(global.Translate("sys_system.rebootSuccess"), c) diff --git a/server/api/v1/system/sys_user.go b/server/api/v1/system/sys_user.go index 13d28ca9e..d6ca6bde1 100644 --- a/server/api/v1/system/sys_user.go +++ b/server/api/v1/system/sys_user.go @@ -93,7 +93,7 @@ func (b *BaseApi) TokenNext(c *gin.Context, user system.SysUser) { } if jwtStr, err := jwtService.GetRedisJWT(user.Username); err == redis.Nil { - if err := jwtService.SetRedisJWT(token, user.Username); err != nil { + if err := utils.SetRedisJWT(token, user.Username); err != nil { global.GVA_LOG.Error(global.Translate("sys_user.loginStatusFail"), zap.Error(err)) response.FailWithMessage(global.Translate("sys_user.loginStatusFailErr"), c) return @@ -114,7 +114,7 @@ func (b *BaseApi) TokenNext(c *gin.Context, user system.SysUser) { response.FailWithMessage(global.Translate("sys_user.jwtInvalidationFailed"), c) return } - if err := jwtService.SetRedisJWT(token, user.GetUsername()); err != nil { + if err := utils.SetRedisJWT(token, user.GetUsername()); err != nil { response.FailWithMessage(global.Translate("sys_user.loginStatusFailErr"), c) return } @@ -467,13 +467,13 @@ func (b *BaseApi) GetUserInfo(c *gin.Context) { // @Success 200 {object} response.Response{msg=string} "重置用户密码" // @Router /user/resetPassword [post] func (b *BaseApi) ResetPassword(c *gin.Context) { - var user system.SysUser - err := c.ShouldBindJSON(&user) + var rps systemReq.ResetPassword + err := c.ShouldBindJSON(&rps) if err != nil { response.FailWithMessage(err.Error(), c) return } - err = userService.ResetPassword(user.ID) + err = userService.ResetPassword(rps.ID, rps.Password) if err != nil { global.GVA_LOG.Error(global.Translate("sys_user.resetPWFail"), zap.Error(err)) response.FailWithMessage(global.Translate("sys_user.resetPWFailErr")+" "+err.Error(), c) diff --git a/server/config.yaml b/server/config.yaml index 30355d12e..a5f58e3a5 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -1,210 +1,3 @@ -# github.com/flipped-aurora/gin-vue-admin/server Global Configuration - -# jwt configuration -jwt: - signing-key: qmPlus - expires-time: 7d - buffer-time: 1d - issuer: qmPlus -# zap logger configuration -zap: - level: info - format: console - prefix: "[github.com/flipped-aurora/gin-vue-admin/server]" - director: log - show-line: true - encode-level: LowercaseColorLevelEncoder - stacktrace-key: stacktrace - log-in-console: true - retention-day: -1 - -# redis configuration -redis: - #是否使用redis集群模式 - useCluster: false - #使用集群模式addr和db默认无效 - addr: 127.0.0.1:6379 - password: "" - db: 0 - clusterAddrs: - - "172.21.0.3:7000" - - "172.21.0.4:7001" - - "172.21.0.2:7002" - -# redis-list configuration -redis-list: - - name: cache # 数据库的名称,注意: name 需要在 redis-list 中唯一 - useCluster: false # 是否使用redis集群模式 - addr: 127.0.0.1:6379 # 使用集群模式addr和db默认无效 - password: "" - db: 0 - clusterAddrs: - - "172.21.0.3:7000" - - "172.21.0.4:7001" - - "172.21.0.2:7002" - -# mongo configuration -mongo: - coll: '' - options: '' - database: '' - username: '' - password: '' - auth-source: '' - min-pool-size: 0 - max-pool-size: 100 - socket-timeout-ms: 0 - connect-timeout-ms: 0 - is-zap: false - hosts: - - host: '' - port: '' - -# email configuration -email: - to: xxx@qq.com - port: 465 - from: xxx@163.com - host: smtp.163.com - is-ssl: true - secret: xxx - nickname: test - -# system configuration -system: - env: local # 修改为public可以关闭路由日志输出 - addr: 8888 - db-type: mysql - oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置 - use-redis: false # 使用redis - use-mongo: false # 使用mongo - use-multipoint: false - # IP限制次数 一个小时15000次 - iplimit-count: 15000 - # IP限制一个小时 - iplimit-time: 3600 - # 路由全局前缀 - router-prefix: "" - # 严格角色模式 打开后权限将会存在上下级关系 - use-strict-auth: false - -# captcha configuration -captcha: - key-long: 6 - img-width: 240 - img-height: 80 - open-captcha: 0 # 0代表一直开启,大于0代表限制次数 - open-captcha-timeout: 3600 # open-captcha大于0时才生效 - -# mysql connect configuration -# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) -mysql: - path: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - max-idle-conns: 10 - max-open-conns: 100 - log-mode: "" - log-zap: false - -# pgsql connect configuration -# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) -pgsql: - path: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - max-idle-conns: 10 - max-open-conns: 100 - log-mode: "" - log-zap: false -oracle: - path: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - max-idle-conns: 10 - max-open-conns: 100 - log-mode: "" - log-zap: false -mssql: - path: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - max-idle-conns: 10 - max-open-conns: 100 - log-mode: "" - log-zap: false -sqlite: - path: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - max-idle-conns: 10 - max-open-conns: 100 - log-mode: "" - log-zap: false -db-list: - - disable: true # 是否禁用 - type: "" # 数据库的类型,目前支持mysql、pgsql、mssql、oracle - alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一 - path: "" - port: "" - config: "" - db-name: "" - username: "" - password: "" - max-idle-conns: 10 - max-open-conns: 100 - log-mode: "" - log-zap: false - -# local configuration -local: - path: uploads/file - store-path: uploads/file - -# autocode configuration -autocode: - web: web/src - root: "" # root 自动适配项目根目录, 请不要手动配置,他会在项目加载的时候识别出根路径 - server: server - module: 'github.com/flipped-aurora/gin-vue-admin/server' - ai-path: "" # AI服务路径 - -# qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址) -qiniu: - zone: ZoneHuaDong - bucket: "" - img-path: "" - use-https: false - access-key: "" - secret-key: "" - use-cdn-domains: false - -# minio oss configuration -minio: - endpoint: yourEndpoint - access-key-id: yourAccessKeyId - access-key-secret: yourAccessKeySecret - bucket-name: yourBucketName - use-ssl: false - base-path: "" - bucket-url: "http://host:9000/yourBucketName" - -# aliyun oss configuration aliyun-oss: endpoint: yourEndpoint access-key-id: yourAccessKeyId @@ -212,29 +5,28 @@ aliyun-oss: bucket-name: yourBucketName bucket-url: yourBucketUrl base-path: yourBasePath - -# tencent cos configuration -tencent-cos: - bucket: xxxxx-10005608 - region: ap-shanghai - secret-id: your-secret-id - secret-key: your-secret-key - base-url: https://gin.vue.admin - path-prefix: github.com/flipped-aurora/gin-vue-admin/server - -# aws s3 configuration (minio compatible) +autocode: + web: web/src + root: D:\Projects\Go\gin-vue-admin + server: server + module: github.com/flipped-aurora/gin-vue-admin/server + ai-path: "" aws-s3: bucket: xxxxx-10005608 region: ap-shanghai endpoint: "" - s3-force-path-style: false - disable-ssl: false secret-id: your-secret-id secret-key: your-secret-key base-url: https://gin.vue.admin path-prefix: github.com/flipped-aurora/gin-vue-admin/server - -# cloudflare r2 configuration + s3-force-path-style: false + disable-ssl: false +captcha: + key-long: 6 + img-width: 240 + img-height: 80 + open-captcha: 0 + open-captcha-timeout: 3600 cloudflare-r2: bucket: xxxx0bucket base-url: https://gin.vue.admin.com @@ -242,41 +34,219 @@ cloudflare-r2: account-id: xxx_account_id access-key-id: xxx_key_id secret-access-key: xxx_secret_key - -# huawei obs configuration +cors: + mode: strict-whitelist + whitelist: + - allow-origin: example1.com + allow-methods: POST, GET + allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + allow-credentials: true + - allow-origin: example2.com + allow-methods: GET, POST + allow-headers: content-type + expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type + allow-credentials: true +db-list: + - type: "" + alias-name: "" + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false + disable: true +disk-list: + - mount-point: / +email: + to: xxx@qq.com + from: xxx@163.com + host: smtp.163.com + secret: xxx + nickname: test + port: 465 + is-ssl: true + is-loginauth: false +excel: + dir: ./resource/excel/ hua-wei-obs: path: you-path bucket: you-bucket endpoint: you-endpoint access-key: you-access-key secret-key: you-secret-key - -# excel configuration -excel: - dir: ./resource/excel/ - -# disk usage configuration -disk-list: - - mount-point: "/" - -# 跨域配置 -# 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用 -cors: - mode: strict-whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝 - whitelist: - - allow-origin: example1.com - allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id - allow-methods: POST, GET - expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - - allow-credentials: true # 布尔值 - - allow-origin: example2.com - allow-headers: content-type - allow-methods: GET, POST - expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type - allow-credentials: true # 布尔值 - +jwt: + signing-key: aa114301-5cf5-4f5f-95ba-5352d951031d + expires-time: 7d + buffer-time: 1d + issuer: qmPlus language: language: en - default-language: zh # default language to be used as a fullback in case of translation to current language was not exist. - dir: ./resource/lang/ \ No newline at end of file + default-language: zh + dir: ./resource/lang/ +local: + path: uploads/file + store-path: uploads/file +mcp: + name: GVA_MCP + version: v1.0.0 + sse_path: /sse + message_path: /message + url_prefix: "" +minio: + endpoint: yourEndpoint + access-key-id: yourAccessKeyId + access-key-secret: yourAccessKeySecret + bucket-name: yourBucketName + use-ssl: false + base-path: "" + bucket-url: http://host:9000/yourBucketName +mongo: + coll: "" + options: "" + database: "" + username: "" + password: "" + auth-source: "" + min-pool-size: 0 + max-pool-size: 100 + socket-timeout-ms: 0 + connect-timeout-ms: 0 + is-zap: false + hosts: + - host: "" + port: "" +mssql: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +mysql: + prefix: "" + port: "3306" + config: charset=utf8mb4&parseTime=True&loc=Local + db-name: gva + username: mhassan + password: P@$$w0rD + path: 127.0.0.1 + engine: "" + log-mode: error + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +oracle: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +pgsql: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +qiniu: + zone: ZoneHuaDong + bucket: "" + img-path: "" + access-key: "" + secret-key: "" + use-https: false + use-cdn-domains: false +redis: + name: "" + addr: 127.0.0.1:6379 + password: "" + db: 0 + useCluster: false + clusterAddrs: + - 172.21.0.3:7000 + - 172.21.0.4:7001 + - 172.21.0.2:7002 +redis-list: + - name: cache + addr: 127.0.0.1:6379 + password: "" + db: 0 + useCluster: false + clusterAddrs: + - 172.21.0.3:7000 + - 172.21.0.4:7001 + - 172.21.0.2:7002 +sqlite: + prefix: "" + port: "" + config: "" + db-name: "" + username: "" + password: "" + path: "" + engine: "" + log-mode: "" + max-idle-conns: 10 + max-open-conns: 100 + singular: false + log-zap: false +system: + db-type: mysql + oss-type: local + router-prefix: "" + addr: 8888 + iplimit-count: 15000 + iplimit-time: 3600 + use-multipoint: false + use-redis: false + use-mongo: false + use-strict-auth: false +tencent-cos: + bucket: xxxxx-10005608 + region: ap-shanghai + secret-id: your-secret-id + secret-key: your-secret-key + base-url: https://gin.vue.admin + path-prefix: github.com/flipped-aurora/gin-vue-admin/server +zap: + level: info + prefix: '[github.com/flipped-aurora/gin-vue-admin/server]' + format: console + director: log + encode-level: LowercaseColorLevelEncoder + stacktrace-key: stacktrace + show-line: true + log-in-console: true + retention-day: -1 diff --git a/server/config/captcha.go b/server/config/captcha.go index 074a9bfad..d678a4147 100644 --- a/server/config/captcha.go +++ b/server/config/captcha.go @@ -4,6 +4,6 @@ type Captcha struct { KeyLong int `mapstructure:"key-long" json:"key-long" yaml:"key-long"` // 验证码长度 ImgWidth int `mapstructure:"img-width" json:"img-width" yaml:"img-width"` // 验证码宽度 ImgHeight int `mapstructure:"img-height" json:"img-height" yaml:"img-height"` // 验证码高度 - OpenCaptcha int `mapstructure:"open-captcha" json:"open-captcha" yaml:"open-captcha"` // 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码此数,如3代表错误三次后出现验证码 + OpenCaptcha int `mapstructure:"open-captcha" json:"open-captcha" yaml:"open-captcha"` // 防爆破验证码开启此数,0代表每次登录都需要验证码,其他数字代表错误密码次数,如3代表错误三次后出现验证码 OpenCaptchaTimeOut int `mapstructure:"open-captcha-timeout" json:"open-captcha-timeout" yaml:"open-captcha-timeout"` // 防爆破验证码超时时间,单位:s(秒) } diff --git a/server/config/config.go b/server/config/config.go index 9eabe9f79..47c364c7d 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -35,6 +35,8 @@ type Server struct { // 跨域配置 Cors CORS `mapstructure:"cors" json:"cors" yaml:"cors"` + // MCP配置 + MCP MCP `mapstructure:"mcp" json:"mcp" yaml:"mcp"` // added by mohamed hassan to support multi-language Language Language `mapstructure:"language" json:"language" yaml:"language"` } diff --git a/server/config/db_list.go b/server/config/db_list.go index 39767f53b..17674b76e 100644 --- a/server/config/db_list.go +++ b/server/config/db_list.go @@ -1,8 +1,9 @@ package config import ( - "gorm.io/gorm/logger" "strings" + + "gorm.io/gorm/logger" ) type DsnProvider interface { @@ -31,13 +32,13 @@ type GeneralDB struct { func (c GeneralDB) LogLevel() logger.LogLevel { switch strings.ToLower(c.LogMode) { - case "silent", "Silent": + case "silent": return logger.Silent - case "error", "Error": + case "error": return logger.Error - case "warn", "Warn": + case "warn": return logger.Warn - case "info", "Info": + case "info": return logger.Info default: return logger.Info diff --git a/server/config/email.go b/server/config/email.go index 0984616b2..9fd76428f 100644 --- a/server/config/email.go +++ b/server/config/email.go @@ -1,11 +1,12 @@ package config type Email struct { - To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 - From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 - Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 - Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 - Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 - Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 - IsSSL bool `mapstructure:"is-ssl" json:"is-ssl" yaml:"is-ssl"` // 是否SSL 是否开启SSL + To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 + Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 + Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool `mapstructure:"is-ssl" json:"is-ssl" yaml:"is-ssl"` // 是否SSL 是否开启SSL + IsLoginAuth bool `mapstructure:"is-loginauth" json:"is-loginauth" yaml:"is-loginauth"` // 是否LoginAuth 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等) } diff --git a/server/config/mcp.go b/server/config/mcp.go new file mode 100644 index 000000000..81f4bff30 --- /dev/null +++ b/server/config/mcp.go @@ -0,0 +1,9 @@ +package config + +type MCP struct { + Name string `mapstructure:"name" json:"name" yaml:"name"` // MCP名称 + Version string `mapstructure:"version" json:"version" yaml:"version"` // MCP版本 + SSEPath string `mapstructure:"sse_path" json:"sse_path" yaml:"sse_path"` // SSE路径 + MessagePath string `mapstructure:"message_path" json:"message_path" yaml:"message_path"` // 消息路径 + UrlPrefix string `mapstructure:"url_prefix" json:"url_prefix" yaml:"url_prefix"` // URL前缀 +} diff --git a/server/core/server.go b/server/core/server.go index 6665d4248..05cd85286 100644 --- a/server/core/server.go +++ b/server/core/server.go @@ -3,21 +3,21 @@ package core import ( "fmt" + "time" + "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/initialize" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "go.uber.org/zap" ) -type server interface { - ListenAndServe() error -} - -func RunWindowsServer() { - if global.GVA_CONFIG.System.UseMultipoint || global.GVA_CONFIG.System.UseRedis { +func RunServer() { + if global.GVA_CONFIG.System.UseRedis { // 初始化redis服务 initialize.Redis() - initialize.RedisList() + if global.GVA_CONFIG.System.UseMultipoint { + initialize.RedisList() + } } if global.GVA_CONFIG.System.UseMongo { @@ -34,37 +34,34 @@ func RunWindowsServer() { Router := initialize.Routers() address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr) - s := initServer(address, Router) - - global.GVA_LOG.Info("server run success on ", zap.String("address", address)) fmt.Printf(` - %s gin-vue-admin - %s: v2.7.9 - %s - %s: https://github.com/flipped-aurora/gin-vue-admin - %s: https://plugin.gin-vue-admin.com - %s: https://support.qq.com/products/371961 - %s: http://127.0.0.1%s/swagger/index.html - %s: http://127.0.0.1:8080 - %s - %s - %s - %s - `, - global.Translate("core.server.welcomeTo"), + %s gin-vue-admin + %s:v2.8.2 + %s + %s:https://github.com/flipped-aurora/gin-vue-admin + %s:https://plugin.gin-vue-admin.com + %s:https://support.qq.com/products/371961 + %s:http://127.0.0.1%s/swagger/index.html + 默认MCP SSE地址:http://127.0.0.1%s%s + 默认MCP Message地址:http://127.0.0.1%s%s + %s:http://127.0.0.1:8080 + %s + %s + %s + %s +`, global.Translate("core.server.welcomeTo"), global.Translate("core.server.currentVersion"), global.Translate("core.server.joinGroup"), global.Translate("core.server.website"), global.Translate("core.server.pluginMarket"), global.Translate("core.server.community"), global.Translate("core.server.swagger"), - address, + address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath, global.Translate("core.server.frontend"), global.Translate("core.server.copyright1"), global.Translate("core.server.copyright2"), global.Translate("core.server.copyright3"), - global.Translate("core.server.copyright4"), - ) - global.GVA_LOG.Error(s.ListenAndServe().Error()) + global.Translate("core.server.copyright4")) + initServer(address, Router, 10*time.Minute, 10*time.Minute) } diff --git a/server/core/server_other.go b/server/core/server_other.go deleted file mode 100644 index 83645fced..000000000 --- a/server/core/server_other.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !windows -// +build !windows - -package core - -import ( - "time" - - "github.com/fvbock/endless" - "github.com/gin-gonic/gin" -) - -func initServer(address string, router *gin.Engine) server { - s := endless.NewServer(address, router) - s.ReadHeaderTimeout = 10 * time.Minute - s.WriteTimeout = 10 * time.Minute - s.MaxHeaderBytes = 1 << 20 - return s -} diff --git a/server/core/server_run.go b/server/core/server_run.go new file mode 100644 index 000000000..067ce6b6e --- /dev/null +++ b/server/core/server_run.go @@ -0,0 +1,60 @@ +package core + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type server interface { + ListenAndServe() error + Shutdown(context.Context) error +} + +// initServer 启动服务并实现优雅关闭 +func initServer(address string, router *gin.Engine, readTimeout, writeTimeout time.Duration) { + // 创建服务 + srv := &http.Server{ + Addr: address, + Handler: router, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + MaxHeaderBytes: 1 << 20, + } + + // 在goroutine中启动服务 + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("listen: %s\n", err) + zap.L().Error("server启动失败", zap.Error(err)) + os.Exit(1) + } + }() + + // 等待中断信号以优雅地关闭服务器 + quit := make(chan os.Signal, 1) + // kill (无参数) 默认发送 syscall.SIGTERM + // kill -2 发送 syscall.SIGINT + // kill -9 发送 syscall.SIGKILL,但是无法被捕获,所以不需要添加 + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + zap.L().Info("关闭WEB服务...") + + // 设置5秒的超时时间 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + zap.L().Fatal("WEB服务关闭异常", zap.Error(err)) + } + + zap.L().Info("WEB服务已关闭") +} diff --git a/server/core/server_win.go b/server/core/server_win.go deleted file mode 100644 index 89412f942..000000000 --- a/server/core/server_win.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build windows -// +build windows - -package core - -import ( - "net/http" - "time" - - "github.com/gin-gonic/gin" -) - -func initServer(address string, router *gin.Engine) server { - return &http.Server{ - Addr: address, - Handler: router, - ReadTimeout: 30 * time.Second, // increasing timeout from 20 sec to 30 sec by mohamed hassan - WriteTimeout: 30 * time.Second, // increasing timeout from 20 sec to 30 sec by mohamed hassan - MaxHeaderBytes: 1 << 20, - } -} diff --git a/server/core/viper.go b/server/core/viper.go index eeba8b056..d846c9065 100644 --- a/server/core/viper.go +++ b/server/core/viper.go @@ -3,55 +3,26 @@ package core import ( "flag" "fmt" - "github.com/flipped-aurora/gin-vue-admin/server/core/internal" - "github.com/gin-gonic/gin" "os" "path/filepath" - "github.com/fsnotify/fsnotify" - "github.com/spf13/viper" - + "github.com/flipped-aurora/gin-vue-admin/server/core/internal" "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/fsnotify/fsnotify" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" ) -// Viper // -// 优先级: 命令行 > 环境变量 > 默认值 -// Author [SliverHorn](https://github.com/SliverHorn) -func Viper(path ...string) *viper.Viper { - var config string - - if len(path) == 0 { - flag.StringVar(&config, "c", "", "choose config file.") - flag.Parse() - if config == "" { // 判断命令行参数是否为空 - if configEnv := os.Getenv(internal.ConfigEnv); configEnv == "" { // 判断 internal.ConfigEnv 常量存储的环境变量是否为空 - switch gin.Mode() { - case gin.DebugMode: - config = internal.ConfigDefaultFile - case gin.ReleaseMode: - config = internal.ConfigReleaseFile - case gin.TestMode: - config = internal.ConfigTestFile - } - fmt.Printf(global.Translate("system.modeGinEnvName"), gin.Mode(), config) - } else { // internal.ConfigEnv 常量存储的环境变量不为空 将值赋值于config - config = configEnv - fmt.Printf(global.Translate("system.envVariable"), internal.ConfigEnv, config) - } - } else { // 命令行参数不为空 将值赋值于config - fmt.Printf(global.Translate("system.commandLineParam"), config) - } - } else { // 函数传递的可变参数的第一个值赋值于config - config = path[0] - fmt.Printf(global.Translate("system.viperFunc"), config) - } +// Viper 配置 +func Viper() *viper.Viper { + config := getConfigPath() v := viper.New() v.SetConfigFile(config) v.SetConfigType("yaml") err := v.ReadInConfig() if err != nil { - panic(fmt.Errorf("Fatal error config file: %s \n", err)) + panic(fmt.Errorf("fatal error config file: %w", err)) } v.WatchConfig() @@ -62,10 +33,44 @@ func Viper(path ...string) *viper.Viper { } }) if err = v.Unmarshal(&global.GVA_CONFIG); err != nil { - panic(err) + panic(fmt.Errorf("fatal error unmarshal config: %w", err)) } // root 适配性 根据root位置去找到对应迁移位置,保证root路径有效 global.GVA_CONFIG.AutoCode.Root, _ = filepath.Abs("..") return v } + +// getConfigPath 获取配置文件路径, 优先级: 命令行 > 环境变量 > 默认值 +func getConfigPath() (config string) { + // `-c` flag parse + flag.StringVar(&config, "c", "", "choose config file.") + flag.Parse() + if config != "" { // 命令行参数不为空 将值赋值于config + fmt.Printf("您正在使用命令行的 '-c' 参数传递的值, config 的路径为 %s\n", config) + return + } + if env := os.Getenv(internal.ConfigEnv); env != "" { // 判断环境变量 GVA_CONFIG + config = env + fmt.Printf("您正在使用 %s 环境变量, config 的路径为 %s\n", internal.ConfigEnv, config) + return + } + + switch gin.Mode() { // 根据 gin 模式文件名 + case gin.DebugMode: + config = internal.ConfigDebugFile + case gin.ReleaseMode: + config = internal.ConfigReleaseFile + case gin.TestMode: + config = internal.ConfigTestFile + } + fmt.Printf("您正在使用 gin 的 %s 模式运行, config 的路径为 %s\n", gin.Mode(), config) + + _, err := os.Stat(config) + if err != nil || os.IsNotExist(err) { + config = internal.ConfigDefaultFile + fmt.Printf("配置文件路径不存在, 使用默认配置文件路径: %s\n", config) + } + + return +} diff --git a/server/docs/docs.go b/server/docs/docs.go index f81a8275f..6f0eb2a0d 100644 --- a/server/docs/docs.go +++ b/server/docs/docs.go @@ -9296,7 +9296,7 @@ const docTemplate = `{ // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ - Version: "v2.7.9", + Version: "v2.8.2", Host: "", BasePath: "", Schemes: []string{}, diff --git a/server/global/global.go b/server/global/global.go index c6eaee88a..16998ac7f 100644 --- a/server/global/global.go +++ b/server/global/global.go @@ -2,6 +2,7 @@ package global import ( "fmt" + "github.com/mark3labs/mcp-go/server" "sync" "github.com/gin-gonic/gin" @@ -36,6 +37,7 @@ var ( GVA_Concurrency_Control = &singleflight.Group{} GVA_ROUTERS gin.RoutesInfo GVA_ACTIVE_DBNAME *string + GVA_MCP_SERVER *server.MCPServer BlackCache local_cache.Cache lock sync.RWMutex diff --git a/server/go.mod b/server/go.mod index 2fe1db286..f9ac52deb 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,6 +1,8 @@ module github.com/flipped-aurora/gin-vue-admin/server -go 1.22.2 +go 1.23 + +toolchain go1.23.9 require ( github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible @@ -8,17 +10,17 @@ require ( github.com/casbin/casbin/v2 v2.103.0 github.com/casbin/gorm-adapter/v3 v3.32.0 github.com/fsnotify/fsnotify v1.8.0 - github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 github.com/gin-gonic/gin v1.10.0 github.com/glebarez/sqlite v1.11.0 github.com/go-sql-driver/mysql v1.8.1 github.com/goccy/go-json v0.10.4 - github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang-jwt/jwt/v5 v5.2.2 github.com/google/uuid v1.6.0 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/mholt/archiver/v4 v4.0.0-alpha.9 + github.com/mark3labs/mcp-go v0.31.0 + github.com/mholt/archives v0.1.1 github.com/minio/minio-go/v7 v7.0.84 github.com/mojocn/base64Captcha v1.3.8 github.com/nicksnyder/go-i18n/v2 v2.2.0 @@ -57,7 +59,7 @@ require ( filippo.io/edwards25519 v1.1.0 // indirect github.com/BurntSushi/toml v1.4.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/STARRY-S/zip v0.1.0 // indirect + github.com/STARRY-S/zip v0.2.1 // indirect github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/bmatcuk/doublestar/v4 v4.8.0 // indirect @@ -116,6 +118,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/microsoft/go-mssqldb v1.8.0 // indirect github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minlz v1.0.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -123,7 +126,7 @@ require ( github.com/montanaflynn/stats v0.7.1 // indirect github.com/mozillazg/go-httpheader v0.4.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect - github.com/nwaples/rardecode/v2 v2.0.1 // indirect + github.com/nwaples/rardecode/v2 v2.1.0 // indirect github.com/otiai10/mint v1.6.3 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect @@ -157,6 +160,7 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/xuri/efp v0.0.0-20241211021726-c4e992084aa6 // indirect github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 // indirect + github.com/yosida95/uritemplate/v3 v3.0.2 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/server/go.sum b/server/go.sum index 4e5964fa6..6592a892f 100644 --- a/server/go.sum +++ b/server/go.sum @@ -47,8 +47,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= -github.com/STARRY-S/zip v0.1.0 h1:eUER3jKmHKXjv+iy3BekLa+QnNSo1Lqz4eTzYBcGDqo= -github.com/STARRY-S/zip v0.1.0/go.mod h1:qj/mTZkvb3AvfGQ2e775/3AODRvB4peSw8KNMvrM8/I= +github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= +github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= @@ -114,8 +114,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= -github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= @@ -170,8 +168,9 @@ github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= @@ -312,13 +311,15 @@ github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mark3labs/mcp-go v0.31.0 h1:4UxSV8aM770OPmTvaVe/b1rA2oZAjBMhGBfUgOGut+4= +github.com/mark3labs/mcp-go v0.31.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mholt/archiver/v4 v4.0.0-alpha.9 h1:EZgAsW6DsuawxDgTtIdjCUBa2TQ6AOe9pnCidofSRtE= -github.com/mholt/archiver/v4 v4.0.0-alpha.9/go.mod h1:5D3uct315OMkMRXKwEuMB+wQi/2m5NQngKDmApqwVlo= +github.com/mholt/archives v0.1.1 h1:c7J3qXN1FB54y0qiUXiq9Bxk4eCUc8pdXWwOhZdRzeY= +github.com/mholt/archives v0.1.1/go.mod h1:FQVz01Q2uXKB/35CXeW/QFO23xT+hSCGZHVtha78U4I= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= github.com/microsoft/go-mssqldb v1.8.0 h1:7cyZ/AT7ycDsEoWPIXibd+aVKFtteUNhDGf3aobP+tw= github.com/microsoft/go-mssqldb v1.8.0/go.mod h1:6znkekS3T2vp0waiMhen4GPU1BiAsrP+iXHcE7a7rFo= @@ -326,6 +327,8 @@ github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.84 h1:D1HVmAF8JF8Bpi6IU4V9vIEj+8pc+xU88EWMs2yed0E= github.com/minio/minio-go/v7 v7.0.84/go.mod h1:57YXpvc5l3rjPdhqNrDsvVlY0qPI6UTk1bflAe+9doY= +github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= +github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -349,8 +352,8 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nicksnyder/go-i18n/v2 v2.2.0 h1:MNXbyPvd141JJqlU6gJKrczThxJy+kdCNivxZpBQFkw= github.com/nicksnyder/go-i18n/v2 v2.2.0/go.mod h1:4OtLfzqyAxsscyCb//3gfqSvBc81gImX91LrZzczN1o= -github.com/nwaples/rardecode/v2 v2.0.1 h1:3MN6/R+Y4c7e+21U3yhWuUcf72sYmcmr6jtiuAVSH1A= -github.com/nwaples/rardecode/v2 v2.0.1/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= +github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= +github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= @@ -489,6 +492,8 @@ github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71 h1:hOh7aVDrvGJRxzXrQbDY8E github.com/xuri/nfp v0.0.0-20250111060730-82a408b9aa71/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/server/initialize/ensure_tables.go b/server/initialize/ensure_tables.go index 3ef33490f..c58227e3b 100644 --- a/server/initialize/ensure_tables.go +++ b/server/initialize/ensure_tables.go @@ -5,6 +5,7 @@ import ( adapter "github.com/casbin/gorm-adapter/v3" "github.com/flipped-aurora/gin-vue-admin/server/model/example" sysModel "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/flipped-aurora/gin-vue-admin/server/plugin/announcement/model" "github.com/flipped-aurora/gin-vue-admin/server/service/system" "gorm.io/gorm" ) @@ -59,6 +60,9 @@ func (e *ensureTables) MigrateTable(ctx context.Context) (context.Context, error example.ExaCustomer{}, example.ExaFileChunk{}, example.ExaFileUploadAndDownload{}, + example.ExaAttachmentCategory{}, + + model.Info{}, } for _, t := range tables { _ = db.AutoMigrate(&t) @@ -97,6 +101,9 @@ func (e *ensureTables) TableCreated(ctx context.Context) bool { example.ExaCustomer{}, example.ExaFileChunk{}, example.ExaFileUploadAndDownload{}, + example.ExaAttachmentCategory{}, + + model.Info{}, } yes := true for _, t := range tables { diff --git a/server/initialize/gorm_mysql.go b/server/initialize/gorm_mysql.go index 6e496a4d3..61e50ba29 100644 --- a/server/initialize/gorm_mysql.go +++ b/server/initialize/gorm_mysql.go @@ -12,18 +12,31 @@ import ( // GormMysql 初始化Mysql数据库 // Author [piexlmax](https://github.com/piexlmax) // Author [SliverHorn](https://github.com/SliverHorn) +// Author [ByteZhou-2018](https://github.com/ByteZhou-2018) func GormMysql() *gorm.DB { m := global.GVA_CONFIG.Mysql + return initMysqlDatabase(m) +} + +// GormMysqlByConfig 通过传入配置初始化Mysql数据库 +func GormMysqlByConfig(m config.Mysql) *gorm.DB { + return initMysqlDatabase(m) +} + +// initMysqlDatabase 初始化Mysql数据库的辅助函数 +func initMysqlDatabase(m config.Mysql) *gorm.DB { if m.Dbname == "" { return nil } + mysqlConfig := mysql.Config{ DSN: m.Dsn(), // DSN data source name DefaultStringSize: 191, // string 类型字段的默认长度 SkipInitializeWithVersion: false, // 根据版本自动配置 } + if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { - return nil + panic(err) } else { db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) sqlDB, _ := db.DB() @@ -32,24 +45,3 @@ func GormMysql() *gorm.DB { return db } } - -// GormMysqlByConfig 初始化Mysql数据库用过传入配置 -func GormMysqlByConfig(m config.Mysql) *gorm.DB { - if m.Dbname == "" { - return nil - } - mysqlConfig := mysql.Config{ - DSN: m.Dsn(), // DSN data source name - DefaultStringSize: 191, // string 类型字段的默认长度 - SkipInitializeWithVersion: false, // 根据版本自动配置 - } - if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { - panic(err) - } else { - db.InstanceSet("gorm:table_options", "ENGINE=InnoDB") - sqlDB, _ := db.DB() - sqlDB.SetMaxIdleConns(m.MaxIdleConns) - sqlDB.SetMaxOpenConns(m.MaxOpenConns) - return db - } -} diff --git a/server/initialize/gorm_oracle.go b/server/initialize/gorm_oracle.go index 4d18c8a84..513359f0f 100644 --- a/server/initialize/gorm_oracle.go +++ b/server/initialize/gorm_oracle.go @@ -15,32 +15,25 @@ import ( // 如果需要Oracle库 放开import里的注释 把下方 mysql.Config 改为 oracle.Config ; mysql.New 改为 oracle.New func GormOracle() *gorm.DB { m := global.GVA_CONFIG.Oracle - if m.Dbname == "" { - return nil - } - oracleConfig := mysql.Config{ - DSN: m.Dsn(), // DSN data source name - DefaultStringSize: 191, // string 类型字段的默认长度 - } - if db, err := gorm.Open(mysql.New(oracleConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { - panic(err) - } else { - sqlDB, _ := db.DB() - sqlDB.SetMaxIdleConns(m.MaxIdleConns) - sqlDB.SetMaxOpenConns(m.MaxOpenConns) - return db - } + return initOracleDatabase(m) } // GormOracleByConfig 初始化Oracle数据库用过传入配置 func GormOracleByConfig(m config.Oracle) *gorm.DB { + return initOracleDatabase(m) +} + +// initOracleDatabase 初始化Oracle数据库的辅助函数 +func initOracleDatabase(m config.Oracle) *gorm.DB { if m.Dbname == "" { return nil } + oracleConfig := mysql.Config{ DSN: m.Dsn(), // DSN data source name DefaultStringSize: 191, // string 类型字段的默认长度 } + if db, err := gorm.Open(mysql.New(oracleConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil { panic(err) } else { diff --git a/server/initialize/gorm_pgsql.go b/server/initialize/gorm_pgsql.go index 625c87385..6abde5892 100644 --- a/server/initialize/gorm_pgsql.go +++ b/server/initialize/gorm_pgsql.go @@ -13,25 +13,16 @@ import ( // Author [SliverHorn](https://github.com/SliverHorn) func GormPgSql() *gorm.DB { p := global.GVA_CONFIG.Pgsql - if p.Dbname == "" { - return nil - } - pgsqlConfig := postgres.Config{ - DSN: p.Dsn(), // DSN data source name - PreferSimpleProtocol: false, - } - if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(p.Prefix, p.Singular)); err != nil { - return nil - } else { - sqlDB, _ := db.DB() - sqlDB.SetMaxIdleConns(p.MaxIdleConns) - sqlDB.SetMaxOpenConns(p.MaxOpenConns) - return db - } + return initPgSqlDatabase(p) } -// GormPgSqlByConfig 初始化 Postgresql 数据库 通过参数 +// GormPgSqlByConfig 初始化 Postgresql 数据库 通过指定参数 func GormPgSqlByConfig(p config.Pgsql) *gorm.DB { + return initPgSqlDatabase(p) +} + +// initPgSqlDatabase 初始化 Postgresql 数据库的辅助函数 +func initPgSqlDatabase(p config.Pgsql) *gorm.DB { if p.Dbname == "" { return nil } diff --git a/server/initialize/gorm_sqlite.go b/server/initialize/gorm_sqlite.go index 041264107..9d158bf16 100644 --- a/server/initialize/gorm_sqlite.go +++ b/server/initialize/gorm_sqlite.go @@ -11,22 +11,16 @@ import ( // GormSqlite 初始化Sqlite数据库 func GormSqlite() *gorm.DB { s := global.GVA_CONFIG.Sqlite - if s.Dbname == "" { - return nil - } - - if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(s.Prefix, s.Singular)); err != nil { - panic(err) - } else { - sqlDB, _ := db.DB() - sqlDB.SetMaxIdleConns(s.MaxIdleConns) - sqlDB.SetMaxOpenConns(s.MaxOpenConns) - return db - } + return initSqliteDatabase(s) } // GormSqliteByConfig 初始化Sqlite数据库用过传入配置 func GormSqliteByConfig(s config.Sqlite) *gorm.DB { + return initSqliteDatabase(s) +} + +// initSqliteDatabase 初始化Sqlite数据库辅助函数 +func initSqliteDatabase(s config.Sqlite) *gorm.DB { if s.Dbname == "" { return nil } diff --git a/server/initialize/init.go b/server/initialize/init.go new file mode 100644 index 000000000..4dc48f316 --- /dev/null +++ b/server/initialize/init.go @@ -0,0 +1,15 @@ +// 假设这是初始化逻辑的一部分 + +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/utils" +) + +// 初始化全局函数 +func SetupHandlers() { + // 注册系统重载处理函数 + utils.GlobalSystemEvents.RegisterReloadHandler(func() error { + return Reload() + }) +} diff --git a/server/initialize/mcp.go b/server/initialize/mcp.go new file mode 100644 index 000000000..5e03f2940 --- /dev/null +++ b/server/initialize/mcp.go @@ -0,0 +1,25 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + mcpTool "github.com/flipped-aurora/gin-vue-admin/server/mcp" + "github.com/mark3labs/mcp-go/server" +) + +func McpRun() *server.SSEServer { + config := global.GVA_CONFIG.MCP + + s := server.NewMCPServer( + config.Name, + config.Version, + ) + + global.GVA_MCP_SERVER = s + + mcpTool.RegisterAllTools(s) + + return server.NewSSEServer(s, + server.WithSSEEndpoint(config.SSEPath), + server.WithMessageEndpoint(config.MessagePath), + server.WithBaseURL(config.UrlPrefix)) +} diff --git a/server/initialize/mongo.go b/server/initialize/mongo.go index 7b69192ed..aba777829 100644 --- a/server/initialize/mongo.go +++ b/server/initialize/mongo.go @@ -45,7 +45,7 @@ func (m *mongo) Initialization() error { opts = internal.Mongo.GetClientOptions() } ctx := context.Background() - client, err := qmgo.Open(ctx, &qmgo.Config{ + config := &qmgo.Config{ Uri: global.GVA_CONFIG.Mongo.Uri(), Coll: global.GVA_CONFIG.Mongo.Coll, Database: global.GVA_CONFIG.Mongo.Database, @@ -53,12 +53,16 @@ func (m *mongo) Initialization() error { MaxPoolSize: &global.GVA_CONFIG.Mongo.MaxPoolSize, SocketTimeoutMS: &global.GVA_CONFIG.Mongo.SocketTimeoutMs, ConnectTimeoutMS: &global.GVA_CONFIG.Mongo.ConnectTimeoutMs, - Auth: &qmgo.Credential{ + } + if global.GVA_CONFIG.Mongo.Username != "" && global.GVA_CONFIG.Mongo.Password != "" { + config.Auth = &qmgo.Credential{ Username: global.GVA_CONFIG.Mongo.Username, Password: global.GVA_CONFIG.Mongo.Password, AuthSource: global.GVA_CONFIG.Mongo.AuthSource, - }, - }, opts...) + } + } + client, err := qmgo.Open(ctx, config, opts...) + if err != nil { return errors.Wrap(err, global.Translate("initialize.mongoConnectionFailed")) } diff --git a/server/initialize/plugin_biz_v1.go b/server/initialize/plugin_biz_v1.go index b66eadb2b..1f3cb88a1 100644 --- a/server/initialize/plugin_biz_v1.go +++ b/server/initialize/plugin_biz_v1.go @@ -2,6 +2,7 @@ package initialize import ( "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/plugin/email" "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin" @@ -29,6 +30,7 @@ func bizPluginV1(group ...*gin.RouterGroup) { global.GVA_CONFIG.Email.Nickname, global.GVA_CONFIG.Email.Port, global.GVA_CONFIG.Email.IsSSL, + global.GVA_CONFIG.Email.IsLoginAuth, )) holder(public, private) } diff --git a/server/initialize/reload.go b/server/initialize/reload.go new file mode 100644 index 000000000..8fd27e691 --- /dev/null +++ b/server/initialize/reload.go @@ -0,0 +1,45 @@ +package initialize + +import ( + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +// Reload 优雅地重新加载系统配置 +func Reload() error { + global.GVA_LOG.Info("正在重新加载系统配置...") + + // 重新加载配置文件 + if err := global.GVA_VP.ReadInConfig(); err != nil { + global.GVA_LOG.Error("重新读取配置文件失败!", zap.Error(err)) + return err + } + + // 重新初始化数据库连接 + if global.GVA_DB != nil { + db, _ := global.GVA_DB.DB() + err := db.Close() + if err != nil { + global.GVA_LOG.Error("关闭原数据库连接失败!", zap.Error(err)) + return err + } + } + + // 重新建立数据库连接 + global.GVA_DB = Gorm() + + // 重新初始化其他配置 + OtherInit() + DBList() + + if global.GVA_DB != nil { + // 确保数据库表结构是最新的 + RegisterTables() + } + + // 重新初始化定时任务 + Timer() + + global.GVA_LOG.Info("系统配置重新加载完成") + return nil +} diff --git a/server/initialize/router.go b/server/initialize/router.go index 1a6eec9cc..2803594d3 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -40,13 +40,24 @@ func Routers() *gin.Engine { Router.Use(gin.Logger()) } + sseServer := McpRun() + + // 注册mcp服务 + Router.GET(global.GVA_CONFIG.MCP.SSEPath, func(c *gin.Context) { + sseServer.SSEHandler().ServeHTTP(c.Writer, c.Request) + }) + + Router.POST(global.GVA_CONFIG.MCP.MessagePath, func(c *gin.Context) { + sseServer.MessageHandler().ServeHTTP(c.Writer, c.Request) + }) + systemRouter := router.RouterGroupApp.System exampleRouter := router.RouterGroupApp.Example // 如果想要不使用nginx代理前端网页,可以修改 web/.env.production 下的 // VUE_APP_BASE_API = / // VUE_APP_BASE_PATH = http://localhost // 然后执行打包命令 npm run build。在打开下面3行注释 - // Router.Static("/favicon.ico", "./dist/favicon.ico") + // Router.StaticFile("/favicon.ico", "./dist/favicon.ico") // Router.Static("/assets", "./dist/assets") // dist里面的静态资源 // Router.StaticFile("/", "./dist/index.html") // 前端网页入口页面 @@ -81,24 +92,24 @@ func Routers() *gin.Engine { } { - systemRouter.InitApiRouter(PrivateGroup, PublicGroup) // 注册功能api路由 - systemRouter.InitJwtRouter(PrivateGroup) // jwt相关路由 - systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由 - systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由 - systemRouter.InitSystemRouter(PrivateGroup) // system相关路由 - systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由 - systemRouter.InitAutoCodeRouter(PrivateGroup, PublicGroup) // 创建自动化代码 - systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由 - systemRouter.InitSysDictionaryRouter(PrivateGroup) // 字典管理 - systemRouter.InitAutoCodeHistoryRouter(PrivateGroup) // 自动化代码历史 - systemRouter.InitSysOperationRecordRouter(PrivateGroup) // 操作记录 - systemRouter.InitSysDictionaryDetailRouter(PrivateGroup) // 字典详情管理 - systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 按钮权限管理 - systemRouter.InitSysExportTemplateRouter(PrivateGroup) // 导出模板 - systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理 - exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由 - exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由 - exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类 + systemRouter.InitApiRouter(PrivateGroup, PublicGroup) // 注册功能api路由 + systemRouter.InitJwtRouter(PrivateGroup) // jwt相关路由 + systemRouter.InitUserRouter(PrivateGroup) // 注册用户路由 + systemRouter.InitMenuRouter(PrivateGroup) // 注册menu路由 + systemRouter.InitSystemRouter(PrivateGroup) // system相关路由 + systemRouter.InitCasbinRouter(PrivateGroup) // 权限相关路由 + systemRouter.InitAutoCodeRouter(PrivateGroup, PublicGroup) // 创建自动化代码 + systemRouter.InitAuthorityRouter(PrivateGroup) // 注册角色路由 + systemRouter.InitSysDictionaryRouter(PrivateGroup) // 字典管理 + systemRouter.InitAutoCodeHistoryRouter(PrivateGroup) // 自动化代码历史 + systemRouter.InitSysOperationRecordRouter(PrivateGroup) // 操作记录 + systemRouter.InitSysDictionaryDetailRouter(PrivateGroup) // 字典详情管理 + systemRouter.InitAuthorityBtnRouterRouter(PrivateGroup) // 按钮权限管理 + systemRouter.InitSysExportTemplateRouter(PrivateGroup, PublicGroup) // 导出模板 + systemRouter.InitSysParamsRouter(PrivateGroup, PublicGroup) // 参数管理 + exampleRouter.InitCustomerRouter(PrivateGroup) // 客户路由 + exampleRouter.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由 + exampleRouter.InitAttachmentCategoryRouterRouter(PrivateGroup) // 文件上传下载分类 } diff --git a/server/main.go b/server/main.go index ad9303c60..47ff720bd 100644 --- a/server/main.go +++ b/server/main.go @@ -4,9 +4,9 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/core" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/initialize" + "github.com/flipped-aurora/gin-vue-admin/server/utils/translate" _ "go.uber.org/automaxprocs" "go.uber.org/zap" - "github.com/flipped-aurora/gin-vue-admin/server/utils/translate" ) //go:generate go env -w GO111MODULE=on @@ -22,30 +22,38 @@ import ( // @Tag.Description 用户 // @title Gin-Vue-Admin Swagger API接口文档 -// @version v2.7.9 +// @version v2.8.2 // @description 使用gin+vue进行极速开发的全栈开发基础平台 // @securityDefinitions.apikey ApiKeyAuth // @in header // @name x-token // @BasePath / func main() { - global.GVA_VP = core.Viper() // Initializing viper + // 初始化系统 + initializeSystem() + // 运行服务器 + core.RunServer() +} + +// initializeSystem 初始化系统所有组件 +// 提取为单独函数以便于系统重载时调用 +func initializeSystem() { + global.GVA_VP = core.Viper() // 初始化Viper initialize.OtherInit() global.GVA_LOG = core.Zap() // Initializing the zap log library zap.ReplaceGlobals(global.GVA_LOG) global.GVA_DB = initialize.Gorm() // Conneting to database using gorm initialize.Timer() initialize.DBList() + // added by mohamed hassan to support multilanguage global.GVA_TRANSLATOR = translate.Translator{} // create translator inestance here //global.GVA_TRANSLATOR.InitTranslator(global.GVA_CONFIG.Language.Language, global.GVA_CONFIG.Language.Dir) global.GVA_TRANSLATOR.InitTranslatorEx(global.GVA_CONFIG.Language.Language, global.GVA_CONFIG.Language.DefaultLanguage, global.GVA_CONFIG.Language.Dir) // end of adding + + initialize.SetupHandlers() // 注册全局函数 if global.GVA_DB != nil { - initialize.RegisterTables() // Initializing database tables - // defer close the database connection before the end of the program - db, _ := global.GVA_DB.DB() - defer db.Close() + initialize.RegisterTables() // 初始化表 } - core.RunWindowsServer() } diff --git a/server/mcp/client/client.go b/server/mcp/client/client.go new file mode 100644 index 000000000..7e5db1ef6 --- /dev/null +++ b/server/mcp/client/client.go @@ -0,0 +1,39 @@ +package client + +import ( + "context" + "errors" + mcpClient "github.com/mark3labs/mcp-go/client" + "github.com/mark3labs/mcp-go/mcp" +) + +func NewClient(baseUrl, name, version, serverName string) (*mcpClient.Client, error) { + client, err := mcpClient.NewSSEMCPClient(baseUrl) + if err != nil { + return nil, err + } + + ctx := context.Background() + + // 启动client + if err := client.Start(ctx); err != nil { + return nil, err + } + + // 初始化 + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: name, + Version: version, + } + + result, err := client.Initialize(ctx, initRequest) + if err != nil { + return nil, err + } + if result.ServerInfo.Name != serverName { + return nil, errors.New("server name mismatch") + } + return client, nil +} diff --git a/server/mcp/client/client_test.go b/server/mcp/client/client_test.go new file mode 100644 index 000000000..917d22d39 --- /dev/null +++ b/server/mcp/client/client_test.go @@ -0,0 +1,132 @@ +package client + +import ( + "context" + "fmt" + "github.com/mark3labs/mcp-go/mcp" + "testing" +) + +// 测试 MCP 客户端连接 +func TestMcpClientConnection(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf(err.Error()) + } +} + +func TestTools(t *testing.T) { + t.Run("currentTime", func(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + request := mcp.CallToolRequest{} + request.Params.Name = "currentTime" + request.Params.Arguments = map[string]interface{}{ + "timezone": "UTC+8", + } + + result, err := c.CallTool(ctx, request) + if err != nil { + t.Fatalf("方法调用错误: %v", err) + } + + if len(result.Content) != 1 { + t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content)) + } + if content, ok := result.Content[0].(mcp.TextContent); ok { + t.Logf("成功返回信息%s", content.Text) + } else { + t.Logf("返回为止类型信息%+v", content) + } + }) + + t.Run("getNickname", func(t *testing.T) { + + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + // Initialize + initRequest := mcp.InitializeRequest{} + initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION + initRequest.Params.ClientInfo = mcp.Implementation{ + Name: "test-client", + Version: "1.0.0", + } + + _, err = c.Initialize(ctx, initRequest) + if err != nil { + t.Fatalf("初始化失败: %v", err) + } + + request := mcp.CallToolRequest{} + request.Params.Name = "getNickname" + request.Params.Arguments = map[string]interface{}{ + "username": "admin", + } + + result, err := c.CallTool(ctx, request) + if err != nil { + t.Fatalf("方法调用错误: %v", err) + } + + if len(result.Content) != 1 { + t.Errorf("应该有且仅返回1条信息,但是现在有 %d", len(result.Content)) + } + if content, ok := result.Content[0].(mcp.TextContent); ok { + t.Logf("成功返回信息%s", content.Text) + } else { + t.Logf("返回为止类型信息%+v", content) + } + }) +} + +func TestGetTools(t *testing.T) { + c, err := NewClient("http://localhost:8888/sse", "test-client", "1.0.0", "gin-vue-admin MCP服务") + defer c.Close() + if err != nil { + t.Fatalf("Failed to create client: %v", err) + } + ctx := context.Background() + + toolsRequest := mcp.ListToolsRequest{} + + toolListResult, err := c.ListTools(ctx, toolsRequest) + if err != nil { + t.Fatalf("获取工具列表失败: %v", err) + } + for i := range toolListResult.Tools { + tool := toolListResult.Tools[i] + fmt.Printf("工具名称: %s\n", tool.Name) + fmt.Printf("工具描述: %s\n", tool.Description) + + // 打印参数信息 + if tool.InputSchema.Properties != nil { + fmt.Println("参数列表:") + for paramName, prop := range tool.InputSchema.Properties { + required := "否" + // 检查参数是否在必填列表中 + for _, reqField := range tool.InputSchema.Required { + if reqField == paramName { + required = "是" + break + } + } + fmt.Printf(" - %s (类型: %s, 描述: %s, 必填: %s)\n", + paramName, prop.(map[string]any)["type"], prop.(map[string]any)["description"], required) + } + } else { + fmt.Println("该工具没有参数") + } + fmt.Println("-------------------") + } +} diff --git a/server/mcp/current_time.go b/server/mcp/current_time.go new file mode 100644 index 000000000..41eaea308 --- /dev/null +++ b/server/mcp/current_time.go @@ -0,0 +1,81 @@ +package mcpTool + +import ( + "context" + "errors" + "fmt" + "github.com/mark3labs/mcp-go/mcp" + "time" +) + +func init() { + RegisterTool(&CurrentTime{}) +} + +type CurrentTime struct { +} + +// 获取当前系统时间 +func (t *CurrentTime) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 获取当前系统时间 + + timezone, ok := request.GetArguments()["timezone"].(string) + + if !ok { + return nil, errors.New("参数错误:timezone 必须是字符串类型") + } + // 根据timezone参数加载对应时区 + loc, err := loadTimeZone(timezone) + if err != nil { + return nil, err + } + + // 获取当前时间并转换为指定时区 + currentTime := time.Now().In(loc).Format("2006-01-02 15:04:05") + //返回 + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("%s 时区的当前时间是:%s", timezone, currentTime), + }, + }, + }, nil +} + +func (t *CurrentTime) New() mcp.Tool { + return mcp.NewTool("currentTime", + mcp.WithDescription("获取当前系统时间"), + mcp.WithString("timezone", + mcp.Required(), + mcp.Description("时区"), + mcp.Enum("UTC", "CST", "PST", "EST", "GMT", "CET", "JST", "MST", "IST", "AST", "HST"), + )) +} + +// 将简写时区转换为IANA标准时区 +func loadTimeZone(timezone string) (*time.Location, error) { + // 时区映射表 + timezoneMap := map[string]string{ + "UTC": "UTC", + "CST": "Asia/Shanghai", // 中国标准时间 + "PST": "America/Los_Angeles", + "EST": "America/New_York", + "GMT": "GMT", + "CET": "Europe/Paris", + "JST": "Asia/Tokyo", + "MST": "America/Denver", + "IST": "Asia/Kolkata", + "AST": "Asia/Riyadh", // 阿拉伯标准时间 + "HST": "Pacific/Honolulu", + } + + // 获取标准时区名称 + tzName, exists := timezoneMap[timezone] + if !exists { + return nil, errors.New("不支持的时区: " + timezone) + } + + // 加载时区 + return time.LoadLocation(tzName) +} diff --git a/server/mcp/enter.go b/server/mcp/enter.go new file mode 100644 index 000000000..ca19f54c0 --- /dev/null +++ b/server/mcp/enter.go @@ -0,0 +1,31 @@ +package mcpTool + +import ( + "context" + "github.com/mark3labs/mcp-go/mcp" + "github.com/mark3labs/mcp-go/server" +) + +// McpTool 定义了MCP工具必须实现的接口 +type McpTool interface { + // Handle 返回工具调用信息 + Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + // New 返回工具注册信息 + New() mcp.Tool +} + +// 工具注册表 +var toolRegister = make(map[string]McpTool) + +// RegisterTool 供工具在init时调用,将自己注册到工具注册表中 +func RegisterTool(tool McpTool) { + mcpTool := tool.New() + toolRegister[mcpTool.Name] = tool +} + +// RegisterAllTools 将所有注册的工具注册到MCP服务中 +func RegisterAllTools(mcpServer *server.MCPServer) { + for _, tool := range toolRegister { + mcpServer.AddTool(tool.New(), tool.Handle) + } +} diff --git a/server/mcp/get_nickname.go b/server/mcp/get_nickname.go new file mode 100644 index 000000000..6ad55f6ab --- /dev/null +++ b/server/mcp/get_nickname.go @@ -0,0 +1,79 @@ +package mcpTool + +import ( + "context" + "errors" + "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system" + "github.com/mark3labs/mcp-go/mcp" + "gorm.io/gorm" +) + +func init() { + RegisterTool(&GetNickname{}) +} + +type GetNickname struct{} + +// 根据用户username获取nickname +func (t *GetNickname) New() mcp.Tool { + return mcp.NewTool("getNickname", + mcp.WithDescription("根据用户username获取nickname"), + mcp.WithString("username", + mcp.Required(), + mcp.Description("用户的username"), + )) +} + +// Handle 处理获取昵称的请求 +func (t *GetNickname) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // 1. 参数验证 + username, ok := request.GetArguments()["username"].(string) + if !ok { + return nil, errors.New("参数错误:username 必须是字符串类型") + } + + if username == "" { + return nil, errors.New("参数错误:username 不能为空") + } + + // 2. 记录操作日志 + global.GVA_LOG.Info("getNickname 工具被调用") + + // 3. 优化查询,只选择需要的字段 + var user struct { + NickName string + } + + err := global.GVA_DB.Model(&system.SysUser{}). + Select("nick_name"). + Where("username = ?", username). + First(&user).Error + + // 4. 优化错误处理 + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("用户 %s 不存在", username), + }, + }, + }, nil + } + global.GVA_LOG.Error("数据库查询错误") + return nil, errors.New("系统错误,请稍后再试") + } + + // 构造回复信息 + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.TextContent{ + Type: "text", + Text: fmt.Sprintf("用户 %s 的昵称是 %s", username, user.NickName), + }, + }, + }, nil +} diff --git a/server/middleware/casbin_rbac.go b/server/middleware/casbin_rbac.go index 8592a4a26..344fe9c64 100644 --- a/server/middleware/casbin_rbac.go +++ b/server/middleware/casbin_rbac.go @@ -6,13 +6,10 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" - "github.com/flipped-aurora/gin-vue-admin/server/service" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" ) -var casbinService = service.ServiceGroupApp.SystemServiceGroup.CasbinService - // CasbinHandler 拦截器 func CasbinHandler() gin.HandlerFunc { return func(c *gin.Context) { @@ -24,7 +21,7 @@ func CasbinHandler() gin.HandlerFunc { act := c.Request.Method // 获取用户的角色 sub := strconv.Itoa(int(waitUse.AuthorityId)) - e := casbinService.Casbin() // 判断策略中是否存在 + e := utils.GetCasbin() // 判断策略中是否存在 success, _ := e.Enforce(sub, obj, act) if !success { response.FailWithDetailed(gin.H{}, global.Translate("general.insufficientPermissions"), c) diff --git a/server/middleware/email.go b/server/middleware/email.go index 4a07561c9..1bc976cfb 100644 --- a/server/middleware/email.go +++ b/server/middleware/email.go @@ -11,13 +11,10 @@ import ( "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" ) -var userService = service.ServiceGroupApp.SystemServiceGroup.UserService - func ErrorToEmail() gin.HandlerFunc { return func(c *gin.Context) { var username string @@ -26,11 +23,12 @@ func ErrorToEmail() gin.HandlerFunc { username = claims.Username } else { id, _ := strconv.Atoi(c.Request.Header.Get("x-user-id")) - user, err := userService.FindUserById(id) + var u system.SysUser + err := global.GVA_DB.Where("id = ?", id).First(&u).Error if err != nil { username = "Unknown" } - username = user.Username + username = u.Username } body, _ := io.ReadAll(c.Request.Body) // 再重新写回请求体body中,ioutil.ReadAll会清空c.Request.Body中的数据 diff --git a/server/middleware/jwt.go b/server/middleware/jwt.go index 291d5e3d7..db71ed6b9 100644 --- a/server/middleware/jwt.go +++ b/server/middleware/jwt.go @@ -9,12 +9,9 @@ import ( "time" "github.com/flipped-aurora/gin-vue-admin/server/model/common/response" - "github.com/flipped-aurora/gin-vue-admin/server/service" "github.com/gin-gonic/gin" ) -var jwtService = service.ServiceGroupApp.SystemServiceGroup.JwtService - func JWTAuth() gin.HandlerFunc { return func(c *gin.Context) { // 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录 @@ -24,7 +21,7 @@ func JWTAuth() gin.HandlerFunc { c.Abort() return } - if jwtService.IsBlacklist(token) { + if isBlacklist(token) { response.NoAuth("您的帐户异地登陆或令牌失效", c) utils.ClearToken(c) c.Abort() @@ -65,7 +62,7 @@ func JWTAuth() gin.HandlerFunc { utils.SetToken(c, newToken, int(dr.Seconds())) if global.GVA_CONFIG.System.UseMultipoint { // 记录新的活跃jwt - _ = jwtService.SetRedisJWT(newToken, newClaims.Username) + _ = utils.SetRedisJWT(newToken, newClaims.Username) } } c.Next() @@ -78,3 +75,14 @@ func JWTAuth() gin.HandlerFunc { } } } + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: IsBlacklist +//@description: 判断JWT是否在黑名单内部 +//@param: jwt string +//@return: bool + +func isBlacklist(jwt string) bool { + _, ok := global.BlackCache.Get(jwt) + return ok +} diff --git a/server/middleware/operation.go b/server/middleware/operation.go index f34cf68ee..dd545f56f 100644 --- a/server/middleware/operation.go +++ b/server/middleware/operation.go @@ -15,13 +15,10 @@ import ( "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" ) -var operationRecordService = service.ServiceGroupApp.SystemServiceGroup.OperationRecordService - var respPool sync.Pool var bufferSize = 1024 @@ -115,8 +112,7 @@ func OperationRecord() gin.HandlerFunc { record.Body = "超出记录长度" } } - - if err := operationRecordService.CreateSysOperationRecord(record); err != nil { + if err := global.GVA_DB.Create(&record).Error; err != nil { global.GVA_LOG.Error("create operation record error:", zap.Error(err)) } } diff --git a/server/model/system/request/sys_auto_code.go b/server/model/system/request/sys_auto_code.go index c5d63024c..667031611 100644 --- a/server/model/system/request/sys_auto_code.go +++ b/server/model/system/request/sys_auto_code.go @@ -158,7 +158,7 @@ func (r *AutoCode) Pretreatment() error { r.NeedJSON = true case "time.Time": r.HasTimer = true - if r.Fields[i].FieldSearchType != "" { + if r.Fields[i].FieldSearchType != "" && r.Fields[i].FieldSearchType != "BETWEEN" && r.Fields[i].FieldSearchType != "NOT BETWEEN" { r.HasSearchTimer = true } } diff --git a/server/model/system/request/sys_auto_code_mcp.go b/server/model/system/request/sys_auto_code_mcp.go new file mode 100644 index 000000000..a52ec7c50 --- /dev/null +++ b/server/model/system/request/sys_auto_code_mcp.go @@ -0,0 +1,16 @@ +package request + +type AutoMcpTool struct { + Name string `json:"name" form:"name" binding:"required"` + Description string `json:"description" form:"description" binding:"required"` + Params []struct { + Name string `json:"name" form:"name" binding:"required"` + Description string `json:"description" form:"description" binding:"required"` + Type string `json:"type" form:"type" binding:"required"` // string, number, boolean, object, array + Required bool `json:"required" form:"required"` + Default string `json:"default" form:"default"` + } `json:"params" form:"params"` + Response []struct { + Type string `json:"type" form:"type" binding:"required"` // text, image + } `json:"response" form:"response"` +} diff --git a/server/model/system/request/sys_user.go b/server/model/system/request/sys_user.go index 45e478769..a48c46f2c 100644 --- a/server/model/system/request/sys_user.go +++ b/server/model/system/request/sys_user.go @@ -33,6 +33,11 @@ type ChangePasswordReq struct { NewPassword string `json:"newPassword"` // 新密码 } +type ResetPassword struct { + ID uint `json:"ID" form:"ID"` + Password string `json:"password" form:"password" gorm:"comment:用户登录密码"` // 用户登录密码 +} + // SetUserAuth Modify user's auth structure type SetUserAuth struct { AuthorityId uint `json:"authorityId"` // 角色ID diff --git a/server/model/system/sys_base_menu.go b/server/model/system/sys_base_menu.go index 41cf37631..99eea2197 100644 --- a/server/model/system/sys_base_menu.go +++ b/server/model/system/sys_base_menu.go @@ -6,35 +6,36 @@ import ( type SysBaseMenu struct { global.GVA_MODEL - MenuLevel uint `json:"-"` - ParentId uint `json:"parentId" gorm:"comment:父菜单ID"` // 父菜单ID - Path string `json:"path" gorm:"comment:路由path"` // 路由path - Name string `json:"name" gorm:"comment:路由name"` // 路由name - Hidden bool `json:"hidden" gorm:"comment:是否在列表隐藏"` // 是否在列表隐藏 - Component string `json:"component" gorm:"comment:对应前端文件路径"` // 对应前端文件路径 - Sort int `json:"sort" gorm:"comment:排序标记"` // 排序标记 - Meta `json:"meta" gorm:"embedded;comment:附加属性"` // 附加属性 - SysAuthoritys []SysAuthority `json:"authoritys" gorm:"many2many:sys_authority_menus;"` - Children []SysBaseMenu `json:"children" gorm:"-"` - Parameters []SysBaseMenuParameter `json:"parameters"` - MenuBtn []SysBaseMenuBtn `json:"menuBtn"` + MenuLevel uint `json:"-"` + ParentId uint `json:"parentId" gorm:"comment:父菜单ID"` // 父菜单ID + Path string `json:"path" gorm:"comment:路由path"` // 路由path + Name string `json:"name" gorm:"comment:路由name"` // 路由name + Hidden bool `json:"hidden" gorm:"comment:是否在列表隐藏"` // 是否在列表隐藏 + Component string `json:"component" gorm:"comment:对应前端文件路径"` // 对应前端文件路径 + Sort int `json:"sort" gorm:"comment:排序标记"` // 排序标记 + Meta `json:"meta" gorm:"embedded;comment:附加属性"` // 附加属性 + SysAuthoritys []SysAuthority `json:"authoritys" gorm:"many2many:sys_authority_menus;"` + Children []SysBaseMenu `json:"children" gorm:"-"` + Parameters []SysBaseMenuParameter `json:"parameters"` + MenuBtn []SysBaseMenuBtn `json:"menuBtn"` } type Meta struct { - ActiveName string `json:"activeName" gorm:"comment:高亮菜单"` - KeepAlive bool `json:"keepAlive" gorm:"comment:是否缓存"` // 是否缓存 - DefaultMenu bool `json:"defaultMenu" gorm:"comment:是否是基础路由(开发中)"` // 是否是基础路由(开发中) - Title string `json:"title" gorm:"comment:菜单名"` // 菜单名 - Icon string `json:"icon" gorm:"comment:菜单图标"` // 菜单图标 - CloseTab bool `json:"closeTab" gorm:"comment:自动关闭tab"` // 自动关闭tab + ActiveName string `json:"activeName" gorm:"comment:高亮菜单"` + KeepAlive bool `json:"keepAlive" gorm:"comment:是否缓存"` // 是否缓存 + DefaultMenu bool `json:"defaultMenu" gorm:"comment:是否是基础路由(开发中)"` // 是否是基础路由(开发中) + Title string `json:"title" gorm:"comment:菜单名"` // 菜单名 + Icon string `json:"icon" gorm:"comment:菜单图标"` // 菜单图标 + CloseTab bool `json:"closeTab" gorm:"comment:自动关闭tab"` // 自动关闭tab + TransitionType string `json:"transitionType" gorm:"comment:路由切换动画"` // 路由切换动画 } type SysBaseMenuParameter struct { global.GVA_MODEL SysBaseMenuID uint Type string `json:"type" gorm:"comment:地址栏携带参数为params还是query"` // 地址栏携带参数为params还是query - Key string `json:"key" gorm:"comment:地址栏携带参数的key"` // 地址栏携带参数的key - Value string `json:"value" gorm:"comment:地址栏携带参数的值"` // 地址栏携带参数的值 + Key string `json:"key" gorm:"comment:地址栏携带参数的key"` // 地址栏携带参数的key + Value string `json:"value" gorm:"comment:地址栏携带参数的值"` // 地址栏携带参数的值 } func (SysBaseMenu) TableName() string { diff --git a/server/plugin/email/README.MD b/server/plugin/email/README.MD index 17202838d..685cdd6d4 100644 --- a/server/plugin/email/README.MD +++ b/server/plugin/email/README.MD @@ -14,6 +14,7 @@ global.GVA_CONFIG.Email.Nickname, global.GVA_CONFIG.Email.Port, global.GVA_CONFIG.Email.IsSSL, + global.GVA_CONFIG.Email.IsLoginAuth, )) 同样也可以再传入时写死 @@ -26,6 +27,7 @@ "登录密钥", 465, true, + true, )) ### 2. 配置说明 @@ -34,13 +36,14 @@ //其中 Form 和 Secret 通常来说就是用户名和密码 type Email struct { - To string // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 此处配置主要用于发送错误监控邮件 - From string // 发件人 你自己要发邮件的邮箱 - Host string // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 - Secret string // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 - Nickname string // 昵称 发件人昵称 自定义即可 可以不填 - Port int // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 - IsSSL bool // 是否SSL 是否开启SSL + To string // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 此处配置主要用于发送错误监控邮件 + From string // 发件人 你自己要发邮件的邮箱 + Host string // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string // 昵称 发件人昵称 自定义即可 可以不填 + Port int // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool // 是否SSL 是否开启SSL + IsLoginAuth bool // 是否LoginAuth 是否使用LoginAuth认证方式(适用于IBM、微软邮箱服务器等) } #### 2-2 入参结构说明 //其中 Form 和 Secret 通常来说就是用户名和密码 diff --git a/server/plugin/email/config/email.go b/server/plugin/email/config/email.go index c535348c0..412b5a8e1 100644 --- a/server/plugin/email/config/email.go +++ b/server/plugin/email/config/email.go @@ -1,11 +1,12 @@ package config type Email struct { - To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 - From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 - Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 - Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 - Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 - Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 - IsSSL bool `mapstructure:"is-ssl" json:"isSSL" yaml:"is-ssl"` // 是否SSL 是否开启SSL + To string `mapstructure:"to" json:"to" yaml:"to"` // 收件人:多个以英文逗号分隔 例:a@qq.com b@qq.com 正式开发中请把此项目作为参数使用 + From string `mapstructure:"from" json:"from" yaml:"from"` // 发件人 你自己要发邮件的邮箱 + Host string `mapstructure:"host" json:"host" yaml:"host"` // 服务器地址 例如 smtp.qq.com 请前往QQ或者你要发邮件的邮箱查看其smtp协议 + Secret string `mapstructure:"secret" json:"secret" yaml:"secret"` // 密钥 用于登录的密钥 最好不要用邮箱密码 去邮箱smtp申请一个用于登录的密钥 + Nickname string `mapstructure:"nickname" json:"nickname" yaml:"nickname"` // 昵称 发件人昵称 通常为自己的邮箱 + Port int `mapstructure:"port" json:"port" yaml:"port"` // 端口 请前往QQ或者你要发邮件的邮箱查看其smtp协议 大多为 465 + IsSSL bool `mapstructure:"is-ssl" json:"isSSL" yaml:"is-ssl"` // 是否SSL 是否开启SSL + IsLoginAuth bool `mapstructure:"is-loginauth" json:"is-loginauth" yaml:"is-loginauth"` // 是否LoginAuth 是否使用LoginAuth认证 } diff --git a/server/plugin/email/main.go b/server/plugin/email/main.go index cfc8c46b1..37a635976 100644 --- a/server/plugin/email/main.go +++ b/server/plugin/email/main.go @@ -8,7 +8,7 @@ import ( type emailPlugin struct{} -func CreateEmailPlug(To, From, Host, Secret, Nickname string, Port int, IsSSL bool) *emailPlugin { +func CreateEmailPlug(To, From, Host, Secret, Nickname string, Port int, IsSSL bool, IsLoginAuth bool) *emailPlugin { global.GlobalConfig.To = To global.GlobalConfig.From = From global.GlobalConfig.Host = Host @@ -16,6 +16,7 @@ func CreateEmailPlug(To, From, Host, Secret, Nickname string, Port int, IsSSL bo global.GlobalConfig.Nickname = Nickname global.GlobalConfig.Port = Port global.GlobalConfig.IsSSL = IsSSL + global.GlobalConfig.IsLoginAuth = IsLoginAuth return &emailPlugin{} } diff --git a/server/plugin/email/utils/email.go b/server/plugin/email/utils/email.go index aa82e1c89..dd732d86b 100644 --- a/server/plugin/email/utils/email.go +++ b/server/plugin/email/utils/email.go @@ -60,8 +60,14 @@ func send(to []string, subject string, body string) error { host := global.GlobalConfig.Host port := global.GlobalConfig.Port isSSL := global.GlobalConfig.IsSSL + isLoginAuth := global.GlobalConfig.IsLoginAuth - auth := smtp.PlainAuth("", from, secret, host) + var auth smtp.Auth + if isLoginAuth { + auth = LoginAuth(from, secret) + } else { + auth = smtp.PlainAuth("", from, secret, host) + } e := email.NewEmail() if nickname != "" { e.From = fmt.Sprintf("%s <%s>", nickname, from) @@ -80,3 +86,37 @@ func send(to []string, subject string, body string) error { } return err } + +// LoginAuth 用于IBM、微软邮箱服务器的LOGIN认证方式 +type loginAuth struct { + username, password string +} + +func LoginAuth(username, password string) smtp.Auth { + return &loginAuth{username, password} +} + +func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", []byte{}, nil +} + +func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(a.username), nil + case "Password:": + return []byte(a.password), nil + default: + // 邮箱服务器可能发送的其他提示信息 + prompt := strings.ToLower(string(fromServer)) + if strings.Contains(prompt, "username") || strings.Contains(prompt, "user") { + return []byte(a.username), nil + } + if strings.Contains(prompt, "password") || strings.Contains(prompt, "pass") { + return []byte(a.password), nil + } + } + } + return nil, nil +} diff --git a/server/resource/function/api.go.tpl b/server/resource/function/api.go.tpl index 4b5bd666a..35a1cd5a6 100644 --- a/server/resource/function/api.go.tpl +++ b/server/resource/function/api.go.tpl @@ -7,8 +7,10 @@ // @Success 200 {object} response.Response{data=object,msg=string} "获取成功" // @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] func (a *{{.Abbreviation}}) {{.FuncName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() // 请添加自己的业务逻辑 - err := service{{ .StructName }}.{{.FuncName}}() + err := service{{ .StructName }}.{{.FuncName}}(ctx) if err != nil { global.GVA_LOG.Error("失败!", zap.Error(err)) response.FailWithMessage("失败", c) @@ -28,8 +30,10 @@ func (a *{{.Abbreviation}}) {{.FuncName}}(c *gin.Context) { // @Success 200 {object} response.Response{data=object,msg=string} "成功" // @Router /{{.Abbreviation}}/{{.Router}} [{{.Method}}] func ({{.Abbreviation}}Api *{{.StructName}}Api){{.FuncName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() // 请添加自己的业务逻辑 - err := {{.Abbreviation}}Service.{{.FuncName}}() + err := {{.Abbreviation}}Service.{{.FuncName}}(ctx) if err != nil { global.GVA_LOG.Error("失败!", zap.Error(err)) response.FailWithMessage("失败", c) diff --git a/server/resource/function/server.go.tpl b/server/resource/function/server.go.tpl index 1c5191c4b..732760486 100644 --- a/server/resource/function/server.go.tpl +++ b/server/resource/function/server.go.tpl @@ -8,7 +8,7 @@ // {{.FuncName}} {{.FuncDesc}} // Author [yourname](https://github.com/yourname) -func (s *{{.Abbreviation}}) {{.FuncName}}() (err error) { +func (s *{{.Abbreviation}}) {{.FuncName}}(ctx context.Context) (err error) { db := {{$db}}.Model(&model.{{.StructName}}{}) return db.Error } @@ -17,9 +17,9 @@ func (s *{{.Abbreviation}}) {{.FuncName}}() (err error) { // {{.FuncName}} {{.FuncDesc}} // Author [yourname](https://github.com/yourname) -func ({{.Abbreviation}}Service *{{.StructName}}Service){{.FuncName}}() (err error) { +func ({{.Abbreviation}}Service *{{.StructName}}Service){{.FuncName}}(ctx context.Context) (err error) { // 请在这里实现自己的业务逻辑 db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) return db.Error } -{{end}} \ No newline at end of file +{{end}} diff --git a/server/resource/mcp/tools.tpl b/server/resource/mcp/tools.tpl new file mode 100644 index 000000000..49bfa20b0 --- /dev/null +++ b/server/resource/mcp/tools.tpl @@ -0,0 +1,56 @@ +package mcpTool + +import ( + "context" + "github.com/mark3labs/mcp-go/mcp" +) + +func init() { + RegisterTool(&{{.Name | title}}{}) +} + +type {{.Name | title}} struct { +} + +// {{.Description}} +func (t *{{.Name | title}}) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // TODO: 实现工具逻辑 + // 参数示例: + // {{- range .Params}} + // {{.Name}} := request.GetArguments()["{{.Name}}"] + // {{- end}} + return &mcp.CallToolResult{ + Content: []mcp.Content{ + {{- range .Response}} + mcp.{{.Type | title}}Content{ + Type: "{{.Type}}", + // TODO: 填充{{.Type}}内容 + }, + {{- end}} + }, + }, nil +} + +func (t *{{.Name | title}}) New() mcp.Tool { + return mcp.NewTool("{{.Name}}", + mcp.WithDescription("{{.Description}}"), + {{- range .Params}} + mcp.With{{.Type | title}}("{{.Name}}", + {{- if .Required}}mcp.Required(),{{end}} + mcp.Description("{{.Description}}"), + {{- if .Default}} + {{- if eq .Type "string"}} + mcp.DefaultString("{{.Default}}"), + {{- else if eq .Type "number"}} + mcp.DefaultNumber({{.Default}}), + {{- else if eq .Type "boolean"}} + mcp.DefaultBoolean({{if or (eq .Default "true") (eq .Default "True")}}true{{else}}false{{end}}), + {{- else if eq .Type "array"}} + // 注意:数组默认值需要在后端代码中预处理为正确的格式 + // mcp.DefaultArray({{.Default}}), + {{- end}} + {{- end}} + ), + {{- end}} + ) +} diff --git a/server/resource/package/server/api/api.go.tpl b/server/resource/package/server/api/api.go.tpl index 5d9af2c5d..3daef1bf8 100644 --- a/server/resource/package/server/api/api.go.tpl +++ b/server/resource/package/server/api/api.go.tpl @@ -33,6 +33,9 @@ type {{.StructName}}Api struct {} // @Success 200 {object} response.Response{msg=string} "创建成功" // @Router /{{.Abbreviation}}/create{{.StructName}} [post] func ({{.Abbreviation}}Api *{{.StructName}}Api) Create{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + var {{.Abbreviation}} {{.Package}}.{{.StructName}} err := c.ShouldBindJSON(&{{.Abbreviation}}) if err != nil { @@ -42,7 +45,7 @@ func ({{.Abbreviation}}Api *{{.StructName}}Api) Create{{.StructName}}(c *gin.Con {{- if .AutoCreateResource }} {{.Abbreviation}}.CreatedBy = utils.GetUserID(c) {{- end }} - err = {{.Abbreviation}}Service.Create{{.StructName}}(&{{.Abbreviation}}) + err = {{.Abbreviation}}Service.Create{{.StructName}}(ctx,&{{.Abbreviation}}) if err != nil { global.GVA_LOG.Error(global.Translate("general.creationFail"), zap.Error(err)) response.FailWithMessage(global.Translate("general.creationFailErr"), c) @@ -61,11 +64,14 @@ func ({{.Abbreviation}}Api *{{.StructName}}Api) Create{{.StructName}}(c *gin.Con // @Success 200 {object} response.Response{msg=string} "删除成功" // @Router /{{.Abbreviation}}/delete{{.StructName}} [delete] func ({{.Abbreviation}}Api *{{.StructName}}Api) Delete{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") {{- if .AutoCreateResource }} userID := utils.GetUserID(c) {{- end }} - err := {{.Abbreviation}}Service.Delete{{.StructName}}({{.PrimaryField.FieldJson}} {{- if .AutoCreateResource -}},userID{{- end -}}) + err := {{.Abbreviation}}Service.Delete{{.StructName}}(ctx,{{.PrimaryField.FieldJson}} {{- if .AutoCreateResource -}},userID{{- end -}}) if err != nil { global.GVA_LOG.Error(global.Translate("general.deleteFail"), zap.Error(err)) response.FailWithMessage(global.Translate("general.deleteFailErr"), c) @@ -83,11 +89,14 @@ func ({{.Abbreviation}}Api *{{.StructName}}Api) Delete{{.StructName}}(c *gin.Con // @Success 200 {object} response.Response{msg=string} "批量删除成功" // @Router /{{.Abbreviation}}/delete{{.StructName}}ByIds [delete] func ({{.Abbreviation}}Api *{{.StructName}}Api) Delete{{.StructName}}ByIds(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + {{.PrimaryField.FieldJson}}s := c.QueryArray("{{.PrimaryField.FieldJson}}s[]") {{- if .AutoCreateResource }} userID := utils.GetUserID(c) {{- end }} - err := {{.Abbreviation}}Service.Delete{{.StructName}}ByIds({{.PrimaryField.FieldJson}}s{{- if .AutoCreateResource }},userID{{- end }}) + err := {{.Abbreviation}}Service.Delete{{.StructName}}ByIds(ctx,{{.PrimaryField.FieldJson}}s{{- if .AutoCreateResource }},userID{{- end }}) if err != nil { global.GVA_LOG.Error(global.Translate("system.sys_operation_record.batchDeleteFail"), zap.Error(err)) response.FailWithMessage(global.Translate("system.sys_operation_record.batchDeleteFailErr"), c) @@ -106,6 +115,9 @@ func ({{.Abbreviation}}Api *{{.StructName}}Api) Delete{{.StructName}}ByIds(c *gi // @Success 200 {object} response.Response{msg=string} "更新成功" // @Router /{{.Abbreviation}}/update{{.StructName}} [put] func ({{.Abbreviation}}Api *{{.StructName}}Api) Update{{.StructName}}(c *gin.Context) { + // 从ctx获取标准context进行业务行为 + ctx := c.Request.Context() + var {{.Abbreviation}} {{.Package}}.{{.StructName}} err := c.ShouldBindJSON(&{{.Abbreviation}}) if err != nil { @@ -115,7 +127,7 @@ func ({{.Abbreviation}}Api *{{.StructName}}Api) Update{{.StructName}}(c *gin.Con {{- if .AutoCreateResource }} {{.Abbreviation}}.UpdatedBy = utils.GetUserID(c) {{- end }} - err = {{.Abbreviation}}Service.Update{{.StructName}}({{.Abbreviation}}) + err = {{.Abbreviation}}Service.Update{{.StructName}}(ctx,{{.Abbreviation}}) if err != nil { global.GVA_LOG.Error(global.Translate("general.updateFail"), zap.Error(err)) response.FailWithMessage(global.Translate("general.updateFailErr"), c) @@ -134,8 +146,11 @@ func ({{.Abbreviation}}Api *{{.StructName}}Api) Update{{.StructName}}(c *gin.Con // @Success 200 {object} response.Response{data={{.Package}}.{{.StructName}},msg=string} "查询成功" // @Router /{{.Abbreviation}}/find{{.StructName}} [get] func ({{.Abbreviation}}Api *{{.StructName}}Api) Find{{.StructName}}(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + {{.PrimaryField.FieldJson}} := c.Query("{{.PrimaryField.FieldJson}}") - re{{.Abbreviation}}, err := {{.Abbreviation}}Service.Get{{.StructName}}({{.PrimaryField.FieldJson}}) + re{{.Abbreviation}}, err := {{.Abbreviation}}Service.Get{{.StructName}}(ctx,{{.PrimaryField.FieldJson}}) if err != nil { global.GVA_LOG.Error(global.Translate("general.queryFail"), zap.Error(err)) response.FailWithMessage(global.Translate("general.queryFailErr"), c) @@ -154,7 +169,10 @@ func ({{.Abbreviation}}Api *{{.StructName}}Api) Find{{.StructName}}(c *gin.Conte // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" // @Router /{{.Abbreviation}}/get{{.StructName}}List [get] func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}List(c *gin.Context) { - list, err := {{.Abbreviation}}Service.Get{{.StructName}}InfoList() + // 创建业务用Context + ctx := c.Request.Context() + + list, err := {{.Abbreviation}}Service.Get{{.StructName}}InfoList(ctx) if err != nil { global.GVA_LOG.Error("获取失败!", zap.Error(err)) response.FailWithMessage("获取失败:" + err.Error(), c) @@ -173,13 +191,16 @@ func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}List(c *gin.Co // @Success 200 {object} response.Response{data=response.PageResult,msg=string} "获取成功" // @Router /{{.Abbreviation}}/get{{.StructName}}List [get] func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}List(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + var pageInfo {{.Package}}Req.{{.StructName}}Search err := c.ShouldBindQuery(&pageInfo) if err != nil { response.FailWithMessage(err.Error(), c) return } - list, total, err := {{.Abbreviation}}Service.Get{{.StructName}}InfoList(pageInfo) + list, total, err := {{.Abbreviation}}Service.Get{{.StructName}}InfoList(ctx,pageInfo) if err != nil { global.GVA_LOG.Error(global.Translate("general.getDataFail"), zap.Error(err)) response.FailWithMessage(global.Translate("general.getDataFailErr"), c) @@ -203,8 +224,11 @@ func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}List(c *gin.Co // @Success 200 {object} response.Response{data=object,msg=string} "查询成功" // @Router /{{.Abbreviation}}/get{{.StructName}}DataSource [get] func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}DataSource(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + // 此接口为获取数据源定义的数据 - dataSource, err := {{.Abbreviation}}Service.Get{{.StructName}}DataSource() + dataSource, err := {{.Abbreviation}}Service.Get{{.StructName}}DataSource(ctx) if err != nil { global.GVA_LOG.Error(global.Translate("general.queryFail"), zap.Error(err)) response.FailWithMessage(global.Translate("general.queryFailErr"), c) @@ -224,9 +248,12 @@ func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}DataSource(c * // @Success 200 {object} response.Response{data=object,msg=string} "获取成功" // @Router /{{.Abbreviation}}/get{{.StructName}}Public [get] func ({{.Abbreviation}}Api *{{.StructName}}Api) Get{{.StructName}}Public(c *gin.Context) { + // 创建业务用Context + ctx := c.Request.Context() + // 此接口不需要鉴权 // 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑 - {{.Abbreviation}}Service.Get{{.StructName}}Public() + {{.Abbreviation}}Service.Get{{.StructName}}Public(ctx) response.OkWithDetailed(gin.H{ "info": "不需要鉴权的{{.Description}}接口信息", }, "获取成功", c) diff --git a/server/resource/package/server/model/model.go.tpl b/server/resource/package/server/model/model.go.tpl index 0330057c6..7589e658b 100644 --- a/server/resource/package/server/model/model.go.tpl +++ b/server/resource/package/server/model/model.go.tpl @@ -1,25 +1,7 @@ {{- if .IsAdd}} // 在结构体中新增如下字段 {{- range .Fields}} -{{- if eq .FieldType "enum" }} -{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};type:enum({{.DataTypeLong}});comment:{{.Comment}};" {{- if .Require }} binding:"required"{{- end -}}` -{{- else if eq .FieldType "picture" }} -{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` -{{- else if eq .FieldType "video" }} -{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` -{{- else if eq .FieldType "file" }} -{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` -{{- else if eq .FieldType "pictures" }} -{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` -{{- else if eq .FieldType "richtext" }} -{{.FieldName}} *string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}}` -{{- else if eq .FieldType "json" }} -{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end }} swaggertype:"object"` -{{- else if eq .FieldType "array" }} -{{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` -{{- else }} -{{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` -{{- end }} {{ if .FieldDesc }}//{{.FieldDesc}}{{ end }} + {{ GenerateField . }} {{- end }} {{ else }} @@ -47,25 +29,7 @@ type {{.StructName}} struct { global.GVA_MODEL {{- end }} {{- range .Fields}} - {{- if eq .FieldType "enum" }} - {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};type:enum({{.DataTypeLong}});comment:{{.Comment}};" {{- if .Require }} binding:"required"{{- end -}}` - {{- else if eq .FieldType "picture" }} - {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` - {{- else if eq .FieldType "video" }} - {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` - {{- else if eq .FieldType "file" }} - {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` - {{- else if eq .FieldType "pictures" }} - {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` - {{- else if eq .FieldType "richtext" }} - {{.FieldName}} *string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end -}}` - {{- else if eq .FieldType "json" }} - {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end }} swaggertype:"object"` - {{- else if eq .FieldType "array" }} - {{.FieldName}} datatypes.JSON `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}type:text;" {{- if .Require }} binding:"required"{{- end }} swaggertype:"array,object"` - {{- else }} - {{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" gorm:"{{- if ne .FieldIndexType "" -}}{{ .FieldIndexType }};{{- end -}}{{- if .PrimaryKey -}}primarykey;{{- end -}}{{- if .DefaultValue -}}default:{{ .DefaultValue }};{{- end -}}column:{{.ColumnName}};comment:{{.Comment}};{{- if .DataTypeLong -}}size:{{.DataTypeLong}};{{- end -}}" {{- if .Require }} binding:"required"{{- end -}}` - {{- end }} {{ if .FieldDesc }}//{{.FieldDesc}}{{ end }} + {{ GenerateField . }} {{- end }} {{- if .AutoCreateResource }} CreatedBy uint `gorm:"column:created_by;comment:创建者"` diff --git a/server/resource/package/server/model/request/request.go.tpl b/server/resource/package/server/model/request/request.go.tpl index ee5816da3..f8749f331 100644 --- a/server/resource/package/server/model/request/request.go.tpl +++ b/server/resource/package/server/model/request/request.go.tpl @@ -2,16 +2,7 @@ // 在结构体中新增如下字段 {{- range .Fields}} {{- if ne .FieldSearchType ""}} - {{- if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} -Start{{.FieldName}} *{{.FieldType}} `json:"start{{.FieldName}}" form:"start{{.FieldName}}"` -End{{.FieldName}} *{{.FieldType}} `json:"end{{.FieldName}}" form:"end{{.FieldName}}"` - {{- else }} - {{- if or (eq .FieldType "enum") (eq .FieldType "picture") (eq .FieldType "pictures") (eq .FieldType "video") (eq .FieldType "json") }} -{{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` - {{- else }} -{{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` - {{- end }} - {{- end }} + {{ GenerateSearchField . }} {{- end}} {{- end }} {{- if .NeedSort}} @@ -24,28 +15,18 @@ package request import ( {{- if not .OnlyTemplate }} "{{.Module}}/model/common/request" - {{ if or .HasSearchTimer .GvaModel}}"time"{{ end }} + {{ if or .HasSearchTimer .GvaModel }}"time"{{ end }} {{- end }} ) type {{.StructName}}Search struct{ {{- if not .OnlyTemplate}} {{- if .GvaModel }} - StartCreatedAt *time.Time `json:"startCreatedAt" form:"startCreatedAt"` - EndCreatedAt *time.Time `json:"endCreatedAt" form:"endCreatedAt"` + CreatedAtRange []time.Time `json:"createdAtRange" form:"createdAtRange[]"` {{- end }} {{- range .Fields}} {{- if ne .FieldSearchType ""}} - {{- if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} - Start{{.FieldName}} *{{.FieldType}} `json:"start{{.FieldName}}" form:"start{{.FieldName}}"` - End{{.FieldName}} *{{.FieldType}} `json:"end{{.FieldName}}" form:"end{{.FieldName}}"` - {{- else }} - {{- if or (eq .FieldType "enum") (eq .FieldType "picture") (eq .FieldType "pictures") (eq .FieldType "video") (eq .FieldType "json") }} - {{.FieldName}} string `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` - {{- else }} - {{.FieldName}} *{{.FieldType}} `json:"{{.FieldJson}}" form:"{{.FieldJson}}" ` - {{- end }} - {{- end }} + {{ GenerateSearchField . }} {{- end}} {{- end }} request.PageInfo diff --git a/server/resource/package/server/service/service.go.tpl b/server/resource/package/server/service/service.go.tpl index 0d6e6a83c..8226d189f 100644 --- a/server/resource/package/server/service/service.go.tpl +++ b/server/resource/package/server/service/service.go.tpl @@ -8,29 +8,7 @@ {{- if .IsAdd}} // Get{{.StructName}}InfoList 新增搜索语句 - {{- range .Fields}} - {{- if .FieldSearchType}} - {{- if or (eq .FieldType "enum") (eq .FieldType "pictures") (eq .FieldType "picture") (eq .FieldType "video") (eq .FieldType "json") }} -if info.{{.FieldName}} != "" { - {{- if or (eq .FieldType "enum") }} - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+ {{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) - {{- else}} -// 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 - {{- end}} -} - {{- else if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} -if info.Start{{.FieldName}} != nil && info.End{{.FieldName}} != nil { - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ? AND ? ",info.Start{{.FieldName}},info.End{{.FieldName}}) -} - {{- else}} -if info.{{.FieldName}} != nil{{- if eq .FieldType "string" }} && *info.{{.FieldName}} != ""{{- end }} { - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+{{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) -} - {{- end }} - {{- end }} - {{- end }} - - + {{ GenerateSearchConditions .Fields }} // Get{{.StructName}}InfoList 新增排序语句 请自行在搜索语句中添加orderMap内容 {{- range .Fields}} {{- if .Sort}} @@ -58,6 +36,7 @@ package {{.Package}} import ( {{- if not .OnlyTemplate }} + "context" "{{.Module}}/global" "{{.Module}}/model/{{.Package}}" {{- if not .IsTree}} @@ -77,14 +56,14 @@ type {{.StructName}}Service struct {} {{- if not .OnlyTemplate }} // Create{{.StructName}} 创建{{.Description}}记录 // Author [yourname](https://github.com/yourname) -func ({{.Abbreviation}}Service *{{.StructName}}Service) Create{{.StructName}}({{.Abbreviation}} *{{.Package}}.{{.StructName}}) (err error) { +func ({{.Abbreviation}}Service *{{.StructName}}Service) Create{{.StructName}}(ctx context.Context, {{.Abbreviation}} *{{.Package}}.{{.StructName}}) (err error) { err = {{$db}}.Create({{.Abbreviation}}).Error return err } // Delete{{.StructName}} 删除{{.Description}}记录 // Author [yourname](https://github.com/yourname) -func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}({{.PrimaryField.FieldJson}} string{{- if .AutoCreateResource -}},userID uint{{- end -}}) (err error) { +func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string{{- if .AutoCreateResource -}},userID uint{{- end -}}) (err error) { {{- if .IsTree }} var count int64 err = {{$db}}.Find(&{{.Package}}.{{.StructName}}{},"parent_id = ?",{{.PrimaryField.FieldJson}}).Count(&count).Error @@ -114,7 +93,7 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}({{. // Delete{{.StructName}}ByIds 批量删除{{.Description}}记录 // Author [yourname](https://github.com/yourname) -func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}ByIds({{.PrimaryField.FieldJson}}s []string {{- if .AutoCreateResource }},deleted_by uint{{- end}}) (err error) { +func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}ByIds(ctx context.Context, {{.PrimaryField.FieldJson}}s []string {{- if .AutoCreateResource }},deleted_by uint{{- end}}) (err error) { {{- if .AutoCreateResource }} err = {{$db}}.Transaction(func(tx *gorm.DB) error { if err := tx.Model(&{{.Package}}.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} in ?", {{.PrimaryField.FieldJson}}s).Update("deleted_by", deleted_by).Error; err != nil { @@ -133,14 +112,14 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Delete{{.StructName}}ById // Update{{.StructName}} 更新{{.Description}}记录 // Author [yourname](https://github.com/yourname) -func ({{.Abbreviation}}Service *{{.StructName}}Service)Update{{.StructName}}({{.Abbreviation}} {{.Package}}.{{.StructName}}) (err error) { +func ({{.Abbreviation}}Service *{{.StructName}}Service)Update{{.StructName}}(ctx context.Context, {{.Abbreviation}} {{.Package}}.{{.StructName}}) (err error) { err = {{$db}}.Model(&{{.Package}}.{{.StructName}}{}).Where("{{.PrimaryField.ColumnName}} = ?",{{.Abbreviation}}.{{.PrimaryField.FieldName}}).Updates(&{{.Abbreviation}}).Error return err } // Get{{.StructName}} 根据{{.PrimaryField.FieldJson}}获取{{.Description}}记录 // Author [yourname](https://github.com/yourname) -func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}({{.PrimaryField.FieldJson}} string) ({{.Abbreviation}} {{.Package}}.{{.StructName}}, err error) { +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}(ctx context.Context, {{.PrimaryField.FieldJson}} string) ({{.Abbreviation}} {{.Package}}.{{.StructName}}, err error) { err = {{$db}}.Where("{{.PrimaryField.ColumnName}} = ?", {{.PrimaryField.FieldJson}}).First(&{{.Abbreviation}}).Error return } @@ -149,7 +128,7 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}({{.Pri {{- if .IsTree }} // Get{{.StructName}}InfoList 分页获取{{.Description}}记录,Tree模式下不添加分页和搜索 // Author [yourname](https://github.com/yourname) -func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoList() (list []*{{.Package}}.{{.StructName}},err error) { +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoList(ctx context.Context) (list []*{{.Package}}.{{.StructName}},err error) { // 创建db db := {{$db}}.Model(&{{.Package}}.{{.StructName}}{}) var {{.Abbreviation}}s []*{{.Package}}.{{.StructName}} @@ -161,7 +140,7 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoLis {{- else }} // Get{{.StructName}}InfoList 分页获取{{.Description}}记录 // Author [yourname](https://github.com/yourname) -func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoList(info {{.Package}}Req.{{.StructName}}Search) (list []{{.Package}}.{{.StructName}}, total int64, err error) { +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoList(ctx context.Context, info {{.Package}}Req.{{.StructName}}Search) (list []{{.Package}}.{{.StructName}}, total int64, err error) { limit := info.PageSize offset := info.PageSize * (info.Page - 1) // 创建db @@ -169,31 +148,11 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoLis var {{.Abbreviation}}s []{{.Package}}.{{.StructName}} // 如果有条件搜索 下方会自动创建搜索语句 {{- if .GvaModel }} - if info.StartCreatedAt !=nil && info.EndCreatedAt !=nil { - db = db.Where("created_at BETWEEN ? AND ?", info.StartCreatedAt, info.EndCreatedAt) + if len(info.CreatedAtRange) == 2 { + db = db.Where("created_at BETWEEN ? AND ?", info.CreatedAtRange[0], info.CreatedAtRange[1]) } {{- end }} - {{- range .Fields}} - {{- if .FieldSearchType}} - {{- if or (eq .FieldType "enum") (eq .FieldType "pictures") (eq .FieldType "picture") (eq .FieldType "video") (eq .FieldType "json") }} - if info.{{.FieldName}} != "" { - {{- if or (eq .FieldType "enum")}} - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+ {{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) - {{- else}} - // 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 - {{- end}} - } - {{- else if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} - if info.Start{{.FieldName}} != nil && info.End{{.FieldName}} != nil { - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ? AND ? ",info.Start{{.FieldName}},info.End{{.FieldName}}) - } - {{- else}} - if info.{{.FieldName}} != nil{{- if eq .FieldType "string" }} && *info.{{.FieldName}} != ""{{- end }} { - db = db.Where("{{.ColumnName}} {{.FieldSearchType}} ?",{{if eq .FieldSearchType "LIKE"}}"%"+{{ end }}*info.{{.FieldName}}{{if eq .FieldSearchType "LIKE"}}+"%"{{ end }}) - } - {{- end }} - {{- end }} - {{- end }} + {{ GenerateSearchConditions .Fields }} err = db.Count(&total).Error if err!=nil { return @@ -201,6 +160,10 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoLis {{- if .NeedSort}} var OrderStr string orderMap := make(map[string]bool) + {{- if .GvaModel }} + orderMap["ID"] = true + orderMap["CreatedAt"] = true + {{- end }} {{- range .Fields}} {{- if .Sort}} orderMap["{{.ColumnName}}"] = true @@ -226,13 +189,13 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}InfoLis {{- end }} {{- if .HasDataSource }} -func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}DataSource() (res map[string][]map[string]any, err error) { +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}DataSource(ctx context.Context) (res map[string][]map[string]any, err error) { res = make(map[string][]map[string]any) {{range $key, $value := .DataSourceMap}} {{$key}} := make([]map[string]any, 0) {{ $dataDB := "" }} {{- if eq $value.DBName "" }} - {{ $dataDB = $db }} + {{ $dataDB = "global.GVA_DB" }} {{- else}} {{ $dataDB = printf "global.MustGetGlobalDBByDBName(\"%s\")" $value.DBName }} {{- end}} @@ -243,7 +206,7 @@ func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}DataSou } {{- end }} {{- end }} -func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}Public() { +func ({{.Abbreviation}}Service *{{.StructName}}Service)Get{{.StructName}}Public(ctx context.Context) { // 此方法为获取数据源定义的数据 // 请自行实现 } diff --git a/server/resource/package/web/view/form.vue.tpl b/server/resource/package/web/view/form.vue.tpl index 3f9bf6b98..d1047a410 100644 --- a/server/resource/package/web/view/form.vue.tpl +++ b/server/resource/package/web/view/form.vue.tpl @@ -2,75 +2,10 @@ {{- if .IsAdd }} // 新增表单中增加如下代码 {{- range .Fields}} - {{- if .Form}} - - {{- if .CheckDataSource}} - - - - {{- else }} - {{- if eq .FieldType "bool" }} - - {{- end }} - {{- if eq .FieldType "string" }} - {{- if .DictType}} - - - - {{- else }} - - {{- end }} - {{- end }} - {{- if eq .FieldType "richtext" }} - - {{- end }} - {{- if eq .FieldType "json" }} - // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 - {{"{{"}} formData.{{.FieldJson}} {{"}}"}} - {{- end }} - {{- if eq .FieldType "array" }} - - {{- end }} - {{- if eq .FieldType "int" }} - - {{- end }} - {{- if eq .FieldType "time.Time" }} - - {{- end }} - {{- if eq .FieldType "float64" }} - - {{- end }} - {{- if eq .FieldType "enum" }} - - - - {{- end }} - {{- if eq .FieldType "picture" }} - - {{- end }} - {{- if eq .FieldType "pictures" }} - - {{- end }} - {{- if eq .FieldType "video" }} - - {{- end }} - {{- if eq .FieldType "file" }} - - {{- end }} - {{- end }} - - {{- end }} - {{- end }} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} +{{- end }} // 字典增加如下代码 {{- range $index, $element := .DictTypes}} @@ -86,42 +21,7 @@ const {{ $element }}Options = ref([]) // 基础formData结构增加如下字段 {{- range .Fields}} {{- if .Form}} - {{- if eq .FieldType "bool" }} -{{.FieldJson}}: false, - {{- end }} - {{- if eq .FieldType "string" }} -{{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "richtext" }} -{{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "int" }} -{{.FieldJson}}: {{- if or .DataSource}} undefined{{ else }} 0{{- end }}, - {{- end }} - {{- if eq .FieldType "time.Time" }} -{{.FieldJson}}: new Date(), - {{- end }} - {{- if eq .FieldType "float64" }} -{{.FieldJson}}: 0, - {{- end }} - {{- if eq .FieldType "picture" }} -{{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "video" }} -{{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "pictures" }} -{{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "file" }} -{{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "json" }} -{{.FieldJson}}: {}, - {{- end }} - {{- if eq .FieldType "array" }} -{{.FieldJson}}: [], - {{- end }} + {{ GenerateDefaultFormValue . }} {{- end }} {{- end }} // 验证规则中增加如下字段 @@ -182,62 +82,7 @@ getDataSourceFunc() {{- end }} {{- range .Fields}} {{- if .Form }} - - {{- if .CheckDataSource}} - - - - {{- else }} - {{- if eq .FieldType "bool" }} - - {{- end }} - {{- if eq .FieldType "string" }} - {{- if .DictType}} - - - - {{- else }} - - {{- end }} - {{- end }} - {{- if eq .FieldType "richtext" }} - - {{- end }} - {{- if eq .FieldType "int" }} - - {{- end }} - {{- if eq .FieldType "time.Time" }} - - {{- end }} - {{- if eq .FieldType "float64" }} - - {{- end }} - {{- if eq .FieldType "enum" }} - - - - {{- end }} - {{- if eq .FieldType "picture" }} - - {{- end }} - {{- if eq .FieldType "video" }} - - {{- end }} - {{- if eq .FieldType "pictures" }} - - {{- end }} - {{- if eq .FieldType "file" }} - - {{- end }} - {{- if eq .FieldType "json" }} - // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 - {{"{{"}} formData.{{.FieldJson}} {{"}}"}} - {{- end }} - {{- if eq .FieldType "array" }} - - {{- end }} - {{- end }} - + {{ GenerateFormItem . }} {{- end }} {{- end }} @@ -336,42 +181,7 @@ const formData = ref({ {{- end }} {{- range .Fields}} {{- if .Form }} - {{- if eq .FieldType "bool" }} - {{.FieldJson}}: false, - {{- end }} - {{- if eq .FieldType "string" }} - {{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "richtext" }} - {{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "int" }} - {{.FieldJson}}: {{- if or .DataSource }} undefined{{ else }} 0{{- end }}, - {{- end }} - {{- if eq .FieldType "time.Time" }} - {{.FieldJson}}: new Date(), - {{- end }} - {{- if eq .FieldType "float64" }} - {{.FieldJson}}: 0, - {{- end }} - {{- if eq .FieldType "picture" }} - {{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "video" }} - {{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "pictures" }} - {{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "file" }} - {{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "json" }} - {{.FieldJson}}: {}, - {{- end }} - {{- if eq .FieldType "array" }} - {{.FieldJson}}: [], - {{- end }} + {{ GenerateDefaultFormValue . }} {{- end }} {{- end }} }) diff --git a/server/resource/package/web/view/table.vue.tpl b/server/resource/package/web/view/table.vue.tpl index c8b72e1dd..d73e97aa2 100644 --- a/server/resource/package/web/view/table.vue.tpl +++ b/server/resource/package/web/view/table.vue.tpl @@ -3,501 +3,60 @@ {{- $templateID := printf "%s_%s" .Package .StructName }} {{- if .IsAdd }} + // 请在搜索条件中增加如下代码 -{{- range .Fields}} {{- if .FieldSearchType}} {{- if eq .FieldType "bool" }} - - - - - - - - - {{- else if .DictType}} - - - - - - {{- else if .CheckDataSource}} - - - - - - {{- else}} - - {{- if eq .FieldType "float64" "int"}} - {{if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} - -— - - {{- else}} - - {{- end}} - {{- else if eq .FieldType "time.Time"}} - {{if eq .FieldSearchType "BETWEEN" "NOT BETWEEN"}} - - -— - - {{- else}} - - {{- end}} - {{- else}} - - {{- end}} -{{ end }}{{ end }}{{ end }} +{{- range .Fields}} + {{- if .FieldSearchType}} +{{ GenerateSearchFormItem .}} + {{ end }} +{{ end }} // 表格增加如下列代码 {{- range .Fields}} - {{- if .Table}} - {{- if .CheckDataSource }} - - - - {{- else if .DictType}} - - - -{{- else if eq .FieldType "bool" }} - - - - {{- else if eq .FieldType "time.Time" }} - - - - {{- else if eq .FieldType "picture" }} - - - - {{- else if eq .FieldType "pictures" }} - - - - {{- else if eq .FieldType "video" }} - - - - {{- else if eq .FieldType "richtext" }} - - - - {{- else if eq .FieldType "file" }} - - - - {{- else if eq .FieldType "json" }} - - - - {{- else if eq .FieldType "array" }} - - - - {{- else }} - - {{- end }} -{{- end }} + {{- if .Table}} + {{ GenerateTableColumn . }} + {{- end }} {{- end }} // 新增表单中增加如下代码 {{- range .Fields}} -{{- if .Form}} - -{{- if .CheckDataSource}} - - - -{{- else }} -{{- if eq .FieldType "bool" }} - -{{- end }} -{{- if eq .FieldType "string" }} -{{- if .DictType}} - - - -{{- else }} - -{{- end }} -{{- end }} -{{- if eq .FieldType "richtext" }} - -{{- end }} -{{- if eq .FieldType "json" }} - // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.{{.FieldJson}} 后端会按照json的类型进行存取 - {{"{{"}} formData.{{.FieldJson}} {{"}}"}} - {{- end }} - {{- if eq .FieldType "array" }} - {{- if .DictType}} - - - - {{- else }} - - {{- end }} - {{- end }} - {{- if eq .FieldType "int" }} - - {{- end }} - {{- if eq .FieldType "time.Time" }} - - {{- end }} - {{- if eq .FieldType "float64" }} - -{{- end }} -{{- if eq .FieldType "enum" }} - - - -{{- end }} -{{- if eq .FieldType "picture" }} - -{{- end }} -{{- if eq .FieldType "pictures" }} - -{{- end }} -{{- if eq .FieldType "video" }} - - {{- end }} -{{- if eq .FieldType "file" }} - -{{- end }} -{{- end }} - -{{- end }} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} {{- end }} // 查看抽屉中增加如下代码 {{- range .Fields}} {{- if .Desc }} - -{{- if .CheckDataSource }} - - {{- else if and (ne .FieldType "picture" ) (ne .FieldType "pictures" ) (ne .FieldType "file" ) (ne .FieldType "array" ) }} - {{"{{"}} detailFrom.{{.FieldJson}} {{"}}"}} - {{- else }} - {{- if eq .FieldType "picture" }} - - {{- end }} - {{- if eq .FieldType "array" }} - - {{- end }} - {{- if eq .FieldType "pictures" }} - - {{- end }} - {{- if eq .FieldType "richtext" }} - - {{- end }} - {{- if eq .FieldType "file" }} -
- - - {{"{{"}}item.name{{"}}"}} - -
- {{- end }} - {{- end }} -
+ {{ GenerateDescriptionItem . }} {{- end }} {{- end }} -// 字典增加如下代码 - {{- range $index, $element := .DictTypes}} -const {{ $element }}Options = ref([]) - {{- end }} - -// setOptions方法中增加如下调用 - -{{- range $index, $element := .DictTypes }} - {{ $element }}Options.value = await getDictFunc('{{$element}}') -{{- end }} - -// 基础formData结构(变量处和关闭表单处)增加如下字段 -{{- range .Fields}} - {{- if .Form}} - {{- if eq .FieldType "bool" }} -{{.FieldJson}}: false, - {{- end }} - {{- if eq .FieldType "string" }} -{{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "richtext" }} -{{.FieldJson}}: '', - {{- end }} - {{- if eq .FieldType "int" }} -{{.FieldJson}}: {{- if .DataSource}} undefined{{ else }} 0{{- end }}, - {{- end }} - {{- if eq .FieldType "time.Time" }} -{{.FieldJson}}: new Date(), - {{- end }} - {{- if eq .FieldType "float64" }} -{{.FieldJson}}: 0, - {{- end }} - {{- if eq .FieldType "picture" }} -{{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "video" }} -{{.FieldJson}}: "", - {{- end }} - {{- if eq .FieldType "pictures" }} -{{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "file" }} -{{.FieldJson}}: [], - {{- end }} - {{- if eq .FieldType "json" }} -{{.FieldJson}}: {}, - {{- end }} - {{- if eq .FieldType "array" }} -{{.FieldJson}}: [], - {{- end }} - {{- end }} - {{- end }} -// 验证规则中增加如下字段 - -{{- range .Fields }} - {{- if .Form }} - {{- if eq .Require true }} -{{.FieldJson }} : [{ - required: true, - message: '{{ .ErrorText }}', - trigger: ['input','blur'], -}, - {{- if eq .FieldType "string" }} -{ - whitespace: true, - message: t('general.noOnlySpace'), - trigger: ['input', 'blur'], -} - {{- end }} -], - {{- end }} - {{- end }} - {{- end }} - - - -{{- if .HasDataSource }} -// 请引用 -get{{.StructName}}DataSource, - -// 获取数据源 -const dataSource = ref({}) -const getDataSourceFunc = async()=>{ - const res = await get{{.StructName}}DataSource() - if (res.code === 0) { - dataSource.value = res.data - } -} -getDataSourceFunc() -{{- end }} - -{{- else }} - -{{- if not .OnlyTemplate}} - - - - - -{{- else}} - - - -{{- end }} - -{{- end }} diff --git a/server/resource/plugin/web/view/view.vue.tpl b/server/resource/plugin/web/view/view.vue.tpl new file mode 100644 index 000000000..6880840a7 --- /dev/null +++ b/server/resource/plugin/web/view/view.vue.tpl @@ -0,0 +1,689 @@ +{{- $global := . }} +{{- $templateID := printf "%s_%s" .Package .StructName }} +{{- if .IsAdd }} +// 请在搜索条件中增加如下代码 +{{- range .Fields}} + {{- if .FieldSearchType}} +{{ GenerateSearchFormItem .}} + {{ end }} +{{ end }} + + +// 表格增加如下列代码 + +{{- range .Fields}} + {{- if .Table}} + {{ GenerateTableColumn . }} + {{- end }} +{{- end }} + +// 新增表单中增加如下代码 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateFormItem . }} + {{- end }} +{{- end }} + +// 查看抽屉中增加如下代码 + +{{- range .Fields}} + {{- if .Desc }} + {{ GenerateDescriptionItem . }} + {{- end }} + {{- end }} + +// 字典增加如下代码 + {{- range $index, $element := .DictTypes}} +const {{ $element }}Options = ref([]) + {{- end }} + +// setOptions方法中增加如下调用 + +{{- range $index, $element := .DictTypes }} + {{ $element }}Options.value = await getDictFunc('{{$element}}') +{{- end }} + +// 基础formData结构(变量处和关闭表单处)增加如下字段 +{{- range .Fields}} + {{- if .Form}} + {{ GenerateDefaultFormValue . }} + {{- end }} + {{- end }} +// 验证规则中增加如下字段 + +{{- range .Fields }} + {{- if .Form }} + {{- if eq .Require true }} +{{.FieldJson }} : [{ + required: true, + message: '{{ .ErrorText }}', + trigger: ['input','blur'], +}, + {{- if eq .FieldType "string" }} +{ + whitespace: true, + message: '不能只输入空格', + trigger: ['input', 'blur'], +} + {{- end }} +], + {{- end }} + {{- end }} + {{- end }} + + + +{{- if .HasDataSource }} +// 请引用 +get{{.StructName}}DataSource, + +// 获取数据源 +const dataSource = ref({}) +const getDataSourceFunc = async()=>{ + const res = await get{{.StructName}}DataSource() + if (res.code === 0) { + dataSource.value = res.data || [] + } +} +getDataSourceFunc() +{{- end }} + +{{- else }} + +{{- if not .OnlyTemplate}} + + + + + +{{- else}} + + + +{{- end }} + +{{- end }} diff --git a/server/router/system/sys_auto_code.go b/server/router/system/sys_auto_code.go index e25e1cef8..ef89245ca 100644 --- a/server/router/system/sys_auto_code.go +++ b/server/router/system/sys_auto_code.go @@ -19,6 +19,11 @@ func (s *AutoCodeRouter) InitAutoCodeRouter(Router *gin.RouterGroup, RouterPubli autoCodeRouter.POST("createTemp", autoCodeTemplateApi.Create) // 创建自动化代码 autoCodeRouter.POST("addFunc", autoCodeTemplateApi.AddFunc) // 为代码插入方法 } + { + autoCodeRouter.POST("mcp", autoCodeTemplateApi.MCP) // 自动创建Mcp Tool模板 + autoCodeRouter.POST("mcpList", autoCodeTemplateApi.MCPList) // 获取MCP ToolList + autoCodeRouter.POST("mcpTest", autoCodeTemplateApi.MCPTest) // MCP 工具测试 + } { autoCodeRouter.POST("getPackage", autoCodePackageApi.All) // 获取package包 autoCodeRouter.POST("delPackage", autoCodePackageApi.Delete) // 删除package包 diff --git a/server/router/system/sys_export_template.go b/server/router/system/sys_export_template.go index 3e92a90c1..e7fd5beef 100644 --- a/server/router/system/sys_export_template.go +++ b/server/router/system/sys_export_template.go @@ -9,20 +9,26 @@ type SysExportTemplateRouter struct { } // InitSysExportTemplateRouter 初始化 导出模板 路由信息 -func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.RouterGroup) { +func (s *SysExportTemplateRouter) InitSysExportTemplateRouter(Router *gin.RouterGroup, pubRouter *gin.RouterGroup) { sysExportTemplateRouter := Router.Group("sysExportTemplate").Use(middleware.OperationRecord()) sysExportTemplateRouterWithoutRecord := Router.Group("sysExportTemplate") + sysExportTemplateRouterWithoutAuth := pubRouter.Group("sysExportTemplate") + { sysExportTemplateRouter.POST("createSysExportTemplate", exportTemplateApi.CreateSysExportTemplate) // 新建导出模板 sysExportTemplateRouter.DELETE("deleteSysExportTemplate", exportTemplateApi.DeleteSysExportTemplate) // 删除导出模板 sysExportTemplateRouter.DELETE("deleteSysExportTemplateByIds", exportTemplateApi.DeleteSysExportTemplateByIds) // 批量删除导出模板 sysExportTemplateRouter.PUT("updateSysExportTemplate", exportTemplateApi.UpdateSysExportTemplate) // 更新导出模板 - sysExportTemplateRouter.POST("importExcel", exportTemplateApi.ImportExcel) // 更新导出模板 + sysExportTemplateRouter.POST("importExcel", exportTemplateApi.ImportExcel) // 导入excel模板数据 } { sysExportTemplateRouterWithoutRecord.GET("findSysExportTemplate", exportTemplateApi.FindSysExportTemplate) // 根据ID获取导出模板 sysExportTemplateRouterWithoutRecord.GET("getSysExportTemplateList", exportTemplateApi.GetSysExportTemplateList) // 获取导出模板列表 - sysExportTemplateRouterWithoutRecord.GET("exportExcel", exportTemplateApi.ExportExcel) // 导出表格 + sysExportTemplateRouterWithoutRecord.GET("exportExcel", exportTemplateApi.ExportExcel) // 获取导出token sysExportTemplateRouterWithoutRecord.GET("exportTemplate", exportTemplateApi.ExportTemplate) // 导出表格模板 } + { + sysExportTemplateRouterWithoutAuth.GET("exportExcelByToken", exportTemplateApi.ExportExcelByToken) // 通过token导出表格 + sysExportTemplateRouterWithoutAuth.GET("exportTemplateByToken", exportTemplateApi.ExportTemplateByToken) // 通过token导出模板 + } } diff --git a/server/router/system/sys_operation_record.go b/server/router/system/sys_operation_record.go index 11b841db7..d158d5e70 100644 --- a/server/router/system/sys_operation_record.go +++ b/server/router/system/sys_operation_record.go @@ -9,7 +9,6 @@ type OperationRecordRouter struct{} func (s *OperationRecordRouter) InitSysOperationRecordRouter(Router *gin.RouterGroup) { operationRecordRouter := Router.Group("sysOperationRecord") { - operationRecordRouter.POST("createSysOperationRecord", operationRecordApi.CreateSysOperationRecord) // 新建SysOperationRecord operationRecordRouter.DELETE("deleteSysOperationRecord", operationRecordApi.DeleteSysOperationRecord) // 删除SysOperationRecord operationRecordRouter.DELETE("deleteSysOperationRecordByIds", operationRecordApi.DeleteSysOperationRecordByIds) // 批量删除SysOperationRecord operationRecordRouter.GET("findSysOperationRecord", operationRecordApi.FindSysOperationRecord) // 根据ID获取SysOperationRecord diff --git a/server/service/system/auto_code_mcp.go b/server/service/system/auto_code_mcp.go new file mode 100644 index 000000000..3b6eb8418 --- /dev/null +++ b/server/service/system/auto_code_mcp.go @@ -0,0 +1,45 @@ +package system + +import ( + "context" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" + "github.com/flipped-aurora/gin-vue-admin/server/utils/autocode" + "os" + "path/filepath" + "text/template" +) + +func (s *autoCodeTemplate) CreateMcp(ctx context.Context, info request.AutoMcpTool) (toolFilePath string, err error) { + mcpTemplatePath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "mcp", "tools.tpl") + mcpToolPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "mcp") + + var files *template.Template + + templateName := filepath.Base(mcpTemplatePath) + + files, err = template.New(templateName).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(mcpTemplatePath) + if err != nil { + return + } + + fileName := utils.HumpToUnderscore(info.Name) + + toolFilePath = filepath.Join(mcpToolPath, fileName+".go") + + f, err := os.Create(toolFilePath) + if err != nil { + return + } + defer f.Close() + + // 执行模板,将内容写入文件 + err = files.Execute(f, info) + if err != nil { + return + } + + return + +} diff --git a/server/service/system/auto_code_package.go b/server/service/system/auto_code_package.go index a5c03a1e2..681904b1d 100644 --- a/server/service/system/auto_code_package.go +++ b/server/service/system/auto_code_package.go @@ -15,6 +15,7 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" + "github.com/flipped-aurora/gin-vue-admin/server/utils/autocode" "github.com/pkg/errors" "gorm.io/gorm" ) @@ -59,7 +60,7 @@ func (s *autoCodePackage) Create(ctx context.Context, info *request.SysAutoCodeP } for key, value := range creates { // key 为 模版绝对路径 var files *template.Template - files, err = template.ParseFiles(key) + files, err = template.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(key) if err != nil { translation := global.Translate("sys_auto_code.templateFileReadFailed") formattedMessage := fmt.Sprintf(translation, key) @@ -238,6 +239,9 @@ func (s *autoCodePackage) Templates(ctx context.Context) ([]string, error) { if entries[i].Name() == "preview" { continue } // preview 为预览代码生成器的代码 + if entries[i].Name() == "mcp" { + continue + } // preview 为mcp生成器的代码 templates = append(templates, entries[i].Name()) } } @@ -272,7 +276,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod three := filepath.Join(second, secondDirs[j].Name()) if !secondDirs[j].IsDir() { ext := filepath.Ext(secondDirs[j].Name()) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf(global.Translate("sys_auto_code.illegalTemplateSuffix"), three) } name := strings.TrimSuffix(secondDirs[j].Name(), ext) @@ -306,7 +310,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod return nil, nil, nil, errors.Errorf(global.Translate("sys_auto_code.illegalTemplateDirectory"), four) } ext := filepath.Ext(four) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf(global.Translate("sys_auto_code.illegalTemplateSuffix"), four) } api := strings.Index(threeDirs[k].Name(), "api") @@ -478,7 +482,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod return nil, nil, nil, errors.Errorf(global.Translate("sys_auto_code.illegalTemplateDirectory"), four) } ext := filepath.Ext(four) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf(global.Translate("sys_auto_code.illegalTemplateSuffix"), four) } gen := strings.Index(threeDirs[k].Name(), "gen") @@ -562,7 +566,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod return nil, nil, nil, errors.Errorf(global.Translate("sys_auto_code.illegalTemplateDirectory"), five) } ext := filepath.Ext(five) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf(global.Translate("sys_auto_code.illegalTemplateSuffix"), five) } hasRequest := strings.Index(fourDirs[l].Name(), "request") @@ -578,7 +582,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod continue } ext := filepath.Ext(threeDirs[k].Name()) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf(global.Translate("sys_auto_code.illegalTemplateSuffix"), four) } hasModel := strings.Index(threeDirs[k].Name(), "model") @@ -639,7 +643,7 @@ func (s *autoCodePackage) templates(ctx context.Context, entity model.SysAutoCod return nil, nil, nil, errors.Errorf(global.Translate("sys_auto_code.illegalTemplateDirectory"), four) } ext := filepath.Ext(four) - if ext != ".template" && ext != ".tpl" { + if ext != ".tpl" { return nil, nil, nil, errors.Errorf(global.Translate("sys_auto_code.illegalTemplateSuffix"), four) } api := strings.Index(threeDirs[k].Name(), "api") diff --git a/server/service/system/auto_code_package_test.go b/server/service/system/auto_code_package_test.go index 935401b8e..f54ec83dc 100644 --- a/server/service/system/auto_code_package_test.go +++ b/server/service/system/auto_code_package_test.go @@ -2,11 +2,12 @@ package system import ( "context" + "reflect" + "testing" + "github.com/flipped-aurora/gin-vue-admin/server/global" model "github.com/flipped-aurora/gin-vue-admin/server/model/system" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" - "reflect" - "testing" ) func Test_autoCodePackage_Create(t *testing.T) { @@ -54,9 +55,10 @@ func Test_autoCodePackage_Create(t *testing.T) { func Test_autoCodePackage_templates(t *testing.T) { type args struct { - ctx context.Context - entity model.SysAutoCodePackage - info request.AutoCode + ctx context.Context + entity model.SysAutoCodePackage + info request.AutoCode + isPackage bool } tests := []struct { name string @@ -79,6 +81,7 @@ func Test_autoCodePackage_templates(t *testing.T) { Abbreviation: "user", HumpPackageName: "user", }, + isPackage: false, }, wantErr: false, }, @@ -86,7 +89,7 @@ func Test_autoCodePackage_templates(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &autoCodePackage{} - gotCode, gotEnter, gotCreates, err := s.templates(tt.args.ctx, tt.args.entity, tt.args.info) + gotCode, gotEnter, gotCreates, err := s.templates(tt.args.ctx, tt.args.entity, tt.args.info, tt.args.isPackage) if (err != nil) != tt.wantErr { t.Errorf("templates() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/server/service/system/auto_code_plugin.go b/server/service/system/auto_code_plugin.go index 601d021ca..618ee0180 100644 --- a/server/service/system/auto_code_plugin.go +++ b/server/service/system/auto_code_plugin.go @@ -9,7 +9,7 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/flipped-aurora/gin-vue-admin/server/utils/ast" - "github.com/mholt/archiver/v4" + "github.com/mholt/archives" cp "github.com/otiai10/copy" "github.com/pkg/errors" "go.uber.org/zap" @@ -154,7 +154,7 @@ func (s *autoCodePlugin) PubPlug(plugName string) (zipPath string, err error) { fileName := plugName + ".zip" // 创建一个新的zip文件 - files, err := archiver.FilesFromDisk(nil, map[string]string{ + files, err := archives.FilesFromDisk(context.Background(), nil, map[string]string{ webPath: plugName + "/web/plugin/" + plugName, serverPath: plugName + "/server/plugin/" + plugName, }) @@ -168,8 +168,9 @@ func (s *autoCodePlugin) PubPlug(plugName string) (zipPath string, err error) { // we can use the CompressedArchive type to gzip a tarball // (compression is not required; you could use Tar directly) - format := archiver.Archive{ - Archival: archiver.Zip{}, + format := archives.CompressedArchive{ + //Compression: archives.Gz{}, + Archival: archives.Zip{}, } // create the archive diff --git a/server/service/system/auto_code_template.go b/server/service/system/auto_code_template.go index 0b98ad785..82da1936a 100644 --- a/server/service/system/auto_code_template.go +++ b/server/service/system/auto_code_template.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/flipped-aurora/gin-vue-admin/server/utils/autocode" "go/ast" "go/format" "go/parser" @@ -242,7 +243,7 @@ func (s *autoCodeTemplate) generate(ctx context.Context, info request.AutoCode, code := make(map[string]strings.Builder) for key, create := range templates { var files *template.Template - files, err = template.ParseFiles(key) + files, err = template.New(filepath.Base(key)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(key) if err != nil { return nil, nil, nil, errors.Wrapf(err, "[filpath:%s]读取模版文件失败!", key) } @@ -340,7 +341,7 @@ func (s *autoCodeTemplate) GetApiAndServer(info request.AutoFunc) (map[string]st func (s *autoCodeTemplate) getTemplateStr(t string, info request.AutoFunc) (string, error) { tempPath := filepath.Join(global.GVA_CONFIG.AutoCode.Root, global.GVA_CONFIG.AutoCode.Server, "resource", "function", t+".tpl") - files, err := template.ParseFiles(tempPath) + files, err := template.New(filepath.Base(tempPath)).Funcs(autocode.GetTemplateFuncMap()).ParseFiles(tempPath) if err != nil { return "", errors.Wrapf(err, "[filepath:%s]读取模版文件失败!", tempPath) } diff --git a/server/service/system/jwt_black_list.go b/server/service/system/jwt_black_list.go index 78ae38a7e..6c34bbbac 100644 --- a/server/service/system/jwt_black_list.go +++ b/server/service/system/jwt_black_list.go @@ -7,7 +7,6 @@ import ( "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/utils" ) type JwtService struct{} @@ -29,20 +28,6 @@ func (jwtService *JwtService) JsonInBlacklist(jwtList system.JwtBlacklist) (err return } -//@author: [piexlmax](https://github.com/piexlmax) -//@function: IsBlacklist -//@description: 判断JWT是否在黑名单内部 -//@param: jwt string -//@return: bool - -func (jwtService *JwtService) IsBlacklist(jwt string) bool { - _, ok := global.BlackCache.Get(jwt) - return ok - // err := global.GVA_DB.Where("jwt = ?", jwt).First(&system.JwtBlacklist{}).Error - // isNotFound := errors.Is(err, gorm.ErrRecordNotFound) - // return !isNotFound -} - //@author: [piexlmax](https://github.com/piexlmax) //@function: GetRedisJWT //@description: 从redis取jwt @@ -54,23 +39,6 @@ func (jwtService *JwtService) GetRedisJWT(userName string) (redisJWT string, err return redisJWT, err } -//@author: [piexlmax](https://github.com/piexlmax) -//@function: SetRedisJWT -//@description: jwt存入redis并设置过期时间 -//@param: jwt string, userName string -//@return: err error - -func (jwtService *JwtService) SetRedisJWT(jwt string, userName string) (err error) { - // 此处过期时间等于jwt过期时间 - dr, err := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) - if err != nil { - return err - } - timer := dr - err = global.GVA_REDIS.Set(context.Background(), userName, jwt, timer).Err() - return err -} - func LoadAll() { var data []string err := global.GVA_DB.Model(&system.JwtBlacklist{}).Select("jwt").Find(&data).Error diff --git a/server/service/system/sys_authority.go b/server/service/system/sys_authority.go index 1db198503..d5cee1db6 100644 --- a/server/service/system/sys_authority.go +++ b/server/service/system/sys_authority.go @@ -331,5 +331,8 @@ func (authorityService *AuthorityService) findChildrenAuthority(authority *syste func (authorityService *AuthorityService) GetParentAuthorityID(authorityID uint) (parentID uint, err error) { var authority system.SysAuthority err = global.GVA_DB.Where("authority_id = ?", authorityID).First(&authority).Error - return *authority.ParentId, err + if err != nil { + return + } + return *authority.ParentId, nil } diff --git a/server/service/system/sys_base_menu.go b/server/service/system/sys_base_menu.go index 1aac5f2e2..9216bdcdc 100644 --- a/server/service/system/sys_base_menu.go +++ b/server/service/system/sys_base_menu.go @@ -72,6 +72,7 @@ func (baseMenuService *BaseMenuService) UpdateBaseMenu(menu system.SysBaseMenu) var oldMenu system.SysBaseMenu upDateMap := make(map[string]interface{}) upDateMap["keep_alive"] = menu.KeepAlive + upDateMap["transition_type"] = menu.TransitionType upDateMap["close_tab"] = menu.CloseTab upDateMap["default_menu"] = menu.DefaultMenu upDateMap["parent_id"] = menu.ParentId diff --git a/server/service/system/sys_casbin.go b/server/service/system/sys_casbin.go index 26ff0f1e9..3fdb27af0 100644 --- a/server/service/system/sys_casbin.go +++ b/server/service/system/sys_casbin.go @@ -3,17 +3,14 @@ package system import ( "errors" "strconv" - "sync" "gorm.io/gorm" - "github.com/casbin/casbin/v2" - "github.com/casbin/casbin/v2/model" gormadapter "github.com/casbin/gorm-adapter/v3" "github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "github.com/flipped-aurora/gin-vue-admin/server/utils" _ "github.com/go-sql-driver/mysql" - "go.uber.org/zap" ) //@author: [piexlmax](https://github.com/piexlmax) @@ -68,7 +65,7 @@ func (casbinService *CasbinService) UpdateCasbin(adminAuthorityID, AuthorityID u if len(rules) == 0 { return nil } // 设置空权限无需调用 AddPolicies 方法 - e := casbinService.Casbin() + e := utils.GetCasbin() success, _ := e.AddPolicies(rules) if !success { return errors.New(global.Translate("sys_auto_code.duplicateApi")) @@ -91,7 +88,7 @@ func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath stri return err } - e := casbinService.Casbin() + e := utils.GetCasbin() return e.LoadPolicy() } @@ -102,7 +99,7 @@ func (casbinService *CasbinService) UpdateCasbinApi(oldPath string, newPath stri //@return: pathMaps []request.CasbinInfo func (casbinService *CasbinService) GetPolicyPathByAuthorityId(AuthorityID uint) (pathMaps []request.CasbinInfo) { - e := casbinService.Casbin() + e := utils.GetCasbin() authorityId := strconv.Itoa(int(AuthorityID)) list, _ := e.GetFilteredPolicy(0, authorityId) for _, v := range list { @@ -121,7 +118,7 @@ func (casbinService *CasbinService) GetPolicyPathByAuthorityId(AuthorityID uint) //@return: bool func (casbinService *CasbinService) ClearCasbin(v int, p ...string) bool { - e := casbinService.Casbin() + e := utils.GetCasbin() success, _ := e.RemoveFilteredPolicy(v, p...) return success } @@ -170,52 +167,7 @@ func (casbinService *CasbinService) AddPolicies(db *gorm.DB, rules [][]string) e } func (casbinService *CasbinService) FreshCasbin() (err error) { - e := casbinService.Casbin() + e := utils.GetCasbin() err = e.LoadPolicy() return err } - -//@author: [piexlmax](https://github.com/piexlmax) -//@function: Casbin -//@description: 持久化到数据库 引入自定义规则 -//@return: *casbin.Enforcer - -var ( - syncedCachedEnforcer *casbin.SyncedCachedEnforcer - once sync.Once -) - -func (casbinService *CasbinService) Casbin() *casbin.SyncedCachedEnforcer { - once.Do(func() { - a, err := gormadapter.NewAdapterByDB(global.GVA_DB) - if err != nil { - zap.L().Error(global.Translate("sys_auto_code.adaptDatabaseFailed"), zap.Error(err)) - return - } - text := ` - [request_definition] - r = sub, obj, act - - [policy_definition] - p = sub, obj, act - - [role_definition] - g = _, _ - - [policy_effect] - e = some(where (p.eft == allow)) - - [matchers] - m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act - ` - m, err := model.NewModelFromString(text) - if err != nil { - zap.L().Error(global.Translate("sys_auto_code.stringLoadModelFailed"), zap.Error(err)) - return - } - syncedCachedEnforcer, _ = casbin.NewSyncedCachedEnforcer(m, a) - syncedCachedEnforcer.SetExpireTime(60 * 60) - _ = syncedCachedEnforcer.LoadPolicy() - }) - return syncedCachedEnforcer -} diff --git a/server/service/system/sys_export_template.go b/server/service/system/sys_export_template.go index 077ebf095..182ce6298 100644 --- a/server/service/system/sys_export_template.go +++ b/server/service/system/sys_export_template.go @@ -3,6 +3,7 @@ package system import ( "bytes" "encoding/json" + "errors" "fmt" "mime/multipart" "net/url" @@ -127,6 +128,11 @@ func (sysExportTemplateService *SysExportTemplateService) GetSysExportTemplateIn // ExportExcel 导出Excel // Author [piexlmax](https://github.com/piexlmax) func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID string, values url.Values) (file *bytes.Buffer, name string, err error) { + var params = values.Get("params") + paramsValues, err := url.ParseQuery(params) + if err != nil { + return nil, "", fmt.Errorf("解析 params 参数失败: %v", err) + } var template system.SysExportTemplate err = global.GVA_DB.Preload("Conditions").Preload("JoinTemplate").First(&template, "template_id = ?", templateID).Error if err != nil { @@ -175,10 +181,38 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID db = db.Select(selects).Table(template.TableName) + filterDeleted := false + + filterParam := paramsValues.Get("filterDeleted") + if filterParam == "true" { + filterDeleted = true + } + + if filterDeleted { + // 自动过滤主表的软删除 + db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", template.TableName)) + + // 过滤关联表的软删除(如果有) + if len(template.JoinTemplate) > 0 { + for _, join := range template.JoinTemplate { + // 检查关联表是否有deleted_at字段 + hasDeletedAt := sysExportTemplateService.hasDeletedAtColumn(join.Table) + if hasDeletedAt { + db = db.Where(fmt.Sprintf("%s.deleted_at IS NULL", join.Table)) + } + } + } + } + if len(template.Conditions) > 0 { for _, condition := range template.Conditions { sql := fmt.Sprintf("%s %s ?", condition.Column, condition.Operator) - value := values.Get(condition.From) + value := paramsValues.Get(condition.From) + + if condition.Operator == "IN" || condition.Operator == "NOT IN" { + sql = fmt.Sprintf("%s %s (?)", condition.Column, condition.Operator) + } + if value != "" { if condition.Operator == "LIKE" { value = "%" + value + "%" @@ -188,7 +222,7 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID } } // 通过参数传入limit - limit := values.Get("limit") + limit := paramsValues.Get("limit") if limit != "" { l, e := strconv.Atoi(limit) if e == nil { @@ -201,7 +235,7 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID } // 通过参数传入offset - offset := values.Get("offset") + offset := paramsValues.Get("offset") if offset != "" { o, e := strconv.Atoi(offset) if e == nil { @@ -224,7 +258,7 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID } // 通过参数传入order - order := values.Get("order") + order := paramsValues.Get("order") if order == "" && template.Order != "" { // 如果没有order入参,这里会使用模板的默认排序 @@ -281,7 +315,17 @@ func (sysExportTemplateService *SysExportTemplateService) ExportExcel(templateID } for i, row := range rows { for j, colCell := range row { - sErr := f.SetCellValue("Sheet1", fmt.Sprintf("%s%d", getColumnName(j+1), i+1), colCell) + cell := fmt.Sprintf("%s%d", getColumnName(j+1), i+1) + + var sErr error + if v, err := strconv.ParseFloat(colCell, 64); err == nil { + sErr = f.SetCellValue("Sheet1", cell, v) + } else if v, err := strconv.ParseInt(colCell, 10, 64); err == nil { + sErr = f.SetCellValue("Sheet1", cell, v) + } else { + sErr = f.SetCellValue("Sheet1", cell, colCell) + } + if sErr != nil { return nil, "", sErr } @@ -344,6 +388,13 @@ func (sysExportTemplateService *SysExportTemplateService) ExportTemplate(templat return file, template.Name, nil } +// 辅助函数:检查表是否有deleted_at列 +func (s *SysExportTemplateService) hasDeletedAtColumn(tableName string) bool { + var count int64 + global.GVA_DB.Raw("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = 'deleted_at'", tableName).Count(&count) + return count > 0 +} + // ImportExcel 导入Excel // Author [piexlmax](https://github.com/piexlmax) func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID string, file *multipart.FileHeader) (err error) { @@ -368,6 +419,9 @@ func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID if err != nil { return err } + if len(rows) < 2 { + return errors.New("Excel data is not enough.\nIt should contain title row and data") + } var templateInfoMap = make(map[string]string) err = json.Unmarshal([]byte(template.TemplateInfo), &templateInfoMap) @@ -387,11 +441,17 @@ func (sysExportTemplateService *SysExportTemplateService) ImportExcel(templateID return db.Transaction(func(tx *gorm.DB) error { excelTitle := rows[0] + for i, str := range excelTitle { + excelTitle[i] = strings.TrimSpace(str) + } values := rows[1:] items := make([]map[string]interface{}, 0, len(values)) for _, row := range values { var item = make(map[string]interface{}) for ii, value := range row { + if _, ok := titleKeyMap[excelTitle[ii]]; !ok { + continue // excel中多余的标题,在模板信息中没有对应的字段,因此key为空,必须跳过 + } key := titleKeyMap[excelTitle[ii]] item[key] = value } diff --git a/server/service/system/sys_operation_record.go b/server/service/system/sys_operation_record.go index adfc25efd..ef131db9a 100644 --- a/server/service/system/sys_operation_record.go +++ b/server/service/system/sys_operation_record.go @@ -17,11 +17,6 @@ type OperationRecordService struct{} var OperationRecordServiceApp = new(OperationRecordService) -func (operationRecordService *OperationRecordService) CreateSysOperationRecord(sysOperationRecord system.SysOperationRecord) (err error) { - err = global.GVA_DB.Create(&sysOperationRecord).Error - return err -} - //@author: [granty1](https://github.com/granty1) //@author: [piexlmax](https://github.com/piexlmax) //@function: DeleteSysOperationRecordByIds diff --git a/server/service/system/sys_user.go b/server/service/system/sys_user.go index 1d724bf6a..8a831921b 100644 --- a/server/service/system/sys_user.go +++ b/server/service/system/sys_user.go @@ -315,7 +315,7 @@ func (userService *UserService) FindUserByUuid(uuid string) (user *system.SysUse //@param: ID uint //@return: err error -func (userService *UserService) ResetPassword(ID uint) (err error) { - err = global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", ID).Update("password", utils.BcryptHash("123456")).Error +func (userService *UserService) ResetPassword(ID uint, password string) (err error) { + err = global.GVA_DB.Model(&system.SysUser{}).Where("id = ?", ID).Update("password", utils.BcryptHash(password)).Error return err } diff --git a/server/source/system/api.go b/server/source/system/api.go index ba150ae9b..cf20017a4 100644 --- a/server/source/system/api.go +++ b/server/source/system/api.go @@ -120,6 +120,9 @@ func (i *initApi) InitializeData(ctx context.Context) (context.Context, error) { {ApiGroup: "system.api.group.customer", Method: "POST", Path: "/autoCode/createPlug", Description: "system.api.desc.createPluginPackage"}, {ApiGroup: "system.api.group.customer", Method: "POST", Path: "/autoCode/installPlugin", Description: "system.api.desc.installPlugin"}, {ApiGroup: "system.api.group.customer", Method: "POST", Path: "/autoCode/pubPlug", Description: "system.api.desc.packagePlugin"}, + {ApiGroup: "system.api.group.customer", Method: "POST", Path: "/autoCode/mcp", Description: "自动生成 MCP Tool 模板"}, + {ApiGroup: "system.api.group.customer", Method: "POST", Path: "/autoCode/mcpTest", Description: "MCP Tool 测试"}, + {ApiGroup: "system.api.group.customer", Method: "POST", Path: "/autoCode/mcpList", Description: "获取 MCP ToolList"}, {ApiGroup: "system.api.group.templateConfiguration", Method: "POST", Path: "/autoCode/createPackage", Description: "system.api.desc.configurationTemplates"}, {ApiGroup: "system.api.group.templateConfiguration", Method: "GET", Path: "/autoCode/getTemplates", Description: "system.api.desc.getTemplateFile"}, diff --git a/server/source/system/authorities_menus.go b/server/source/system/authorities_menus.go index e6799bc33..0bf72fd68 100644 --- a/server/source/system/authorities_menus.go +++ b/server/source/system/authorities_menus.go @@ -35,35 +35,72 @@ func (i *initMenuAuthority) InitializeData(ctx context.Context) (next context.Co if !ok { return ctx, system.ErrMissingDBContext } + initAuth := &initAuthority{} authorities, ok := ctx.Value(initAuth.InitializerName()).([]sysModel.SysAuthority) if !ok { return ctx, errors.Wrap(system.ErrMissingDependentContext, "创建 [菜单-权限] 关联失败, 未找到权限表初始化数据") } - menus, ok := ctx.Value(new(initMenu).InitializerName()).([]sysModel.SysBaseMenu) + + allMenus, ok := ctx.Value(new(initMenu).InitializerName()).([]sysModel.SysBaseMenu) if !ok { return next, errors.Wrap(errors.New(""), "创建 [菜单-权限] 关联失败, 未找到菜单表初始化数据") } next = ctx - // 888 - if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(menus); err != nil { - return next, err + + // 构建菜单ID映射,方便快速查找 + menuMap := make(map[uint]sysModel.SysBaseMenu) + for _, menu := range allMenus { + menuMap[menu.ID] = menu + } + + // 为不同角色分配不同权限 + // 1. 超级管理员角色(888) - 拥有所有菜单权限 + if err = db.Model(&authorities[0]).Association("SysBaseMenus").Replace(allMenus); err != nil { + return next, errors.Wrap(err, "为超级管理员分配菜单失败") + } + + // 2. 普通用户角色(8881) - 仅拥有基础功能菜单 + // 仅选择部分父级菜单及其子菜单 + var menu8881 []sysModel.SysBaseMenu + + // 添加仪表盘、关于我们和个人信息菜单 + for _, menu := range allMenus { + if menu.ParentId == 0 && (menu.Name == "dashboard" || menu.Name == "about" || menu.Name == "person" || menu.Name == "state") { + menu8881 = append(menu8881, menu) + } } - // 8881 - menu8881 := menus[:2] - menu8881 = append(menu8881, menus[7]) if err = db.Model(&authorities[1]).Association("SysBaseMenus").Replace(menu8881); err != nil { - return next, err + return next, errors.Wrap(err, "为普通用户分配菜单失败") } - // 9528 - if err = db.Model(&authorities[2]).Association("SysBaseMenus").Replace(menus[:11]); err != nil { - return next, err + // 3. 测试角色(9528) - 拥有部分菜单权限 + var menu9528 []sysModel.SysBaseMenu + + // 添加所有父级菜单 + for _, menu := range allMenus { + if menu.ParentId == 0 { + menu9528 = append(menu9528, menu) + } } - if err = db.Model(&authorities[2]).Association("SysBaseMenus").Append(menus[12:17]); err != nil { - return next, err + + // 添加部分子菜单 - 系统工具、示例文件等模块的子菜单 + for _, menu := range allMenus { + parentName := "" + if menu.ParentId > 0 && menuMap[menu.ParentId].Name != "" { + parentName = menuMap[menu.ParentId].Name + } + + if menu.ParentId > 0 && (parentName == "systemTools" || parentName == "example") { + menu9528 = append(menu9528, menu) + } } + + if err = db.Model(&authorities[2]).Association("SysBaseMenus").Replace(menu9528); err != nil { + return next, errors.Wrap(err, "为测试角色分配菜单失败") + } + return next, nil } diff --git a/server/source/system/casbin.go b/server/source/system/casbin.go index eda525d10..4e37c1474 100644 --- a/server/source/system/casbin.go +++ b/server/source/system/casbin.go @@ -130,6 +130,9 @@ func (i *initCasbin) InitializeData(ctx context.Context) (context.Context, error {Ptype: "p", V0: "888", V1: "/autoCode/installPlugin", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/autoCode/pubPlug", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/autoCode/addFunc", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcp", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcpTest", V2: "POST"}, + {Ptype: "p", V0: "888", V1: "/autoCode/mcpList", V2: "POST"}, {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/findSysDictionaryDetail", V2: "GET"}, {Ptype: "p", V0: "888", V1: "/sysDictionaryDetail/updateSysDictionaryDetail", V2: "PUT"}, diff --git a/server/source/system/menu.go b/server/source/system/menu.go index 91052c706..e8f024ed1 100644 --- a/server/source/system/menu.go +++ b/server/source/system/menu.go @@ -3,7 +3,6 @@ package system import ( "context" - "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/system" "github.com/pkg/errors" @@ -51,43 +50,74 @@ func (i *initMenu) InitializeData(ctx context.Context) (next context.Context, er if !ok { return ctx, system.ErrMissingDBContext } - entities := []SysBaseMenu{ + + // 定义所有菜单 + allMenus := []SysBaseMenu{ {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "dashboard", Name: "dashboard", Component: "view/dashboard/index.vue", Sort: 1, Meta: Meta{Title: "system.menu.dashboard", Icon: "odometer"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "about", Name: "about", Component: "view/about/index.vue", Sort: 9, Meta: Meta{Title: "system.menu.about", Icon: "info-filled"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "admin", Name: "superAdmin", Component: "view/superAdmin/index.vue", Sort: 3, Meta: Meta{Title: "system.menu.adminTools", Icon: "user"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "system.menu.roleManage", Icon: "avatar"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "system.menu.menuManage", Icon: "tickets", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "system.menu.apiManage", Icon: "platform", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "system.menu.userManage", Icon: "coordinate"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "system.menu.dictManage", Icon: "notebook"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "system.menu.operationLog", Icon: "pie-chart"}}, {MenuLevel: 0, Hidden: true, ParentId: 0, Path: "person", Name: "person", Component: "view/person/person.vue", Sort: 4, Meta: Meta{Title: "system.menu.personalInfo", Icon: "message"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "example", Name: "example", Component: "view/example/index.vue", Sort: 7, Meta: Meta{Title: "system.menu.examples", Icon: "management"}}, - {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 5, Meta: Meta{Title: "system.menu.mediaLibUpDown", Icon: "upload"}}, - {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "system.menu.breakPoint", Icon: "upload-filled"}}, - {MenuLevel: 0, Hidden: false, ParentId: 11, Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "system.menu.customerList", Icon: "avatar"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "systemTools", Name: "systemTools", Component: "view/systemTools/index.vue", Sort: 5, Meta: Meta{Title: "system.menu.systemTools", Icon: "tools"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "system.menu.autoCode", Icon: "cpu", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "system.menu.formCreator", Icon: "magic-stick", KeepAlive: true}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "system.menu.system", Icon: "operation"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "system.menu.autoCodeManage", Icon: "magic-stick"}}, - {MenuLevel: 0, Hidden: true, ParentId: 15, Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "system.menu.autoCodeEdit" + " - ${id}", Icon: "magic-stick"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "system.menu.templateConfig", Icon: "folder"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "https://www.gin-vue-admin.com", Name: "https://www.gin-vue-admin.com", Component: "/", Sort: 0, Meta: Meta{Title: "system.menu.website", Icon: "customer-gva"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "state", Name: "state", Component: "view/system/state.vue", Sort: 8, Meta: Meta{Title: "system.menu.serverStatus", Icon: "cloudy"}}, {MenuLevel: 0, Hidden: false, ParentId: 0, Path: "plugin", Name: "plugin", Component: "view/routerHolder.vue", Sort: 6, Meta: Meta{Title: "system.menu.pluginSystem", Icon: "cherry"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "system.menu.pluginMarket", Icon: "shop"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "system.menu.pluginInstall", Icon: "box"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "system.menu.packagePlugin", Icon: "files"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "system.menu.emailPlugin", Icon: "message"}}, - {MenuLevel: 0, Hidden: false, ParentId: 15, Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "system.menu.tableTemplate", Icon: "reading"}}, - {MenuLevel: 0, Hidden: false, ParentId: 24, Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "system.menu.announcementManage", Icon: "scaleToOriginal"}}, - {MenuLevel: 0, Hidden: false, ParentId: 3, Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "system.menu.parameterManagement", Icon: "compass"}}, } - if err = db.Create(&entities).Error; err != nil { - return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+" "+global.Translate("general.tabelDataInitFail")) + + // 先创建父级菜单(ParentId = 0 的菜单) + if err = db.Create(&allMenus).Error; err != nil { + return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"父级菜单初始化失败!") } - next = context.WithValue(ctx, i.InitializerName(), entities) + + // 建立菜单映射 - 通过Name查找已创建的菜单及其ID + menuNameMap := make(map[string]uint) + for _, menu := range allMenus { + menuNameMap[menu.Name] = menu.ID + } + + // 定义子菜单,并设置正确的ParentId + childMenus := []SysBaseMenu{ + // superAdmin子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "authority", Name: "authority", Component: "view/superAdmin/authority/authority.vue", Sort: 1, Meta: Meta{Title: "system.menu.roleManage", Icon: "avatar"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "menu", Name: "menu", Component: "view/superAdmin/menu/menu.vue", Sort: 2, Meta: Meta{Title: "system.menu.menuManage", Icon: "tickets", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "api", Name: "api", Component: "view/superAdmin/api/api.vue", Sort: 3, Meta: Meta{Title: "system.menu.apiManage", Icon: "platform", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "user", Name: "user", Component: "view/superAdmin/user/user.vue", Sort: 4, Meta: Meta{Title: "system.menu.userManage", Icon: "coordinate"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "dictionary", Name: "dictionary", Component: "view/superAdmin/dictionary/sysDictionary.vue", Sort: 5, Meta: Meta{Title: "system.menu.dictManage", Icon: "notebook"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "operation", Name: "operation", Component: "view/superAdmin/operation/sysOperationRecord.vue", Sort: 6, Meta: Meta{Title: "system.menu.operationLog", Icon: "pie-chart"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["superAdmin"], Path: "sysParams", Name: "sysParams", Component: "view/superAdmin/params/sysParams.vue", Sort: 7, Meta: Meta{Title: "system.menu.parameterManagement", Icon: "compass"}}, + + // example子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "upload", Name: "upload", Component: "view/example/upload/upload.vue", Sort: 5, Meta: Meta{Title: "system.menu.mediaLibUpDown", Icon: "upload"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "breakpoint", Name: "breakpoint", Component: "view/example/breakpoint/breakpoint.vue", Sort: 6, Meta: Meta{Title: "system.menu.breakPoint", Icon: "upload-filled"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["example"], Path: "customer", Name: "customer", Component: "view/example/customer/customer.vue", Sort: 7, Meta: Meta{Title: "system.menu.customerList", Icon: "avatar"}}, + + // systemTools子菜单 + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCode", Name: "autoCode", Component: "view/systemTools/autoCode/index.vue", Sort: 1, Meta: Meta{Title: "system.menu.autoCode", Icon: "cpu", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "formCreate", Name: "formCreate", Component: "view/systemTools/formCreate/index.vue", Sort: 3, Meta: Meta{Title: "system.menu.formCreator", Icon: "magic-stick", KeepAlive: true}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "system", Name: "system", Component: "view/systemTools/system/system.vue", Sort: 4, Meta: Meta{Title: "system.menu.system", Icon: "operation"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoCodeAdmin", Name: "autoCodeAdmin", Component: "view/systemTools/autoCodeAdmin/index.vue", Sort: 2, Meta: Meta{Title: "system.menu.autoCodeManage", Icon: "magic-stick"}}, + {MenuLevel: 1, Hidden: true, ParentId: menuNameMap["systemTools"], Path: "autoCodeEdit/:id", Name: "autoCodeEdit", Component: "view/systemTools/autoCode/index.vue", Sort: 0, Meta: Meta{Title: "system.menu.autoCodeEdit" + " - ${id}", Icon: "magic-stick"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "autoPkg", Name: "autoPkg", Component: "view/systemTools/autoPkg/autoPkg.vue", Sort: 0, Meta: Meta{Title: "system.menu.templateConfig", Icon: "folder"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "exportTemplate", Name: "exportTemplate", Component: "view/systemTools/exportTemplate/exportTemplate.vue", Sort: 5, Meta: Meta{Title: "system.menu.tableTemplate", Icon: "reading"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "picture", Name: "picture", Component: "view/systemTools/autoCode/picture.vue", Sort: 6, Meta: Meta{Title: "AI页面绘制", Icon: "picture-filled"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTool", Name: "mcpTool", Component: "view/systemTools/autoCode/mcp.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools模板", Icon: "magnet"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["systemTools"], Path: "mcpTest", Name: "mcpTest", Component: "view/systemTools/autoCode/mcpTest.vue", Sort: 7, Meta: Meta{Title: "Mcp Tools测试", Icon: "partly-cloudy"}}, + + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "https://plugin.gin-vue-admin.com/", Name: "https://plugin.gin-vue-admin.com/", Component: "https://plugin.gin-vue-admin.com/", Sort: 0, Meta: Meta{Title: "system.menu.pluginMarket", Icon: "shop"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "installPlugin", Name: "installPlugin", Component: "view/systemTools/installPlugin/index.vue", Sort: 1, Meta: Meta{Title: "system.menu.pluginInstall", Icon: "box"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "pubPlug", Name: "pubPlug", Component: "view/systemTools/pubPlug/pubPlug.vue", Sort: 3, Meta: Meta{Title: "system.menu.packagePlugin", Icon: "files"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "plugin-email", Name: "plugin-email", Component: "plugin/email/view/index.vue", Sort: 4, Meta: Meta{Title: "system.menu.emailPlugin", Icon: "message"}}, + {MenuLevel: 1, Hidden: false, ParentId: menuNameMap["plugin"], Path: "anInfo", Name: "anInfo", Component: "plugin/announcement/view/info.vue", Sort: 5, Meta: Meta{Title: "system.menu.announcementManage", Icon: "scaleToOriginal"}}, + } + + // 创建子菜单 + if err = db.Create(&childMenus).Error; err != nil { + return ctx, errors.Wrap(err, SysBaseMenu{}.TableName()+"子菜单初始化失败!") + } + + // 组合所有菜单作为返回结果 + allEntities := append(allMenus, childMenus...) + next = context.WithValue(ctx, i.InitializerName(), allEntities) return next, nil } diff --git a/server/utils/autocode/template_funcs.go b/server/utils/autocode/template_funcs.go new file mode 100644 index 000000000..28e347d7d --- /dev/null +++ b/server/utils/autocode/template_funcs.go @@ -0,0 +1,703 @@ +package autocode + +import ( + "fmt" + systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request" + "slices" + "strings" + "text/template" +) + +// GetTemplateFuncMap 返回模板函数映射,用于在模板中使用 +func GetTemplateFuncMap() template.FuncMap { + return template.FuncMap{ + "title": strings.Title, + "GenerateField": GenerateField, + "GenerateSearchField": GenerateSearchField, + "GenerateSearchConditions": GenerateSearchConditions, + "GenerateSearchFormItem": GenerateSearchFormItem, + "GenerateTableColumn": GenerateTableColumn, + "GenerateFormItem": GenerateFormItem, + "GenerateDescriptionItem": GenerateDescriptionItem, + "GenerateDefaultFormValue": GenerateDefaultFormValue, + } +} + +// 渲染Model中的字段 +func GenerateField(field systemReq.AutoCodeField) string { + // 构建gorm标签 + gormTag := `` + + if field.FieldIndexType != "" { + gormTag += field.FieldIndexType + ";" + } + + if field.PrimaryKey { + gormTag += "primarykey;" + } + + if field.DefaultValue != "" { + gormTag += fmt.Sprintf("default:%s;", field.DefaultValue) + } + + if field.Comment != "" { + gormTag += fmt.Sprintf("comment:%s;", field.Comment) + } + + gormTag += "column:" + field.ColumnName + ";" + + if field.DataTypeLong != "" && field.FieldType != "enum" { + gormTag += fmt.Sprintf("size:%s;", field.DataTypeLong) + } + + requireTag := ` binding:"required"` + "`" + + // 根据字段类型构建不同的字段定义 + var result string + switch field.FieldType { + case "enum": + result = fmt.Sprintf(`%s string `+"`"+`json:"%s" form:"%s" gorm:"%stype:enum(%s);"`+"`", + field.FieldName, field.FieldJson, field.FieldJson, gormTag, field.DataTypeLong) + case "picture", "video": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s string `+"`"+`%s`+"`"+``, field.FieldName, tagContent) + case "file", "pictures", "array": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s datatypes.JSON `+"`"+`%s swaggertype:"array,object"`+"`"+``, + field.FieldName, tagContent) + case "richtext": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s *string `+"`"+`%stype:text;"`+"`"+``, + field.FieldName, tagContent) + case "json": + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s datatypes.JSON `+"`"+`%s swaggertype:"object"`+"`"+``, + field.FieldName, tagContent) + default: + tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`, + field.FieldJson, field.FieldJson, gormTag) + + result = fmt.Sprintf(`%s *%s `+"`"+`%s`+"`"+``, + field.FieldName, field.FieldType, tagContent) + } + + if field.Require { + result = result[0:len(result)-1] + requireTag + } + + // 添加字段描述 + if field.FieldDesc != "" { + result += fmt.Sprintf(" //%s", field.FieldDesc) + } + + return result +} + +// 格式化搜索条件语句 +func GenerateSearchConditions(fields []*systemReq.AutoCodeField) string { + var conditions []string + + for _, field := range fields { + if field.FieldSearchType == "" { + continue + } + + var condition string + + if slices.Contains([]string{"enum", "pictures", "picture", "video", "json", "richtext", "array"}, field.FieldType) { + if field.FieldType == "enum" { + if field.FieldSearchType == "LIKE" { + condition = fmt.Sprintf(` + if info.%s != "" { + db = db.Where("%s LIKE ?", "%%"+ info.%s+"%%") + }`, + field.FieldName, field.ColumnName, field.FieldName) + } else { + condition = fmt.Sprintf(` + if info.%s != "" { + db = db.Where("%s %s ?", info.%s) + }`, + field.FieldName, field.ColumnName, field.FieldSearchType, field.FieldName) + } + } else { + condition = fmt.Sprintf(` + if info.%s != "" { + // TODO 数据类型为复杂类型,请根据业务需求自行实现复杂类型的查询业务 + }`, field.FieldName) + } + + } else if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + if field.FieldType == "time.Time" { + condition = fmt.Sprintf(` + if len(info.%sRange) == 2 { + db = db.Where("%s %s ? AND ? ", info.%sRange[0], info.%sRange[1]) + }`, + field.FieldName, field.ColumnName, field.FieldSearchType, field.FieldName, field.FieldName) + } else { + condition = fmt.Sprintf(` + if info.Start%s != nil && info.End%s != nil { + db = db.Where("%s %s ? AND ? ", *info.Start%s, *info.End%s) + }`, + field.FieldName, field.FieldName, field.ColumnName, + field.FieldSearchType, field.FieldName, field.FieldName) + } + } else { + nullCheck := "info." + field.FieldName + " != nil" + if field.FieldType == "string" { + condition = fmt.Sprintf(` + if %s && *info.%s != "" {`, nullCheck, field.FieldName) + } else { + condition = fmt.Sprintf(` + if %s {`, nullCheck) + } + + if field.FieldSearchType == "LIKE" { + condition += fmt.Sprintf(` + db = db.Where("%s LIKE ?", "%%"+ *info.%s+"%%") + }`, + field.ColumnName, field.FieldName) + } else { + condition += fmt.Sprintf(` + db = db.Where("%s %s ?", *info.%s) + }`, + field.ColumnName, field.FieldSearchType, field.FieldName) + } + } + + conditions = append(conditions, condition) + } + + return strings.Join(conditions, "") +} + +// 格式化前端搜索条件 +func GenerateSearchFormItem(field systemReq.AutoCodeField) string { + // 开始构建表单项 + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + + // 根据字段属性生成不同的输入类型 + if field.FieldType == "bool" { + result += fmt.Sprintf(` +`, field.FieldJson) + result += ` +` + result += ` +` + result += ` +` + } else if field.DictType != "" { + multipleAttr := "" + if field.FieldType == "array" { + multipleAttr = "multiple " + } + result += fmt.Sprintf(` +`, + multipleAttr, field.FieldJson, field.FieldJson) + result += fmt.Sprintf(` +`, + field.DictType) + result += ` +` + } else if field.CheckDataSource { + multipleAttr := "" + if field.DataSource.Association == 2 { + multipleAttr = "multiple " + } + result += fmt.Sprintf(` +`, + multipleAttr, field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.FieldJson) + result += ` +` + } else if field.FieldType == "float64" || field.FieldType == "int" { + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + result += fmt.Sprintf(` +`, field.FieldName) + result += ` — +` + result += fmt.Sprintf(` +`, field.FieldName) + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + } else if field.FieldType == "time.Time" { + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + result += ` +` + result += fmt.Sprintf(``, field.FieldJson) + } else { + result += fmt.Sprintf(``, field.FieldJson) + } + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + + // 关闭表单项 + result += `` + + return result +} + +// GenerateTableColumn generates HTML for table column based on field properties +func GenerateTableColumn(field systemReq.AutoCodeField) string { + // Add sortable attribute if needed + sortAttr := "" + if field.Sort { + sortAttr = " sortable" + } + + // Handle different field types + if field.CheckDataSource { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += ` +` + result += `
` + return result + } else if field.DictType != "" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "bool" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += fmt.Sprintf(` +`, field.FieldJson) + result += `` + return result + } else if field.FieldType == "time.Time" { + result := fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + result += fmt.Sprintf(` +`, field.FieldJson) + result += `` + return result + } else if field.FieldType == "picture" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "pictures" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "video" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "richtext" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "file" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "json" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else if field.FieldType == "array" { + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + result += ` +` + result += `` + return result + } else { + return fmt.Sprintf(` +`, + sortAttr, field.FieldDesc, field.FieldJson) + } +} + +func GenerateFormItem(field systemReq.AutoCodeField) string { + // 开始构建表单项 + result := fmt.Sprintf(` +`, field.FieldDesc, field.FieldJson) + + // 处理不同字段类型 + if field.CheckDataSource { + multipleAttr := "" + if field.DataSource.Association == 2 { + multipleAttr = " multiple" + } + result += fmt.Sprintf(` +`, + multipleAttr, field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.FieldJson) + result += ` +` + } else { + switch field.FieldType { + case "bool": + result += fmt.Sprintf(` +`, + field.FieldJson) + + case "string": + if field.DictType != "" { + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.DictType) + result += ` +` + } else { + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable, field.FieldDesc) + } + + case "richtext": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "json": + result += fmt.Sprintf(` // 此字段为json结构,可以前端自行控制展示和数据绑定模式 需绑定json的key为 formData.%s 后端会按照json的类型进行存取 +`, field.FieldJson) + result += fmt.Sprintf(` {{ formData.%s }} +`, field.FieldJson) + + case "array": + if field.DictType != "" { + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.DictType) + result += ` +` + } else { + result += fmt.Sprintf(` +`, field.FieldJson) + } + + case "int": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable, field.FieldDesc) + + case "time.Time": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable) + + case "float64": + result += fmt.Sprintf(` +`, + field.FieldJson, field.Clearable) + + case "enum": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldDesc, field.Clearable) + result += fmt.Sprintf(` +`, + field.DataTypeLong) + result += ` +` + + case "picture": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "pictures": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "video": + result += fmt.Sprintf(` +`, field.FieldJson) + + case "file": + result += fmt.Sprintf(` +`, field.FieldJson) + } + } + + // 关闭表单项 + result += `` + + return result +} + +func GenerateDescriptionItem(field systemReq.AutoCodeField) string { + // 开始构建描述项 + result := fmt.Sprintf(` +`, field.FieldDesc) + + if field.CheckDataSource { + result += ` +` + } else if field.FieldType != "picture" && field.FieldType != "pictures" && + field.FieldType != "file" && field.FieldType != "array" && + field.FieldType != "richtext" { + result += fmt.Sprintf(` {{ detailFrom.%s }} +`, field.FieldJson) + } else { + switch field.FieldType { + case "picture": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldJson) + case "array": + result += fmt.Sprintf(` +`, field.FieldJson) + case "pictures": + result += fmt.Sprintf(` +`, + field.FieldJson, field.FieldJson) + case "richtext": + result += fmt.Sprintf(` +`, field.FieldJson) + case "file": + result += fmt.Sprintf(`
+`, field.FieldJson) + result += ` +` + result += ` +` + result += ` {{ item.name }} +` + result += ` +` + result += `
+` + } + } + + // 关闭描述项 + result += `
` + + return result +} + +func GenerateDefaultFormValue(field systemReq.AutoCodeField) string { + // 根据字段类型确定默认值 + var defaultValue string + + switch field.FieldType { + case "bool": + defaultValue = "false" + case "string", "richtext": + defaultValue = "''" + case "int": + if field.DataSource != nil { // 检查数据源是否存在 + defaultValue = "undefined" + } else { + defaultValue = "0" + } + case "time.Time": + defaultValue = "new Date()" + case "float64": + defaultValue = "0" + case "picture", "video": + defaultValue = "\"\"" + case "pictures", "file", "array": + defaultValue = "[]" + case "json": + defaultValue = "{}" + default: + defaultValue = "null" + } + + // 返回格式化后的默认值字符串 + return fmt.Sprintf(`%s: %s,`, field.FieldJson, defaultValue) +} + +// GenerateSearchField 根据字段属性生成搜索结构体中的字段定义 +func GenerateSearchField(field systemReq.AutoCodeField) string { + var result string + + if field.FieldSearchType == "" { + return "" // 如果没有搜索类型,返回空字符串 + } + + if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" { + // 生成范围搜索字段 + // time 的情况 + if field.FieldType == "time.Time" { + result = fmt.Sprintf("%sRange []time.Time `json:\"%sRange\" form:\"%sRange[]\"`", + field.FieldName, field.FieldJson, field.FieldJson) + } else { + startField := fmt.Sprintf("Start%s *%s `json:\"start%s\" form:\"start%s\"`", + field.FieldName, field.FieldType, field.FieldName, field.FieldName) + endField := fmt.Sprintf("End%s *%s `json:\"end%s\" form:\"end%s\"`", + field.FieldName, field.FieldType, field.FieldName, field.FieldName) + result = startField + "\n" + endField + } + } else { + // 生成普通搜索字段 + if field.FieldType == "enum" || field.FieldType == "picture" || + field.FieldType == "pictures" || field.FieldType == "video" || + field.FieldType == "json" || field.FieldType == "richtext" || field.FieldType == "array" { + result = fmt.Sprintf("%s string `json:\"%s\" form:\"%s\"` ", + field.FieldName, field.FieldJson, field.FieldJson) + } else { + result = fmt.Sprintf("%s *%s `json:\"%s\" form:\"%s\"` ", + field.FieldName, field.FieldType, field.FieldJson, field.FieldJson) + } + } + + return result +} diff --git a/server/utils/captcha/redis.go b/server/utils/captcha/redis.go index a13b7cc11..ffb4dbf78 100644 --- a/server/utils/captcha/redis.go +++ b/server/utils/captcha/redis.go @@ -5,7 +5,6 @@ import ( "time" "github.com/flipped-aurora/gin-vue-admin/server/global" - "github.com/mojocn/base64Captcha" "go.uber.org/zap" ) @@ -23,8 +22,10 @@ type RedisStore struct { Context context.Context } -func (rs *RedisStore) UseWithCtx(ctx context.Context) base64Captcha.Store { - rs.Context = ctx +func (rs *RedisStore) UseWithCtx(ctx context.Context) *RedisStore { + if ctx == nil { + rs.Context = ctx + } return rs } diff --git a/server/utils/casbin_util.go b/server/utils/casbin_util.go new file mode 100644 index 000000000..62d44fcfc --- /dev/null +++ b/server/utils/casbin_util.go @@ -0,0 +1,52 @@ +package utils + +import ( + "sync" + + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + gormadapter "github.com/casbin/gorm-adapter/v3" + "github.com/flipped-aurora/gin-vue-admin/server/global" + "go.uber.org/zap" +) + +var ( + syncedCachedEnforcer *casbin.SyncedCachedEnforcer + once sync.Once +) + +// GetCasbin 获取casbin实例 +func GetCasbin() *casbin.SyncedCachedEnforcer { + once.Do(func() { + a, err := gormadapter.NewAdapterByDB(global.GVA_DB) + if err != nil { + zap.L().Error("适配数据库失败请检查casbin表是否为InnoDB引擎!", zap.Error(err)) + return + } + text := ` + [request_definition] + r = sub, obj, act + + [policy_definition] + p = sub, obj, act + + [role_definition] + g = _, _ + + [policy_effect] + e = some(where (p.eft == allow)) + + [matchers] + m = r.sub == p.sub && keyMatch2(r.obj,p.obj) && r.act == p.act + ` + m, err := model.NewModelFromString(text) + if err != nil { + zap.L().Error("字符串加载模型失败!", zap.Error(err)) + return + } + syncedCachedEnforcer, _ = casbin.NewSyncedCachedEnforcer(m, a) + syncedCachedEnforcer.SetExpireTime(60 * 60) + _ = syncedCachedEnforcer.LoadPolicy() + }) + return syncedCachedEnforcer +} diff --git a/server/utils/claims.go b/server/utils/claims.go index acaa2c4ae..695a78fe1 100644 --- a/server/utils/claims.go +++ b/server/utils/claims.go @@ -40,10 +40,10 @@ func SetToken(c *gin.Context, token string, maxAge int) { } func GetToken(c *gin.Context) string { - token, _ := c.Cookie("x-token") + token := c.Request.Header.Get("x-token") if token == "" { j := NewJWT() - token = c.Request.Header.Get("x-token") + token, _ = c.Cookie("x-token") claims, err := j.ParseToken(token) if err != nil { global.GVA_LOG.Error(global.Translate("utils.cookieRewriteFailed")) diff --git a/server/utils/fmt_plus.go b/server/utils/fmt_plus.go index 6f34cb47c..1257a6599 100644 --- a/server/utils/fmt_plus.go +++ b/server/utils/fmt_plus.go @@ -68,6 +68,23 @@ func MaheHump(s string) string { return strings.Join(words, "") } +// HumpToUnderscore 将驼峰命名转换为下划线分割模式 +func HumpToUnderscore(s string) string { + var result strings.Builder + + for i, char := range s { + if i > 0 && char >= 'A' && char <= 'Z' { + // 在大写字母前添加下划线 + result.WriteRune('_') + result.WriteRune(char - 'A' + 'a') // 转小写 + } else { + result.WriteRune(char) + } + } + + return strings.ToLower(result.String()) +} + // RandomString 随机字符串 func RandomString(n int) string { var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") diff --git a/server/utils/jwt.go b/server/utils/jwt.go index f34b792fa..b4e6b3b2b 100644 --- a/server/utils/jwt.go +++ b/server/utils/jwt.go @@ -1,6 +1,7 @@ package utils import ( + "context" "errors" "time" @@ -85,3 +86,20 @@ func (j *JWT) ParseToken(tokenString string) (*request.CustomClaims, error) { } return nil, TokenValid } + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: SetRedisJWT +//@description: jwt存入redis并设置过期时间 +//@param: jwt string, userName string +//@return: err error + +func SetRedisJWT(jwt string, userName string) (err error) { + // 此处过期时间等于jwt过期时间 + dr, err := ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime) + if err != nil { + return err + } + timer := dr + err = global.GVA_REDIS.Set(context.Background(), userName, jwt, timer).Err() + return err +} diff --git a/server/utils/reload.go b/server/utils/reload.go deleted file mode 100644 index 2d0f9e4f1..000000000 --- a/server/utils/reload.go +++ /dev/null @@ -1,19 +0,0 @@ -package utils - -import ( - "errors" - "github.com/flipped-aurora/gin-vue-admin/server/global" - "os" - "os/exec" - "runtime" - "strconv" -) - -func Reload() error { - if runtime.GOOS == "windows" { - return errors.New(global.Translate("utils.sysNoSupport")) - } - pid := os.Getpid() - cmd := exec.Command("kill", "-1", strconv.Itoa(pid)) - return cmd.Run() -} diff --git a/server/utils/system_events.go b/server/utils/system_events.go new file mode 100644 index 000000000..736ea747f --- /dev/null +++ b/server/utils/system_events.go @@ -0,0 +1,34 @@ +package utils + +import ( + "sync" +) + +// SystemEvents 定义系统级事件处理 +type SystemEvents struct { + reloadHandlers []func() error + mu sync.RWMutex +} + +// 全局事件管理器 +var GlobalSystemEvents = &SystemEvents{} + +// RegisterReloadHandler 注册系统重载处理函数 +func (e *SystemEvents) RegisterReloadHandler(handler func() error) { + e.mu.Lock() + defer e.mu.Unlock() + e.reloadHandlers = append(e.reloadHandlers, handler) +} + +// TriggerReload 触发所有注册的重载处理函数 +func (e *SystemEvents) TriggerReload() error { + e.mu.RLock() + defer e.mu.RUnlock() + + for _, handler := range e.reloadHandlers { + if err := handler(); err != nil { + return err + } + } + return nil +} diff --git a/web/.env.development b/web/.env.development index ad07c3836..b80eedd3f 100644 --- a/web/.env.development +++ b/web/.env.development @@ -5,7 +5,7 @@ VITE_BASE_API = /api VITE_FILE_API = /api VITE_BASE_PATH = http://127.0.0.1 VITE_POSITION = close -VITE_EDITOR = vscode +VITE_EDITOR = code // VITE_EDITOR = webstorm 如果使用webstorm开发且要使用dom定位到代码行功能 请先自定添加 webstorm到环境变量 再将VITE_EDITOR值修改为webstorm // 如果使用docker-compose开发模式,设置为下面的地址或本机主机IP //VITE_BASE_PATH = http://177.7.0.12 diff --git a/web/index.html b/web/index.html index 370d188e9..0bc2f2fe4 100644 --- a/web/index.html +++ b/web/index.html @@ -8,7 +8,7 @@ content="Gin,Vue,Admin.Gin-Vue-Admin,GVA,gin-vue-admin,后台管理框架,vue后台管理框架,gin-vue-admin文档,gin-vue-admin首页,gin-vue-admin" name="keywords" /> - + + diff --git a/web/src/components/selectFile/selectFile.vue b/web/src/components/selectFile/selectFile.vue index 3354d3b2f..e36ec0ee5 100644 --- a/web/src/components/selectFile/selectFile.vue +++ b/web/src/components/selectFile/selectFile.vue @@ -11,6 +11,7 @@ :limit="limit" :accept="accept" class="upload-btn" + :headers="{'x-token': token}" > {{ t('components.selectFile.uploadFiles') }} @@ -23,9 +24,10 @@ import { ref } from 'vue' import { ElMessage } from 'element-plus' import { getBaseUrl } from '@/utils/format' - import { useI18n } from 'vue-i18n' // added by mohamed hassan to support multilanguage + import { useUserStore } from "@/pinia"; + import { useI18n } from 'vue-i18n' // added by mohamed hassan to support multilingual - const { t } = useI18n() // added by mohamed hassan to support multilanguage + const { t } = useI18n() // added by mohamed hassan to support multilingual defineOptions({ name: 'UploadCommon' @@ -42,6 +44,10 @@ } }) + const userStore = useUserStore() + + const token = userStore.token + const fullscreenLoading = ref(false) const model = defineModel({ type: Array }) diff --git a/web/src/components/selectImage/selectImage.vue b/web/src/components/selectImage/selectImage.vue index 4f1dabc7c..cca0fcb86 100644 --- a/web/src/components/selectImage/selectImage.vue +++ b/web/src/components/selectImage/selectImage.vue @@ -2,10 +2,28 @@
- - + + +
@@ -140,6 +158,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { ArrowLeftBold, CloseBold, + Menu, MoreFilled, Picture as IconPicture, Plus, @@ -149,6 +168,7 @@ import selectComponent from '@/components/selectImage/selectComponent.vue' import { addCategory, deleteCategory, getCategoryList } from '@/api/attachmentCategory' import CropperImage from "@/components/upload/cropper.vue"; import QRCodeUpload from "@/components/upload/QR-code.vue"; +import draggable from 'vuedraggable' import { useI18n } from 'vue-i18n' // added by mohamed hassan to support multilingual const { t } = useI18n() // added by mohamed hassan to support multilingual @@ -427,6 +447,20 @@ const useSelectedImages = () => { selectedImages.value = [] } +const onDragStart = () => { + // 拖拽开始时的处理 + document.body.style.cursor = 'grabbing' +} + +const onDragEnd = () => { + // 拖拽结束时的处理 + document.body.style.cursor = 'default' + // 确保model是数组类型 + if (!Array.isArray(model.value)) { + model.value = [] + } +} + diff --git a/web/src/components/upload/common.vue b/web/src/components/upload/common.vue index 84df41276..c80036e08 100644 --- a/web/src/components/upload/common.vue +++ b/web/src/components/upload/common.vue @@ -7,6 +7,7 @@ :on-success="uploadSuccess" :show-file-list="false" :data="{'classId': props.classId}" + :headers="{'x-token': token}" multiple class="upload-btn" > @@ -20,7 +21,8 @@ import { ElMessage } from 'element-plus' import { isVideoMime, isImageMime } from '@/utils/image' import { getBaseUrl } from '@/utils/format' - import {Upload} from "@element-plus/icons-vue"; + import { Upload } from "@element-plus/icons-vue"; + import { useUserStore } from "@/pinia"; import { useI18n } from 'vue-i18n' // added by mohamed hassan to support multilingual const { t } = useI18n() // added by mohamed hassan to support multilingual @@ -29,6 +31,10 @@ name: 'UploadCommon' }) + const userStore = useUserStore() + + const token = userStore.token + const props = defineProps({ classId: { type: Number, diff --git a/web/src/components/upload/cropper.vue b/web/src/components/upload/cropper.vue index ae8cbd71d..1506a8f8c 100644 --- a/web/src/components/upload/cropper.vue +++ b/web/src/components/upload/cropper.vue @@ -8,6 +8,7 @@ :data="{'classId': props.classId}" :on-success="handleImageSuccess" :on-change="handleFileChange" + :headers="{'x-token': token}" > 裁剪上传 @@ -87,6 +88,7 @@ import { RefreshLeft, RefreshRight, Plus, Minus } from '@element-plus/icons-vue' import 'vue-cropper/dist/index.css' import { VueCropper } from 'vue-cropper' import { getBaseUrl } from '@/utils/format' +import { useUserStore } from "@/pinia"; defineOptions({ name: 'CropperImage' diff --git a/web/src/components/upload/image.vue b/web/src/components/upload/image.vue index f7676ecec..5c21b004d 100644 --- a/web/src/components/upload/image.vue +++ b/web/src/components/upload/image.vue @@ -7,6 +7,7 @@ :before-upload="beforeImageUpload" :multiple="false" :data="{'classId': props.classId}" + :headers="{'x-token': token}" > {{ t('components.upload.image.compressedUpload') }} @@ -17,7 +18,8 @@ import ImageCompress from '@/utils/image' import { ElMessage } from 'element-plus' import { getBaseUrl } from '@/utils/format' - import {Upload} from "@element-plus/icons-vue"; + import { Upload } from "@element-plus/icons-vue"; + import { useUserStore } from "@/pinia"; import { useI18n } from 'vue-i18n' // added by mohamed hassan to support multilingual const { t } = useI18n() // added by mohamed hassan to support multilingual @@ -46,6 +48,10 @@ } }) + const userStore = useUserStore() + + const token = userStore.token + const beforeImageUpload = (file) => { const isJPG = file.type?.toLowerCase() === 'image/jpeg' const isPng = file.type?.toLowerCase() === 'image/png' diff --git a/web/src/core/config.js b/web/src/core/config.js index a4c562e6e..bb44caffb 100644 --- a/web/src/core/config.js +++ b/web/src/core/config.js @@ -17,7 +17,7 @@ export const viteLogo = (env) => { `> 欢迎使用Gin-Vue-Admin,开源地址:https://github.com/flipped-aurora/gin-vue-admin` ) ) - console.log(greenText(`> 当前版本:v2.7.9`)) + console.log(greenText(`> 当前版本:v2.8.2`)) console.log(greenText(`> 加群方式:微信:shouzi_1994 QQ群:470239250`)) console.log( greenText(`> 项目地址:https://github.com/flipped-aurora/gin-vue-admin`) diff --git a/web/src/core/gin-vue-admin.js b/web/src/core/gin-vue-admin.js index af4289dd7..0823d950b 100644 --- a/web/src/core/gin-vue-admin.js +++ b/web/src/core/gin-vue-admin.js @@ -10,7 +10,7 @@ export default { register(app) console.log(` 欢迎使用 Gin-Vue-Admin - 当前版本:v2.7.9 + 当前版本:v2.8.2 加群方式:微信:shouzi_1994 QQ群:622360840 项目地址:https://github.com/flipped-aurora/gin-vue-admin 插件市场:https://plugin.gin-vue-admin.com diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json index d92616626..e8821f66f 100644 --- a/web/src/locales/ar.json +++ b/web/src/locales/ar.json @@ -780,6 +780,7 @@ "enterSenderEmail": "يرجى إدخال البريد الإلكتروني للمرسل", "enterHost": "يرجى إدخال المضيف", "isSSL": "هل تريد تمكين SSL", + "useLoginAuth": "استخدام مصادقة LoginAuth", "enterSecret": "يرجى إدخال السر", "testEmail": "اختبار البريد الإلكتروني", "mongodbOptions": "يرجى إدخال خيارات MongoDB", diff --git a/web/src/locales/en.json b/web/src/locales/en.json index 279a6fecb..d6df3928c 100644 --- a/web/src/locales/en.json +++ b/web/src/locales/en.json @@ -782,6 +782,7 @@ "enterSenderEmail": "Please Enter Sender Email", "enterHost": "Please Enter Host", "isSSL": "Is SSL", + "useLoginAuth": "Use LoginAuth authentication", "enterSecret": "Please Enter Secret", "testEmail": "Test Email", "mongodbOptions": "Please Enter MongoDB Options", diff --git a/web/src/locales/zh-TW.json b/web/src/locales/zh-TW.json index d0498fcc0..52107d8fb 100644 --- a/web/src/locales/zh-TW.json +++ b/web/src/locales/zh-TW.json @@ -639,6 +639,7 @@ "enterSenderEmail": "請輸入發送者郵箱", "enterHost": "請輸入host", "isSSL": "是否為ssl", + "useLoginAuth": "是否LoginAuth認證", "enterSecret": "請輸入secret", "testEmail": "測試郵件", "mongodbOptions": "請輸入mongodb 選項", diff --git a/web/src/locales/zh.json b/web/src/locales/zh.json index 6a31bf243..8fb2f672a 100644 --- a/web/src/locales/zh.json +++ b/web/src/locales/zh.json @@ -782,6 +782,7 @@ "enterSenderEmail": "请输入发送者邮箱", "enterHost": "请输入host", "isSSL": "是否为ssl", + "useLoginAuth": "是否LoginAuth认证", "enterSecret": "请输入secret", "testEmail": "测试邮件", "mongodbOptions": "请输入mongodb 选项", diff --git a/web/src/pathInfo.json b/web/src/pathInfo.json index 9757994d1..099d5d671 100644 --- a/web/src/pathInfo.json +++ b/web/src/pathInfo.json @@ -26,6 +26,7 @@ "/src/view/layout/aside/headMode.vue": "GvaAside", "/src/view/layout/aside/index.vue": "Index", "/src/view/layout/aside/normalMode.vue": "GvaAside", + "/src/view/layout/aside/sidebarMode.vue": "SidebarMode", "/src/view/layout/header/index.vue": "Index", "/src/view/layout/header/tools.vue": "Tools", "/src/view/layout/iframe.vue": "GvaLayoutIframe", @@ -56,6 +57,9 @@ "/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog", "/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog", "/src/view/systemTools/autoCode/index.vue": "AutoCode", + "/src/view/systemTools/autoCode/mcp.vue": "Mcp", + "/src/view/systemTools/autoCode/mcpTest.vue": "McpTest", + "/src/view/systemTools/autoCode/picture.vue": "Picture", "/src/view/systemTools/autoCodeAdmin/index.vue": "AutoCodeAdmin", "/src/view/systemTools/autoPkg/autoPkg.vue": "AutoPkg", "/src/view/systemTools/exportTemplate/exportTemplate.vue": "ExportTemplate", diff --git a/web/src/permission.js b/web/src/permission.js index 33cd6e1e2..5276aea53 100644 --- a/web/src/permission.js +++ b/web/src/permission.js @@ -13,7 +13,7 @@ Nprogress.configure({ }) // 白名单路由 -const WHITE_LIST = ['Login', 'Init', 'ScanUpload'] +const WHITE_LIST = ['Login', 'Init'] // 处理路由加载 const setupRouter = async (userStore) => { @@ -86,14 +86,15 @@ router.beforeEach(async (to, from) => { // 白名单路由处理 if (WHITE_LIST.includes(to.name)) { - if ( - token && - !routerStore.asyncRouterFlag && - !WHITE_LIST.includes(from.name) - ) { - await setupRouter(userStore) + if (token) { + if(!routerStore.asyncRouterFlag){ + await setupRouter(userStore) + } + if(userStore.userInfo.authority.defaultRouter){ + return { name: userStore.userInfo.authority.defaultRouter } + } } - return true + return true } // 需要登录的路由处理 @@ -114,7 +115,7 @@ router.beforeEach(async (to, from) => { return { name: 'Login', - query: { redirect: to.href } + query: { redirect: to.fullPath } } } @@ -125,7 +126,7 @@ router.beforeEach(async (to, from) => { return { name: 'Login', query: { - redirect: document.location.hash + redirect: to.fullPath } } }) diff --git a/web/src/pinia/modules/app.js b/web/src/pinia/modules/app.js index 8b3223535..9eb9dea1f 100644 --- a/web/src/pinia/modules/app.js +++ b/web/src/pinia/modules/app.js @@ -99,6 +99,27 @@ export const useAppStore = defineStore('app', () => { config.transition_type = e } + const baseCoinfg = { + weakness: false, + grey: false, + primaryColor: '#3b82f6', + showTabs: true, + darkMode: 'auto', + layout_side_width: 256, + layout_side_collapsed_width: 80, + layout_side_item_height: 48, + show_watermark: true, + side_mode: 'normal', + // 页面过渡动画配置 + transition_type: 'slide' + } + + const resetConfig = () => { + for (let baseCoinfgKey in baseCoinfg) { + config[baseCoinfgKey] = baseCoinfg[baseCoinfgKey] + } + } + // 监听色弱模式和灰色模式 watchEffect(() => { document.documentElement.classList.toggle('html-weakenss', config.weakness) @@ -128,6 +149,7 @@ export const useAppStore = defineStore('app', () => { toggleConfigSideItemHeight, toggleConfigWatermark, toggleSideMode, - toggleTransition + toggleTransition, + resetConfig } }) diff --git a/web/src/pinia/modules/router.js b/web/src/pinia/modules/router.js index 15c4ae18e..8b9f8290f 100644 --- a/web/src/pinia/modules/router.js +++ b/web/src/pinia/modules/router.js @@ -5,6 +5,7 @@ import { defineStore } from 'pinia' import { ref, watchEffect } from 'vue' import i18n from '@/i18n' // added by mohamed hassan to multilangauge import pathInfo from '@/pathInfo.json' +import {useRoute} from "vue-router"; const notLayoutRouterArr = [] const keepAliveRoutersArr = [] @@ -59,6 +60,9 @@ export const useRouterStore = defineStore('router', () => { }) keepAliveRouters.value = Array.from(new Set(keepArrTemp)) } + + const route = useRoute() + emitter.on('setKeepAlive', setKeepAliveRouters) const asyncRouters = ref([]) @@ -81,14 +85,32 @@ export const useRouterStore = defineStore('router', () => { return menuMap[name]?.children } + const findTopActive = (menuMap, routeName) => { + for (let topName in menuMap) { + const topItem = menuMap[topName]; + if (topItem.children?.some(item => item.name === routeName)) { + return topName; + } + const foundName = findTopActive(topItem.children || {}, routeName); + if (foundName) { + return topName; + } + } + return null; + }; + watchEffect(() => { let topActive = sessionStorage.getItem('topActive') + // 初始化菜单内容,防止重复添加 + topMenu.value = []; asyncRouters.value[0]?.children.forEach((item) => { if (item.hidden) return menuMap[item.name] = item topMenu.value.push({ ...item, children: [] }) }) - + if (!topActive || topActive === 'undefined' || topActive === 'null') { + topActive = findTopActive(menuMap, route.name); + } setLeftMenu(topActive) }) diff --git a/web/src/pinia/modules/user.js b/web/src/pinia/modules/user.js index daee29013..ddd58a00f 100644 --- a/web/src/pinia/modules/user.js +++ b/web/src/pinia/modules/user.js @@ -32,7 +32,7 @@ export const useUserStore = defineStore('user', () => { userInfo.value = val if (val.originSetting) { Object.keys(appStore.config).forEach((key) => { - if (val.originSetting[key]) { + if (val.originSetting[key] !== undefined) { appStore.config[key] = val.originSetting[key] } }) @@ -85,7 +85,6 @@ export const useUserStore = defineStore('user', () => { const res = await login(loginInfo) if (res.code !== 0) { - ElMessage.error(res.message || i18n.global.t('pinia.modules.user.loginFailed')) return false } // 登陆成功,设置用户信息和权限相关信息 @@ -102,8 +101,13 @@ export const useUserStore = defineStore('user', () => { router.addRoute(asyncRouter) }) + if(router.currentRoute.value.query.redirect) { + await router.replace(router.currentRoute.value.query.redirect) + return true + } + if (!router.hasRoute(userInfo.value.authority.defaultRouter)) { - ElMessage.error('请联系管理员进行授权') + ElMessage.error(i18n.global.t('pinia.modules.user.connectAdmin')) } else { await router.replace({ name: userInfo.value.authority.defaultRouter }) } @@ -138,9 +142,12 @@ export const useUserStore = defineStore('user', () => { /* 清理数据 */ const ClearStorage = async () => { token.value = '' - xToken.value = '' + // 使用remove方法正确删除cookie + xToken.remove() sessionStorage.clear() + // 清理所有相关的localStorage项 localStorage.removeItem('originSetting') + localStorage.removeItem('token') } return { diff --git a/web/src/plugin/announcement/view/info.vue b/web/src/plugin/announcement/view/info.vue index 6d102db89..2cbb83756 100644 --- a/web/src/plugin/announcement/view/info.vue +++ b/web/src/plugin/announcement/view/info.vue @@ -99,12 +99,7 @@ > - + diff --git a/web/src/router/index.js b/web/src/router/index.js index e99c82af3..4181603c1 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -19,7 +19,8 @@ const routes = [ path: '/scanUpload', name: 'ScanUpload', meta: { - title: '扫码上传' + title: '扫码上传', + client: true }, component: () => import('@/view/example/upload/scanUpload.vue') }, diff --git a/web/src/style/element_visiable.scss b/web/src/style/element_visiable.scss index 4d33aef7b..112c0bf4f 100644 --- a/web/src/style/element_visiable.scss +++ b/web/src/style/element_visiable.scss @@ -118,6 +118,10 @@ } } +.el-menu-item.is-active{ + color: var(--el-color-primary)!important; +} + .el-sub-menu__title.el-tooltip__trigger, .el-menu-item .el-menu-tooltip__trigger { justify-content: center; diff --git a/web/src/utils/request.js b/web/src/utils/request.js index a0851a520..5913dbb6a 100644 --- a/web/src/utils/request.js +++ b/web/src/utils/request.js @@ -5,6 +5,9 @@ import router from '@/router/index' import { ElLoading } from 'element-plus' import i18n from '@/i18n' // added by mohamed hassan to multilangauge +// 添加一个状态变量,用于跟踪是否已有错误弹窗显示 +let errorBoxVisible = false + const service = axios.create({ baseURL: import.meta.env.VITE_BASE_API, timeout: 99999 @@ -95,7 +98,13 @@ service.interceptors.response.use( closeLoading() } + // 如果已经有错误弹窗显示,则不再显示新的弹窗 + if (errorBoxVisible) { + return error + } + if (!error.response) { + errorBoxVisible = true ElMessageBox.confirm( i18n.global.t('utils.request.requestErrorDetected') + `

${error}

@@ -107,11 +116,15 @@ service.interceptors.response.use( confirmButtonText: i18n.global.t('utils.request.tryAgainLater'), cancelButtonText: i18n.global.t('general.cancel') } - ) + ).finally(() => { + // 弹窗关闭后重置状态 + errorBoxVisible = false + }) return } switch (error.response.status) { case 500: + errorBoxVisible = true ElMessageBox.confirm( i18n.global.t('utils.request.interfaceErrorDetected') + `

${error}

` + @@ -131,9 +144,13 @@ service.interceptors.response.use( const userStore = useUserStore() userStore.ClearStorage() router.push({ name: 'Login', replace: true }) + }).finally(() => { + // 弹窗关闭后重置状态 + errorBoxVisible = false }) break case 404: + errorBoxVisible = true ElMessageBox.confirm( i18n.global.t('utils.request.interfaceErrorDetected') + `

${error}

` + @@ -149,9 +166,13 @@ service.interceptors.response.use( confirmButtonText: i18n.global.t('utils.request.iGotIt'), cancelButtonText: i18n.global.t('general.cancel') } - ) + ).finally(() => { + // 弹窗关闭后重置状态 + errorBoxVisible = false + }) break case 401: + errorBoxVisible = true ElMessageBox.confirm( i18n.global.t('utils.request.invalidToken') + `

` + @@ -170,6 +191,9 @@ service.interceptors.response.use( const userStore = useUserStore() userStore.ClearStorage() router.push({ name: 'Login', replace: true }) + }).finally(() => { + // 弹窗关闭后重置状态 + errorBoxVisible = false }) break } diff --git a/web/src/view/example/upload/scanUpload.vue b/web/src/view/example/upload/scanUpload.vue index 8fd922673..59845d7e9 100644 --- a/web/src/view/example/upload/scanUpload.vue +++ b/web/src/view/example/upload/scanUpload.vue @@ -84,6 +84,7 @@ import 'vue-cropper/dist/index.css' import { VueCropper } from 'vue-cropper' import { getBaseUrl } from '@/utils/format' import { useRouter } from 'vue-router' +import { useUserStore } from "@/pinia"; defineOptions({ name: 'scanUpload' diff --git a/web/src/view/layout/aside/asideComponent/index.vue b/web/src/view/layout/aside/asideComponent/index.vue index 32e7d4186..cebcc4a57 100644 --- a/web/src/view/layout/aside/asideComponent/index.vue +++ b/web/src/view/layout/aside/asideComponent/index.vue @@ -37,7 +37,7 @@ const menuComponent = computed(() => { if ( props.routerInfo.children && - props.routerInfo.children.filter((item) => !item.hidden).length + props.routerInfo.children?.filter((item) => !item.hidden).length ) { return AsyncSubmenu } else { diff --git a/web/src/view/layout/aside/asideComponent/menuItem.vue b/web/src/view/layout/aside/asideComponent/menuItem.vue index b6b4ff777..1bcf67fca 100644 --- a/web/src/view/layout/aside/asideComponent/menuItem.vue +++ b/web/src/view/layout/aside/asideComponent/menuItem.vue @@ -1,14 +1,16 @@ -