diff --git a/define/middleare.go b/define/middleare.go index 0e05c83..e6f1b08 100644 --- a/define/middleare.go +++ b/define/middleare.go @@ -14,3 +14,9 @@ type IsBlackIP func(ctx *gin.Context, clientIP string) bool // IsWhiteIP 是否白名单IP type IsWhiteIP func(ctx *gin.Context, clientIP string) bool + +type RateLimitConfig struct { + Use int `json:"use" dc:"单次访问使用令牌数"` + Total int `json:"total" dc:"单位时间内允许访问的总次数"` + TimeInterval int64 `json:"time_interval" dc:"单位时间间隔"` +} diff --git a/define/route.go b/define/route.go new file mode 100644 index 0000000..bcb9174 --- /dev/null +++ b/define/route.go @@ -0,0 +1,30 @@ +// Package define ... +// +// Description : define ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-04 14:45 +package define + +import "reflect" + +// UriConfig 接口配置 +type UriConfig struct { + Path string `json:"path"` // 接口路由, 必须配置 + RequestMethod []string `json:"request_method"` // 接口请求方法, 必须配置 + TagList []string `json:"tag_list"` // 接口分组 + Desc string `json:"desc"` // 接口描述 + IsSse bool `json:"is_sse"` // 是否 SSE 连接 + IsWebsocket bool `json:"is_ws"` // 是否 websocket 连接 + OutputStrict bool `json:"output_strict"` // 接口是否为严格模式 : 不配置,可返回任意类型, 配置, 必须返回结构体或者map + MaxExecTime int64 `json:"max_exec_time"` // 接口最大执行时间, 单位: ms, 配置为0则不验证, 注意, 超时后不会报错, 会打印一条warn日志, 如果配置了报警策略, 也会发送报警信息 + HookSync bool `json:"hook_sync"` // 接口主逻辑执行完成之后,hook是否同步执行, 默认异步执行 + NoLogin bool `json:"no_login"` // 接口是否需要登录(无需登录, 则有token就验证, 无token不验证) + ParamIsPtr bool `json:"param_is_ptr"` // 参数是否指针类型 + RateLimitConfig RateLimitConfig `json:"rate_limit_config"` // 接口限流配置 + FormDataType reflect.Type `json:"-"` // 表单数据类型 + ResultDataType reflect.Type `json:"-"` // 返回值数据类型 + ApiStructValue reflect.Value `json:"-"` // 逻辑函数所属结构体取值 + ApiLogicFunc reflect.Method `json:"-"` // 自定义的接口逻辑 +} diff --git a/go.mod b/go.mod index 660bd30..be3359f 100644 --- a/go.mod +++ b/go.mod @@ -12,11 +12,13 @@ require ( git.zhangdeman.cn/zhangdeman/graceful v0.0.0-20250529070945-92833db6f3a4 git.zhangdeman.cn/zhangdeman/logger v0.0.0-20251231045117-f05cb1e30afe git.zhangdeman.cn/zhangdeman/network v0.0.0-20251220063845-8403861d36e5 + git.zhangdeman.cn/zhangdeman/rate_limit v0.0.0-20260104044956-ab445203aadd git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20251013044511-86c1a4a3a9dd git.zhangdeman.cn/zhangdeman/trace v0.0.0-20251013092356-b7b9fb5f8a76 git.zhangdeman.cn/zhangdeman/util v0.0.0-20260103142706-1f6145405ec4 git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20251225094759-09c64ba2541c github.com/gin-contrib/pprof v1.5.3 + github.com/gin-contrib/timeout v1.1.0 github.com/gin-gonic/gin v1.11.0 github.com/go-playground/validator/v10 v10.30.1 github.com/jedib0t/go-pretty/v6 v6.7.8 @@ -34,11 +36,12 @@ require ( github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.14.2 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/gin-contrib/timeout v1.1.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect github.com/go-openapi/jsonreference v0.21.2 // indirect @@ -52,6 +55,7 @@ require ( github.com/go-openapi/swag/yamlutils v0.25.1 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-redis/redis_rate/v10 v10.0.1 // indirect github.com/go-webtools/knife4go v1.0.4 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.1 // indirect @@ -72,6 +76,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.58.0 // indirect + github.com/redis/go-redis/v9 v9.17.2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sbabiv/xml2map v1.2.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect @@ -92,6 +97,7 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.40.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 // indirect diff --git a/go.sum b/go.sum index efe2985..c0d7972 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,7 @@ git.zhangdeman.cn/zhangdeman/api-doc v1.0.3-0.20251013152001-868ee8955623 h1:QiqETEQx2PBv2fF3UwPS11dsbVDcF3WNCmRRA8zrqAk= git.zhangdeman.cn/zhangdeman/api-doc v1.0.3-0.20251013152001-868ee8955623/go.mod h1:ryyMI2gPgotFD1lZC50p2vRyX8bf3MRwO9tos41z4WU= -git.zhangdeman.cn/zhangdeman/consts v0.0.0-20251229032747-c3b314cb72c6 h1:Yf5PPiRqR2/Prj2Z6ZrpEE4W8W+PHYT9nucV/QyqEgs= -git.zhangdeman.cn/zhangdeman/consts v0.0.0-20251229032747-c3b314cb72c6/go.mod h1:5p8CEKGBxi7qPtTXDI3HDmqKAfIm5i/aBWdrbkbdNjc= git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260102134357-189dc7f57c1a h1:ppfx1Yedkz8mNMi3kydXvmu+gfL2aYApp0YfWWC4rAk= git.zhangdeman.cn/zhangdeman/consts v0.0.0-20260102134357-189dc7f57c1a/go.mod h1:5p8CEKGBxi7qPtTXDI3HDmqKAfIm5i/aBWdrbkbdNjc= -git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20251013092857-dcf591d4e8a8 h1:Pw981jG3hH9ZHrB3s1xPpsZafgX3USuMAjnGi2GEP9Y= -git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20251013092857-dcf591d4e8a8/go.mod h1:xtCw3om5DRrG30EfQd/lfQPXgptfK7l9oBSt4Kdhjok= git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20251207071238-9aa24be3d708 h1:yVr38JAgPwS/6JeYdHLDa8PXU3fTa2dnENL1JGdu3ns= git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20251207071238-9aa24be3d708/go.mod h1:iwegDT/6mX2INyxMAYxsL9pZdpEixiu1GdpHIG2It1k= git.zhangdeman.cn/zhangdeman/easylock v0.0.0-20230731062340-983985c12eda h1:bMD6r9gjRy7cO+T4zRQVYAesgIblBdTnhzT1vN5wjvI= @@ -16,48 +12,51 @@ git.zhangdeman.cn/zhangdeman/exception v0.0.0-20250510123912-a0d52fc093ab h1:O0X git.zhangdeman.cn/zhangdeman/exception v0.0.0-20250510123912-a0d52fc093ab/go.mod h1:Voc8J4ordx7nuMWpgACXXZULQy7ZIuBzcEIoS8VnDIw= git.zhangdeman.cn/zhangdeman/graceful v0.0.0-20250529070945-92833db6f3a4 h1:d1B3IXRitiMlY5ssEWRa//RQD24SqRPk/bCpL4/WyOA= git.zhangdeman.cn/zhangdeman/graceful v0.0.0-20250529070945-92833db6f3a4/go.mod h1:faaKb5d5tz3NmQQ+NrTnBDa1G/tN9/CVuKun1V4WBIE= -git.zhangdeman.cn/zhangdeman/logger v0.0.0-20251031042950-416e962cbf3b h1:+ca511XlP+HoBa2zB/ERSIE6yTiqiXpsgqvs64kRAZA= -git.zhangdeman.cn/zhangdeman/logger v0.0.0-20251031042950-416e962cbf3b/go.mod h1:tlQR2nfdP291Lug+b3BSu0q+kTPoSVNRL5dPq2nv9zQ= git.zhangdeman.cn/zhangdeman/logger v0.0.0-20251231045117-f05cb1e30afe h1:/AeCVO9xqsERJbhsSA6Uw+F5Dc+shR6MQbG30i4OS7c= git.zhangdeman.cn/zhangdeman/logger v0.0.0-20251231045117-f05cb1e30afe/go.mod h1:mzy5JiGVcx8KnT8hUaXjngboHQm1k/b5JymO01ujZSI= -git.zhangdeman.cn/zhangdeman/network v0.0.0-20251013095944-5b89fff39bde h1:+3zIOditaUwzSpl2ybM1PYN4OYTIKiemMBt+pNv3Yko= -git.zhangdeman.cn/zhangdeman/network v0.0.0-20251013095944-5b89fff39bde/go.mod h1:Ewh0UYOqXxEh0khgHj9bDz1rbnd7cCCsJrcOTFX/8wg= git.zhangdeman.cn/zhangdeman/network v0.0.0-20251220063845-8403861d36e5 h1:Dx5xvkIx3GHF6gIPhrJnANFYNWG514/9+PHhdGowNfc= git.zhangdeman.cn/zhangdeman/network v0.0.0-20251220063845-8403861d36e5/go.mod h1:HG6674mvHXXADavdVRHebkcBOz6Zq6kCGKnTN0ukAXE= git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20251013024601-da007da2fb42 h1:VjYrb4adud7FHeiYS9XA0B/tOaJjfRejzQAlwimrrDc= git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20251013024601-da007da2fb42/go.mod h1:VHb9qmhaPDAQDcS6vUiDCamYjZ4R5lD1XtVsh55KsMI= +git.zhangdeman.cn/zhangdeman/rate_limit v0.0.0-20260104044956-ab445203aadd h1:4AL9ZDIsnORrBXF50IsJ0yIX0VQFaC0blVvFd7faaUI= +git.zhangdeman.cn/zhangdeman/rate_limit v0.0.0-20260104044956-ab445203aadd/go.mod h1:MZN/QnNf1zBwT5oB+dvuTPIkrPCWGC/843kmbcizZqM= +git.zhangdeman.cn/zhangdeman/redis v0.0.0-20240620104455-27e193a0ea47 h1:Taq1XyvDgFM/xTSEw3cUOWh4krgqfEDIzcVrqQ7Z3UE= +git.zhangdeman.cn/zhangdeman/redis v0.0.0-20240620104455-27e193a0ea47/go.mod h1:OImVgoNTw9qWxGrgrym0wtEMdx0ekaH7vJJKIgSpdEg= git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20251013044511-86c1a4a3a9dd h1:kTZOpR8iHx27sUufMWVYhDZx9Q4h80j7RWlaR8GIBiU= git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20251013044511-86c1a4a3a9dd/go.mod h1:pLrQ63JICi81/3w2BrD26QZiu+IpddvEVfMJ6No3Xb4= git.zhangdeman.cn/zhangdeman/trace v0.0.0-20251013092356-b7b9fb5f8a76 h1:Y1GME5dg3jFmoURKvuujK07VUNMpsuRR4FkhinfBQIs= git.zhangdeman.cn/zhangdeman/trace v0.0.0-20251013092356-b7b9fb5f8a76/go.mod h1:avN/muzbNjUM2qamRZeY4fuLEmA9cmfslk1dkdtRQBE= -git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e h1:Q973S6CcWr1ICZhFI1STFOJ+KUImCl2BaIXm6YppBqI= -git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e/go.mod h1:VpPjBlwz8U+OxZuxzHQBv1aEEZ3pStH6bZvT21ADEbI= git.zhangdeman.cn/zhangdeman/util v0.0.0-20260103142706-1f6145405ec4 h1:x/xEkukbXS8WNN0N2A79C/q1Gh+fm3XrAdr1d/nEcXI= git.zhangdeman.cn/zhangdeman/util v0.0.0-20260103142706-1f6145405ec4/go.mod h1:OKI+RVVfRTUf/ox9uDMydtos2YDFr+/UuU4FFZKgPYY= git.zhangdeman.cn/zhangdeman/websocket v0.0.0-20251013144324-313024336a6f h1:InxNoLPHBLwCLblW0lsL2P1ZBYoUEzw9Yh+KNLt4Xmg= git.zhangdeman.cn/zhangdeman/websocket v0.0.0-20251013144324-313024336a6f/go.mod h1:Pbs7tusW6RNcqrNCVcLE2zrM8JfPaO7lBJQuRiAzzLs= -git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20251014035305-c0ec06fa6dff h1:ym1Qs4diJe27CK/0K6vy7RvgH90mXgslWA++L8mXaKE= -git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20251014035305-c0ec06fa6dff/go.mod h1:mBvTwcdqHRF3QIkAh92j/JRhru2LzyJ2LBqolxjzzKE= git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20251225094759-09c64ba2541c h1:lTShwLIIBojEInQlQYmKvpoio5Srasn05bPyBIEXBsY= git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20251225094759-09c64ba2541c/go.mod h1:oSmLgHs4EausBSmH4GdpGUtjubPek8hLXTAUc4cPCb0= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= @@ -103,6 +102,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-redis/redis_rate/v10 v10.0.1 h1:calPxi7tVlxojKunJwQ72kwfozdy25RjA0bCj1h0MUo= +github.com/go-redis/redis_rate/v10 v10.0.1/go.mod h1:EMiuO9+cjRkR7UvdvwMO7vbgqJkltQHtwbdIQvaBKIU= github.com/go-webtools/knife4go v1.0.4 h1:p32SApmM0sx2/Y5p0QfeaGv5KD96R1mj2CaHdyH8jy8= github.com/go-webtools/knife4go v1.0.4/go.mod h1:trOlXN1tqBJ7R44sHON3exGvzCwjbsVriIHEenry3d8= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= @@ -170,6 +171,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug= github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= +github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -219,8 +222,6 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -262,6 +263,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/logger/instance.go b/logger/instance.go index aa37af4..7de869e 100644 --- a/logger/instance.go +++ b/logger/instance.go @@ -47,11 +47,13 @@ const ( ) const ( - CodeInjectCommonParam = "inject-common-param" - CodeLogicHook = "logic-hook" - CodeParamValidateFailure = "param-validate-failure" - CodeLogicErrorWrapper = "logic-error-wrapper" - CodeLogicSseInitError = "sse-init-error" - CodeLogicSseClosedError = "sse-closed-error" - CodeServicePanic = "service-panic" // 服务发生 panic + CodeInjectCommonParam = "inject-common-param" + CodeLogicHook = "logic-hook" + CodeParamValidateFailure = "param-validate-failure" + CodeLogicErrorWrapper = "logic-error-wrapper" + CodeLogicSseInitError = "sse-init-error" + CodeLogicSseClosedError = "sse-closed-error" + CodeServicePanic = "service-panic" // 服务发生 panic + CodeServiceRateLimitFailure = "service-rate-limit-failure" // 服务流控出现异常 + CodeServiceRateLimitDisable = "service-rate-limit-disable" // 服务流控校验未通过 ) diff --git a/middleware/rate_limit.go b/middleware/rate_limit.go new file mode 100644 index 0000000..78258cc --- /dev/null +++ b/middleware/rate_limit.go @@ -0,0 +1,72 @@ +// Package middleware ... +// +// Description : middleware ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2026-01-04 14:17 +package middleware + +import ( + "fmt" + "net/http" + + "git.zhangdeman.cn/zhangdeman/gin/define" + "git.zhangdeman.cn/zhangdeman/gin/logger" + "git.zhangdeman.cn/zhangdeman/gin/util" + loggerPkg "git.zhangdeman.cn/zhangdeman/logger" + "git.zhangdeman.cn/zhangdeman/rate_limit/abstract" + rateLimitDefine "git.zhangdeman.cn/zhangdeman/rate_limit/define" + "github.com/gin-gonic/gin" +) + +// RateLimit 接口流控 +func RateLimit(rl abstract.IRateLimit, uriConfigTable map[string]define.UriConfig) gin.HandlerFunc { + return func(ctx *gin.Context) { + if nil == rl { + ctx.Next() + return + } + requestUri := ctx.Request.URL.Path + if _, exist := uriConfigTable[requestUri]; !exist { + ctx.Next() + return + } + if uriConfigTable[requestUri].RateLimitConfig.Total <= 0 || uriConfigTable[requestUri].RateLimitConfig.TimeInterval <= 0 { + // 配置异常, 不做处理 + ctx.Next() + return + } + var ( + allow bool + err error + ) + + if allow, err = rl.AllowN(ctx, &rateLimitDefine.LimitConfig{ + Key: fmt.Sprintf("__gin_pkg_%v", requestUri), // TODO: 支持配置key的格式 + Total: uriConfigTable[requestUri].RateLimitConfig.Total, + Rate: uriConfigTable[requestUri].RateLimitConfig.Use, + TimeInterval: uriConfigTable[requestUri].RateLimitConfig.TimeInterval, + }, uriConfigTable[requestUri].RateLimitConfig.Use); nil != err { + // 留空处理出现异常, 默认放行, 避免放大问题, 但是记录 Error 日志 + logger.Instance.Error("无法正确执行流量控制程序", loggerPkg.NewLogData(util.GinCtxToContext(ctx), logger.RecordType, logger.CodeServiceRateLimitFailure, map[string]any{ + "err_msg": err.Error(), + }).ToFieldList()...) + ctx.Next() + return + } + if !allow { + logger.Instance.Error("流量负载已饱和, 请稍后再试", loggerPkg.NewLogData(util.GinCtxToContext(ctx), logger.RecordType, logger.CodeServiceRateLimitDisable, map[string]any{ + "rate_limit_config": uriConfigTable[requestUri].RateLimitConfig, + }).ToFieldList()...) + // 429 : Too Many Requests + ctx.JSON(http.StatusTooManyRequests, gin.H{ + "err_msg": "流量负载已饱和, 请稍后再试", + }) + ctx.Abort() + return + } + // 流控校验通过 + ctx.Next() + } +} diff --git a/router/controller.go b/router/controller.go index 3c058a2..bd093f0 100644 --- a/router/controller.go +++ b/router/controller.go @@ -11,6 +11,7 @@ import ( "reflect" "strings" + "git.zhangdeman.cn/zhangdeman/gin/define" "git.zhangdeman.cn/zhangdeman/gin/logger" "git.zhangdeman.cn/zhangdeman/util" ) @@ -26,8 +27,8 @@ type controllerParser struct { // 1. 函数接受两个入参, 第一个参数为 gin.Context , 第二个参数为 任意结构体指针, 但是必须声明 Meta 相关信息, 否则会报错 // // 2. 函数有两个返回值, 第一个返回值为任意结构体/结构体指针(限制死不能为map/slice, 方便后续统一标准化) , 第二个返回值为 error , 代表处理的异常, 会自动适配 exception.IException 类型 -func (c controllerParser) Parse(inputController any) map[string]UriConfig { - parseRes := make(map[string]UriConfig) +func (c controllerParser) Parse(inputController any) map[string]define.UriConfig { + parseRes := make(map[string]define.UriConfig) if nil == inputController { return parseRes } @@ -47,7 +48,7 @@ func (c controllerParser) Parse(inputController any) map[string]UriConfig { } // preCheckMethod 预检查方法是否可以注册为接口 -func (c controllerParser) preCheckMethod(reflectMethod reflect.Method, config *UriConfig) (bool, reflect.Type, reflect.StructField) { +func (c controllerParser) preCheckMethod(reflectMethod reflect.Method, config *define.UriConfig) (bool, reflect.Type, reflect.StructField) { var ( metaField reflect.StructField metaFieldExist bool @@ -84,11 +85,11 @@ func (c controllerParser) preCheckMethod(reflectMethod reflect.Method, config *U // 参数 : 方法反射结果 // // 返回值 : 第一个 -> 解析出的接口配置 第二个 -> 是否要注册为接口 -func (c controllerParser) methodConfig(reflectMethod reflect.Method) (UriConfig, bool) { +func (c controllerParser) methodConfig(reflectMethod reflect.Method) (define.UriConfig, bool) { var ( needRegister bool metaField reflect.StructField - cfg UriConfig + cfg define.UriConfig ) methodType := reflectMethod.Type @@ -97,7 +98,7 @@ func (c controllerParser) methodConfig(reflectMethod reflect.Method) (UriConfig, // num2: 第二个参数 if needRegister, cfg.FormDataType, metaField = c.preCheckMethod(reflectMethod, &cfg); !needRegister { logger.Instance.Info("接口方法不符合要求, 不注册为接口, 方法名: " + reflectMethod.Name + ", 方法签名: " + methodType.String()) - return UriConfig{}, false + return cfg, false } cfg.ResultDataType = methodType.Out(0) @@ -135,7 +136,7 @@ func (c controllerParser) methodConfig(reflectMethod reflect.Method) (UriConfig, } // setUriMeta 设置接口的 meta 信息 -func (c controllerParser) setUriMeta(metaField reflect.StructField, cfg *UriConfig) { +func (c controllerParser) setUriMeta(metaField reflect.StructField, cfg *define.UriConfig) { // 解析 meta 信息 cfg.Path = metaField.Tag.Get(TagNamePath) // 接口路由 cfg.RequestMethod = strings.Split(strings.ToUpper(metaField.Tag.Get(TagNameMethod)), ",") // 请求方法 @@ -159,4 +160,25 @@ func (c controllerParser) setUriMeta(metaField reflect.StructField, cfg *UriConf panic(cfg.Path + " : 最大执行时间配置错误(配置的值必须是无符号整型), 请检查配置 : " + err.Error()) } } + + // 流控配置, 格式: {单次消耗次数}/{总次数}/{时间间隔} + rateLimitStr := strings.TrimSpace(metaField.Tag.Get(TagNameRateLimit)) + if rateLimitStr != "" { + cfgArr := strings.Split(rateLimitStr, "/") + if len(cfgArr) != 3 { + panic(cfg.Path + " : 流控配置错误(配置的值必须为3个用 / 分隔的数字, {单次消耗次数}/{总次数}/{时间间隔}), 请检查配置 : " + rateLimitStr) + } + if err := util.ConvertAssign(&cfg.RateLimitConfig.Use, cfgArr[0]); nil != err { + panic(cfg.Path + " : 流控单次消耗次数不是一个合法数字 : " + err.Error()) + } + if err := util.ConvertAssign(&cfg.RateLimitConfig.Total, cfgArr[1]); nil != err { + panic(cfg.Path + " : 流控总次数不是一个合法数字 : " + err.Error()) + } + if cfg.RateLimitConfig.Total < cfg.RateLimitConfig.Use { + panic(cfg.Path + " : 流控总次数不能小于单次消耗次数 : " + rateLimitStr) + } + if err := util.ConvertAssign(&cfg.RateLimitConfig.TimeInterval, cfgArr[2]); nil != err { + panic(cfg.Path + " : 流控时间间隔不是一个合法数字 : " + err.Error()) + } + } } diff --git a/router/define.go b/router/define.go index 5881a93..1a4c3b6 100644 --- a/router/define.go +++ b/router/define.go @@ -7,10 +7,6 @@ // Date : 2024-07-20 22:57 package router -import ( - "reflect" -) - const ( PrefixFuncName = "RouterPrefix" // 路由前缀函数名称 MiddlewareFuncName = "RouterMiddleware" // 路由中间件函数名称 @@ -30,30 +26,12 @@ const ( TagNameHookSync = "hook-sync" // hook 同步执行 TagNameNoLogin = "no-login" // 接口是否需要登录(无需登录, 则有token就验证, 无token不验证) TagNameMaxExecTime = "max-exec-time" // 接口最大执行时间, 单位: s, 配置为0则不验证 + TagNameRateLimit = "rate-limit" // 接口限流 TagNameBinding = "binding" // gin 内置的验证规则tag TagNameValidate = "validate" // validator v10 默认的验证规则tag TagNameErrMsg = "err" // 验证失败错误信息 tag ) -// UriConfig 接口配置 -type UriConfig struct { - Path string `json:"path"` // 接口路由, 必须配置 - RequestMethod []string `json:"request_method"` // 接口请求方法, 必须配置 - TagList []string `json:"tag_list"` // 接口分组 - Desc string `json:"desc"` // 接口描述 - IsSse bool `json:"is_sse"` // 是否 SSE 连接 - IsWebsocket bool `json:"is_ws"` // 是否 websocket 连接 - OutputStrict bool `json:"output_strict"` // 接口是否为严格模式 : 不配置,可返回任意类型, 配置, 必须返回结构体或者map - MaxExecTime int64 `json:"max_exec_time"` // 接口最大执行时间, 单位: ms, 配置为0则不验证, 注意, 超时后不会报错, 会打印一条warn日志, 如果配置了报警策略, 也会发送报警信息 - HookSync bool `json:"hook_sync"` // 接口主逻辑执行完成之后,hook是否同步执行, 默认异步执行 - NoLogin bool `json:"no_login"` // 接口是否需要登录(无需登录, 则有token就验证, 无token不验证) - ParamIsPtr bool `json:"param_is_ptr"` // 参数是否指针类型 - FormDataType reflect.Type `json:"-"` // 表单数据类型 - ResultDataType reflect.Type `json:"-"` // 返回值数据类型 - ApiStructValue reflect.Value `json:"-"` // 逻辑函数所属结构体取值 - ApiLogicFunc reflect.Method `json:"-"` // 自定义的接口逻辑 -} - // UriParam 接口参数配置 type UriParam struct { Field string `json:"field"` // 结构体字段 diff --git a/router/handler.go b/router/handler.go index 6aeec13..dd15128 100644 --- a/router/handler.go +++ b/router/handler.go @@ -29,7 +29,7 @@ import ( "github.com/mcuadros/go-defaults" ) -func (s *server) getFormInitValue(ctx *gin.Context, uriCfg UriConfig) (any, error) { +func (s *server) getFormInitValue(ctx *gin.Context, uriCfg define.UriConfig) (any, error) { var ( formParam reflect.Value formValue any @@ -58,7 +58,7 @@ func (s *server) getFormInitValue(ctx *gin.Context, uriCfg UriConfig) (any, erro return formValue, nil } -func (s *server) initRequest(ctx *gin.Context, uriCfg UriConfig) (any, reflect.Value, exception.IException) { +func (s *server) initRequest(ctx *gin.Context, uriCfg define.UriConfig) (any, reflect.Value, exception.IException) { var ( err error formValue any @@ -92,7 +92,7 @@ func (s *server) initRequest(ctx *gin.Context, uriCfg UriConfig) (any, reflect.V } // callFunc 调用方法 -func (s *server) callFunc(ctx *gin.Context, uriCfg UriConfig, inputValue reflect.Value) (any, exception.IException) { +func (s *server) callFunc(ctx *gin.Context, uriCfg define.UriConfig, inputValue reflect.Value) (any, exception.IException) { var ( firstParam = reflect.ValueOf(ctx) @@ -135,7 +135,7 @@ func (s *server) formatError(ctx *gin.Context, err any) exception.IException { } // RequestHandler 获取请求处理方法 -func (s *server) RequestHandler(uriCfg UriConfig) gin.HandlerFunc { +func (s *server) RequestHandler(uriCfg define.UriConfig) gin.HandlerFunc { return func(ctx *gin.Context) { var ( e exception.IException @@ -164,7 +164,7 @@ func (s *server) RequestHandler(uriCfg UriConfig) gin.HandlerFunc { } // SseHandler sse连接请求 -func (s *server) SseHandler(uriCfg UriConfig) gin.HandlerFunc { +func (s *server) SseHandler(uriCfg define.UriConfig) gin.HandlerFunc { return func(ctx *gin.Context) { var ( err error diff --git a/router/hook.go b/router/hook.go index c1fe960..ae85e7d 100644 --- a/router/hook.go +++ b/router/hook.go @@ -17,7 +17,7 @@ import ( ) // hook 执行hook逻辑 -func (s *server) hook(ctx *gin.Context, uriCfg UriConfig) { +func (s *server) hook(ctx *gin.Context, uriCfg define.UriConfig) { var ( exists bool isSuccess any @@ -47,7 +47,7 @@ func (s *server) hook(ctx *gin.Context, uriCfg UriConfig) { } } -func (s *server) hookAfter(ctx *gin.Context, uriCfg UriConfig, hookInstance *define.LogicAfterResponse, success bool) { +func (s *server) hookAfter(ctx *gin.Context, uriCfg define.UriConfig, hookInstance *define.LogicAfterResponse, success bool) { innerContext := util.GinCtxToContext(ctx) defer func() { if err := recover(); err != nil { diff --git a/router/option.go b/router/option.go index 88b862d..d95697d 100644 --- a/router/option.go +++ b/router/option.go @@ -12,6 +12,7 @@ import ( apiDocDefine "git.zhangdeman.cn/zhangdeman/api-doc/define" "git.zhangdeman.cn/zhangdeman/gin/middleware" + "git.zhangdeman.cn/zhangdeman/rate_limit/abstract" "github.com/gin-gonic/gin" ) @@ -30,6 +31,14 @@ type serverOption struct { disableInitRequest bool // 禁用初始化请求 loggerCfg *middleware.AccessConfig // 日志配置 initContextData gin.HandlerFunc // 初始化一些请求数据 + rateLimitInstance abstract.IRateLimit // 服务流控实例 +} + +// WithRateLimitInstance 设置流控实例, 配置为 nil, 代表禁用 +func WithRateLimitInstance(i abstract.IRateLimit) SetServerOptionFunc { + return func(so *serverOption) { + so.rateLimitInstance = i + } } // WithDisableInitRequest 禁用自从初始化请求 diff --git a/router/server.go b/router/server.go index 7ac12bc..9176d7f 100644 --- a/router/server.go +++ b/router/server.go @@ -16,6 +16,7 @@ import ( "strings" "sync" + "git.zhangdeman.cn/zhangdeman/gin/define" "git.zhangdeman.cn/zhangdeman/graceful" "github.com/jedib0t/go-pretty/v6/table" "github.com/jedib0t/go-pretty/v6/text" @@ -90,11 +91,13 @@ func NewServer(port int, optionList ...SetServerOptionFunc) *server { lock: &sync.RWMutex{}, commonParam: map[string]GetCommonParam{}, consoleOutput: [][]any{}, + globalMiddlewareList: []gin.HandlerFunc{}, globalMiddlewareDescList: []string{}, + uriTable: map[string]define.UriConfig{}, } - globalMiddlewareList := s.getGlobalMiddlewareList(option) + s.getGlobalMiddlewareList(option) // 注册全局中间件 - s.router.Use(globalMiddlewareList...) + s.router.Use(s.globalMiddlewareList...) // 启用pprof, 注册相关路由 if option.enablePprof { pprof.Register(s.router) @@ -109,27 +112,30 @@ type server struct { option *serverOption commonParam map[string]GetCommonParam // 结构体字段名, 注意, 不是TAG lock *sync.RWMutex - consoleOutput [][]any // 控制台输出的提示信息 - globalMiddlewareDescList []string // 全局中间件描述列表 + consoleOutput [][]any // 控制台输出的提示信息 + globalMiddlewareList []gin.HandlerFunc // 全局中间件列表 + globalMiddlewareDescList []string // 全局中间件描述列表 + uriTable map[string]define.UriConfig // uri配置表 } -func (s *server) getGlobalMiddlewareList(option *serverOption) []gin.HandlerFunc { - globalMiddlewareList := make([]gin.HandlerFunc, 0) +func (s *server) getGlobalMiddlewareList(option *serverOption) { // 全局 panic 捕获 - globalMiddlewareList = append(globalMiddlewareList, middleware.CustomRecover()) + s.globalMiddlewareList = append(s.globalMiddlewareList, middleware.CustomRecover()) + // 全局流控中间件 + s.globalMiddlewareList = append(s.globalMiddlewareList, middleware.RateLimit(s.option.rateLimitInstance, s.uriTable)) if nil != option.initContextData { - globalMiddlewareList = append(globalMiddlewareList, option.initContextData) // 初始化一些全局的变量 + s.globalMiddlewareList = append(s.globalMiddlewareList, option.initContextData) // 初始化一些全局的变量 } if !option.disableInitRequest { // 启用了初始化请求 - globalMiddlewareList = append(globalMiddlewareList, middleware.InitRequest) // 初始化请求 + s.globalMiddlewareList = append(s.globalMiddlewareList, middleware.InitRequest) // 初始化请求 } if nil != option.loggerCfg { // 请求日志记录中间件 - globalMiddlewareList = append(globalMiddlewareList, middleware.LogRequest(option.loggerCfg)) + s.globalMiddlewareList = append(s.globalMiddlewareList, middleware.LogRequest(option.loggerCfg)) } if option.enableCors { // 跨域中间件 - globalMiddlewareList = append(globalMiddlewareList, request_cors.New(request_cors.Config{ + s.globalMiddlewareList = append(s.globalMiddlewareList, request_cors.New(request_cors.Config{ AllowAllOrigins: true, AllowOrigins: nil, AllowOriginFunc: nil, @@ -146,10 +152,9 @@ func (s *server) getGlobalMiddlewareList(option *serverOption) []gin.HandlerFunc } if len(option.globalMiddlewareList) > 0 { // 自定义全局中间件追加 - globalMiddlewareList = append(globalMiddlewareList, option.globalMiddlewareList...) + s.globalMiddlewareList = append(s.globalMiddlewareList, option.globalMiddlewareList...) } - s.globalMiddlewareDescList = s.getMiddlewareDescList(globalMiddlewareList) - return globalMiddlewareList + s.globalMiddlewareDescList = s.getMiddlewareDescList(s.globalMiddlewareList) } // Start 启动服务 @@ -205,7 +210,7 @@ func (s *server) Group(routerPrefix string, middlewareList []gin.HandlerFunc, co } else { apiMiddlewareList = append(apiMiddlewareList, runtime.FuncForPC(reflect.ValueOf(s.RequestHandler).Pointer()).Name()) } - // 设置 超时 函数描述 + // 设置 超时 函数 apiMiddlewareList = append(apiMiddlewareList, runtime.FuncForPC(reflect.ValueOf(middleware.Timeout).Pointer()).Name()) // 设置 logic 函数描述 apiMiddlewareList = append(apiMiddlewareList, runtime.FuncForPC(itemUriCfg.ApiLogicFunc.Func.Pointer()).Name()) @@ -222,8 +227,10 @@ func (s *server) Group(routerPrefix string, middlewareList []gin.HandlerFunc, co if len(routerPrefix) > 0 { routerPrefix = "/" + strings.TrimSuffix(strings.TrimPrefix(routerPrefix, "/"), "/") } + fullUriPath := routerPrefix + "/" + strings.TrimPrefix(itemUriCfg.Path, "/") + s.uriTable[fullUriPath] = itemUriCfg // 注册路由时存储 接口路径 => 接口配置的信息 s.consoleOutput = append(s.consoleOutput, []any{ - routerPrefix + "/" + strings.TrimPrefix(itemUriCfg.Path, "/"), + fullUriPath, strings.ToUpper(method), strings.Join(apiMiddlewareList, "\n"), }) @@ -234,7 +241,7 @@ func (s *server) Group(routerPrefix string, middlewareList []gin.HandlerFunc, co } // registerRouter 注册路由 -func (s *server) registerRouter(routerGroup *gin.RouterGroup, method string, itemUriCfg UriConfig, handleFunc gin.HandlerFunc) { +func (s *server) registerRouter(routerGroup *gin.RouterGroup, method string, itemUriCfg define.UriConfig, handleFunc gin.HandlerFunc) { funcList := []gin.HandlerFunc{ middleware.Timeout(itemUriCfg.MaxExecTime), // 超时处理 handleFunc, diff --git a/router/server_test.go b/router/server_test.go index 7151270..a75e433 100644 --- a/router/server_test.go +++ b/router/server_test.go @@ -10,6 +10,7 @@ package router import ( "testing" + "git.zhangdeman.cn/zhangdeman/rate_limit" "github.com/gin-gonic/gin" ) @@ -18,13 +19,13 @@ type testCommon struct { } type testForm struct { - Meta `json:"-" method:"get" path:"test"` + Meta `json:"-" method:"get" path:"test" rate-limit:"1/5/60"` testCommon Name string `json:"name"` } func TestNewServer(t *testing.T) { - s := NewServer(9087) + s := NewServer(9087, WithRateLimitInstance(rate_limit.MemoryClient)) s.AddCommonParamRule("UserID", func(ctx *gin.Context) (any, error) { return uint(123456), nil })