├── logs └── .gitignore ├── cache └── .gitignore ├── conf ├── rsa │ └── .gitignore ├── rbac_model.conf ├── config.prd.yml ├── config.st.yml └── config.se.yml ├── internal └── .gitignore ├── wiki ├── image │ ├── role.png │ ├── casdoor.png │ └── index.png ├── casdoor │ └── install.md ├── cd │ └── k8s-deploy.yaml └── ci │ ├── kaniko-build-job.yaml │ ├── buildkit-build-job.yaml │ └── buildkit-argo-workflow-template.yaml ├── .dockerignore ├── .gitignore ├── models ├── req.go ├── token.go ├── sys │ ├── sys_data.go │ ├── sys_router.go │ ├── sys_api_log.go │ ├── sys_lock.go │ ├── sys_cronjob_log.go │ ├── sys_role.go │ ├── sys_req_api_log.go │ └── sys_system.go ├── model.go └── resp.go ├── pkg ├── utils │ ├── tools.go │ ├── safego.go │ ├── casbin.go │ └── debug.go └── global │ ├── global.go │ └── config.go ├── router └── sys │ ├── sys_public.go │ ├── sys_base.go │ ├── sys_data.go │ ├── sys_router.go │ ├── sys_change_log.go │ ├── sys_cronjob_log.go │ ├── sys_api_log.go │ ├── sys_req_api_log.go │ ├── sys_user.go │ ├── sys_system.go │ └── sys_role.go ├── initialize ├── data.go ├── cron.go ├── casdoor.go ├── sentinel.go ├── logger.go ├── config.go ├── validate.go ├── casbin.go ├── rsa.go ├── pgsql.go ├── mysql.go └── router.go ├── middleware ├── exception.go ├── casbin_rbac.go ├── access_log.go └── jwt.go ├── api ├── heath_check.go └── v1 │ └── sys │ ├── sys_router.go │ ├── sys_cronjob_log.go │ ├── sys_change_log.go │ ├── sys_data.go │ ├── sys_api_log.go │ ├── sys_req_api_log.go │ ├── sys_user.go │ ├── sys_system.go │ └── sys_role.go ├── .nocalhost └── config.yaml ├── cronjob └── clean_log.go ├── client └── client_example.go ├── Dockerfile ├── main.go ├── go.mod ├── README.md ├── LICENSE └── docs └── swagger.yaml /logs/.gitignore: -------------------------------------------------------------------------------- 1 | # 忽略所有文件 2 | * 3 | # 除了这个文件 4 | !.gitignore -------------------------------------------------------------------------------- /cache/.gitignore: -------------------------------------------------------------------------------- 1 | # 忽略所有文件 2 | * 3 | # 除了这个文件 4 | !.gitignore -------------------------------------------------------------------------------- /conf/rsa/.gitignore: -------------------------------------------------------------------------------- 1 | # 忽略所有文件 2 | * 3 | # 除了这个文件 4 | !.gitignore -------------------------------------------------------------------------------- /internal/.gitignore: -------------------------------------------------------------------------------- 1 | # 忽略所有文件 2 | * 3 | # 除了这个文件 4 | !.gitignore -------------------------------------------------------------------------------- /wiki/image/role.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linclin/go-gin-rest-api/HEAD/wiki/image/role.png -------------------------------------------------------------------------------- /wiki/image/casdoor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linclin/go-gin-rest-api/HEAD/wiki/image/casdoor.png -------------------------------------------------------------------------------- /wiki/image/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linclin/go-gin-rest-api/HEAD/wiki/image/index.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | ./tmp/ 4 | ./logs/* 5 | ./wiki/* 6 | go-gin-rest-api 7 | go-gin-rest-api.exe 8 | go-gin-rest-api.exe~ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | ./tmp/ 4 | ./logs/* 5 | main.exe 6 | main 7 | go-gin-rest-api 8 | go-gin-rest-api.exe 9 | go-gin-rest-api.exe~ -------------------------------------------------------------------------------- /models/req.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/a8m/rql" 5 | ) 6 | 7 | // http请求结构体 8 | type Req struct { 9 | rql.Query 10 | } 11 | -------------------------------------------------------------------------------- /pkg/utils/tools.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | func JsonStr(data interface{}) string { 8 | jsonData, err := json.Marshal(data) 9 | if err != nil { 10 | return "" 11 | } 12 | return string(jsonData) 13 | } 14 | -------------------------------------------------------------------------------- /router/sys/sys_public.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | // 公共路由, 任何人可访问 8 | func InitPublicRouter(r *gin.RouterGroup) (R gin.IRoutes) { 9 | router := r.Group("public") 10 | { 11 | 12 | } 13 | return router 14 | } 15 | -------------------------------------------------------------------------------- /initialize/data.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "go-gin-rest-api/models/sys" 5 | "go-gin-rest-api/pkg/global" 6 | ) 7 | 8 | // 初始化数据 9 | func InitData() { 10 | go sys.InitSysSystem() 11 | go sys.InitSysRole() 12 | global.Log.Info("初始化表数据完成") 13 | } 14 | -------------------------------------------------------------------------------- /wiki/casdoor/install.md: -------------------------------------------------------------------------------- 1 | ## 安装casdoor 2 | - 1.Docker安装 3 | ``` shell 4 | docker run --name casdoor -d -e driverName=mysql -e dataSourceName='root:mysql@tcp(172.31.116.212:3306)/' -v /etc/localtime:/etc/localtime -p 8000:8000 registry.cn-shenzhen.aliyuncs.com/dev-ops/casdoor:v1.702.0 5 | ``` -------------------------------------------------------------------------------- /pkg/utils/safego.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/pkg/global" 6 | "runtime/debug" 7 | ) 8 | 9 | func SafeGo(f func()) { 10 | go func() { 11 | defer func() { 12 | if err := recover(); err != nil { 13 | global.Log.Error(fmt.Sprintf("运行panic异常: %v\n堆栈信息: %v", err, string(debug.Stack()))) 14 | } 15 | }() 16 | f() 17 | }() 18 | } 19 | -------------------------------------------------------------------------------- /models/token.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type ReqToken struct { 6 | AppId string `json:"AppId" binding:"required"` // AppId 7 | AppSecret string `json:"AppSecret" binding:"required"` // AppSecret 8 | } 9 | 10 | // token返回结构体 11 | type Token struct { 12 | Success bool `json:"success"` // 请求是否成功 13 | Token string `json:"token"` // token 14 | Expires time.Time `json:"expires"` // 过期时间 15 | } 16 | -------------------------------------------------------------------------------- /conf/rbac_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, obj1, obj2, act 3 | 4 | [policy_definition] 5 | p = sub, obj, obj1, obj2, act, eft 6 | 7 | [role_definition] 8 | g = _, _, (_, _) 9 | 10 | [policy_effect] 11 | e = some(where (p_eft == allow)) && !some(where (p_eft == deny)) 12 | 13 | [matchers] 14 | m = (g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && keyMatch2(r.obj1, p.obj1) && keyMatch2(r.obj2, p.obj2) && regexMatch(r.act, p.act)) || r.sub == "root" 15 | -------------------------------------------------------------------------------- /router/sys/sys_base.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | jwt "github.com/appleboy/gin-jwt/v2" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | // 基础路由 9 | func InitBaseRouter(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) (R gin.IRoutes) { 10 | router := r.Group("base") 11 | { 12 | router.POST("/auth", authMiddleware.LoginHandler) 13 | router.POST("/logout", authMiddleware.LogoutHandler) 14 | router.POST("/refresh_token", authMiddleware.RefreshHandler) 15 | } 16 | return router 17 | } 18 | -------------------------------------------------------------------------------- /router/sys/sys_data.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/api/v1/sys" 5 | "go-gin-rest-api/middleware" 6 | 7 | jwt "github.com/appleboy/gin-jwt/v2" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // 系统路由 12 | func InitDataRouter(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) (R gin.IRoutes) { 13 | router := r.Group("data").Use(authMiddleware.MiddlewareFunc()).Use(middleware.CasbinMiddleware) 14 | { 15 | router.GET("/list", sys.GetSysData) 16 | } 17 | return router 18 | } 19 | -------------------------------------------------------------------------------- /router/sys/sys_router.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/api/v1/sys" 5 | "go-gin-rest-api/middleware" 6 | 7 | jwt "github.com/appleboy/gin-jwt/v2" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // 系统路由 12 | func InitRouterRouter(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) (R gin.IRoutes) { 13 | router := r.Group("router").Use(authMiddleware.MiddlewareFunc()).Use(middleware.CasbinMiddleware) 14 | { 15 | router.POST("/list", sys.GetRouters) 16 | } 17 | return router 18 | } 19 | -------------------------------------------------------------------------------- /router/sys/sys_change_log.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/api/v1/sys" 5 | "go-gin-rest-api/middleware" 6 | 7 | jwt "github.com/appleboy/gin-jwt/v2" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // 数据审计日志 12 | func InitChangeLogRouter(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) (R gin.IRoutes) { 13 | router := r.Group("changelog").Use(authMiddleware.MiddlewareFunc()).Use(middleware.CasbinMiddleware) 14 | { 15 | router.POST("/list", sys.GetChangeLog) 16 | } 17 | return router 18 | } 19 | -------------------------------------------------------------------------------- /router/sys/sys_cronjob_log.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/api/v1/sys" 5 | "go-gin-rest-api/middleware" 6 | 7 | jwt "github.com/appleboy/gin-jwt/v2" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // 任务日志 12 | func InitCronjobLogRouter(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) (R gin.IRoutes) { 13 | router := r.Group("cronjoblog").Use(authMiddleware.MiddlewareFunc()).Use(middleware.CasbinMiddleware) 14 | { 15 | router.POST("/list", sys.GetCronjobLog) 16 | } 17 | return router 18 | } 19 | -------------------------------------------------------------------------------- /router/sys/sys_api_log.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/api/v1/sys" 5 | "go-gin-rest-api/middleware" 6 | 7 | jwt "github.com/appleboy/gin-jwt/v2" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // 服务接口日志 12 | func InitApiLogRouter(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) (R gin.IRoutes) { 13 | router := r.Group("apilog").Use(authMiddleware.MiddlewareFunc()).Use(middleware.CasbinMiddleware) 14 | { 15 | router.POST("/list", sys.GetApiLog) 16 | router.GET("/get/:requestid", sys.GetApiLogById) 17 | } 18 | return router 19 | } 20 | -------------------------------------------------------------------------------- /router/sys/sys_req_api_log.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/api/v1/sys" 5 | "go-gin-rest-api/middleware" 6 | 7 | jwt "github.com/appleboy/gin-jwt/v2" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // 请求接口日志 12 | func InitReqApiLogRouter(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) (R gin.IRoutes) { 13 | router := r.Group("reqapilog").Use(authMiddleware.MiddlewareFunc()).Use(middleware.CasbinMiddleware) 14 | { 15 | router.POST("/list", sys.GetReqApiLog) 16 | router.GET("/get/:requestid", sys.GetReqApiLogById) 17 | } 18 | return router 19 | } 20 | -------------------------------------------------------------------------------- /pkg/global/global.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "log" 5 | "log/slog" 6 | 7 | "github.com/casbin/casbin/v2" 8 | ut "github.com/go-playground/universal-translator" 9 | "github.com/go-playground/validator/v10" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | var ( 14 | // 系统配置 15 | Conf Configuration 16 | // slog日志 17 | Logger *log.Logger 18 | Log *slog.Logger 19 | // mysql实例 20 | DB *gorm.DB 21 | // Casbin实例 22 | CasbinACLEnforcer *casbin.SyncedEnforcer 23 | // validation.v10校验器 24 | Validate *validator.Validate 25 | // validation.v10相关翻译器 26 | Translator ut.Translator 27 | ) 28 | -------------------------------------------------------------------------------- /middleware/exception.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/models" 6 | "go-gin-rest-api/pkg/global" 7 | "runtime/debug" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // 全局异常处理中间件 13 | func Exception(c *gin.Context) { 14 | defer func() { 15 | if err := recover(); err != nil { 16 | // 将异常写入日志 17 | global.Log.Error(fmt.Sprintf("未知panic异常: %v\n堆栈信息: %v", err, string(debug.Stack()))) 18 | models.FailWithDetailed(fmt.Sprintf("未知panic异常: %v\n堆栈信息: %v", err, string(debug.Stack())), models.CustomError[models.InternalServerError], c) 19 | c.Abort() 20 | return 21 | } 22 | }() 23 | c.Next() 24 | } 25 | -------------------------------------------------------------------------------- /router/sys/sys_user.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/api/v1/sys" 5 | "go-gin-rest-api/middleware" 6 | 7 | jwt "github.com/appleboy/gin-jwt/v2" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // 用户权限 12 | func InitUserRouter(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) (R gin.IRoutes) { 13 | router := r.Group("user").Use(authMiddleware.MiddlewareFunc()).Use(middleware.CasbinMiddleware) 14 | { 15 | router.POST("/login", sys.Login) 16 | router.GET("/info", sys.GetUserInfo) 17 | router.GET("/perm/:user", sys.GetPermission) 18 | router.POST("/auth/:user", sys.AuthPermission) 19 | } 20 | return router 21 | } 22 | -------------------------------------------------------------------------------- /initialize/cron.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/cronjob" 6 | "go-gin-rest-api/pkg/global" 7 | "time" 8 | 9 | "github.com/robfig/cron/v3" 10 | ) 11 | 12 | // 初始化定时任务 13 | func Cron() { 14 | loc, err := time.LoadLocation("Asia/Shanghai") 15 | if err != nil { 16 | panic(fmt.Sprintf("初始化Cron定时任务失败: %v", err)) 17 | } 18 | c := cron.New(cron.WithLocation(loc), cron.WithSeconds(), cron.WithLogger(cron.VerbosePrintfLogger(global.Logger))) 19 | //清理超过一周的日志表数据 20 | c.AddJob("0 0 1 * * *", cron.NewChain(cron.Recover(cron.VerbosePrintfLogger(global.Logger)), cron.SkipIfStillRunning(cron.VerbosePrintfLogger(global.Logger))).Then(&cronjob.CleanLog{})) 21 | c.Start() 22 | global.Log.Info("初始化Cron定时任务完成") 23 | } 24 | -------------------------------------------------------------------------------- /initialize/casdoor.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "go-gin-rest-api/pkg/global" 5 | "os" 6 | 7 | "github.com/casdoor/casdoor-go-sdk/casdoorsdk" 8 | ) 9 | 10 | // 初始化casdoor客户端 11 | func InitCasdoor() { 12 | certificate, err := os.ReadFile("./conf/rsa/" + global.Conf.Casdoor.CertificatePath) 13 | if err != nil { 14 | return 15 | //panic(fmt.Sprintf("初始化casdoor客户端失败: %v", err)) 16 | } 17 | global.Conf.Casdoor.Certificate = string(certificate) 18 | casdoorsdk.InitConfig( 19 | global.Conf.Casdoor.Endpoint, 20 | global.Conf.Casdoor.ClientID, 21 | global.Conf.Casdoor.ClientSecret, 22 | global.Conf.Casdoor.Certificate, 23 | global.Conf.Casdoor.Organization, 24 | global.Conf.Casdoor.Application, 25 | ) 26 | global.Log.Info("初始化casdoor客户端完成") 27 | } 28 | -------------------------------------------------------------------------------- /models/sys/sys_data.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | // 系统运营数据 4 | type SysData struct { 5 | ApiCount int64 `json:"ApiCount"` 6 | AllApiCount int64 `json:"AllApiCount"` 7 | WeekApiCount []DateCount `json:"WeekApiCount"` 8 | WeekClientApiCount []ClientIPCount `json:"WeekClientApiCount"` 9 | ReqApiCount int64 `json:"ReqApiCount"` 10 | AllReqApiCount int64 `json:"AllReqApiCount"` 11 | SystemCount int64 `json:"SystemCount"` 12 | RouterCount int64 `json:"RouterCount"` 13 | } 14 | type DateCount struct { 15 | Date string `json:"Date"` 16 | Count int64 `json:"Count"` 17 | } 18 | type ClientIPCount struct { 19 | ClientIP string `json:"ClientIP"` 20 | Count int64 `json:"Count"` 21 | } 22 | -------------------------------------------------------------------------------- /middleware/casbin_rbac.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "go-gin-rest-api/models" 5 | "go-gin-rest-api/pkg/global" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // Casbin中间件, 基于RBAC的权限访问控制模型 12 | func CasbinMiddleware(c *gin.Context) { 13 | AppId, _ := c.Get("AppId") 14 | // 请求URL路径作为casbin访问资源obj(需先清除path前缀) 15 | obj := c.Request.URL.Path 16 | // 请求方式作为casbin访问动作act 17 | act := c.Request.Method 18 | // 检查策略 19 | permResult, err := global.CasbinACLEnforcer.Enforce(AppId, obj, "*", "*", act) 20 | if err != nil || !permResult { 21 | c.JSON(http.StatusForbidden, models.Resp{ 22 | Success: models.ERROR, 23 | Data: models.ForbiddenMsg, 24 | Msg: models.CustomError[models.Forbidden], 25 | }) 26 | c.Abort() 27 | return 28 | } else { 29 | // 处理请求 30 | c.Next() 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /router/sys/sys_system.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/api/v1/sys" 5 | "go-gin-rest-api/middleware" 6 | 7 | jwt "github.com/appleboy/gin-jwt/v2" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // 系统 12 | func InitSystemRouter(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) (R gin.IRoutes) { 13 | router := r.Group("system").Use(authMiddleware.MiddlewareFunc()).Use(middleware.CasbinMiddleware) 14 | { 15 | router.POST("/list", sys.GetSystems) 16 | router.GET("/get/:id", sys.GetSystemById) 17 | router.POST("/create", sys.CreateSystem) 18 | router.PATCH("/update/:id", sys.UpdateSystemById) 19 | router.DELETE("/delete/:id", sys.DeleteSystemById) 20 | //系统权限 21 | router.GET("/perm/get/:id", sys.GetSystemPermById) 22 | router.POST("/perm/create/:id", sys.CreateSystemPerm) 23 | router.DELETE("/perm/delete/:id", sys.DeleteSystemPermById) 24 | 25 | } 26 | return router 27 | } 28 | -------------------------------------------------------------------------------- /initialize/sentinel.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "fmt" 5 | 6 | sentinel "github.com/alibaba/sentinel-golang/api" 7 | "github.com/alibaba/sentinel-golang/core/config" 8 | "github.com/alibaba/sentinel-golang/core/flow" 9 | 10 | "go-gin-rest-api/pkg/global" 11 | ) 12 | 13 | func InitSentinel() { 14 | //初始化 sentinel 15 | conf := config.NewDefaultConfig() 16 | conf.Sentinel.App.Name = global.Conf.System.AppName 17 | conf.Sentinel.Log.Dir = "./logs/sentinel" 18 | err := sentinel.InitWithConfig(conf) 19 | if err != nil { 20 | global.Log.Error("初始化Sentinel配置出错", err) 21 | } 22 | if _, err := flow.LoadRules([]*flow.Rule{ 23 | { 24 | Resource: "POST:/api/v1/base/auth", 25 | Threshold: 10, 26 | TokenCalculateStrategy: flow.Direct, 27 | ControlBehavior: flow.Reject, 28 | }, 29 | }); err != nil { 30 | global.Log.Error(fmt.Sprintf("初始化Sentinel流控规则出错: %+v", err)) 31 | return 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/utils/casbin.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | // TimeMatchFunc is the wrapper for TimeMatch. 9 | func TimeMatchFunc(args ...string) (bool, error) { 10 | if len(args) != 2 { 11 | return false, fmt.Errorf("args error") 12 | } 13 | return TimeMatch(args[0], args[1]) 14 | } 15 | 16 | // TimeMatch determines whether the current time is between startTime and endTime. 17 | // You can use "_" to indicate that the parameter is ignored 18 | func TimeMatch(startTime, endTime string) (bool, error) { 19 | now := time.Now() 20 | if startTime != "_" { 21 | if start, err := time.Parse("2006-01-02 15:04:05", startTime); err != nil { 22 | return false, err 23 | } else if !now.After(start) { 24 | return false, nil 25 | } 26 | } 27 | if endTime != "_" { 28 | if end, err := time.Parse("2006-01-02 15:04:05", endTime); err != nil { 29 | return false, err 30 | } else if !now.Before(end) { 31 | return false, nil 32 | } 33 | } 34 | return true, nil 35 | } 36 | -------------------------------------------------------------------------------- /api/heath_check.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "go-gin-rest-api/models" 5 | "go-gin-rest-api/pkg/global" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // @Summary [系统内部]健康检查接口 12 | // @Id HeathCheck 13 | // @Tags [系统内部]路由 14 | // @version 1.0 15 | // @Accept application/x-json-stream 16 | // @Success 200 object models.Resp 返回列表 17 | // @Failure 500 object models.Resp 查询失败 18 | // @Router /heatch_check [get] 19 | func HeathCheck(c *gin.Context) { 20 | errStr := "" 21 | // MySQL连接检查 22 | db, _ := global.DB.DB() 23 | err := db.Ping() 24 | if err != nil { 25 | errStr += "健康检查失败 数据库连接错误:" + err.Error() + "\r\n" 26 | } 27 | if errStr != "" { 28 | c.JSON(http.StatusInternalServerError, models.Resp{ 29 | Success: models.SUCCESS, 30 | Data: errStr, 31 | Msg: models.CustomError[models.NotOk], 32 | }) 33 | return 34 | } 35 | c.JSON(http.StatusOK, models.Resp{ 36 | Success: models.SUCCESS, 37 | Data: "健康检查完成", 38 | Msg: models.CustomError[models.Ok], 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /models/sys/sys_router.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | // 系统路由表 鉴权用 8 | type SysRouter struct { 9 | gorm.Model 10 | Name string `gorm:"column:Name;comment:接口名称" json:"Name" rql:"filter,sort,column=Name"` // 接口名称 11 | Group string `gorm:"column:Group;comment:路由分组" json:"Group" rql:"filter,sort,column=Group"` // 路由分组 12 | HttpMethod string `gorm:"column:HttpMethod;index:idx_sysrouter_httpmethod_absolutepath;comment:HTTP方法" json:"HttpMethod" rql:"filter,sort,column=HttpMethod"` // HTTP方法 13 | AbsolutePath string `gorm:"column:AbsolutePath;index:idx_sysrouter_httpmethod_absolutepath;comment:路由地址" json:"AbsolutePath" rql:"filter,sort,column=AbsolutePath"` // 路由地址 14 | HandlerName string `gorm:"column:HandlerName;index:idx_sysrouter_httpmethod_absolutepath;comment:控制器" json:"HandlerName" rql:"filter,sort,column=HandlerName"` // 控制器 15 | } 16 | -------------------------------------------------------------------------------- /router/sys/sys_role.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/api/v1/sys" 5 | "go-gin-rest-api/middleware" 6 | 7 | jwt "github.com/appleboy/gin-jwt/v2" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // 角色 12 | func InitRoleRouter(r *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) (R gin.IRoutes) { 13 | router := r.Group("role").Use(authMiddleware.MiddlewareFunc()).Use(middleware.CasbinMiddleware) 14 | { 15 | router.POST("/list", sys.GetRoles) 16 | router.GET("/get/:id", sys.GetRoleById) 17 | router.POST("/create", sys.CreateRole) 18 | router.PATCH("/update/:id", sys.UpdateRoleById) 19 | router.DELETE("/delete/:id", sys.DeleteRoleById) 20 | //角色权限 21 | router.GET("/perm/get/:id", sys.GetRolePermById) 22 | router.POST("/perm/create/:id", sys.CreateRolePerm) 23 | router.DELETE("/perm/delete/:id", sys.DeleteRolePermById) 24 | //角色用户 25 | router.GET("/users/get/:id", sys.GetRoleUsersById) 26 | router.POST("/users/create/:id", sys.CreateRoleUser) 27 | router.DELETE("/users/delete/:id", sys.DeleteRoleUserById) 28 | } 29 | return router 30 | } 31 | -------------------------------------------------------------------------------- /.nocalhost/config.yaml: -------------------------------------------------------------------------------- 1 | name: "go-gin-rest-api" 2 | serviceType: "deployment" 3 | containers: 4 | - 5 | name: "go-gin-rest-api" 6 | dev: 7 | #gitUrl: "https://gitee.com/dev-ops/go-gin-rest-api.git" 8 | image: "registry.cn-shenzhen.aliyuncs.com/dev-ops/golang:1.23.0-alpine3.20" 9 | sidecarImage: "registry.cn-shenzhen.aliyuncs.com/dev-ops/nocalhost-sidecar:syncthing" 10 | workDir: "/data/go-gin-rest-api" 11 | shell: "bash" 12 | command: 13 | run: 14 | - "go" 15 | - "run" 16 | - "main.go" 17 | env: 18 | - 19 | name: "RunMode" 20 | value: "se" 21 | resources: 22 | limits: 23 | memory: 4Gi 24 | cpu: "2" 25 | requests: 26 | memory: 2Gi 27 | cpu: "1" 28 | portForward: 29 | - "8080:8080" 30 | patches: 31 | - patch: '{"spec":{"template":{"spec":{"containers":[{"name":"nocalhost-sidecar","securityContext":{"privileged":"false","runAsUser":"1000","runAsGroup":"1000"}}]}}}}' 32 | type: strategic 33 | 34 | -------------------------------------------------------------------------------- /initialize/logger.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/pkg/global" 6 | "io" 7 | "log/slog" 8 | "os" 9 | "runtime/debug" 10 | 11 | "github.com/natefinch/lumberjack" 12 | ) 13 | 14 | // 初始化日志 15 | func Logger() { 16 | fileName := fmt.Sprintf("%s/go-gin-rest-api.log", global.Conf.Logs.Path) 17 | logFile := &lumberjack.Logger{ 18 | Filename: fileName, // 日志文件路径 19 | MaxSize: global.Conf.Logs.MaxSize, // 最大尺寸, M 20 | MaxBackups: global.Conf.Logs.MaxBackups, // 备份数 21 | MaxAge: global.Conf.Logs.MaxAge, // 存放天数 22 | Compress: global.Conf.Logs.Compress, // 是否压缩 23 | } 24 | logOpts := slog.HandlerOptions{ 25 | AddSource: true, 26 | Level: global.Conf.Logs.Level, 27 | } 28 | logger := slog.New(slog.NewJSONHandler(io.MultiWriter(os.Stdout, logFile), &logOpts)) 29 | slog.SetDefault(logger) 30 | global.Log = logger 31 | global.Logger = slog.NewLogLogger(slog.NewJSONHandler(io.MultiWriter(os.Stdout, logFile), &logOpts), slog.LevelInfo) 32 | global.Log.Info("初始化日志完成") 33 | panicFile, err := os.Create(fmt.Sprintf("%s/panic.log", global.Conf.Logs.Path)) 34 | if err != nil { 35 | global.Log.Info(fmt.Sprint("初始化panic日志完成错误", err.Error())) 36 | panic(err) 37 | } 38 | debug.SetCrashOutput(panicFile, debug.CrashOptions{}) 39 | global.Log.Info("初始化panic日志完成") 40 | } 41 | -------------------------------------------------------------------------------- /conf/config.prd.yml: -------------------------------------------------------------------------------- 1 | # prod 2 | system: 3 | # 系统名称 4 | app-name: go-gin-rest-api 5 | # 系统环境 6 | run-mode: prd 7 | # url前缀 8 | url-path-prefix: api 9 | # 程序监听端口 10 | port: 8080 11 | # API地址 12 | base-api: http://127.0.0.1:8080 13 | # 开启全局事务管理器 14 | transaction: true 15 | # 是否初始化数据(没有初始数据时使用, 已发布正式版谨慎使用) 16 | init-data: true 17 | logs: 18 | # 日志等级(-4:Debug, 0:Info,4:Warn, 8:Error, 参照slog.level源码) 19 | level: 0 20 | # 日志路径 21 | path: logs 22 | # 文件最大大小, M 23 | max-size: 50 24 | # 备份数 25 | max-backups: 100 26 | # 存放时间, 天 27 | max-age: 7 28 | # 是否压缩 29 | compress: true 30 | mysql: 31 | # 用户名 32 | username: root 33 | # 密码 34 | password: mysql 35 | # 数据库名 36 | database: go-gin-rest-api 37 | # 主机地址 38 | host: 127.0.0.1 39 | # 端口 40 | port: 3306 41 | # 连接字符串查询参数 42 | query: charset=utf8mb4&parseTime=True&loc=Local&timeout=10000ms 43 | # casbin配置 44 | casbin: 45 | # 模型配置文件, 默认以conf目录为根目录 46 | model-path: "rbac_model.conf" 47 | # jwt配置 48 | jwt: 49 | # token过期时间, 小时 50 | timeout: 2 51 | # token更新时间, 小时 52 | max-refresh: 2 53 | # casdoor配置 54 | casdoor: 55 | endpoint: "https://door.casdoor.com" 56 | client-id: "294b09fbc17f95daf2fe" 57 | client-secret: "dd8982f7046ccba1bbd7851d5c1ece4e52bf039d" 58 | certificate-path: "casdoor-prd.key" 59 | organization: "casbin" 60 | application: "app-vue-python-example" 61 | frontend-url: "http://localhost:3000" 62 | -------------------------------------------------------------------------------- /conf/config.st.yml: -------------------------------------------------------------------------------- 1 | # stage 2 | system: 3 | # 系统名称 4 | app-name: go-gin-rest-api 5 | # 系统环境 6 | run-mode: st 7 | # url前缀 8 | url-path-prefix: api 9 | # 程序监听端口 10 | port: 8080 11 | # API地址 12 | base-api: http://127.0.0.1:8080 13 | # 开启全局事务管理器 14 | transaction: true 15 | # 是否初始化数据(没有初始数据时使用, 已发布正式版谨慎使用) 16 | init-data: true 17 | logs: 18 | # 日志等级(-4:Debug, 0:Info,4:Warn, 8:Error, 参照slog.level源码) 19 | level: 0 20 | # 日志路径 21 | path: logs 22 | # 文件最大大小, M 23 | max-size: 50 24 | # 备份数 25 | max-backups: 100 26 | # 存放时间, 天 27 | max-age: 30 28 | # 是否压缩 29 | compress: false 30 | mysql: 31 | # 用户名 32 | username: root 33 | # 密码 34 | password: mysql 35 | # 数据库名 36 | database: go-gin-rest-api 37 | # 主机地址 38 | host: 127.0.0.1 39 | # 端口 40 | port: 3306 41 | # 连接字符串查询参数 42 | query: charset=utf8mb4&parseTime=True&loc=Local&timeout=10000ms 43 | # casbin配置 44 | casbin: 45 | # 模型配置文件, 默认以conf目录为根目录 46 | model-path: "rbac_model.conf" 47 | # jwt配置 48 | jwt: 49 | # token过期时间, 小时 50 | timeout: 2 51 | # token更新时间, 小时 52 | max-refresh: 2 53 | # casdoor配置 54 | casdoor: 55 | endpoint: "https://door.casdoor.com" 56 | client-id: "294b09fbc17f95daf2fe" 57 | client-secret: "dd8982f7046ccba1bbd7851d5c1ece4e52bf039d" 58 | certificate-path: "casdoor-st.key" 59 | organization: "casbin" 60 | application: "app-vue-python-example" 61 | frontend-url: "http://localhost:3000" 62 | -------------------------------------------------------------------------------- /conf/config.se.yml: -------------------------------------------------------------------------------- 1 | # dev 2 | system: 3 | # 系统名称 4 | app-name: go-gin-rest-api 5 | # 系统环境 6 | run-mode: se 7 | # url前缀 8 | url-path-prefix: api 9 | # 程序监听端口 10 | port: 8080 11 | # API地址 12 | base-api: http://127.0.0.1:8080 13 | # 开启全局事务管理器 14 | transaction: true 15 | # 是否初始化数据(没有初始数据时使用, 已发布正式版谨慎使用) 16 | init-data: true 17 | logs: 18 | # 日志等级(-4:Debug, 0:Info,4:Warn, 8:Error, 参照slog.level源码) 19 | level: -4 20 | # 日志路径 21 | path: logs 22 | # 文件最大大小, M 23 | max-size: 50 24 | # 备份数 25 | max-backups: 100 26 | # 存放时间, 天 27 | max-age: 30 28 | # 是否压缩 29 | compress: false 30 | mysql: 31 | # 用户名 32 | username: root 33 | # 密码 34 | password: mysql 35 | # 数据库名 36 | database: go-gin-rest-api 37 | # 主机地址 38 | host: 127.0.0.1 39 | # 端口 40 | port: 3306 41 | # 连接字符串查询参数 42 | query: charset=utf8mb4&parseTime=True&loc=Local&timeout=10000ms 43 | # casbin配置 44 | casbin: 45 | # 模型配置文件, 默认以conf目录为根目录 46 | model-path: "rbac_model.conf" 47 | # jwt配置 48 | jwt: 49 | # token过期时间, 小时 50 | timeout: 2 51 | # token更新时间, 小时 52 | max-refresh: 2 53 | # casdoor配置 54 | casdoor: 55 | endpoint: "http://127.0.0.1:8000" 56 | client-id: "fcbf0997646218329673" 57 | client-secret: "255b86f18821dfa6072e3d56db7a0e907dd0924a" 58 | certificate-path: "casdoor-se.pem" 59 | organization: "built-in" 60 | application: "app-built-in" 61 | frontend-url: "http://localhost:3000" 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /initialize/config.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/pkg/global" 6 | "os" 7 | "strings" 8 | 9 | "github.com/fsnotify/fsnotify" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | const ( 14 | configType = "yml" 15 | configPath = "./conf" 16 | devConfig = "config.se.yml" 17 | stageConfig = "config.st.yml" 18 | prodConfig = "config.prd.yml" 19 | ) 20 | 21 | // 初始化配置文件 22 | func InitConfig() { 23 | 24 | // 获取实例(可创建多实例读取多个配置文件, 这里不做演示) 25 | v := viper.New() 26 | // 读取当前go运行环境变量 27 | env := os.Getenv("RunMode") 28 | configName := devConfig 29 | if env == "st" { 30 | configName = stageConfig 31 | } else if env == "prd" { 32 | configName = prodConfig 33 | } 34 | v.SetConfigName(configName) 35 | v.SetConfigType(configType) 36 | v.AddConfigPath(configPath) 37 | v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 38 | // 绑定环境变量 39 | v.AutomaticEnv() 40 | err := v.ReadInConfig() 41 | if err != nil { 42 | panic(fmt.Sprintf("初始化配置文件失败: %v", err)) 43 | } 44 | // 转换为结构体 45 | if err := v.Unmarshal(&global.Conf); err != nil { 46 | panic(fmt.Sprintf("初始化配置文件失败: %v", err)) 47 | } 48 | // 监听文件修改,热加载配置。因此不需要重启服务器,就能让配置生效。 49 | v.WatchConfig() 50 | // 监听文件修改回调函数 51 | v.OnConfigChange(func(e fsnotify.Event) { 52 | fmt.Printf("配置文件:%s 发生变更:%s\n", e.Name, e.Op) 53 | // 转换为结构体 54 | if err := v.Unmarshal(&global.Conf); err != nil { 55 | panic(fmt.Sprintf("初始化配置文件失败: %v", err)) 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /models/sys/sys_api_log.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | // 系统API日志表 10 | type SysApiLog struct { 11 | gorm.Model 12 | RequestId string `gorm:"column:RequestId;unique;index;comment:请求ID" json:"RequestId" rql:"filter,sort,column=RequestId"` // 请求ID 13 | RequestMethod string `gorm:"column:RequestMethod;comment:请求方法" json:"RequestMethod" rql:"filter,sort,column=RequestMethod"` // HTTP方法 14 | RequestURI string `gorm:"column:RequestURI;index;comment:请求路径" json:"RequestURI" rql:"filter,sort,column=RequestURI"` // 路由地址 15 | RequestBody string `gorm:"column:RequestBody;type:longText;comment:请求体" json:"RequestBody" rql:"filter,sort,column=RequestBody"` // 请求体 16 | StatusCode int `gorm:"column:StatusCode;index;comment:状态码" json:"StatusCode" rql:"filter,sort,column=StatusCode"` // 状态码 17 | RespBody string `gorm:"column:RespBody;type:longText;comment:返回体" json:"RespBody" rql:"filter,sort,column=RespBody"` // 返回体 18 | ClientIP string `gorm:"column:ClientIP;comment:访问IP" json:"ClientIP" rql:"filter,sort,column=ClientIP"` // 访问IP 19 | StartTime time.Time `gorm:"column:StartTime;comment:访问时间" json:"StartTime" rql:"filter,sort,column=StartTime,layout=2006-01-02 15:04:05"` // 访问时间 20 | ExecTime string `gorm:"column:ExecTime;comment:结束时间" json:"ExecTime" rql:"filter,sort,column=ExecTime,layout=2006-01-02 15:04:05"` // 执行时间 21 | } 22 | -------------------------------------------------------------------------------- /models/sys/sys_lock.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/pkg/global" 6 | "strings" 7 | "time" 8 | 9 | "gorm.io/gorm" 10 | ) 11 | 12 | // 任务锁 13 | type SysLock struct { 14 | gorm.Model 15 | LockMethod string `gorm:"column:LockMethod;uniqueIndex;comment:任务名称" json:"LockMethod"` // 任务名称 16 | ExpireTime int64 `gorm:"column:ExpireTime;comment:过期时间" json:"ExpireTime"` // 过期时间 17 | } 18 | 19 | func NewLock(lockMethod string, expireTime int64) *SysLock { 20 | return &SysLock{ 21 | LockMethod: lockMethod, 22 | ExpireTime: expireTime, 23 | } 24 | } 25 | 26 | func (lock *SysLock) TryLock() bool { 27 | if err := lock.deleteExpiredLock(); err != nil { 28 | global.Log.Error(fmt.Sprint("清理过期任务锁失败", lock.LockMethod, err.Error())) 29 | return false 30 | } 31 | var newlock = SysLock{ 32 | LockMethod: lock.LockMethod, 33 | ExpireTime: time.Now().Unix() + lock.ExpireTime, 34 | } 35 | newlock.DeletedAt = gorm.DeletedAt{ 36 | Time: time.Now(), 37 | Valid: true, 38 | } 39 | err := global.DB.Create(&newlock).Error 40 | if err != nil { 41 | if strings.Contains(err.Error(), "Duplicate entry") { 42 | return false 43 | } 44 | global.Log.Error(fmt.Sprint("获取任务锁失败", err.Error())) 45 | return false 46 | } 47 | return true 48 | } 49 | 50 | func (lock *SysLock) deleteExpiredLock() error { 51 | var now = time.Now().Unix() 52 | return global.DB.Where("LockMethod = ? AND ExpireTime < ?", lock.LockMethod, now).Unscoped().Delete(SysLock{}).Error 53 | } 54 | 55 | func (lock *SysLock) DeleteLock() error { 56 | return global.DB.Where("LockMethod = ? ", lock.LockMethod).Unscoped().Delete(SysLock{}).Error 57 | } 58 | -------------------------------------------------------------------------------- /models/sys/sys_cronjob_log.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/pkg/global" 6 | "time" 7 | 8 | "gorm.io/gorm" 9 | ) 10 | 11 | // 任务日志表 12 | type SysCronjobLog struct { 13 | gorm.Model 14 | CronMethod string `gorm:"column:CronMethod;comment:任务名称" json:"CronMethod" rql:"filter,sort,column=CronMethod"` // 任务名称 15 | CronParam string `gorm:"column:CronParam;comment:任务参数" json:"CronParam"` // 任务参数 16 | StartTime time.Time `gorm:"column:StartTime;comment:开始时间" json:"StartTime" rql:"filter,sort,column=StartTime"` // 开始时间 17 | EndTime time.Time `gorm:"column:EndTime;comment:结束时间" json:"EndTime" rql:"filter,sort,column=EndTime"` // 结束时间 18 | ExecTime float64 `gorm:"column:ExecTime;comment:执行时间(秒)" json:"ExecTime"` // 执行时间(秒) 19 | Status string `gorm:"column:Status;comment:执行状态" json:"Status" rql:"filter,sort,column=StartTime"` // 执行状态 20 | ErrMsg string `gorm:"column:ErrMsg;comment:错误信息" json:"ErrMsg"` // 错误信息 21 | } 22 | 23 | func AddSysCronjobLog(cronMethod, cronParam, status, errMsg string, startTime, endTime time.Time, execTime float64) error { 24 | var cronjoblog = SysCronjobLog{ 25 | CronMethod: cronMethod, 26 | CronParam: cronParam, 27 | StartTime: startTime, 28 | EndTime: endTime, 29 | ExecTime: execTime, 30 | Status: status, 31 | ErrMsg: errMsg, 32 | } 33 | err := global.DB.Create(&cronjoblog).Error 34 | if err != nil { 35 | global.Log.Error(fmt.Sprint("AddSysCronjobLog写入定时任务日志表失败", err.Error())) 36 | return err 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /api/v1/sys/sys_router.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/models" 5 | "go-gin-rest-api/models/sys" 6 | "go-gin-rest-api/pkg/global" 7 | 8 | "github.com/a8m/rql" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // @Summary [系统内部]获取系统路由 13 | // @Id GetRouters 14 | // @Tags [系统内部]路由 15 | // @version 1.0 16 | // @Accept application/x-json-stream 17 | // @Param body body models.Req true "RQL查询json" 18 | // @Success 200 object models.Resp 返回列表 19 | // @Failure 400 object models.Resp 查询失败 20 | // @Security ApiKeyAuth 21 | // @Router /api/v1/router/list [post] 22 | func GetRouters(c *gin.Context) { 23 | rqlQueryParser, err := rql.NewParser(rql.Config{ 24 | Model: sys.SysRouter{}, 25 | DefaultLimit: -1, 26 | }) 27 | if err != nil { 28 | models.FailWithDetailed("", err.Error(), c) 29 | return 30 | } 31 | // 绑定参数 32 | var rqlQuery rql.Query 33 | err = c.ShouldBindJSON(&rqlQuery) 34 | if err != nil { 35 | models.FailWithDetailed("", err.Error(), c) 36 | return 37 | } 38 | rqlParams, err := rqlQueryParser.ParseQuery(&rqlQuery) 39 | if err != nil { 40 | models.FailWithDetailed("", err.Error(), c) 41 | return 42 | } 43 | if rqlParams.Sort == "" { 44 | rqlParams.Sort = "id desc" 45 | } 46 | list := make([]sys.SysRouter, 0) 47 | query := global.DB 48 | count := int64(0) 49 | err = query.Model(sys.SysRouter{}).Where(rqlParams.FilterExp, rqlParams.FilterArgs...).Count(&count).Error 50 | if err != nil { 51 | models.FailWithDetailed("", err.Error(), c) 52 | return 53 | } 54 | err = query.Where(rqlParams.FilterExp, rqlParams.FilterArgs...).Limit(rqlParams.Limit).Offset(rqlParams.Offset).Order(rqlParams.Sort).Find(&list).Error 55 | if err != nil { 56 | models.FailWithDetailed("", err.Error(), c) 57 | return 58 | } 59 | models.OkWithDataList(list, count, c) 60 | } 61 | -------------------------------------------------------------------------------- /api/v1/sys/sys_cronjob_log.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/models" 5 | "go-gin-rest-api/models/sys" 6 | "go-gin-rest-api/pkg/global" 7 | 8 | "github.com/a8m/rql" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // @Summary [系统内部]获取定时任务日志 13 | // @Id GetCronjobLog 14 | // @Tags [系统内部]日志 15 | // @version 1.0 16 | // @Accept application/x-json-stream 17 | // @Param body body models.Req true "RQL查询json" 18 | // @Success 200 object models.Resp 返回列表 19 | // @Failure 400 object models.Resp 查询失败 20 | // @Security ApiKeyAuth 21 | // @Router /api/v1/cronjoblog/list [post] 22 | func GetCronjobLog(c *gin.Context) { 23 | rqlQueryParser, err := rql.NewParser(rql.Config{ 24 | Model: sys.SysCronjobLog{}, 25 | DefaultLimit: -1, 26 | }) 27 | if err != nil { 28 | models.FailWithDetailed("", err.Error(), c) 29 | return 30 | } 31 | // 绑定参数 32 | var rqlQuery rql.Query 33 | err = c.ShouldBindJSON(&rqlQuery) 34 | if err != nil { 35 | models.FailWithDetailed("", err.Error(), c) 36 | return 37 | } 38 | rqlParams, err := rqlQueryParser.ParseQuery(&rqlQuery) 39 | if err != nil { 40 | models.FailWithDetailed("", err.Error(), c) 41 | return 42 | } 43 | if rqlParams.Sort == "" { 44 | rqlParams.Sort = "id desc" 45 | } 46 | list := make([]sys.SysCronjobLog, 0) 47 | query := global.DB 48 | count := int64(0) 49 | err = query.Model(sys.SysCronjobLog{}).Where(rqlParams.FilterExp, rqlParams.FilterArgs...).Count(&count).Error 50 | if err != nil { 51 | models.FailWithDetailed("", err.Error(), c) 52 | return 53 | } 54 | err = query.Where(rqlParams.FilterExp, rqlParams.FilterArgs...).Limit(rqlParams.Limit).Offset(rqlParams.Offset).Order(rqlParams.Sort).Find(&list).Error 55 | if err != nil { 56 | models.FailWithDetailed("", err.Error(), c) 57 | return 58 | } 59 | models.OkWithDataList(list, count, c) 60 | } 61 | -------------------------------------------------------------------------------- /api/v1/sys/sys_change_log.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/models" 5 | "go-gin-rest-api/pkg/global" 6 | 7 | "github.com/a8m/rql" 8 | "github.com/gin-gonic/gin" 9 | loggable "github.com/linclin/gorm2-loggable" 10 | ) 11 | 12 | // @Summary [系统内部]获取数据审计日志 13 | // @Id GetChangeLog 14 | // @Tags [系统内部]日志 15 | // @version 1.0 16 | // @Accept application/x-json-stream 17 | // @Param body body models.Req true "RQL查询json" 18 | // @Success 200 object models.Resp 返回列表 19 | // @Failure 400 object models.Resp 查询失败 20 | // @Security ApiKeyAuth 21 | // @Router /api/v1/changelog/list [post] 22 | func GetChangeLog(c *gin.Context) { 23 | rqlQueryParser, err := rql.NewParser(rql.Config{ 24 | Model: loggable.ChangeLog{}, 25 | DefaultLimit: -1, 26 | }) 27 | if err != nil { 28 | models.FailWithDetailed("", err.Error(), c) 29 | return 30 | } 31 | // 绑定参数 32 | var rqlQuery rql.Query 33 | err = c.ShouldBindJSON(&rqlQuery) 34 | if err != nil { 35 | models.FailWithDetailed("", err.Error(), c) 36 | return 37 | } 38 | rqlParams, err := rqlQueryParser.ParseQuery(&rqlQuery) 39 | if err != nil { 40 | models.FailWithDetailed("", err.Error(), c) 41 | return 42 | } 43 | if rqlParams.Sort == "" { 44 | rqlParams.Sort = "id desc" 45 | } 46 | list := make([]loggable.ChangeLog, 0) 47 | query := global.DB.Table("sys_change_logs") 48 | count := int64(0) 49 | err = query.Model(loggable.ChangeLog{}).Where(rqlParams.FilterExp, rqlParams.FilterArgs...).Count(&count).Error 50 | if err != nil { 51 | models.FailWithDetailed("", err.Error(), c) 52 | return 53 | } 54 | err = query.Where(rqlParams.FilterExp, rqlParams.FilterArgs...).Limit(rqlParams.Limit).Offset(rqlParams.Offset).Order(rqlParams.Sort).Find(&list).Error 55 | if err != nil { 56 | models.FailWithDetailed("", err.Error(), c) 57 | return 58 | } 59 | models.OkWithDataList(list, count, c) 60 | } 61 | -------------------------------------------------------------------------------- /initialize/validate.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/pkg/global" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin/binding" 10 | "github.com/go-playground/locales/en" 11 | "github.com/go-playground/locales/zh" 12 | ut "github.com/go-playground/universal-translator" 13 | "github.com/go-playground/validator/v10" 14 | enTranslations "github.com/go-playground/validator/v10/translations/en" 15 | zhTranslations "github.com/go-playground/validator/v10/translations/zh" 16 | ) 17 | 18 | // 初始化校验器 19 | func Validate(locale string) { 20 | // 修改gin框架中的Validator引擎属性,实现自定制 21 | if v, ok := binding.Validator.Engine().(*validator.Validate); ok { 22 | // 注册一个获取json tag的自定义方法 23 | v.RegisterTagNameFunc(func(fld reflect.StructField) string { 24 | name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] 25 | if name == "-" { 26 | return "" 27 | } 28 | return name 29 | }) 30 | zhT := zh.New() // 中文翻译器 31 | enT := en.New() // 英文翻译器 32 | // 第一个参数是备用(fallback)的语言环境 33 | // 后面的参数是应该支持的语言环境(支持多个) 34 | // uni := ut.New(zhT, zhT) 也是可以的 35 | uni := ut.New(enT, zhT, enT) 36 | // locale 通常取决于 http 请求头的 'Accept-Language' 37 | // 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找 38 | var ok bool 39 | global.Translator, ok = uni.GetTranslator(locale) 40 | if !ok { 41 | global.Log.Error(fmt.Sprintf("初始化validator.v10校验器 uni.GetTranslator(%s) 失败", locale)) 42 | } 43 | var err error 44 | // 注册翻译器 45 | switch locale { 46 | case "en": 47 | err = enTranslations.RegisterDefaultTranslations(v, global.Translator) 48 | case "zh": 49 | err = zhTranslations.RegisterDefaultTranslations(v, global.Translator) 50 | default: 51 | err = zhTranslations.RegisterDefaultTranslations(v, global.Translator) 52 | } 53 | if err != nil { 54 | global.Log.Error(fmt.Sprint("初始化validator.v10校验器失败", err)) 55 | } 56 | } 57 | global.Log.Info("初始化validator.v10校验器完成") 58 | } 59 | -------------------------------------------------------------------------------- /models/sys/sys_role.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/pkg/global" 6 | 7 | "gorm.io/gorm" 8 | ) 9 | 10 | // 系统角色表 11 | type SysRole struct { 12 | gorm.Model 13 | Name string `gorm:"column:Name;uniqueIndex;comment:角色名称" json:"Name" binding:"required" rql:"filter,sort,column=Name"` // 角色名称 14 | Keyword string `gorm:"column:Keyword;comment:角色关键词" json:"Keyword" rql:"filter,sort,column=Keyword"` // 角色关键词 15 | Desc string `gorm:"column:Desc;comment:角色说明" json:"Desc" rql:"filter,sort,column=Desc"` // 角色说明 16 | Status int `gorm:"column:Status;index;type:tinyint(1);default:1;comment:角色状态(正常/禁用, 默认正常)" json:"Status" rql:"filter,sort,column=Status"` // 角色状态(正常/禁用, 默认正常) 17 | Operator string `gorm:"column:Operator;comment:操作人" json:"Operator"` 18 | } 19 | type RolePermission struct { 20 | ID int 21 | Role string 22 | Obj string `validate:"required"` 23 | Obj1 string `validate:"required"` 24 | Obj2 string `validate:"required"` 25 | Action string `validate:"required"` 26 | Eft string `validate:"required"` 27 | } 28 | 29 | func InitSysRole() { 30 | roles := []SysRole{ 31 | { 32 | Model: gorm.Model{ 33 | ID: 1, 34 | }, 35 | Name: "admin", 36 | Keyword: "admin", 37 | Desc: "超级管理员", 38 | }, 39 | { 40 | Model: gorm.Model{ 41 | ID: 2, 42 | }, 43 | Name: "operator", 44 | Keyword: "operator", 45 | Desc: "管理员", 46 | }, 47 | { 48 | Model: gorm.Model{ 49 | ID: 3, 50 | }, 51 | Name: "dev", 52 | Keyword: "dev", 53 | Desc: "开发用户", 54 | }, 55 | { 56 | Model: gorm.Model{ 57 | ID: 4, 58 | }, 59 | Name: "user", 60 | Keyword: "user", 61 | Desc: "普通用户", 62 | }, 63 | } 64 | for _, role := range roles { 65 | err := global.DB.Where(&role).FirstOrCreate(&role).Error 66 | if err != nil { 67 | global.Log.Error(fmt.Sprint("InitSysRole 数据初始化失败", err.Error())) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /cronjob/clean_log.go: -------------------------------------------------------------------------------- 1 | package cronjob 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/models/sys" 6 | "go-gin-rest-api/pkg/global" 7 | "go-gin-rest-api/pkg/utils" 8 | "runtime/debug" 9 | "time" 10 | 11 | loggable "github.com/linclin/gorm2-loggable" 12 | ) 13 | 14 | // 清理超过一周的日志表数据 15 | type CleanLog struct { 16 | } 17 | 18 | func (u CleanLog) Run() { 19 | startTime := time.Now() 20 | global.Log.Debug(fmt.Sprintf("cronjob定时任务:CleanLog开始执行 %s", startTime.Format("2006-01-02 15:04:05"))) 21 | defer func() { 22 | if panicErr := recover(); panicErr != nil { 23 | global.Log.Error(fmt.Sprintf("cronjob定时任务:CleanLog执行失败: %v\n堆栈信息: %v", panicErr, string(debug.Stack()))) 24 | } 25 | lock := sys.NewLock("CleanLog", 600) 26 | lock.DeleteLock() 27 | }() 28 | //获取任务锁 29 | lock := sys.NewLock("CleanLog", 600) 30 | if !lock.TryLock() { 31 | global.Log.Error("cronjob定时任务:CleanLog获取任务锁失败") 32 | return 33 | } 34 | defer lock.DeleteLock() 35 | //删除日志 36 | err := global.DB.Where("StartTime < ? ", time.Now().AddDate(0, 0, -7)).Unscoped().Delete(sys.SysApiLog{}).Error 37 | if err != nil { 38 | global.Log.Error("cronjob定时任务:CleanLog删除SysApiLog失败") 39 | } 40 | err = global.DB.Where("StartTime < ? ", time.Now().AddDate(0, 0, -7)).Unscoped().Delete(sys.SysReqApiLog{}).Error 41 | if err != nil { 42 | global.Log.Error("cronjob定时任务:CleanLog删除SysReqApiLog失败") 43 | } 44 | err = global.DB.Where("StartTime < ? ", time.Now().AddDate(0, 0, -7)).Unscoped().Delete(sys.SysCronjobLog{}).Error 45 | if err != nil { 46 | global.Log.Error("cronjob定时任务:CleanLog删除SysCronjobLog失败") 47 | } 48 | err = global.DB.Table("sys_change_logs").Where("created_at < ? ", time.Now().AddDate(0, 0, -7).Unix()).Unscoped().Delete(loggable.ChangeLog{}).Error 49 | if err != nil { 50 | global.Log.Error("cronjob定时任务:CleanLog删除ChangeLog失败") 51 | } 52 | //记录任务日志表 53 | endTime := time.Now() 54 | execTime := endTime.Sub(startTime).Seconds() 55 | status := "success" 56 | errMsg := "" 57 | if err != nil { 58 | status = "fail" 59 | errMsg = err.Error() 60 | } 61 | utils.SafeGo(func() { 62 | sys.AddSysCronjobLog("CleanLog", "@every 1m", status, errMsg, startTime, endTime, execTime) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /models/sys/sys_req_api_log.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/pkg/global" 6 | "time" 7 | 8 | "github.com/spf13/cast" 9 | 10 | "github.com/gin-gonic/gin" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | // 系统请求API日志表 15 | type SysReqApiLog struct { 16 | gorm.Model 17 | RequestId string `gorm:"column:RequestId;comment:请求ID" json:"RequestId" rql:"filter,sort,column=RequestId"` // 请求ID 18 | RequestMethod string `gorm:"column:RequestMethod;comment:请求方法" json:"RequestMethod" rql:"filter,sort,column=RequestMethod"` // HTTP方法 19 | RequestURI string `gorm:"column:RequestURI;index;comment:请求路径" json:"RequestURI" rql:"filter,sort,column=RequestURI"` // 路由地址 20 | RequestBody string `gorm:"column:RequestBody;type:longText;comment:请求体" json:"RequestBody" rql:"filter,sort,column=RequestBody"` // 请求体 21 | StatusCode int `gorm:"column:StatusCode;index;comment:状态码" json:"StatusCode" rql:"filter,sort,column=StatusCode"` // 状态码 22 | RespBody string `gorm:"column:RespBody;type:longText;comment:返回体" json:"RespBody" rql:"filter,sort,column=RespBody"` // 返回体 23 | StartTime time.Time `gorm:"column:StartTime;comment:访问时间" json:"StartTime" rql:"filter,sort,column=StartTime,layout=2006-01-02 15:04:05"` // 访问时间 24 | ExecTime string `gorm:"column:ExecTime;comment:结束时间" json:"ExecTime" rql:"filter,sort,column=ExecTime,layout=2006-01-02 15:04:05"` // 执行时间 25 | } 26 | 27 | func AddReqApi(c *gin.Context, RequestMethod, RequestURI, RequestBody, RespBody, ExecTime string, StatusCode int, StartTime time.Time) (err error) { 28 | requestId, _ := c.Get("RequestId") 29 | reqapilog := SysReqApiLog{ 30 | RequestId: cast.ToString(requestId), 31 | RequestMethod: RequestMethod, 32 | RequestURI: RequestURI, 33 | RequestBody: RequestBody, 34 | StatusCode: StatusCode, 35 | RespBody: RespBody, 36 | StartTime: StartTime, 37 | ExecTime: ExecTime, 38 | } 39 | err = global.DB.Create(&reqapilog).Error 40 | if err != nil { 41 | global.Log.Error(fmt.Sprint("AddReqApi 写入请求日志数据初始化失败", err.Error())) 42 | } 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /client/client_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/go-resty/resty/v2" 9 | "github.com/patrickmn/go-cache" 10 | ) 11 | 12 | type ReqToken struct { 13 | AppId string `json:"AppId"` // AppId 14 | AppSecret string `json:"AppSecret"` // AppSecret 15 | } 16 | type TokenResp struct { 17 | Token string `json:"token"` // token 18 | Expires time.Time `json:"expires"` // 过期时间 19 | } 20 | 21 | func main() { 22 | token := "" 23 | client := resty.New() 24 | gocache := cache.New(5*time.Minute, 10*time.Minute) 25 | tokencache, found := gocache.Get("token") 26 | if found { 27 | fmt.Println("Cache Token:", tokencache.(TokenResp).Token) 28 | token = tokencache.(TokenResp).Token 29 | } else { 30 | // 1.请求token 31 | tokenresp := &TokenResp{} 32 | resp, err := client.R(). 33 | SetHeader("accept", "application/json"). 34 | SetHeader("Content-Type", "application/json"). 35 | SetBody(ReqToken{AppId: "2023012801", AppSecret: "fa2e25cb060c8d748fd16ac5210581f41"}). 36 | SetResult(tokenresp). 37 | Post("http://127.0.0.1:8080/api/v1/base/auth") 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | fmt.Println("token Response Info:") 42 | fmt.Println("Status Code:", resp.StatusCode()) 43 | fmt.Println("Token:", tokenresp.Token) 44 | fmt.Println("Token Expires:", tokenresp.Expires) 45 | token = tokenresp.Token 46 | //缓存时间小于120分钟 47 | gocache.Set("token", tokenresp, 100*time.Minute) 48 | } 49 | if token != "" { 50 | // 2.请求token 51 | resp, err := client.R(). 52 | SetHeader("accept", "application/json"). 53 | SetHeader("Content-Type", "application/json"). 54 | SetHeader("User", "lc"). 55 | SetAuthToken(token). 56 | SetBody(`{ 57 | "filter": { 58 | "RequestId": "84692443-987c-4df1-b91c-606fcff6b556" 59 | }, 60 | "limit": 10, 61 | "offset": 0, 62 | "sort": [ 63 | "-StartTime" 64 | ] 65 | }`). 66 | Post("http://127.0.0.1:8080/api/v1/apilog/list") 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | fmt.Println("sysapilog Response Info:") 71 | fmt.Println("Status Code:", resp.StatusCode()) 72 | fmt.Println("sysapilog Resp:", string(resp.Body())) 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /wiki/cd/k8s-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: go-gin-rest-api 5 | namespace: default 6 | labels: 7 | name: go-gin-rest-api 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | name: go-gin-rest-api 13 | strategy: 14 | rollingUpdate: 15 | maxSurge: 1 16 | maxUnavailable: 0 17 | type: RollingUpdate 18 | template: 19 | metadata: 20 | labels: 21 | name: go-gin-rest-api 22 | spec: 23 | restartPolicy: Always 24 | containers: 25 | - name: go-gin-rest-api 26 | image: "registry.cn-shenzhen.aliyuncs.com/dev-ops/go-gin-rest-api:1.0.1" 27 | imagePullPolicy: Always 28 | securityContext: 29 | privileged: true 30 | ports: 31 | - containerPort: 8080 32 | name: http-port 33 | env: 34 | - name: RunMode 35 | value: 'se' 36 | resources: 37 | limits: 38 | cpu: 2 39 | memory: 4096Mi 40 | requests: 41 | cpu: 100m 42 | memory: 200Mi 43 | volumeMounts: 44 | - name: localtime 45 | mountPath: /etc/localtime 46 | readOnly: true 47 | terminationGracePeriodSeconds: 60 48 | volumes: 49 | - name: localtime 50 | hostPath: 51 | path: /etc/localtime 52 | 53 | --- 54 | apiVersion: v1 55 | kind: Service 56 | metadata: 57 | name: go-gin-rest-api 58 | namespace: default 59 | labels: 60 | name: go-gin-rest-api 61 | spec: 62 | type: ClusterIP 63 | selector: 64 | name: go-gin-rest-api 65 | ports: 66 | - name: go-gin-rest-api 67 | port: 8080 68 | 69 | --- 70 | apiVersion: networking.k8s.io/v1 71 | kind: Ingress 72 | metadata: 73 | name: go-gin-rest-api 74 | annotations: 75 | nginx.ingress.kubernetes.io/rewrite-target: / 76 | spec: 77 | ingressClassName: nginx-ingress 78 | rules: 79 | - host: "foo.bar.com" 80 | http: 81 | paths: 82 | - path: / 83 | pathType: Prefix 84 | backend: 85 | service: 86 | name: go-gin-rest-api 87 | port: 88 | number: 8080 -------------------------------------------------------------------------------- /models/model.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | "go-gin-rest-api/pkg/global" 7 | "time" 8 | ) 9 | 10 | // 由于gorm提供的base model没有json tag, 使用自定义 11 | type Model struct { 12 | Id uint `gorm:"column:Id;primary_key;comment:'自增编号'" json:"Id" rql:"filter,sort,column=Id" ` 13 | CreatedAt time.Time `gorm:"column:CreatedAt;comment:'创建时间'" json:"CreatedAt" rql:"filter,sort,column=CreatedAt,layout=2006-01-02 15:04:05"` 14 | UpdatedAt time.Time `gorm:"column:UpdatedAt;comment:'更新时间'" json:"UpdatedAt" rql:"filter,sort,column=UpdatedAt,layout=2006-01-02 15:04:05"` 15 | DeletedAt *time.Time `gorm:"column:DeletedAt;comment:'删除时间(软删除)'" sql:"index" json:"DeletedAt" rql:"filter,sort,column=DeletedAt,layout=2006-01-02 15:04:05"` 16 | } 17 | 18 | // 表名设置 19 | func (Model) TableName(name string) string { 20 | // 添加表前缀 21 | if global.Conf.Mysql != nil { 22 | return fmt.Sprintf("%s%s", global.Conf.Mysql.TablePrefix, name) 23 | } 24 | if global.Conf.Pgsql != nil { 25 | return fmt.Sprintf("%s%s", global.Conf.Pgsql.TablePrefix, name) 26 | } 27 | return name 28 | } 29 | 30 | // 自定义时间json转换 31 | const TimeFormat = "2006-01-02 15:04:05" 32 | 33 | type LocalTime struct { 34 | time.Time 35 | } 36 | 37 | func (t *LocalTime) UnmarshalJSON(data []byte) (err error) { 38 | // 空值不进行解析 39 | if len(data) == 2 { 40 | *t = LocalTime{Time: time.Time{}} 41 | return 42 | } 43 | // 指定解析的格式 44 | now, err := time.Parse(`"`+TimeFormat+`"`, string(data)) 45 | *t = LocalTime{Time: now} 46 | return 47 | } 48 | 49 | func (t LocalTime) MarshalJSON() ([]byte, error) { 50 | output := fmt.Sprintf("\"%s\"", t.Format(TimeFormat)) 51 | return []byte(output), nil 52 | } 53 | 54 | // gorm 写入 mysql 时调用 55 | func (t LocalTime) Value() (driver.Value, error) { 56 | var zeroTime time.Time 57 | if t.UnixNano() == zeroTime.UnixNano() { 58 | return nil, nil 59 | } 60 | return t.Time, nil 61 | } 62 | 63 | // gorm 检出 mysql 时调用 64 | func (t *LocalTime) Scan(v interface{}) error { 65 | value, ok := v.(time.Time) 66 | if ok { 67 | *t = LocalTime{Time: value} 68 | return nil 69 | } 70 | return fmt.Errorf("can not convert %v to timestamp", v) 71 | } 72 | 73 | // 用于 fmt.Println 和后续验证场景 74 | func (t LocalTime) String() string { 75 | return t.Format(TimeFormat) 76 | } 77 | -------------------------------------------------------------------------------- /wiki/ci/kaniko-build-job.yaml: -------------------------------------------------------------------------------- 1 | # kubectl create secret generic rsa-secret --from-file=id_rsa=/root/.ssh/id_rsa --from-file=id_rsa.pub=/root/.ssh/id_rsa.pub 2 | # kubectl create secret docker-registry acr-regcred --docker-email=xxx --docker-username=xxxxx --docker-password=xxxxxxxx --docker-server=registry.cn-shenzhen.aliyuncs.com 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: kaniko-build-job-20230705161233 7 | spec: 8 | backoffLimit: 1 9 | activeDeadlineSeconds: 600 10 | ttlSecondsAfterFinished: 120 11 | template: 12 | spec: 13 | containers: 14 | - name: kaniko-build-job 15 | image: registry.cn-shenzhen.aliyuncs.com/dev-ops/kaniko-executor:v1.13.0 16 | args: ["--context=git://gitee.com/dev-ops/go-gin-rest-api.git#refs/heads/main", 17 | #"--context=/workspace/", 18 | #"--dockerfile=/workspace/Dockerfile", 19 | "--destination=registry.cn-shenzhen.aliyuncs.com/dev-ops/go-gin-rest-api:1.0.1", 20 | "--cache", 21 | "--cache-dir=/cache", 22 | #"--cache-repo=registry.cn-shenzhen.aliyuncs.com/dev-ops/go-gin-rest-api-build-cache", 23 | "--cache-copy-layers", 24 | "--cache-run-layers", 25 | "--push-retry=2", 26 | "--verbosity=debug"] 27 | volumeMounts: 28 | - name: rsa-secret 29 | readOnly: true 30 | mountPath: "~/.ssh" 31 | - name: kaniko-secret 32 | mountPath: /kaniko/.docker 33 | # - name: workspace 34 | # mountPath: /workspace 35 | - name: cache 36 | mountPath: /cache 37 | restartPolicy: Never 38 | volumes: 39 | # - name: workspace 40 | # hostPath: 41 | # path: /mnt/d/Kubernetes/kaniko/workspace/ 42 | # type: Directory 43 | - name: cache 44 | hostPath: 45 | path: /mnt/d/project/workspace/cache 46 | type: Directory 47 | - name: rsa-secret 48 | secret: 49 | defaultMode: 0600 50 | secretName: rsa-secret 51 | - name: kaniko-secret 52 | secret: 53 | secretName: acr-regcred 54 | items: 55 | - key: .dockerconfigjson 56 | path: config.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=registry.cn-shenzhen.aliyuncs.com/dev-ops/dockerfile:1.19.0 2 | FROM registry.cn-shenzhen.aliyuncs.com/dev-ops/golang:1.25.5-alpine3.23 as golang 3 | ENV APP go-gin-rest-api 4 | RUN sed -i 's/https/http/' /etc/apk/repositories && \ 5 | sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \ 6 | apk update && \ 7 | apk add --no-cache ca-certificates git && \ 8 | rm -rf /var/cache/apk/* /tmp/* 9 | ADD ./ /app/go-gin-rest-api 10 | ADD .git/ /app/go-gin-rest-api/.git 11 | WORKDIR /app/go-gin-rest-api 12 | RUN export GitBranch=$(git name-rev --name-only HEAD) && \ 13 | export GitRevision=$(git rev-parse --short HEAD) && \ 14 | export GitCommitLog=`git log --pretty=oneline -n 1` && \ 15 | export BuildTime=`date +'%Y.%m.%d.%H%M%S'` && \ 16 | export BuildGoVersion=`go version` && \ 17 | export LDFlags="-s -w -X 'main.GitBranch=${GitBranch}' -X 'main.GitRevision=${GitRevision}' -X 'main.GitCommitLog=${GitCommitLog}' -X 'main.BuildTime=${BuildTime}' -X 'main.BuildGoVersion=${BuildGoVersion}'" && \ 18 | go build -ldflags="$LDFlags" -o ./go-gin-rest-api 19 | 20 | FROM registry.cn-shenzhen.aliyuncs.com/dev-ops/alpine:3.23.0 21 | LABEL MAINTAINER="13579443@qq.com" 22 | ENV TZ='Asia/Shanghai' 23 | ENV LANG UTF-8 24 | ENV LC_ALL zh_CN.UTF-8 25 | ENV LC_CTYPE zh_CN.UTF-8 26 | RUN TERM=linux && export TERM 27 | RUN sed -i 's/https/http/' /etc/apk/repositories && \ 28 | sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \ 29 | apk update && \ 30 | apk add --no-cache ca-certificates tzdata bash sudo busybox-extras less gzip curl net-tools bind-tools && \ 31 | ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtim && \ 32 | echo "Asia/Shanghai" > /etc/timezone && \ 33 | rm -rf /var/cache/apk/* /tmp/* && \ 34 | addgroup -g 1000 app && \ 35 | adduser -u 1000 -G app -D app && \ 36 | adduser -u 1001 -G app -D dev && \ 37 | chmod 4755 /bin/busybox && \ 38 | mkdir -p /app && \ 39 | chown -R app:app /app 40 | WORKDIR /app/go-gin-rest-api/ 41 | COPY --from=golang --chown=app:app --chmod=755 /app/go-gin-rest-api/go-gin-rest-api /app/go-gin-rest-api/go-gin-rest-api 42 | COPY --from=golang --chown=app:app --chmod=755 /app/go-gin-rest-api/conf /app/go-gin-rest-api/conf 43 | CMD ["./go-gin-rest-api"] -------------------------------------------------------------------------------- /models/sys/sys_system.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/pkg/global" 6 | 7 | loggable "github.com/linclin/gorm2-loggable" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | // 系统表 12 | type SysSystem struct { 13 | gorm.Model 14 | AppId string `gorm:"column:AppId;unique;comment:AppId" json:"AppId" binding:"required" rql:"filter,sort,column=AppId"` // AppId 15 | AppSecret string `gorm:"column:AppSecret;comment:AppSecret" json:"AppSecret" binding:"required" ` // AppSecret 16 | SystemName string `gorm:"column:SystemName;comment:系统名称" json:"SystemName" rql:"filter,sort,column=SystemName"` // 系统名称 17 | IP string `gorm:"column:IP;comment:系统来源IP" json:"IP" rql:"filter,sort,column=IP"` // 系统来源IP 18 | Operator string `gorm:"column:Operator;comment:操作人" json:"Operator" rql:"filter,sort,column=Operator"` // 操作人 19 | loggable.LoggableModel 20 | } 21 | 22 | func (system SysSystem) Meta() interface{} { 23 | return struct { 24 | CreatedBy string 25 | }{ 26 | CreatedBy: system.Operator, 27 | } 28 | } 29 | 30 | // 权限 31 | type SystemPermission struct { 32 | ID int 33 | AppId string `validate:"required"` // AppId 34 | AbsolutePath string `validate:"required"` // 路由地址 35 | AbsolutePath1 string `validate:"required"` // 路由地址 36 | AbsolutePath2 string `validate:"required"` // 路由地址 37 | HttpMethod string `validate:"required"` // HTTP方法 38 | Eft string `validate:"required"` // 动作 39 | } 40 | 41 | func InitSysSystem() { 42 | systems := []SysSystem{ 43 | { 44 | Model: gorm.Model{ 45 | ID: 1, 46 | }, 47 | AppId: "api-00000001", 48 | AppSecret: "fa2e25cb060c8d748fd16ac5210581f41", 49 | SystemName: "api", 50 | IP: "", 51 | Operator: "lc", 52 | }, 53 | { 54 | Model: gorm.Model{ 55 | ID: 2, 56 | }, 57 | AppId: "api-00000002", 58 | AppSecret: "61c94399f47c485334b48f8f340bc07b2", 59 | SystemName: "UI", 60 | IP: "", 61 | Operator: "lc", 62 | }, 63 | } 64 | for _, system := range systems { 65 | err := global.DB.Where(&system).FirstOrCreate(&system).Error 66 | if err != nil { 67 | global.Log.Error(fmt.Sprint("InitSysSystem 数据初始化失败", err.Error())) 68 | continue 69 | } 70 | } 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /initialize/casbin.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/pkg/global" 6 | "go-gin-rest-api/pkg/utils" 7 | "time" 8 | 9 | "github.com/casbin/casbin/v2" 10 | defaultrolemanager "github.com/casbin/casbin/v2/rbac/default-role-manager" 11 | gormadapter "github.com/casbin/gorm-adapter/v3" 12 | ) 13 | 14 | // 获取casbin策略管理器 15 | func InitCasbin() { 16 | // 初始化数据库适配器 17 | casbinAdapter, err := gormadapter.NewAdapterByDBUseTableName(global.DB, "sys", "casbin_rule") 18 | if err != nil { 19 | global.Log.Error(fmt.Sprint("Casbin初始化错误", err.Error())) 20 | } 21 | // 读取配置文件 22 | CasbinACLEnforcer, err := casbin.NewSyncedEnforcer("conf/"+global.Conf.Casbin.ModelPath, casbinAdapter, true) 23 | if err != nil { 24 | global.Log.Error(fmt.Sprint("Casbin初始化错误", err.Error())) 25 | panic(err) 26 | } 27 | global.CasbinACLEnforcer = CasbinACLEnforcer 28 | global.CasbinACLEnforcer.SetRoleManager(defaultrolemanager.NewConditionalRoleManager(10)) 29 | global.CasbinACLEnforcer.BuildRoleLinks() 30 | global.CasbinACLEnforcer.LoadPolicy() 31 | global.CasbinACLEnforcer.EnableAutoBuildRoleLinks(true) 32 | // 加载策略 33 | global.CasbinACLEnforcer.StartAutoLoadPolicy(time.Minute * time.Duration(1)) 34 | //global.CasbinACLEnforcer.EnableEnforce(false) 35 | // 添加API系统权限 36 | global.CasbinACLEnforcer.AddPolicy("api-00000001", "/*", "*", "*", "(GET)|(POST)|(PUT)|(DELETE)|(OPTIONS)|(PATCH)", "allow") 37 | global.CasbinACLEnforcer.AddPolicy("api-00000002", "/*", "*", "*", "(GET)|(POST)|(PUT)|(DELETE)|(OPTIONS)|(PATCH)", "allow") 38 | // 添加前台普通用户组权限 39 | global.CasbinACLEnforcer.AddPolicy("group_user", "/*", "*", "*", "GET", "allow") 40 | // 添加前台操作用户组权限 41 | global.CasbinACLEnforcer.AddPolicy("group_operator", "/*", "*", "*", "GET", "allow") 42 | // 添加前台管理员组权限 43 | global.CasbinACLEnforcer.AddPolicy("group_admin", "/*", "*", "*", "(GET)|(POST)|(PUT)|(DELETE)|(OPTIONS)|(PATCH)", "allow") 44 | // global.CasbinACLEnforcer.AddRoleForUser("lc", "group_admin", time.Now().Format("2006-01-02 15:04:05"), time.Now().AddDate(100, 0, 0).Format("2006-01-02 15:04:05")) 45 | global.CasbinACLEnforcer.AddRoleForUser("lc", "group_admin", "2024-09-01 00:00:00", "2054-09-01 00:00:00") 46 | global.CasbinACLEnforcer.GetRoleManager().AddLink("lc", "group_admin") 47 | global.CasbinACLEnforcer.AddNamedLinkConditionFunc("g", "lc", "group_admin", utils.TimeMatchFunc) 48 | } 49 | -------------------------------------------------------------------------------- /api/v1/sys/sys_data.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/models" 5 | "go-gin-rest-api/models/sys" 6 | "go-gin-rest-api/pkg/global" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // @Summary [系统内部]获取系统运营数据 13 | // @Id GetSysData 14 | // @Tags [系统内部]日志 15 | // @version 1.0 16 | // @Accept application/x-json-stream 17 | // @Success 200 object models.Resp 返回列表 18 | // @Failure 400 object models.Resp 查询失败 19 | // @Security ApiKeyAuth 20 | // @Router /api/v1/data/list [get] 21 | func GetSysData(c *gin.Context) { 22 | data := sys.SysData{} 23 | query := global.DB 24 | err := query.Model(sys.SysSystem{}).Count(&data.SystemCount).Error 25 | if err != nil { 26 | models.FailWithDetailed("", err.Error(), c) 27 | return 28 | } 29 | err = query.Model(sys.SysRouter{}).Count(&data.RouterCount).Error 30 | if err != nil { 31 | models.FailWithDetailed("", err.Error(), c) 32 | return 33 | } 34 | err = query.Model(sys.SysApiLog{}).Count(&data.AllApiCount).Error 35 | if err != nil { 36 | models.FailWithDetailed("", err.Error(), c) 37 | return 38 | } 39 | err = query.Model(sys.SysApiLog{}).Where("StartTime >= ?", time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Now().Location())).Count(&data.ApiCount).Error 40 | if err != nil { 41 | models.FailWithDetailed("", err.Error(), c) 42 | return 43 | } 44 | err = query.Model(sys.SysApiLog{}).Select("DATE_FORMAT(StartTime, '%Y-%m-%d') as Date, COUNT(*) as Count").Where("StartTime >= ?", time.Now().AddDate(0, 0, -7)).Group("date").Scan(&data.WeekApiCount).Error 45 | if err != nil { 46 | models.FailWithDetailed("", err.Error(), c) 47 | return 48 | } 49 | err = query.Model(sys.SysApiLog{}).Select("ClientIP , COUNT(*) as Count").Where("StartTime >= ?", time.Now().AddDate(0, 0, -7)).Group("ClientIP").Scan(&data.WeekClientApiCount).Error 50 | if err != nil { 51 | models.FailWithDetailed("", err.Error(), c) 52 | return 53 | } 54 | err = query.Model(sys.SysReqApiLog{}).Count(&data.AllReqApiCount).Error 55 | if err != nil { 56 | models.FailWithDetailed("", err.Error(), c) 57 | return 58 | } 59 | err = query.Model(sys.SysReqApiLog{}).Where("StartTime >= ?", time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Now().Location())).Count(&data.ReqApiCount).Error 60 | if err != nil { 61 | models.FailWithDetailed("", err.Error(), c) 62 | return 63 | } 64 | models.OkWithDataList(data, 0, c) 65 | } 66 | -------------------------------------------------------------------------------- /api/v1/sys/sys_api_log.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/models" 5 | "go-gin-rest-api/models/sys" 6 | "go-gin-rest-api/pkg/global" 7 | 8 | "github.com/a8m/rql" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // @Summary [系统内部]获取服务接口日志 13 | // @Id GetApiLog 14 | // @Tags [系统内部]日志 15 | // @version 1.0 16 | // @Accept application/x-json-stream 17 | // @Param body body models.Req true "RQL查询json" 18 | // @Success 200 object models.Resp 返回列表 19 | // @Failure 400 object models.Resp 查询失败 20 | // @Security ApiKeyAuth 21 | // @Router /api/v1/apilog/list [post] 22 | func GetApiLog(c *gin.Context) { 23 | rqlQueryParser, err := rql.NewParser(rql.Config{ 24 | Model: sys.SysApiLog{}, 25 | DefaultLimit: -1, 26 | }) 27 | if err != nil { 28 | models.FailWithDetailed("", err.Error(), c) 29 | return 30 | } 31 | // 绑定参数 32 | var rqlQuery rql.Query 33 | err = c.ShouldBindJSON(&rqlQuery) 34 | if err != nil { 35 | models.FailWithDetailed("", err.Error(), c) 36 | return 37 | } 38 | rqlParams, err := rqlQueryParser.ParseQuery(&rqlQuery) 39 | if err != nil { 40 | models.FailWithDetailed("", err.Error(), c) 41 | return 42 | } 43 | if rqlParams.Sort == "" { 44 | rqlParams.Sort = "id desc" 45 | } 46 | list := make([]sys.SysApiLog, 0) 47 | query := global.DB 48 | count := int64(0) 49 | err = query.Model(sys.SysApiLog{}).Where(rqlParams.FilterExp, rqlParams.FilterArgs...).Count(&count).Error 50 | if err != nil { 51 | models.FailWithDetailed("", err.Error(), c) 52 | return 53 | } 54 | err = query.Where(rqlParams.FilterExp, rqlParams.FilterArgs...).Limit(rqlParams.Limit).Offset(rqlParams.Offset).Order(rqlParams.Sort).Find(&list).Error 55 | if err != nil { 56 | models.FailWithDetailed("", err.Error(), c) 57 | return 58 | } 59 | models.OkWithDataList(list, count, c) 60 | } 61 | 62 | // @Summary [系统内部]根据ID获取接口日志 63 | // @Id GetApiLogById 64 | // @Tags [系统内部]日志 65 | // @version 1.0 66 | // @Accept application/x-json-stream 67 | // @Param requestid path string true "RequestId" 68 | // @Success 200 object models.Resp 返回列表 69 | // @Failure 400 object models.Resp 查询失败 70 | // @Security ApiKeyAuth 71 | // @Router /api/v1/apilog/get/{requestid} [get] 72 | func GetApiLogById(c *gin.Context) { 73 | var apilog sys.SysApiLog 74 | requestid := c.Param("requestid") 75 | err := global.DB.Where("RequestId = ?", requestid).First(&apilog).Error 76 | if err != nil { 77 | models.FailWithDetailed("", err.Error(), c) 78 | } else { 79 | models.OkWithData(apilog, c) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /initialize/rsa.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/asn1" 8 | "encoding/gob" 9 | "encoding/pem" 10 | "fmt" 11 | "os" 12 | 13 | "go-gin-rest-api/pkg/global" 14 | ) 15 | 16 | func InitRSA() { 17 | _, err := os.Stat("conf/rsa/rsa-private.pem") 18 | if err != nil { 19 | if !os.IsExist(err) { 20 | GenerateRSA() 21 | global.Log.Info("初始化RSA证书供JWT生成Toekn使用完成") 22 | } 23 | } 24 | } 25 | 26 | // openssl genrsa -out conf/rsa/rsa-private.pem 2048 27 | // openssl rsa -in conf/rsa/rsa-private.pem -pubout > conf/rsa/rsa-public.pem 28 | func GenerateRSA() { 29 | key, err := rsa.GenerateKey(rand.Reader, 2048) 30 | checkError(err) 31 | publicKey := key.PublicKey 32 | saveGobKey("conf/rsa/rsa-private.key", key) 33 | savePEMKey("conf/rsa/rsa-private.pem", key) 34 | saveGobKey("conf/rsa/rsa-public.key", publicKey) 35 | savePublicPEMKey("conf/rsa/rsa-public.pem", &publicKey) 36 | } 37 | 38 | func saveGobKey(fileName string, key interface{}) { 39 | outFile, err := os.Create(fileName) 40 | checkError(err) 41 | defer outFile.Close() 42 | encoder := gob.NewEncoder(outFile) 43 | err = encoder.Encode(key) 44 | checkError(err) 45 | } 46 | 47 | func savePEMKey(fileName string, key *rsa.PrivateKey) { 48 | outFile, err := os.Create(fileName) 49 | checkError(err) 50 | defer outFile.Close() 51 | info := struct { 52 | Version int 53 | PrivateKeyAlgorithm []asn1.ObjectIdentifier 54 | PrivateKey []byte 55 | }{} 56 | 57 | info.Version = 0 58 | info.PrivateKeyAlgorithm = make([]asn1.ObjectIdentifier, 1) 59 | info.PrivateKeyAlgorithm[0] = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} 60 | info.PrivateKey = x509.MarshalPKCS1PrivateKey(key) 61 | prvKey, _ := asn1.Marshal(info) 62 | 63 | var privateKey = &pem.Block{ 64 | Type: "RSA PRIVATE KEY", 65 | Bytes: prvKey, 66 | } 67 | err = pem.Encode(outFile, privateKey) 68 | checkError(err) 69 | } 70 | 71 | func savePublicPEMKey(fileName string, pubkey *rsa.PublicKey) { 72 | pemfile, err := os.Create(fileName) 73 | checkError(err) 74 | defer pemfile.Close() 75 | asn1Bytes, err := x509.MarshalPKIXPublicKey(pubkey) 76 | checkError(err) 77 | var pemkey = &pem.Block{ 78 | Type: "PUBLIC KEY", 79 | Bytes: asn1Bytes, 80 | } 81 | err = pem.Encode(pemfile, pemkey) 82 | checkError(err) 83 | } 84 | 85 | func checkError(err error) { 86 | if err != nil { 87 | global.Log.Debug(fmt.Sprint("Fatal error:GenerateRSA", err.Error())) 88 | os.Exit(1) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /middleware/access_log.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go-gin-rest-api/models/sys" 7 | "go-gin-rest-api/pkg/global" 8 | "io/ioutil" 9 | "time" 10 | 11 | "github.com/gin-contrib/requestid" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | type AccessLogWriter struct { 16 | gin.ResponseWriter 17 | body *bytes.Buffer 18 | } 19 | 20 | func (w AccessLogWriter) Write(p []byte) (int, error) { 21 | if n, err := w.body.Write(p); err != nil { 22 | global.Log.Info(fmt.Sprint("AccessLogWriter Write", err.Error())) 23 | return n, err 24 | } 25 | return w.ResponseWriter.Write(p) 26 | } 27 | 28 | func (w AccessLogWriter) WriteString(p string) (int, error) { 29 | if n, err := w.body.WriteString(p); err != nil { 30 | global.Log.Info(fmt.Sprint("AccessLogWriter WriteString", err.Error())) 31 | return n, err 32 | } 33 | return w.ResponseWriter.WriteString(p) 34 | } 35 | 36 | // 访问日志 37 | func AccessLog(c *gin.Context) { 38 | bodyWriter := &AccessLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} 39 | c.Writer = bodyWriter 40 | var bodyBytes []byte // 我们需要的body内容 41 | // 从原有Request.Body读取 42 | bodyBytes, err := ioutil.ReadAll(c.Request.Body) 43 | if err != nil { 44 | global.Log.Info(fmt.Sprint("请求体获取错误", err)) 45 | } 46 | // 新建缓冲区并替换原有Request.body 47 | c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 48 | requestId := requestid.Get(c) 49 | c.Set("RequestId", requestId) 50 | // 开始时间 51 | startTime := time.Now() 52 | // 处理请求 53 | c.Next() 54 | // 结束时间 55 | endTime := time.Now() 56 | // 执行时间 57 | execTime := endTime.Sub(startTime) 58 | // 请求方式 59 | reqMethod := c.Request.Method 60 | // 请求路由 61 | reqUri := c.Request.RequestURI 62 | // 请求体 63 | reqBody := string(bodyBytes) 64 | // 状态码 65 | statusCode := c.Writer.Status() 66 | // 返回体 67 | respBody := bodyWriter.body.String() 68 | // 请求IP 69 | clientIP := c.ClientIP() 70 | if reqMethod != "OPTIONS" { 71 | sysApiLog := sys.SysApiLog{ 72 | RequestId: requestId, 73 | RequestMethod: reqMethod, 74 | RequestURI: reqUri, 75 | RequestBody: reqBody, 76 | StatusCode: statusCode, 77 | //RespBody: respBody, //使用gzip插件不记录返回体,会导致保存数据库失败 78 | ClientIP: clientIP, 79 | StartTime: startTime, 80 | ExecTime: execTime.String(), 81 | } 82 | err = global.DB.Create(&sysApiLog).Error 83 | if err != nil { 84 | global.Log.Info("接口日志存库错误", err, requestId, reqMethod, reqUri, reqBody, respBody, statusCode, execTime.String(), clientIP) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /api/v1/sys/sys_req_api_log.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "go-gin-rest-api/models" 5 | "go-gin-rest-api/models/sys" 6 | "go-gin-rest-api/pkg/global" 7 | 8 | "github.com/a8m/rql" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // @Summary [系统内部]获取请求接口日志 13 | // @Id GetReqApiLog 14 | // @Tags [系统内部]日志 15 | // @version 1.0 16 | // @Accept application/x-json-stream 17 | // @Param body body models.Req true "RQL查询json" 18 | // @Success 200 object models.Resp 返回列表 19 | // @Failure 400 object models.Resp 查询失败 20 | // @Security ApiKeyAuth 21 | // @Router /api/v1/reqapilog/list [post] 22 | func GetReqApiLog(c *gin.Context) { 23 | rqlQueryParser, err := rql.NewParser(rql.Config{ 24 | Model: sys.SysReqApiLog{}, 25 | DefaultLimit: -1, 26 | }) 27 | if err != nil { 28 | models.FailWithDetailed("", err.Error(), c) 29 | return 30 | } 31 | // 绑定参数 32 | var rqlQuery rql.Query 33 | err = c.ShouldBindJSON(&rqlQuery) 34 | if err != nil { 35 | models.FailWithDetailed("", err.Error(), c) 36 | return 37 | } 38 | rqlParams, err := rqlQueryParser.ParseQuery(&rqlQuery) 39 | if err != nil { 40 | models.FailWithDetailed("", err.Error(), c) 41 | return 42 | } 43 | if rqlParams.Sort == "" { 44 | rqlParams.Sort = "id desc" 45 | } 46 | list := make([]sys.SysReqApiLog, 0) 47 | query := global.DB 48 | count := int64(0) 49 | err = query.Model(sys.SysReqApiLog{}).Where(rqlParams.FilterExp, rqlParams.FilterArgs...).Count(&count).Error 50 | if err != nil { 51 | models.FailWithDetailed("", err.Error(), c) 52 | return 53 | } 54 | err = query.Where(rqlParams.FilterExp, rqlParams.FilterArgs...).Limit(rqlParams.Limit).Offset(rqlParams.Offset).Order(rqlParams.Sort).Find(&list).Error 55 | if err != nil { 56 | models.FailWithDetailed("", err.Error(), c) 57 | return 58 | } 59 | models.OkWithDataList(list, count, c) 60 | } 61 | 62 | // @Summary [系统内部]根据ID获取请求接口日志 63 | // @Id GetReqApiLogById 64 | // @Tags [系统内部]日志 65 | // @version 1.0 66 | // @Accept application/x-json-stream 67 | // @Param requestid path string true "RequestId" 68 | // @Success 200 object models.Resp 返回列表 69 | // @Failure 400 object models.Resp 查询失败 70 | // @Security ApiKeyAuth 71 | // @Router /api/v1/reqapilog/get/{requestid} [get] 72 | func GetReqApiLogById(c *gin.Context) { 73 | list := make([]sys.SysReqApiLog, 0) 74 | requestid := c.Param("requestid") 75 | err := global.DB.Where("RequestId = ?", requestid).Find(&list).Error 76 | if err != nil { 77 | models.FailWithDetailed("", err.Error(), c) 78 | } else { 79 | models.OkWithData(list, c) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /models/resp.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/spf13/cast" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | // http请求响应体 12 | type Resp struct { 13 | RequestId string `json:"request_id"` // 请求ID 14 | Success bool `json:"success"` // 请求是否成功 15 | Data interface{} `json:"data"` // 数据内容 16 | Msg string `json:"msg"` // 消息提示 17 | Total int64 `json:"total"` // 数据总条数 18 | } 19 | 20 | // 自定义错误码与错误信息 21 | 22 | const ( 23 | Ok = 201 24 | NotOk = 405 25 | Unauthorized = 401 26 | Forbidden = 403 27 | InternalServerError = 500 28 | ) 29 | 30 | const ( 31 | OkMsg = "操作成功" 32 | NotOkMsg = "操作失败" 33 | UnauthorizedMsg = "登录过期, 需要重新登录" 34 | LoginCheckErrorMsg = "用户名或密码错误" 35 | ForbiddenMsg = "无权访问该资源, 请联系网站管理员授权" 36 | InternalServerErrorMsg = "服务器内部错误" 37 | ) 38 | 39 | var CustomError = map[int]string{ 40 | Ok: OkMsg, 41 | NotOk: NotOkMsg, 42 | Unauthorized: UnauthorizedMsg, 43 | Forbidden: ForbiddenMsg, 44 | InternalServerError: InternalServerErrorMsg, 45 | } 46 | 47 | const ( 48 | ERROR = false 49 | SUCCESS = true 50 | ) 51 | 52 | var EmptyArray = []interface{}{} 53 | 54 | func Result(success bool, data interface{}, msg string, total int64, c *gin.Context) { 55 | requestId, _ := c.Get("RequestId") 56 | c.JSON(http.StatusOK, Resp{ 57 | cast.ToString(requestId), 58 | success, 59 | data, 60 | msg, 61 | total, 62 | }) 63 | } 64 | 65 | func OkResult(c *gin.Context) { 66 | Result(SUCCESS, map[string]interface{}{}, "操作成功", 0, c) 67 | } 68 | 69 | func OkWithMessage(message string, c *gin.Context) { 70 | Result(SUCCESS, map[string]interface{}{}, message, 0, c) 71 | } 72 | 73 | func OkWithData(data interface{}, c *gin.Context) { 74 | Result(SUCCESS, data, "操作成功", 0, c) 75 | } 76 | func OkWithDataList(data interface{}, total int64, c *gin.Context) { 77 | Result(SUCCESS, data, "操作成功", total, c) 78 | } 79 | func OkWithDetailed(data interface{}, message string, c *gin.Context) { 80 | Result(SUCCESS, data, message, 0, c) 81 | } 82 | 83 | func FailResult(c *gin.Context) { 84 | Result(ERROR, map[string]interface{}{}, "操作失败", 0, c) 85 | } 86 | 87 | func FailWithMessage(message string, c *gin.Context) { 88 | Result(ERROR, map[string]interface{}{}, message, 0, c) 89 | } 90 | 91 | func FailWithDetailed(data interface{}, message string, c *gin.Context) { 92 | Result(ERROR, data, message, 0, c) 93 | } 94 | -------------------------------------------------------------------------------- /initialize/pgsql.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "gorm.io/driver/postgres" 9 | "gorm.io/gorm" 10 | "gorm.io/gorm/logger" 11 | 12 | "go-gin-rest-api/models/sys" 13 | "go-gin-rest-api/pkg/global" 14 | ) 15 | 16 | // 初始化pgsql数据库 17 | func Pgsql() { 18 | logLevel := logger.Info 19 | if global.Conf.System.RunMode == "prd" { 20 | logLevel = logger.Warn 21 | } 22 | if global.Conf.Pgsql == nil { 23 | return 24 | } 25 | dsn := fmt.Sprintf( 26 | "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", 27 | global.Conf.Pgsql.Host, 28 | global.Conf.Pgsql.Port, 29 | global.Conf.Pgsql.Username, 30 | global.Conf.Pgsql.Password, 31 | global.Conf.Pgsql.Database, 32 | ) 33 | db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logLevel)}) 34 | if err != nil { 35 | global.Log.Error(fmt.Sprintf("数据库连接错误: %v", err)) 36 | panic(fmt.Sprintf("数据库连接错误: %v", err)) 37 | } 38 | 39 | sqlDB, err := db.DB() 40 | if err != nil { 41 | global.Log.Error(fmt.Sprintf("获取数据库连接错误: %v", err)) 42 | panic(fmt.Sprintf("获取数据库连接错误: %v", err)) 43 | } 44 | 45 | // SetMaxIdleCons 设置连接池中的最大闲置连接数。 46 | sqlDB.SetMaxIdleConns(10) 47 | // SetMaxOpenCons 设置数据库的最大连接数量。 48 | sqlDB.SetMaxOpenConns(500) 49 | // SetConnMaxLifetime 设置连接的最大可复用时间。 50 | sqlDB.SetConnMaxLifetime(time.Hour) 51 | 52 | // 检查数据库是否存在,如果不存在则创建 53 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 54 | defer cancel() 55 | pgdb, err := sqlDB.Conn(ctx) 56 | if err != nil { 57 | global.Log.Error(fmt.Sprintf("连接数据库错误: %v", err)) 58 | panic(fmt.Sprintf("连接数据库错误: %v", err)) 59 | } 60 | defer pgdb.Close() 61 | err = createDatabaseIfNotExists(ctx, db, global.Conf.Pgsql.Database) 62 | 63 | global.DB = db 64 | // 自动迁移表结构 65 | global.DB.AutoMigrate(&sys.SysSystem{}) 66 | global.DB.AutoMigrate(&sys.SysRouter{}) 67 | global.DB.AutoMigrate(&sys.SysRole{}) 68 | global.DB.AutoMigrate(&sys.SysApiLog{}) 69 | global.DB.AutoMigrate(&sys.SysReqApiLog{}) 70 | global.DB.AutoMigrate(&sys.SysCronjobLog{}) 71 | global.DB.AutoMigrate(&sys.SysLock{}) 72 | global.Log.Info("初始化pgsql完成") 73 | } 74 | 75 | func createDatabaseIfNotExists(ctx context.Context, db *gorm.DB, dbName string) error { 76 | // 检查数据库是否存在 77 | var count int64 78 | err := db.Raw("SELECT COUNT(*) FROM pg_database WHERE datname = ?", dbName).Count(&count).Error 79 | if err != nil { 80 | return err 81 | } 82 | 83 | // 如果数据库不存在,则创建数据库 84 | if count == 0 { 85 | _, err = db.Statement.ConnPool.ExecContext(ctx, "CREATE DATABASE "+dbName) 86 | if err != nil { 87 | return err 88 | } 89 | } 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /wiki/ci/buildkit-build-job.yaml: -------------------------------------------------------------------------------- 1 | # kubectl create secret generic rsa-secret --from-file=id_rsa=/home/lc/.ssh/id_rsa --from-file=id_rsa.pub=/home/lc/.ssh/id_rsa.pub 2 | # kubectl create secret docker-registry acr-regcred --docker-email=xxxxxxxxxx --docker-username=xxxxxxxx --docker-password=xxxxxx --docker-server=registry.cn-shenzhen.aliyuncs.com 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: buildkit-build-job-20230705161240 7 | spec: 8 | backoffLimit: 1 9 | activeDeadlineSeconds: 600 10 | ttlSecondsAfterFinished: 120 11 | template: 12 | metadata: 13 | annotations: 14 | container.apparmor.security.beta.kubernetes.io/buildkit-build-job: unconfined 15 | spec: 16 | initContainers: 17 | - name: git 18 | image: registry.cn-shenzhen.aliyuncs.com/dev-ops/git:v2.26.2 19 | workingDir: /workspace 20 | command: 21 | - git-clone-pull.sh 22 | args: 23 | - https://gitee.com/dev-ops/go-gin-rest-api.git 24 | - main 25 | - go-gin-rest-api 26 | volumeMounts: 27 | - name: rsa-secret 28 | readOnly: true 29 | mountPath: "~/.ssh" 30 | - name: workspace 31 | mountPath: /workspace 32 | containers: 33 | - name: buildkit-build-job 34 | image: registry.cn-shenzhen.aliyuncs.com/dev-ops/buildkit:v0.12.1-rootless 35 | env: 36 | - name: BUILDKITD_FLAGS 37 | value: --oci-worker-no-process-sandbox 38 | command: 39 | - buildctl-daemonless.sh 40 | args: ["build", 41 | "--frontend","dockerfile.v0", 42 | "--local","context=/workspace/go-gin-rest-api", 43 | "--local","dockerfile=/workspace/go-gin-rest-api", 44 | "--output","type=image,name=registry.cn-shenzhen.aliyuncs.com/dev-ops/go-gin-rest-api:1.0.0,push=true", 45 | "--export-cache","type=local,mode=max,dest=/cache", 46 | "--import-cache","type=local,src=/cache", 47 | "--export-cache","type=registry,mode=max,ref=registry.cn-shenzhen.aliyuncs.com/dev-ops/go-gin-rest-api:buildkitcache", 48 | "--import-cache","type=registry,ref=registry.cn-shenzhen.aliyuncs.com/dev-ops/go-gin-rest-api:buildkitcache", 49 | "--opt","build-arg:GOPROXY=http://goproxy.goproxy.svc:8081,direct"] 50 | securityContext: 51 | # Needs Kubernetes >= 1.19 52 | seccompProfile: 53 | type: Unconfined 54 | # To change UID/GID, you need to rebuild the image 55 | runAsUser: 1000 56 | runAsGroup: 1000 57 | volumeMounts: 58 | - name: workspace 59 | mountPath: /workspace 60 | - name: cache 61 | mountPath: /cache 62 | - name: docker-secret 63 | mountPath: /home/user/.docker 64 | restartPolicy: Never 65 | volumes: 66 | - name: workspace 67 | hostPath: 68 | path: /mnt/d/project/backup/workspace 69 | type: Directory 70 | - name: cache 71 | hostPath: 72 | path: /mnt/d/project/backup/cache 73 | type: Directory 74 | - name: rsa-secret 75 | secret: 76 | secretName: rsa-secret 77 | - name: docker-secret 78 | secret: 79 | secretName: acr-regcred 80 | items: 81 | - key: .dockerconfigjson 82 | path: config.json -------------------------------------------------------------------------------- /initialize/mysql.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "go-gin-rest-api/models/sys" 7 | "go-gin-rest-api/pkg/global" 8 | "time" 9 | 10 | sqlDriver "github.com/go-sql-driver/mysql" // mysql驱动 11 | loggable "github.com/linclin/gorm2-loggable" 12 | "gorm.io/driver/mysql" 13 | "gorm.io/gorm" 14 | gormlogger "gorm.io/gorm/logger" 15 | ) 16 | 17 | // 初始化mysql数据库 18 | func Mysql() { 19 | logLevel := gormlogger.Info 20 | if global.Conf.System.RunMode == "prd" { 21 | logLevel = gormlogger.Warn 22 | } 23 | if global.Conf.Mysql == nil { 24 | return 25 | } 26 | db, err := gorm.Open(mysql.New(mysql.Config{ 27 | DSN: fmt.Sprintf( 28 | "%s:%s@tcp(%s:%d)/%s?%s", 29 | global.Conf.Mysql.Username, 30 | global.Conf.Mysql.Password, 31 | global.Conf.Mysql.Host, 32 | global.Conf.Mysql.Port, 33 | global.Conf.Mysql.Database, 34 | global.Conf.Mysql.Query, 35 | ), // DSN data source name 36 | DefaultStringSize: 256, // string 类型字段的默认长度 37 | DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 38 | DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 39 | DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 40 | SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置 41 | }), &gorm.Config{Logger: gormlogger.Default.LogMode(logLevel)}) 42 | if err != nil { 43 | switch e := err.(type) { 44 | case *sqlDriver.MySQLError: 45 | // MySQL error unkonw database; 46 | // refer https://dev.mysql.com/doc/refman/5.6/en/error-messages-server.html 47 | if e.Number == 1049 { 48 | createsql := fmt.Sprintf("CREATE DATABASE `%s` CHARSET utf8mb4 COLLATE utf8mb4_general_ci;", global.Conf.Mysql.Database) 49 | 50 | dbForCreateDatabase, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8", global.Conf.Mysql.Username, global.Conf.Mysql.Password, global.Conf.Mysql.Host, global.Conf.Mysql.Port)+"&loc=Asia%2FShanghai") 51 | if err != nil { 52 | global.Log.Error(fmt.Sprintf("数据库连接错误:%v", err)) 53 | panic(fmt.Sprintf("数据库连接错误:%v", err)) 54 | } 55 | defer dbForCreateDatabase.Close() 56 | info, err := dbForCreateDatabase.Exec(createsql) 57 | if err != nil { 58 | global.Log.Error(fmt.Sprintf("数据库创建错误:%v, %v", info, err)) 59 | panic(fmt.Sprintf("数据库创建错误:%v, %v", info, err)) 60 | } else { 61 | global.Log.Info("数据库" + global.Conf.Mysql.Database + "创建成功") 62 | } 63 | } else { 64 | global.Log.Error(fmt.Sprintf("数据库连接错误: %v", err)) 65 | panic(fmt.Sprintf("数据库连接错误: %v", err)) 66 | } 67 | default: 68 | global.Log.Error(fmt.Sprintf("初始化mysql异常: %v", err)) 69 | panic(fmt.Sprintf("初始化mysql异常: %v", err)) 70 | } 71 | } 72 | sqlDB, _ := db.DB() 73 | // SetMaxIdleCons 设置连接池中的最大闲置连接数。 74 | sqlDB.SetMaxIdleConns(10) 75 | // SetMaxOpenCons 设置数据库的最大连接数量。 76 | sqlDB.SetMaxOpenConns(500) 77 | // SetConnMaxLifetiment 设置连接的最大可复用时间。 78 | sqlDB.SetConnMaxLifetime(time.Hour) 79 | global.DB = db 80 | // 自动迁移表结构 81 | global.DB.AutoMigrate(&sys.SysSystem{}) 82 | global.DB.AutoMigrate(&sys.SysRouter{}) 83 | global.DB.AutoMigrate(&sys.SysRole{}) 84 | global.DB.AutoMigrate(&sys.SysApiLog{}) 85 | global.DB.AutoMigrate(&sys.SysReqApiLog{}) 86 | global.DB.AutoMigrate(&sys.SysCronjobLog{}) 87 | global.DB.AutoMigrate(&sys.SysLock{}) 88 | global.DB.Table("sys_change_logs").AutoMigrate(&loggable.ChangeLog{}) 89 | global.Log.Info("初始化mysql完成") 90 | //初始化数据变更记录插件 91 | _, err = loggable.Register(db, "sys_change_logs", loggable.ComputeDiff()) 92 | if err != nil { 93 | panic(err) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /pkg/global/config.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import "log/slog" 4 | 5 | // Configuration 系统配置, 配置字段可参见yml注释 6 | // viper内置了mapstructure, yml文件用"-"区分单词, 转为驼峰方便 7 | type Configuration struct { 8 | System SystemConfiguration `mapstructure:"system" json:"system"` 9 | Logs LogsConfiguration `mapstructure:"logs" json:"logs"` 10 | Mysql *MysqlConfiguration `mapstructure:"mysql" json:"mysql"` 11 | Pgsql *PgsqlConfiguration `mapstructure:"pgsql" json:"pgsql"` 12 | Casbin CasbinConfiguration `mapstructure:"casbin" json:"casbin"` 13 | Jwt JwtConfiguration `mapstructure:"jwt" json:"jwt"` 14 | RateLimit RateLimitConfiguration `mapstructure:"rate-limit" json:"rateLimit"` 15 | Casdoor CasdoorConfiguration `mapstructure:"casdoor" json:"casdoor"` 16 | } 17 | 18 | type SystemConfiguration struct { 19 | AppName string `mapstructure:"app-name" json:"appName"` 20 | RunMode string `mapstructure:"run-mode" json:"runMode"` 21 | UrlPathPrefix string `mapstructure:"url-path-prefix" json:"urlPathPrefix"` 22 | Port int `mapstructure:"port" json:"port"` 23 | BaseApi string `mapstructure:"base-api" json:"baseApi"` 24 | Transaction bool `mapstructure:"transaction" json:"transaction"` 25 | InitData bool `mapstructure:"init-data" json:"initData"` 26 | } 27 | 28 | type LogsConfiguration struct { 29 | Level slog.Level `mapstructure:"level" json:"level"` 30 | Path string `mapstructure:"path" json:"path"` 31 | MaxSize int `mapstructure:"max-size" json:"maxSize"` 32 | MaxBackups int `mapstructure:"max-backups" json:"maxBackups"` 33 | MaxAge int `mapstructure:"max-age" json:"maxAge"` 34 | Compress bool `mapstructure:"compress" json:"compress"` 35 | } 36 | 37 | type MysqlConfiguration struct { 38 | Username string `mapstructure:"username" json:"username"` 39 | Password string `mapstructure:"password" json:"password"` 40 | Database string `mapstructure:"database" json:"database"` 41 | Host string `mapstructure:"host" json:"host"` 42 | Port int `mapstructure:"port" json:"port"` 43 | Query string `mapstructure:"query" json:"query"` 44 | LogMode bool `mapstructure:"log-mode" json:"logMode"` 45 | TablePrefix string `mapstructure:"table-prefix" json:"tablePrefix"` 46 | } 47 | 48 | type PgsqlConfiguration struct { 49 | Username string `mapstructure:"username" json:"username"` 50 | Password string `mapstructure:"password" json:"password"` 51 | Database string `mapstructure:"database" json:"database"` 52 | Host string `mapstructure:"host" json:"host"` 53 | Port int `mapstructure:"port" json:"port"` 54 | SslMode string `mapstructure:"ssl-mode" json:"sslMode"` 55 | LogMode bool `mapstructure:"log-mode" json:"logMode"` 56 | TablePrefix string `mapstructure:"table-prefix" json:"tablePrefix"` 57 | } 58 | type CasbinConfiguration struct { 59 | ModelPath string `mapstructure:"model-path" json:"modelPath"` 60 | } 61 | 62 | type JwtConfiguration struct { 63 | Timeout int `mapstructure:"timeout" json:"timeout"` 64 | MaxRefresh int `mapstructure:"max-refresh" json:"maxRefresh"` 65 | } 66 | 67 | type RateLimitConfiguration struct { 68 | Max int64 `mapstructure:"max" json:"max"` 69 | } 70 | 71 | type CasdoorConfiguration struct { 72 | Endpoint string `mapstructure:"endpoint" json:"endpoint"` 73 | ClientID string `mapstructure:"client-id" json:"client-id"` 74 | ClientSecret string `mapstructure:"client-secret" json:"client-secret"` 75 | CertificatePath string `mapstructure:"certificate-path" json:"certificate-path"` 76 | Certificate string `mapstructure:"certificate" json:"certificate"` 77 | Organization string `mapstructure:"organization" json:"organization"` 78 | Application string `mapstructure:"application" json:"application"` 79 | FrontendUrl string `mapstructure:"frontend-url" json:"frontend-url"` 80 | } 81 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | _ "go-gin-rest-api/docs" 7 | "go-gin-rest-api/initialize" 8 | "go-gin-rest-api/pkg/global" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "runtime" 13 | "runtime/debug" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/gin-gonic/autotls" 18 | "golang.org/x/crypto/acme/autocert" 19 | ) 20 | 21 | var ( 22 | // 初始化为 unknown,如果编译时没有传入这些值,则为 unknown 23 | GitBranch = "unknown" 24 | GitRevision = "unknown" 25 | GitCommitLog = "unknown" 26 | BuildTime = "unknown" 27 | BuildGoVersion = "unknown" 28 | ) 29 | 30 | func init() { 31 | //输出程序分支 commit golang版本 构建时间 32 | fmt.Fprint(os.Stdout, buildInfo()) 33 | // 初始化配置 34 | initialize.InitConfig() 35 | // 初始化日志 36 | initialize.Logger() 37 | // 初始化数据库 38 | initialize.Mysql() 39 | 40 | initialize.Pgsql() 41 | // 初始化Sentinel流控规则 42 | initialize.InitSentinel() 43 | // 初始校验器 44 | initialize.Validate("zh") 45 | // 初始化jwt的rsa证书 46 | initialize.InitRSA() 47 | if global.Conf.System.InitData { 48 | // 初始化数据 49 | initialize.InitData() 50 | } 51 | // 初始化casbin策略管理器 52 | initialize.InitCasbin() 53 | // 初始化定时任务 54 | initialize.Cron() 55 | // 初始化casdoor客户端 56 | initialize.InitCasdoor() 57 | } 58 | 59 | // @title go-gin-rest-api 60 | // @version 1.0.0 61 | // @description go-gin-rest-api Golang后台api开发脚手架 62 | // @termsOfService https://github.com/linclin 63 | // @contact.name LC 64 | // @contact.url https://github.com/linclin 65 | // @contact.email 13579443@qq.com 66 | // @license.name Apache 2.0 67 | // @license.url http://www.apache.org/licenses/LICENSE-2.0.html 68 | // @securityDefinitions.apikey ApiKeyAuth 69 | // @in header 70 | // @name Authorization 71 | 72 | func main() { 73 | defer func() { 74 | if err := recover(); err != nil { 75 | // 将异常写入日志 76 | global.Log.Error(fmt.Sprintf("项目启动失败: %v\n堆栈信息: %v", err, string(debug.Stack()))) 77 | } 78 | }() 79 | // 初始化路由 80 | r := initialize.Routers() 81 | host := "0.0.0.0" 82 | port := global.Conf.System.Port 83 | // 服务启动及优雅关闭 84 | // 参考地址https://github.com/gin-gonic/examples/blob/master/graceful-shutdown/graceful-shutdown/server.go 85 | srv := &http.Server{ 86 | Addr: fmt.Sprintf("%s:%d", host, port), 87 | Handler: r, 88 | } 89 | // Initializing the server in a goroutine so that 90 | // it won't block the graceful shutdown handling below 91 | go func() { 92 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 93 | global.Log.Error(fmt.Sprint("listen error: ", err)) 94 | } 95 | }() 96 | global.Log.Info(fmt.Sprintf("HTTP Server is running at %s:%d/%s", host, port, global.Conf.System.UrlPathPrefix)) 97 | certManager := autocert.Manager{ 98 | Prompt: autocert.AcceptTOS, 99 | HostPolicy: autocert.HostWhitelist(global.Conf.System.BaseApi), 100 | Cache: autocert.DirCache("./cache"), 101 | } 102 | global.Log.Info(fmt.Sprintf("HTTPS Server is running at %s:%d/%s", host, 443, global.Conf.System.UrlPathPrefix)) 103 | autotls.RunWithManager(r, &certManager) 104 | // Wait for interrupt signal to gracefully shutdown the server with 105 | // a timeout of 5 seconds. 106 | quit := make(chan os.Signal, 1) 107 | // kill (no param) default send syscall.SIGTERM 108 | // kill -2 is syscall.SIGINT 109 | // kill -9 is syscall.SIGKILL but can't be catch, so don't need add it 110 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 111 | <-quit 112 | global.Log.Info("Shutting down server...") 113 | // The context is used to inform the server it has 5 seconds to finish 114 | // the request it is currently handling 115 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 116 | defer cancel() 117 | if err := srv.Shutdown(ctx); err != nil { 118 | global.Log.Error(fmt.Sprint("Server forced to shutdown: ", err)) 119 | } 120 | global.Log.Info("Server exiting") 121 | } 122 | 123 | // 返回构建信息 多行格式 124 | func buildInfo() string { 125 | return fmt.Sprintf("GitBranch=%s\nGitRevision=%s\nGitCommitLog=%s\nBuildTime=%s\nGoVersion=%s\nruntime=%s/%s\n", 126 | GitBranch, GitRevision, GitCommitLog, BuildTime, BuildGoVersion, runtime.GOOS, runtime.GOARCH) 127 | } 128 | -------------------------------------------------------------------------------- /middleware/jwt.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/models" 6 | "go-gin-rest-api/models/sys" 7 | "go-gin-rest-api/pkg/global" 8 | "net/http" 9 | "time" 10 | 11 | jwt "github.com/appleboy/gin-jwt/v2" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func InitAuth() (*jwt.GinJWTMiddleware, error) { 16 | return jwt.New(&jwt.GinJWTMiddleware{ 17 | Realm: global.Conf.System.AppName, // jwt标识 18 | SigningAlgorithm: "RS512", 19 | PrivKeyFile: "./conf/rsa/rsa-private.pem", // Private key 20 | PubKeyFile: "./conf/rsa/rsa-public.pem", // Public key 21 | IdentityKey: "AppId", 22 | Timeout: time.Hour * time.Duration(global.Conf.Jwt.Timeout), // token过期时间 23 | MaxRefresh: time.Hour * time.Duration(global.Conf.Jwt.MaxRefresh), // token更新时间 24 | PayloadFunc: payloadFunc, // 有效载荷处理 25 | IdentityHandler: identityHandler, // 解析Claims 26 | Authenticator: auth, // 校验token的正确性, 处理登录逻辑 27 | Authorizator: authorizator, // 用户登录校验成功处理 28 | Unauthorized: unauthorized, // 用户登录校验失败处理 29 | LoginResponse: loginResponse, // 登录成功后的响应 30 | LogoutResponse: logoutResponse, // 登出后的响应 31 | TokenLookup: "header: Authorization", // 自动在这几个地方寻找请求中的token header: Authorization, query: token, cookie: jwt 32 | TokenHeadName: "Bearer", // header名称 33 | TimeFunc: time.Now, 34 | }) 35 | } 36 | 37 | func payloadFunc(data interface{}) jwt.MapClaims { 38 | if v, ok := data.(map[string]interface{}); ok { 39 | return jwt.MapClaims{ 40 | jwt.IdentityKey: v["AppId"], 41 | "AppId": v["AppId"], 42 | } 43 | } 44 | return jwt.MapClaims{} 45 | } 46 | 47 | func identityHandler(c *gin.Context) interface{} { 48 | claims := jwt.ExtractClaims(c) 49 | // 此处返回值类型map[string]interface{}与payloadFunc和authorizator的data类型必须一致, 否则会导致授权失败还不容易找到原因 50 | return map[string]interface{}{ 51 | "IdentityKey": claims[jwt.IdentityKey], 52 | "AppId": claims["AppId"], 53 | } 54 | } 55 | 56 | // @Summary [系统内部]获取token 57 | // @Id auth 58 | // @Tags [系统内部]Token 59 | // @version 1.0 60 | // @Accept application/x-json-stream 61 | // @Param body body models.ReqToken true "token请求" 62 | // @Success 200 object models.Token 返回列表 63 | // @Failure 400 object models.Resp 查询失败 64 | // @Router /api/v1/base/auth [post] 65 | func auth(c *gin.Context) (interface{}, error) { 66 | var req sys.SysSystem 67 | if err := c.ShouldBindJSON(&req); err != nil { 68 | return nil, err 69 | } 70 | var system sys.SysSystem 71 | query := global.DB.Where(req).First(&system) 72 | if query.Error != nil { 73 | return nil, fmt.Errorf("AppId:%s和AppSecret:%s不存在:%s", req.AppId, req.AppSecret, query.Error) 74 | } 75 | // 将AppId以json格式写入, payloadFunc/authorizator会使用到 76 | return map[string]interface{}{ 77 | "AppId": req.AppId, 78 | "exp": time.Now().Unix() + 7200, 79 | }, nil 80 | } 81 | 82 | func authorizator(data interface{}, c *gin.Context) bool { 83 | if v, ok := data.(map[string]interface{}); ok { 84 | // 将用户保存到context, api调用时取数据方便 85 | c.Set("AppId", v["AppId"].(string)) 86 | return true 87 | } 88 | return false 89 | } 90 | 91 | func unauthorized(c *gin.Context, code int, message string) { 92 | global.Log.Error(fmt.Sprintf("JWT认证失败, 错误码%d, 错误信息%s", code, message)) 93 | c.JSON(http.StatusUnauthorized, models.Resp{ 94 | Success: models.ERROR, 95 | Data: "认证失败", 96 | Msg: message, 97 | }) 98 | return 99 | } 100 | 101 | func loginResponse(c *gin.Context, code int, token string, expires time.Time) { 102 | c.JSON(http.StatusOK, models.Token{ 103 | Success: models.SUCCESS, 104 | Token: token, 105 | Expires: expires, 106 | }) 107 | } 108 | 109 | func logoutResponse(c *gin.Context, code int) { 110 | c.JSON(http.StatusOK, models.Resp{ 111 | Success: models.SUCCESS, 112 | Data: models.OkMsg, 113 | Msg: models.CustomError[models.Ok], 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /api/v1/sys/sys_user.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/models" 6 | "go-gin-rest-api/models/sys" 7 | "go-gin-rest-api/pkg/global" 8 | "go-gin-rest-api/pkg/utils" 9 | "time" 10 | 11 | "github.com/casbin/casbin/v2" 12 | "github.com/casdoor/casdoor-go-sdk/casdoorsdk" 13 | "github.com/gin-gonic/gin" 14 | "github.com/go-playground/validator/v10" 15 | ) 16 | 17 | // @Summary [系统内部]用户登录 18 | // @Id Login 19 | // @Tags [系统内部]角色 20 | // @version 1.0 21 | // @Accept application/x-json-stream 22 | // @Param code query string true "code" 23 | // @Param state query string true "state" 24 | // @Success 200 object models.Resp 返回列表 25 | // @Failure 400 object models.Resp 查询失败 26 | // @Security ApiKeyAuth 27 | // @Router /api/v1/user/login [post] 28 | func Login(c *gin.Context) { 29 | startTime := time.Now() 30 | code := c.Query("code") 31 | state := c.Query("state") 32 | token, err := casdoorsdk.GetOAuthToken(code, state) 33 | execTime := time.Now().Sub(startTime) 34 | sys.AddReqApi(c, "POST", fmt.Sprintf("%s/api/login/oauth/authorize", global.Conf.Casdoor.Endpoint), fmt.Sprintf("?state=%s&code=%s", state, code), utils.JsonStr(token), execTime.String(), 0, time.Now()) 35 | if err != nil { 36 | models.FailWithMessage(err.Error(), c) 37 | return 38 | } 39 | models.OkWithData(token, c) 40 | } 41 | 42 | // @Summary [系统内部]用户登录 43 | // @Id GetUserInfo 44 | // @Tags [系统内部]角色 45 | // @version 1.0 46 | // @Accept application/x-json-stream 47 | // @Success 200 object models.Resp 返回列表 48 | // @Failure 400 object models.Resp 查询失败 49 | // @Security ApiKeyAuth 50 | // @Router /api/v1/user/info [get] 51 | func GetUserInfo(c *gin.Context) { 52 | startTime := time.Now() 53 | token := c.GetHeader("X-Auth-Token") 54 | claims, err := casdoorsdk.ParseJwtToken(token) 55 | execTime := time.Now().Sub(startTime) 56 | sys.AddReqApi(c, "POST", fmt.Sprintf("%s/api/login/oauth/access_token", global.Conf.Casdoor.Endpoint), token, utils.JsonStr(claims), execTime.String(), 0, time.Now()) 57 | if err != nil { 58 | models.FailWithMessage(err.Error(), c) 59 | return 60 | } 61 | models.OkWithData(claims.User, c) 62 | } 63 | 64 | // @Summary [系统内部]获取指定用户全部权限 65 | // @Id GetPermission 66 | // @Tags [系统内部]角色 67 | // @version 1.0 68 | // @Accept application/x-json-stream 69 | // @Param user path string true "用户名" 70 | // @Success 200 object models.Resp 返回列表 71 | // @Failure 400 object models.Resp 查询失败 72 | // @Security ApiKeyAuth 73 | // @Router /api/v1/user/perm/{user} [get] 74 | func GetPermission(c *gin.Context) { 75 | user := c.Param("user") 76 | group, err := global.CasbinACLEnforcer.GetImplicitRolesForUser(user) 77 | if err != nil { 78 | models.FailWithMessage(err.Error(), c) 79 | } 80 | perm, err := casbin.CasbinJsGetPermissionForUser(global.CasbinACLEnforcer, user) 81 | if err != nil { 82 | models.FailWithMessage(err.Error(), c) 83 | } else { 84 | models.OkWithData(map[string]interface{}{"group": group, "perm": perm}, c) 85 | } 86 | } 87 | 88 | // @Summary [系统内部]用户操作鉴权 89 | // @Id AuthPermission 90 | // @Tags [系统内部]角色 91 | // @version 1.0 92 | // @Accept application/x-json-stream 93 | // @Param user path string true "用户名" 94 | // @Param body body sys.RolePermission true "权限" 95 | // @Success 200 object models.Resp 返回列表 96 | // @Failure 400 object models.Resp 查询失败 97 | // @Security ApiKeyAuth 98 | // @Router /api/v1/user/auth/{user} [post] 99 | func AuthPermission(c *gin.Context) { 100 | user := c.Param("user") 101 | var role_perms sys.RolePermission 102 | err := c.ShouldBindJSON(&role_perms) 103 | if err != nil { 104 | // 获取validator.ValidationErrors类型的errors 105 | errs, ok := err.(validator.ValidationErrors) 106 | var errInfo interface{} 107 | if !ok { 108 | // 非validator.ValidationErrors类型错误直接返回 109 | errInfo = err 110 | } else { 111 | // validator.ValidationErrors类型错误则进行翻译 112 | errInfo = errs.Translate(global.Translator) // 翻译校验错误提示 113 | } 114 | models.FailWithDetailed(errInfo, models.CustomError[models.NotOk], c) 115 | return 116 | } 117 | ok, reason, err := global.CasbinACLEnforcer.EnforceEx(user, role_perms.Obj, role_perms.Obj1, role_perms.Obj2, role_perms.Action) 118 | if err != nil { 119 | models.FailWithMessage(err.Error(), c) 120 | } else { 121 | models.OkWithData(map[string]interface{}{"auth": ok, "data": reason}, c) 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /wiki/ci/buildkit-argo-workflow-template.yaml: -------------------------------------------------------------------------------- 1 | # kubectl -n argo create secret generic rsa-secret --from-file=id_rsa=/home/lc/.ssh/id_rsa --from-file=id_rsa.pub=/home/lc/.ssh/id_rsa.pub 2 | # kubectl -n argo create secret docker-registry acr-regcred --docker-email=xxxx --docker-username=xxxx --docker-password=xxxxxx --docker-server=registry.cn-shenzhen.aliyuncs.com 3 | apiVersion: argoproj.io/v1alpha1 4 | kind: WorkflowTemplate 5 | metadata: 6 | name: buildkit 7 | spec: 8 | arguments: 9 | parameters: 10 | - name: repo 11 | value: https://gitee.com/dev-ops/go-gin-rest-api.git 12 | - name: branch 13 | value: main 14 | - name: path 15 | value: go-gin-rest-api 16 | - name: image 17 | value: registry.cn-shenzhen.aliyuncs.com/dev-ops/go-gin-rest-api 18 | - name: version 19 | value: 1.0.1 20 | entrypoint: main 21 | volumes: 22 | - name: workspace 23 | hostPath: 24 | path: /mnt/d/project/backup/workspace 25 | type: Directory 26 | - name: cache 27 | hostPath: 28 | path: /mnt/d/project/backup/cache 29 | type: Directory 30 | - name: rsa-secret 31 | secret: 32 | secretName: rsa-secret 33 | - name: docker-secret 34 | secret: 35 | secretName: acr-regcred 36 | items: 37 | - key: .dockerconfigjson 38 | path: config.json 39 | templates: 40 | - name: main 41 | dag: 42 | tasks: 43 | - name: git-clone 44 | template: git-clone 45 | arguments: 46 | parameters: 47 | - name: repo 48 | value: "{{workflow.parameters.repo}}" 49 | - name: branch 50 | value: "{{workflow.parameters.branch}}" 51 | - name: path 52 | value: "{{workflow.parameters.path}}" 53 | - name: build-image 54 | template: build-image 55 | arguments: 56 | parameters: 57 | - name: path 58 | value: "{{workflow.parameters.path}}" 59 | - name: image 60 | value: "{{workflow.parameters.image}}" 61 | - name: version 62 | value: "{{workflow.parameters.version}}" 63 | depends: "git-clone" 64 | - name: git-clone 65 | inputs: 66 | parameters: 67 | - name: repo 68 | - name: branch 69 | - name: path 70 | container: 71 | volumeMounts: 72 | - name: rsa-secret 73 | readOnly: true 74 | mountPath: "~/.ssh" 75 | - mountPath: /workspace 76 | name: workspace 77 | image: registry.cn-shenzhen.aliyuncs.com/dev-ops/git:v2.30.2 78 | workingDir: /workspace 79 | command: 80 | - git-clone-pull.sh 81 | args: 82 | - "{{inputs.parameters.repo}}" 83 | - "{{inputs.parameters.branch}}" 84 | - "{{inputs.parameters.path}}" 85 | - name: build-image 86 | inputs: 87 | parameters: 88 | - name: path 89 | - name: image 90 | - name: version 91 | container: 92 | image: registry.cn-shenzhen.aliyuncs.com/dev-ops/buildkit:v0.12.1-rootless 93 | volumeMounts: 94 | - name: workspace 95 | mountPath: /workspace 96 | - name: cache 97 | mountPath: /cache 98 | - name: docker-secret 99 | mountPath: /home/user/.docker 100 | workingDir: /workspace/{{inputs.parameters.path}} 101 | env: 102 | - name: BUILDKITD_FLAGS 103 | value: --oci-worker-no-process-sandbox 104 | command: 105 | - buildctl-daemonless.sh 106 | args: 107 | - build 108 | - --frontend 109 | - dockerfile.v0 110 | - --local 111 | - context=. 112 | - --local 113 | - dockerfile=. 114 | - --output 115 | - type=image,name={{inputs.parameters.image}}:{{inputs.parameters.version}},push=true 116 | - --export-cache 117 | - type=local,mode=max,dest=/cache 118 | - --import-cache 119 | - type=local,src=/cache 120 | - --export-cache 121 | - type=registry,mode=max,ref={{inputs.parameters.image}}:buildkit-cache 122 | - --import-cache 123 | - type=registry,ref={{inputs.parameters.image}}:buildkit-cache 124 | - --opt 125 | - build-arg:GOPROXY=http://goproxy.goproxy.svc:8081,direct 126 | securityContext: 127 | seccompProfile: 128 | type: Unconfined 129 | runAsUser: 1000 130 | runAsGroup: 1000 -------------------------------------------------------------------------------- /initialize/router.go: -------------------------------------------------------------------------------- 1 | package initialize 2 | 3 | import ( 4 | "fmt" 5 | "go-gin-rest-api/api" 6 | "go-gin-rest-api/middleware" 7 | "go-gin-rest-api/models/sys" 8 | "go-gin-rest-api/pkg/global" 9 | sysRouter "go-gin-rest-api/router/sys" 10 | "time" 11 | 12 | "github.com/gin-contrib/cors" 13 | "github.com/gin-contrib/gzip" 14 | "github.com/gin-contrib/pprof" 15 | "github.com/gin-contrib/requestid" 16 | "github.com/gin-gonic/gin" 17 | sloggin "github.com/samber/slog-gin" 18 | sentinelPlugin "github.com/sentinel-group/sentinel-go-adapters/gin" 19 | swaggerFiles "github.com/swaggo/files" 20 | ginSwagger "github.com/swaggo/gin-swagger" 21 | ginprometheus "github.com/zsais/go-gin-prometheus" 22 | ) 23 | 24 | // 初始化总路由 25 | func Routers() *gin.Engine { 26 | // 初始化路由接口到数据库表中 27 | gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { 28 | //global.Log.Debug(fmt.Sprint("router %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)) 29 | go func(httpMethod, absolutePath, handlerName string) { 30 | sys_router := sys.SysRouter{ 31 | HttpMethod: httpMethod, 32 | AbsolutePath: absolutePath, 33 | HandlerName: handlerName, 34 | } 35 | err := global.DB.Where(&sys_router).FirstOrCreate(&sys_router).Error 36 | if err != nil { 37 | global.Log.Error(fmt.Sprint("SysRouter 数据初始化失败", err.Error())) 38 | } 39 | }(httpMethod, absolutePath, handlerName) 40 | } 41 | if global.Conf.System.RunMode == "prd" { 42 | gin.SetMode(gin.ReleaseMode) 43 | } 44 | gin.ForceConsoleColor() 45 | // 创建带有默认中间件的路由: 46 | // 日志与恢复中间件 47 | // r := gin.Default() 48 | // 创建不带中间件的路由: 49 | r := gin.New() 50 | // 初始化Trace中间件 51 | r.Use(requestid.New()) 52 | // slog日志 53 | r.Use(sloggin.New(global.Log)) 54 | // 添加访问记录 55 | r.Use(middleware.AccessLog) 56 | // 添加全局异常处理中间件 57 | r.Use(middleware.Exception) 58 | // GZip压缩插件 59 | r.Use(gzip.Gzip(gzip.DefaultCompression)) 60 | // 添加跨域中间件, 让请求支持跨域 61 | // 定义跨域配置 62 | crosConfig := cors.Config{ 63 | AllowOrigins: []string{"*", "http://localhost:3000", "http://127.0.0.1:3000"}, 64 | AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, 65 | AllowHeaders: []string{"Origin", "Access-Control-Allow-Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization", "X-Auth-Token", "X-Real-IP"}, 66 | ExposeHeaders: []string{"Content-Length"}, 67 | AllowCredentials: true, 68 | MaxAge: 12 * time.Hour, 69 | } 70 | // 注册跨域中间件 71 | r.Use(cors.New(crosConfig)) 72 | // 添加sentinel中间件 73 | r.Use( 74 | sentinelPlugin.SentinelMiddleware( 75 | // customize resource extractor if required 76 | // method_path by default 77 | // sentinelPlugin.WithResourceExtractor(func(ctx *gin.Context) string { 78 | // return ctx.GetHeader("X-Real-IP") 79 | // }), 80 | // customize block fallback if required 81 | // abort with status 429 by default 82 | sentinelPlugin.WithBlockFallback(func(ctx *gin.Context) { 83 | ctx.AbortWithStatusJSON(429, map[string]interface{}{ 84 | "success": false, 85 | "data": "", 86 | "msg": "too many request; the quota used up", 87 | }) 88 | }), 89 | ), 90 | ) 91 | // 初始化pprof 92 | pprof.Register(r) 93 | // 初始化jwt auth中间件 94 | authMiddleware, err := middleware.InitAuth() 95 | if err != nil { 96 | panic(fmt.Sprintf("初始化JWT auth中间件失败: %v", err)) 97 | } 98 | global.Log.Info("初始化JWT auth中间件完成") 99 | // 初始化Prometheus中间件 100 | prome := ginprometheus.NewPrometheus("gin") 101 | prome.Use(r) 102 | global.Log.Info("初始化Prometheus中间件完成") 103 | if global.Conf.System.RunMode != "prd" { 104 | // 初始化Swagger 105 | url := ginSwagger.URL(global.Conf.System.BaseApi + "/swagger/doc.json") 106 | r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url)) 107 | global.Log.Info("初始化Swagger完成") 108 | } 109 | // 初始化健康检查接口 110 | r.GET("/heatch_check", api.HeathCheck) 111 | global.Log.Info("初始化健康检查接口完成") 112 | // 初始化API路由 113 | apiGroup := r.Group(global.Conf.System.UrlPathPrefix) 114 | // 方便统一添加路由前缀 115 | v1Group := apiGroup.Group("v1") 116 | sysRouter.InitPublicRouter(v1Group) // 注册公共路由 117 | sysRouter.InitBaseRouter(v1Group, authMiddleware) // 注册基础路由, 不会鉴权 118 | sysRouter.InitRoleRouter(v1Group, authMiddleware) // 注册角色路由 119 | sysRouter.InitSystemRouter(v1Group, authMiddleware) // 注册系统路由 120 | sysRouter.InitRouterRouter(v1Group, authMiddleware) // 注册系统路由路由 121 | sysRouter.InitApiLogRouter(v1Group, authMiddleware) // 注册服务接口日志路由 122 | sysRouter.InitReqApiLogRouter(v1Group, authMiddleware) // 注册请求接口日志路由 123 | sysRouter.InitCronjobLogRouter(v1Group, authMiddleware) // 注册定时任务日志路由 124 | sysRouter.InitChangeLogRouter(v1Group, authMiddleware) // 注册数据审计日志路由 125 | sysRouter.InitUserRouter(v1Group, authMiddleware) // 注册用户权限路由 126 | sysRouter.InitDataRouter(v1Group, authMiddleware) 127 | global.Log.Info("初始化基础路由完成") 128 | return r 129 | } 130 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-gin-rest-api 2 | 3 | go 1.25.0 4 | 5 | require ( 6 | github.com/a8m/rql v1.4.0 7 | github.com/alibaba/sentinel-golang v1.0.4 8 | github.com/appleboy/gin-jwt/v2 v2.10.3 9 | github.com/casbin/casbin/v2 v2.134.0 10 | github.com/casbin/gorm-adapter/v3 v3.38.0 11 | github.com/casdoor/casdoor-go-sdk v1.39.0 12 | github.com/fsnotify/fsnotify v1.9.0 13 | github.com/gin-contrib/cors v1.7.6 14 | github.com/gin-contrib/gzip v1.2.5 15 | github.com/gin-contrib/pprof v1.5.3 16 | github.com/gin-contrib/requestid v1.0.5 17 | github.com/gin-gonic/autotls v1.2.2 18 | github.com/gin-gonic/gin v1.11.0 19 | github.com/go-playground/locales v0.14.1 20 | github.com/go-playground/universal-translator v0.18.1 21 | github.com/go-playground/validator/v10 v10.28.0 22 | github.com/go-resty/resty/v2 v2.15.1 23 | github.com/go-sql-driver/mysql v1.9.3 24 | github.com/linclin/gorm2-loggable v1.0.0 25 | github.com/natefinch/lumberjack v2.0.0+incompatible 26 | github.com/patrickmn/go-cache v2.1.0+incompatible 27 | github.com/robfig/cron/v3 v3.0.1 28 | github.com/samber/lo v1.52.0 29 | github.com/samber/slog-gin v1.18.0 30 | github.com/sentinel-group/sentinel-go-adapters v1.0.1 31 | github.com/spf13/cast v1.10.0 32 | github.com/spf13/viper v1.21.0 33 | github.com/swaggo/files v1.0.1 34 | github.com/swaggo/gin-swagger v1.6.1 35 | github.com/swaggo/swag v1.16.6 36 | github.com/zsais/go-gin-prometheus v1.0.2 37 | golang.org/x/crypto v0.45.0 38 | gorm.io/driver/mysql v1.6.0 39 | gorm.io/driver/postgres v1.6.0 40 | gorm.io/gorm v1.31.1 41 | ) 42 | 43 | require ( 44 | filippo.io/edwards25519 v1.1.0 // indirect 45 | github.com/KyleBanks/depth v1.2.1 // indirect 46 | github.com/beorn7/perks v1.0.1 // indirect 47 | github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect 48 | github.com/bytedance/gopkg v0.1.3 // indirect 49 | github.com/bytedance/sonic v1.14.2 // indirect 50 | github.com/bytedance/sonic/loader v0.4.0 // indirect 51 | github.com/casbin/govaluate v1.10.0 // indirect 52 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 53 | github.com/cloudwego/base64x v0.1.6 // indirect 54 | github.com/dustin/go-humanize v1.0.1 // indirect 55 | github.com/gabriel-vasile/mimetype v1.4.11 // indirect 56 | github.com/gin-contrib/sse v1.1.0 // indirect 57 | github.com/glebarez/go-sqlite v1.22.0 // indirect 58 | github.com/glebarez/sqlite v1.11.0 // indirect 59 | github.com/go-ole/go-ole v1.3.0 // indirect 60 | github.com/go-openapi/jsonpointer v0.22.3 // indirect 61 | github.com/go-openapi/jsonreference v0.21.3 // indirect 62 | github.com/go-openapi/spec v0.22.1 // indirect 63 | github.com/go-openapi/swag/conv v0.25.4 // indirect 64 | github.com/go-openapi/swag/jsonname v0.25.4 // indirect 65 | github.com/go-openapi/swag/jsonutils v0.25.4 // indirect 66 | github.com/go-openapi/swag/loading v0.25.4 // indirect 67 | github.com/go-openapi/swag/stringutils v0.25.4 // indirect 68 | github.com/go-openapi/swag/typeutils v0.25.4 // indirect 69 | github.com/go-openapi/swag/yamlutils v0.25.4 // indirect 70 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 71 | github.com/goccy/go-json v0.10.5 // indirect 72 | github.com/goccy/go-yaml v1.19.0 // indirect 73 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 74 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 75 | github.com/golang-sql/sqlexp v0.1.0 // indirect 76 | github.com/google/uuid v1.6.0 // indirect 77 | github.com/jackc/pgpassfile v1.0.0 // indirect 78 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 79 | github.com/jackc/pgx/v5 v5.7.6 // indirect 80 | github.com/jackc/puddle/v2 v2.2.2 // indirect 81 | github.com/jinzhu/copier v0.4.0 // indirect 82 | github.com/jinzhu/inflection v1.0.0 // indirect 83 | github.com/jinzhu/now v1.1.5 // indirect 84 | github.com/josharian/intern v1.0.0 // indirect 85 | github.com/json-iterator/go v1.1.12 // indirect 86 | github.com/klauspost/compress v1.18.2 // indirect 87 | github.com/klauspost/cpuid/v2 v2.3.0 // indirect 88 | github.com/leodido/go-urn v1.4.0 // indirect 89 | github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect 90 | github.com/mailru/easyjson v0.9.1 // indirect 91 | github.com/mattn/go-isatty v0.0.20 // indirect 92 | github.com/microsoft/go-mssqldb v1.9.5 // indirect 93 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 94 | github.com/modern-go/reflect2 v1.0.2 // indirect 95 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 96 | github.com/ncruces/go-strftime v1.0.0 // indirect 97 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 98 | github.com/pkg/errors v0.9.1 // indirect 99 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 100 | github.com/prometheus/client_golang v1.23.2 // indirect 101 | github.com/prometheus/client_model v0.6.2 // indirect 102 | github.com/prometheus/common v0.67.4 // indirect 103 | github.com/prometheus/procfs v0.19.2 // indirect 104 | github.com/quic-go/qpack v0.6.0 // indirect 105 | github.com/quic-go/quic-go v0.57.1 // indirect 106 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 107 | github.com/sagikazarmark/locafero v0.12.0 // indirect 108 | github.com/satori/go.uuid v1.2.0 // indirect 109 | github.com/shirou/gopsutil/v3 v3.24.5 // indirect 110 | github.com/shoenig/go-m1cpu v0.1.7 // indirect 111 | github.com/shopspring/decimal v1.4.0 // indirect 112 | github.com/sirupsen/logrus v1.9.3 // indirect 113 | github.com/spf13/afero v1.15.0 // indirect 114 | github.com/spf13/pflag v1.0.10 // indirect 115 | github.com/subosito/gotenv v1.6.0 // indirect 116 | github.com/tklauser/go-sysconf v0.3.16 // indirect 117 | github.com/tklauser/numcpus v0.11.0 // indirect 118 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 119 | github.com/ugorji/go/codec v1.3.1 // indirect 120 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect 121 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 122 | go.opentelemetry.io/otel v1.38.0 // indirect 123 | go.opentelemetry.io/otel/trace v1.38.0 // indirect 124 | go.yaml.in/yaml/v2 v2.4.3 // indirect 125 | go.yaml.in/yaml/v3 v3.0.4 // indirect 126 | golang.org/x/arch v0.23.0 // indirect 127 | golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect 128 | golang.org/x/mod v0.30.0 // indirect 129 | golang.org/x/net v0.47.0 // indirect 130 | golang.org/x/oauth2 v0.33.0 // indirect 131 | golang.org/x/sync v0.18.0 // indirect 132 | golang.org/x/sys v0.38.0 // indirect 133 | golang.org/x/text v0.31.0 // indirect 134 | golang.org/x/tools v0.39.0 // indirect 135 | google.golang.org/protobuf v1.36.10 // indirect 136 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 137 | gopkg.in/yaml.v2 v2.4.0 // indirect 138 | gorm.io/driver/sqlserver v1.6.3 // indirect 139 | gorm.io/plugin/dbresolver v1.6.2 // indirect 140 | modernc.org/libc v1.67.1 // indirect 141 | modernc.org/mathutil v1.7.1 // indirect 142 | modernc.org/memory v1.11.0 // indirect 143 | modernc.org/sqlite v1.40.1 // indirect 144 | ) 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
6 |
7 |
8 |
9 |