Compare commits

...

38 Commits

Author SHA1 Message Date
0e6bd9e669 update parse content type 2025-05-29 23:19:12 +08:00
0b2546217c 兼容响应数据非200没有响应Body的情况 2025-05-29 23:16:17 +08:00
42b9f26a9a 适配支持从Header或者Body解析请求结果 2025-05-29 23:11:56 +08:00
5ddab034f3 修复流控逻辑判断相反的BUG 2025-05-29 22:54:44 +08:00
a56528f2ee 修复逻辑缺陷导致NPE问题 2025-05-29 22:36:35 +08:00
7b1a36a7c3 Merge pull request '升级RateLimiter接口约束' (#11) from feature/upgrade_rate_limiter into master
Reviewed-on: #11
2025-05-09 11:08:20 +08:00
21060e4913 升级RateLimiter接口约束 2025-05-09 10:58:39 +08:00
70d044e3a3 修复lock key生成的BUG 2025-05-09 09:51:10 +08:00
8ddb1f72b2 Merge pull request '支持自适应请求类型, 根据不同请求类型设置正确Body格式' (#10) from feature/support_any_request_content_type into master
Reviewed-on: #10
2025-05-08 15:13:04 +08:00
1403693fda 支持自适应请求类型, 根据不同请求类型设置正确Body格式 2025-05-08 15:12:37 +08:00
1abe6c1c29 Merge pull request 'resty由 v2 -> v3升级' (#9) from feature/upgrade_resty_v3 into master
Reviewed-on: #9
2025-05-08 11:39:03 +08:00
4767aeb504 限流器使用中间件完成 2025-05-08 11:37:34 +08:00
bfbcecb498 支持static参数的合并 2025-05-07 22:33:59 +08:00
2cb3d44ec9 清理一些无效代码 2025-05-07 22:19:44 +08:00
e02ee1fef9 修复path param处理的BUG 2025-05-07 21:59:25 +08:00
ba47891d3c resty_v2 -> resty_v3 2025-05-07 21:42:48 +08:00
6dcd072570 Merge pull request '响应自适应解析 + 逻辑优化' (#8) from feature/auto_support_response_type into master
Reviewed-on: #8
2025-05-07 17:39:50 +08:00
f558f78e1c 缓存预热升级lockKey升级方式 2025-05-07 17:39:30 +08:00
1fe39e0547 支持响应数据类型自适应解析 2025-05-07 17:31:30 +08:00
38f97bc69f 修复命中限流重试的BUG 2025-05-07 16:37:58 +08:00
b335d7c2bb IResponse约束增加NeedRetry是否需要重试方法 2025-05-07 16:32:20 +08:00
741c9c8ea0 响应解析基于接口实现 2025-05-07 15:48:36 +08:00
088670c7d4 升级代码组织 2025-05-07 14:25:44 +08:00
94fdab4e36 缓存预热加锁 2025-05-07 12:09:17 +08:00
f1a65c1ed8 缓存实例挂载到reqConfig上, 无需单独传入 2025-05-07 11:17:15 +08:00
1ca3c2d094 update go mod 2025-05-06 19:03:19 +08:00
370d98f2a8 Merge pull request '升级validate pkg' (#7) from feature/upgrade_validate into master
Reviewed-on: #7
2025-04-28 12:46:39 +08:00
db679f05e9 升级validate pkg 2025-04-28 12:46:18 +08:00
19992ab41c Merge pull request '请求缓存优化' (#6) from feature/upgrade_res_cache into master
Reviewed-on: #6
2025-04-25 16:28:54 +08:00
9bd3030cb8 请求结果命中缓存, 记录响应日志 2025-04-25 16:25:13 +08:00
78f6b61d65 接口请求部分bug fix + 增加日志记录 2025-04-25 15:56:11 +08:00
5592e0a45c Merge pull request 'feature/upgrade_retry' (#5) from feature/upgrade_retry into master
Reviewed-on: #5
2025-04-17 18:43:42 +08:00
f780551ba6 修改url path param 参数占位格式 {#param_name#} => {param_name} 2025-04-17 18:42:45 +08:00
84cf228b5d fix 2025-04-17 18:39:42 +08:00
e8e02d9848 支持配置哪些http code视作成功, 未指定, 默认 2xx 视为成功 2025-04-17 18:38:22 +08:00
ecf2bd6b55 重试规则支持配置指定 http code / business code 重试 2025-04-17 18:31:04 +08:00
04927ae811 Merge pull request '优化mesh请求' (#4) from feature/upgrade_mesh_request into master
Reviewed-on: #4
2025-04-16 22:04:47 +08:00
1a55576e10 优化mesh请求 2025-04-16 22:02:53 +08:00
22 changed files with 961 additions and 408 deletions

23
go.mod
View File

@ -3,26 +3,26 @@ module git.zhangdeman.cn/zhangdeman/network
go 1.24.1 go 1.24.1
require ( require (
git.zhangdeman.cn/gateway/validate v0.0.0-20250331140536-3477e2ec106d git.zhangdeman.cn/gateway/validate v0.0.0-20250506091017-1d2dfd10f600
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250328040304-7e4a6f9f148c git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250425024726-cc17224cb995
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250504055908-8d68e6106ea9
git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250321102712-1cbfbe959740 git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250321102712-1cbfbe959740
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
github.com/go-resty/resty/v2 v2.16.5
github.com/mssola/user_agent v0.6.0 github.com/mssola/user_agent v0.6.0
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
resty.dev/v3 v3.0.0-beta.2
) )
require ( require (
git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250401073534-5d4788e0fd86 // indirect git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250429065800-fc340b9417cf // indirect
git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250329080213-587a9bbe9dbc // indirect git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250506090824-b0db389ca3d7 // indirect
git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0 // indirect git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0 // indirect
git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e // indirect git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e // indirect
github.com/BurntSushi/toml v1.5.0 // indirect github.com/BurntSushi/toml v1.5.0 // indirect
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect
github.com/creasty/defaults v1.8.0 // indirect github.com/creasty/defaults v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.9 // indirect
github.com/go-ini/ini v1.67.0 // indirect github.com/go-ini/ini v1.67.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
@ -31,16 +31,17 @@ require (
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mozillazg/go-pinyin v0.20.0 // indirect github.com/mozillazg/go-pinyin v0.20.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/sbabiv/xml2map v1.2.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect github.com/stretchr/testify v1.10.0 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.37.0 // indirect golang.org/x/crypto v0.38.0 // indirect
golang.org/x/net v0.39.0 // indirect golang.org/x/net v0.40.0 // indirect
golang.org/x/sys v0.32.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/text v0.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

50
go.sum
View File

@ -1,15 +1,15 @@
git.zhangdeman.cn/gateway/validate v0.0.0-20250331140536-3477e2ec106d h1:rEnbN/j9/xqDAqdjAwIxE1UxoQoZVBl5Qdj+R/zEdCQ= git.zhangdeman.cn/gateway/validate v0.0.0-20250506091017-1d2dfd10f600 h1:o+ggli1YCXiiB/SwlPqb34DePkP6mh+KqhQ/lYhBISE=
git.zhangdeman.cn/gateway/validate v0.0.0-20250331140536-3477e2ec106d/go.mod h1:Bwb9ks9pUp0Em+7CDTCUfnI+o6jRxMLifSHICsWmWXU= git.zhangdeman.cn/gateway/validate v0.0.0-20250506091017-1d2dfd10f600/go.mod h1:DHrM753rWJ7Dvrm7CqWq1Q05jMhiGPNMKXYcKp11BHg=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250328040304-7e4a6f9f148c h1:cl3gQGXQpJ8ugDs0C/hQLfcvF4lGBm5BeABLvROFDoM= git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250425024726-cc17224cb995 h1:LmPRAf0AsxRVFPibdpZR89ajlsz8hof2IvMMyTqiEq4=
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250328040304-7e4a6f9f148c/go.mod h1:IXXaZkb7vGzGnGM5RRWrASAuwrVSNxuoe0DmeXx5g6k= git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250425024726-cc17224cb995/go.mod h1:5p8CEKGBxi7qPtTXDI3HDmqKAfIm5i/aBWdrbkbdNjc=
git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250401073534-5d4788e0fd86 h1:RvEOgfkutynvRjIbSWoB7tHCjJ8wl62O3rqUb9OuUd4= git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250429065800-fc340b9417cf h1:xCPM3U6i62UvLo9VNvDP45Ue3dPl7ratHu1rSEJRE2k=
git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250401073534-5d4788e0fd86/go.mod h1:XR0ElBVxzFGVh3aE9KOSEbjZLO/VRw2vxxFJVKWCuNk= git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250429065800-fc340b9417cf/go.mod h1:onY+qrB+Uwfuv75JlgHlGdkirAfYcINrvCashtVoBX0=
git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250329080213-587a9bbe9dbc h1:jqaoktpx+gLiBLGpW/X03ZZ+oIsrQIlvimLSaHQo0iA= git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250506090824-b0db389ca3d7 h1:fBf+lN63axb8zYwqCVbexzPm8x9W4aWxzzIJIXIxS2U=
git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250329080213-587a9bbe9dbc/go.mod h1:KuVC2+wQ4dXMgqy4RF+wrsDxa+FWYuc7x17Y+/2b1YE= git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250506090824-b0db389ca3d7/go.mod h1:KuVC2+wQ4dXMgqy4RF+wrsDxa+FWYuc7x17Y+/2b1YE=
git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0 h1:gUDlQMuJ4xNfP2Abl1Msmpa3fASLWYkNlqDFF/6GN0Y= git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0 h1:gUDlQMuJ4xNfP2Abl1Msmpa3fASLWYkNlqDFF/6GN0Y=
git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0/go.mod h1:VHb9qmhaPDAQDcS6vUiDCamYjZ4R5lD1XtVsh55KsMI= git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0/go.mod h1:VHb9qmhaPDAQDcS6vUiDCamYjZ4R5lD1XtVsh55KsMI=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd h1:q7GG14qgXKB4MEXQFOe7/UYebsqMfPaSX80TcPdOosI= git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250504055908-8d68e6106ea9 h1:/GLQaFoLb+ciHOtAS2BIyPNnf4O5ME3AC5PUaJY9kfs=
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd/go.mod h1:+D6uPSljwHywjVY5WSBY4TRVMj26TN5f5cFGEYMldjs= git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250504055908-8d68e6106ea9/go.mod h1:ABJ655C5QenQNOzf7LjCe4sSB52CXvaWLX2Zg4uwDJY=
git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e h1:Q973S6CcWr1ICZhFI1STFOJ+KUImCl2BaIXm6YppBqI= 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-20240618042405-6ee2c904644e/go.mod h1:VpPjBlwz8U+OxZuxzHQBv1aEEZ3pStH6bZvT21ADEbI=
git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250321102712-1cbfbe959740 h1:zPUoylfJTbc0EcxW+NEzOTBmoeFZ2I/rLFBnEzxb4Wk= git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250321102712-1cbfbe959740 h1:zPUoylfJTbc0EcxW+NEzOTBmoeFZ2I/rLFBnEzxb4Wk=
@ -24,8 +24,8 @@ 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU= 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/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -36,8 +36,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/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
@ -58,6 +58,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sbabiv/xml2map v1.2.1 h1:1lT7t0hhUvXZCkdxqtq4n8/ZCnwLWGq4rDuDv5XOoFE=
github.com/sbabiv/xml2map v1.2.1/go.mod h1:2TPoAfcaM7+Sd4iriPvzyntb2mx7GY+kkQpB/GQa/eo=
github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY=
github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
@ -82,16 +84,14 @@ 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/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 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -99,3 +99,5 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8X
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
resty.dev/v3 v3.0.0-beta.2 h1:xu4mGAdbCLuc3kbk7eddWfWm4JfhwDtdapwss5nCjnQ=
resty.dev/v3 v3.0.0-beta.2/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=

View File

@ -1,68 +1,36 @@
// Package cache ... // Package abstract ...
// //
// Description : cache ... // Description : cache ...
// //
// Author : go_developer@163.com<白茶清欢> // Author : go_developer@163.com<白茶清欢>
// //
// Date : 2024-06-03 15:49 // Date : 2024-06-03 15:49
package cache package abstract
import ( import (
"git.zhangdeman.cn/zhangdeman/network/httpclient/define" "git.zhangdeman.cn/zhangdeman/network/httpclient/define"
) )
// ICache 缓存定义 // ICache 缓存定义
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:49 2024/6/3
type ICache interface { type ICache interface {
// Enable 是否启用缓存(总开关) // Enable 是否启用缓存(总开关)
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:50 2024/6/3
Enable() bool Enable() bool
// CacheTime 缓存时长, 单位 : s , 默认 1800, 最小值 90, 设置失效时间, 会上下波动60s, 避免缓存集中失效 // CacheTime 缓存时长, 单位 : s , 默认 1800, 最小值 90, 设置失效时间, 会上下波动60s, 避免缓存集中失效
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:50 2024/6/3
CacheTime() int64 CacheTime() int64
// IsAllow 针对当前请求数据和状态, 是否允许缓存 // IsAllow 针对当前请求数据和状态, 是否允许缓存
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:52 2024/6/3
IsAllow(reqCfg *define.Request, response *define.Response) bool IsAllow(reqCfg *define.Request, response *define.Response) bool
// GetKey 获取缓存key // GetKey 获取缓存key
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:51 2024/6/3
GetKey(reqCfg *define.Request) string GetKey(reqCfg *define.Request) string
// GetValue 获取缓存值 // GetValue 获取缓存值
//
// Author : zhangdeman001@ke.com<张德满>
//
// Date : 16:01 2024/6/3
GetValue(cacheKey string) string GetValue(cacheKey string) string
// SetValue 设置缓存 // SetValue 设置缓存
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 16:46 2024/6/3
SetValue(cacheKey string, cacheValue string) error SetValue(cacheKey string, cacheValue string) error
// TTL 缓存剩余生命周期(单位: s) // TTL 缓存剩余生命周期(单位: s)
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:39 2024/10/9
TTL(cacheKey string) int64 TTL(cacheKey string) int64
// PreHeatConfig 缓存预热配置 // PreHeatConfig 缓存预热配置
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:42 2024/10/9
PreHeatConfig() *define.CachePreHeatConfig PreHeatConfig() *define.CachePreHeatConfig
// Lock 设置缓存时加锁
Lock(lockKey string) error
// Unlock 完成缓存设置时, 释放锁
Unlock(lockKey string) error
} }

View File

@ -0,0 +1,15 @@
// Package abstract ...
//
// Description : abstract ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-07 21:13
package abstract
import "git.zhangdeman.cn/zhangdeman/network/httpclient/define"
// RateLimiter v2 流控口约束, v3移除了, 人工补齐
type RateLimiter interface {
Allow(reqCfg *define.Request) bool
}

View File

@ -0,0 +1,16 @@
// Package abstract ...
//
// Description : abstract ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-08 14:18
package abstract
import "resty.dev/v3"
// IRequestBodyWrite 请求信息写入Body的接口约束
type IRequestBodyWrite interface {
// Write 写入请求Body
Write(request *resty.Request, bodyData map[string]any) error
}

View File

@ -0,0 +1,32 @@
// Package abstract ...
//
// Description : abstract ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-07 14:28
package abstract
import (
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
)
// IResponseParser 响应结果的序列化解析器
type IResponseParser interface {
// Unmarshal 反序列化解析
Unmarshal(byteData []byte, receiver any) error
// MarshalForByte 序列化
MarshalForByte(input any) ([]byte, error)
}
// IResponse 响应解析
type IResponse interface {
// Parse 解析响应
Parse(reqConfig *define.Request, response *define.Response) error
// BusinessSuccess 业务状态码是否成功
BusinessSuccess(reqCfg *define.Request, response *define.Response) bool
// HttpSuccess http状态码是否成功
HttpSuccess(reqCfg *define.Request, response *define.Response) bool
// NeedRetry 是否需要重试
NeedRetry(reqCfg *define.Request, response *define.Response) bool
}

View File

@ -9,19 +9,17 @@ package httpclient
import ( import (
"fmt" "fmt"
"git.zhangdeman.cn/zhangdeman/network/httpclient/implement"
"net" "net"
"net/http"
"strings" "strings"
"time" "time"
"git.zhangdeman.cn/zhangdeman/consts" "git.zhangdeman.cn/zhangdeman/consts"
"git.zhangdeman.cn/zhangdeman/network/httpclient/cache"
"git.zhangdeman.cn/zhangdeman/network/httpclient/define" "git.zhangdeman.cn/zhangdeman/network/httpclient/define"
"git.zhangdeman.cn/zhangdeman/network/httpclient/log" "git.zhangdeman.cn/zhangdeman/network/httpclient/log"
"git.zhangdeman.cn/zhangdeman/network/httpclient/validate" "git.zhangdeman.cn/zhangdeman/network/httpclient/validate"
"git.zhangdeman.cn/zhangdeman/serialize" "git.zhangdeman.cn/zhangdeman/serialize"
"github.com/go-resty/resty/v2" "resty.dev/v3"
"github.com/tidwall/gjson"
) )
// NewHttpClient 获取http client // NewHttpClient 获取http client
@ -29,7 +27,14 @@ import (
// Author : go_developer@163.com<白茶清欢> // Author : go_developer@163.com<白茶清欢>
// //
// Date : 15:27 2024/5/31 // Date : 15:27 2024/5/31
func NewHttpClient(reqConfig *define.Request, cacheInstance cache.ICache) (*HttpClient, error) { func NewHttpClient(reqConfig *define.Request, reqOption *RequestOption) (*HttpClient, error) {
if nil == reqOption {
reqOption = &RequestOption{}
}
if nil == reqOption.ResponseParser {
// 没有自定义响应解析实现, 使用内置实现
reqOption.ResponseParser = &implement.Response{}
}
if nil == reqConfig.Logger { if nil == reqConfig.Logger {
reqConfig.Logger = log.Get() // 未单独指定日志实例, 则使用全局日志实例 reqConfig.Logger = log.Get() // 未单独指定日志实例, 则使用全局日志实例
} }
@ -37,6 +42,18 @@ func NewHttpClient(reqConfig *define.Request, cacheInstance cache.ICache) (*Http
if err := validate.RequestConfig(reqConfig); nil != err { if err := validate.RequestConfig(reqConfig); nil != err {
return nil, err return nil, err
} }
if reqConfig.RetryRule == nil {
reqConfig.RetryRule = &define.RequestRetryRule{
RetryCount: 0,
RetryTimeInterval: 0,
RetryHttpCodeList: []int64{},
RetryBusinessCodeList: []string{},
} // 未指定重试规则, 则使用默认重试规则
}
// 初始化成功的 http code list
if len(reqConfig.SuccessHttpCodeList) == 0 {
reqConfig.SuccessHttpCodeList = []int{}
}
if len(reqConfig.Static) > 0 { if len(reqConfig.Static) > 0 {
for loc, valMap := range reqConfig.Static { for loc, valMap := range reqConfig.Static {
if len(valMap) == 0 { if len(valMap) == 0 {
@ -79,27 +96,32 @@ func NewHttpClient(reqConfig *define.Request, cacheInstance cache.ICache) (*Http
reqConfig.Header = make(map[string]any) reqConfig.Header = make(map[string]any)
} }
if ua, exist := reqConfig.Header[consts.HeaderKeyUserAgent.String()]; !exist || nil == ua || fmt.Sprintf("%v", ua) == "" { if ua, exist := reqConfig.Header[consts.HeaderKeyUserAgent.String()]; !exist || nil == ua || fmt.Sprintf("%v", ua) == "" {
reqConfig.Header[consts.HeaderKeyUserAgent.String()] = "resty-v2@network/httpclient" reqConfig.Header[consts.HeaderKeyUserAgent.String()] = "resty-v3@network/httpclient"
} }
restyClient, restyRequest := NewRestyClient(reqConfig) restyClient, restyRequest, err := NewRestyClient(reqConfig, reqOption)
if nil != err {
if nil != restyClient {
_ = restyClient.Close()
}
return nil, err
}
defer restyClient.Close()
hc := &HttpClient{ hc := &HttpClient{
Client: restyClient, Client: restyClient,
request: restyRequest, request: restyRequest,
reqConfig: reqConfig, reqOption: reqOption,
http4xxHandler: make([]define.Http4xxHandler, 0), reqCfg: reqConfig,
http5xxHandler: make([]define.Http5xxHandler, 0), requestFinishHandler: make([]define.RequestFinishHandler, 0),
httpBusinessErrorHandler: make([]define.HttpBusinessErrorHandler, 0),
requestFinishHandler: make([]define.RequestFinishHandler, 0),
cacheInstance: cacheInstance,
} }
hc.OnRequestFinish(func(req *define.Request, rep *define.Response) { hc.OnRequestFinish(func(req *define.Request, rep *define.Response) {
if rep.IsSuccess { if rep.IsSuccess {
// 请求成功 // 请求成功
log.Record(req.Ctx, req.Logger, consts.LogLevelInfo, "接口请求成功", req, rep) log.Record(consts.LogLevelInfo, "接口请求成功", req, rep)
return return
} }
// 请求失败 // 请求失败
log.Record(req.Ctx, req.Logger, consts.LogLevelError, "接口请求失败", req, rep) log.Record(consts.LogLevelError, "接口请求失败", req, rep)
}) })
return hc, nil return hc, nil
} }
@ -111,41 +133,10 @@ func NewHttpClient(reqConfig *define.Request, cacheInstance cache.ICache) (*Http
// Date : 15:27 2024/5/31 // Date : 15:27 2024/5/31
type HttpClient struct { type HttpClient struct {
*resty.Client *resty.Client
request *resty.Request request *resty.Request
reqConfig *define.Request reqOption *RequestOption
http4xxHandler []define.Http4xxHandler reqCfg *define.Request
http5xxHandler []define.Http5xxHandler requestFinishHandler []define.RequestFinishHandler
httpBusinessErrorHandler []define.HttpBusinessErrorHandler
requestSendErrorHandler []define.RequestSendErrorHandler
requestFinishHandler []define.RequestFinishHandler
cacheInstance cache.ICache
}
// OnResponse4xx 4xx处理逻辑
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:20 2024/6/1
func (hc *HttpClient) OnResponse4xx(handlerList ...define.Http4xxHandler) {
hc.http4xxHandler = append(hc.http4xxHandler, handlerList...)
}
// OnResponse5xx 5xx处理逻辑
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:21 2024/6/1
func (hc *HttpClient) OnResponse5xx(handlerList ...define.Http5xxHandler) {
hc.http5xxHandler = append(hc.http5xxHandler, handlerList...)
}
// OnResponseBusinessError 业务错误出路逻辑
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:22 2024/6/1
func (hc *HttpClient) OnResponseBusinessError(handlerList ...define.HttpBusinessErrorHandler) {
hc.httpBusinessErrorHandler = append(hc.httpBusinessErrorHandler, handlerList...)
} }
// OnRequestFinish 请求完成事件 // OnRequestFinish 请求完成事件
@ -157,37 +148,6 @@ func (hc *HttpClient) OnRequestFinish(handlerList ...define.RequestFinishHandler
hc.requestFinishHandler = append(hc.requestFinishHandler, handlerList...) hc.requestFinishHandler = append(hc.requestFinishHandler, handlerList...)
} }
// getRequestValidateMiddleware 请求验证的Middleware
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:40 2024/5/31
func (hc *HttpClient) getRequestValidateMiddleware() resty.RequestMiddleware {
return func(client *resty.Client, request *resty.Request) error {
return nil
}
}
// getResponseValidateMiddleware 获取响应数据验证的middleware
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:42 2024/5/31
func (hc *HttpClient) getResponseValidateMiddleware() resty.ResponseMiddleware {
return func(client *resty.Client, response *resty.Response) error {
return nil
}
}
// SetRestyClient 设置client
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:54 2024/5/31
func (hc *HttpClient) SetRestyClient(restyClient *resty.Client) {
hc.Client = restyClient
}
// GetRestyClient 获取 resty client // GetRestyClient 获取 resty client
// //
// Author : go_developer@163.com<白茶清欢> // Author : go_developer@163.com<白茶清欢>
@ -207,47 +167,114 @@ func (hc *HttpClient) Request() *define.Response {
cacheResult *define.Response cacheResult *define.Response
) )
if cacheResult = hc.getCacheResult(); nil != cacheResult { if cacheResult = hc.getCacheResult(); nil == cacheResult {
// 判断是否开启预热 // 未命中缓存, 直接请求后端接口, 无需检测预热等逻辑
inputCachePreHeatConfig := hc.cacheInstance.PreHeatConfig() return hc.requestBackendApi()
var cachePreHeatConfig define.CachePreHeatConfig }
serialize.JSON.TransitionIgnoreError(inputCachePreHeatConfig, &cachePreHeatConfig) // 上面若命中缓存, 则后续缓存实例不可能为nil, 无需判断
if !cachePreHeatConfig.Enable || (cachePreHeatConfig.MinTTL <= 0 && cachePreHeatConfig.MinPercent <= 0 && !cachePreHeatConfig.Force) { // 判断是否开启预热
// 无预热配置或未启用预热或者未设置预热规则 cachePreHeatConfig := hc.reqOption.CacheInstance.PreHeatConfig()
return cacheResult if nil == cachePreHeatConfig {
} log.RecordDebug("接口请求命中缓存, PreHeatConfig未返回预热配置, 不做预热处理", map[string]any{}, hc.reqCfg)
return nil
}
log.RecordDebug("接口请求命中缓存, 进行预热策略处理", map[string]any{
"cache_info": cacheResult.CacheInfo,
}, hc.reqCfg)
defer func() {
// 命中缓存的情况下, 检测缓存预热策略, 判断是否进行缓存预热
go func() { go func() {
if !cachePreHeatConfig.Enable || (cachePreHeatConfig.MinTTL <= 0 && cachePreHeatConfig.MinPercent <= 0 && !cachePreHeatConfig.Force) {
// 无预热配置或未启用预热或者未设置预热规则
log.RecordDebug("接口请求命中缓存, 未配置缓存预热策略", map[string]any{
"cache_pre_heat_config": cachePreHeatConfig,
}, hc.reqCfg)
return
}
// 判断是否触发预热 // 判断是否触发预热
if cachePreHeatConfig.Force { if cachePreHeatConfig.Force {
log.RecordDebug("接口请求命中缓存, 强制执行缓存预热, 忽略其他策略配置", map[string]any{
"cache_pre_heat_config": cachePreHeatConfig,
}, hc.reqCfg)
_ = hc.requestBackendApi() _ = hc.requestBackendApi()
return return
} }
// 将百分比的配置归一化成最小剩余时间的配置 // 将百分比的配置归一化成最小剩余时间的配置
if cachePreHeatConfig.MinPercent > 0 { if cachePreHeatConfig.MinPercent > 0 {
expectMinTTL := hc.cacheInstance.CacheTime() * cachePreHeatConfig.MinPercent / 100 expectMinTTL := hc.reqOption.CacheInstance.CacheTime() * cachePreHeatConfig.MinPercent / 100
log.RecordDebug("接口请求命中缓存, 配置预热策略:有效时长剩余百分比", map[string]any{
"cache_pre_heat_config": cachePreHeatConfig,
"percent_min_ttl": expectMinTTL,
"min_ttl": cachePreHeatConfig.MinTTL,
}, hc.reqCfg)
if cachePreHeatConfig.MinTTL == 0 || cachePreHeatConfig.MinTTL > expectMinTTL { if cachePreHeatConfig.MinTTL == 0 || cachePreHeatConfig.MinTTL > expectMinTTL {
cachePreHeatConfig.MinTTL = expectMinTTL cachePreHeatConfig.MinTTL = expectMinTTL
} }
} }
if cachePreHeatConfig.MinTTL <= 0 { if cachePreHeatConfig.MinTTL <= 0 {
// 未配置最小剩余时间 // 未配置最小剩余时间
log.RecordDebug("接口请求命中缓存, 未配置预热市场策略, 不执行预热", map[string]any{
"min_ttl": cachePreHeatConfig.MinTTL,
}, hc.reqCfg)
return return
} }
ttl := hc.cacheInstance.TTL(cacheResult.CacheInfo.CacheKey) ttl := hc.reqOption.CacheInstance.TTL(cacheResult.CacheInfo.CacheKey)
if ttl < 0 { if ttl < 0 {
// 不存在或者未设置有效期 // 不存在或者未设置有效期
log.RecordDebug("接口请求命中缓存, 当前缓存结果不存在或未设置有效期, 不执行预热", map[string]any{
"min_ttl": cachePreHeatConfig.MinTTL,
"note": "预热时间至少在缓存过期前10s触发预热, 以保证足够时间进行预热, 以及不会因为预热尚未完成, 但是大量流量涌入, 进而导致流量穿透",
}, hc.reqCfg)
return return
} }
if ttl <= cachePreHeatConfig.MinTTL { if ttl > cachePreHeatConfig.MinTTL {
// 配置了最小剩余时间,并且key剩余有效期小于最小剩余时间 log.RecordDebug("接口请求命中缓存, 缓存结果有效期剩余时长大于配置阈值, 不执行预热", map[string]any{
_ = hc.requestBackendApi() "min_ttl": cachePreHeatConfig.MinTTL,
"remaining_ttl": ttl,
}, hc.reqCfg)
return
} }
log.RecordDebug("接口请求命中缓存, 缓存结果有效期大于剩余时长小于配置阈值, 触发预热", map[string]any{
"min_ttl": cachePreHeatConfig.MinTTL,
"remaining_ttl": ttl,
}, hc.reqCfg)
// 配置了最小剩余时间,并且key剩余有效期小于最小剩余时间
// 预热加锁, 并发请求触发预热, 仅触发一个即可, 使用接口缓存key + LOCK做锁的key {{CACHE_KEY}}_LOCK, 按照一般约定, 写请求不会做缓存, 只有读请求会
lockKey := hc.reqOption.CacheInstance.GetKey(hc.reqCfg) + "_LOCK"
if err := hc.reqOption.CacheInstance.Lock(lockKey); err != nil {
log.RecordWarn("接口请求命中缓存, 缓存结果有效期大于剩余时长小于配置阈值, 触发预热, 加锁失败, 未执行预热", map[string]any{
"min_ttl": cachePreHeatConfig.MinTTL,
"remaining_ttl": ttl,
"err_msg": err.Error(),
}, hc.reqCfg)
return
}
log.RecordDebug("接口请求命中缓存, 缓存结果有效期大于剩余时长小于配置阈值, 触发预热, 加锁成功, 执行预热", map[string]any{
"min_ttl": cachePreHeatConfig.MinTTL,
"remaining_ttl": ttl,
"lock_key": hc.reqCfg.FullUrl,
}, hc.reqCfg)
_ = hc.requestBackendApi()
if err := hc.reqOption.CacheInstance.Unlock(lockKey); nil != err {
log.RecordError("接口请求命中缓存, 缓存结果有效期大于剩余时长小于配置阈值, 触发预热, 执行预热后, 释放锁失败", map[string]any{
"min_ttl": cachePreHeatConfig.MinTTL,
"remaining_ttl": ttl,
"lock_key": hc.reqCfg.FullUrl,
"err_msg": err.Error(),
}, hc.reqCfg)
return
}
log.RecordDebug("接口请求命中缓存, 缓存结果有效期大于剩余时长小于配置阈值, 触发预热, 执行预热后, 释放锁成功", map[string]any{
"min_ttl": cachePreHeatConfig.MinTTL,
"remaining_ttl": ttl,
"lock_key": hc.reqCfg.FullUrl,
}, hc.reqCfg)
}() }()
} }()
// 命中缓存必然请求成功, 直接记录成功日志即可
return hc.requestBackendApi() log.Record(consts.LogLevelInfo, "接口请求成功:命中缓存", hc.reqCfg, cacheResult)
return cacheResult
} }
// requestBackendApi 请求后端接口 // requestBackendApi 请求后端接口
@ -256,44 +283,59 @@ func (hc *HttpClient) Request() *define.Response {
// //
// Date : 18:47 2024/10/9 // Date : 18:47 2024/10/9
func (hc *HttpClient) requestBackendApi() *define.Response { func (hc *HttpClient) requestBackendApi() *define.Response {
hc.Client.OnBeforeRequest(hc.getRequestValidateMiddleware()) // 请求参数验证中间件必注册
hc.Client.OnAfterResponse(hc.getResponseValidateMiddleware()) // 响应验证中间件必注册
var ( var (
err error err error
) )
response := hc.newResponse() var response *define.Response
for i := 0; i < hc.reqConfig.RetryRule.RetryCount+1; i++ { // +1 是因为正常便会请求一次, 正常请求1次 + 重试次数 = 请求总次数
if response.Seq > 0 { for i := 0; i < hc.reqCfg.RetryRule.RetryCount+1; i++ {
if i > 0 && nil != response {
// 说明是重试, 记录上一次的请求信息 // 说明是重试, 记录上一次的请求信息
response.RequestFinishTime = time.Now().UnixMilli() response.RequestFinishTime = time.Now().UnixMilli()
response.UsedTime = response.RequestFinishTime - response.RequestStartTime response.UsedTime = response.RequestFinishTime - response.RequestStartTime
for _, itemAfterResponse := range hc.requestFinishHandler { for _, itemAfterResponse := range hc.requestFinishHandler {
itemAfterResponse(hc.reqConfig, response) itemAfterResponse(hc.reqCfg, response)
} }
// 非首次请求, 说明是重试, 暂停指定时间间隔
time.Sleep(time.Duration(hc.reqCfg.RetryRule.RetryTimeInterval) * time.Millisecond)
} }
response.RequestStartTime = time.Now().UnixMilli() // 每次重置请求时间 response = hc.newResponse()
response.RequestFinishTime = 0 // 清空完成时间 response.Seq = i
response.Seq++ response.RequestCount = i + 1
response.RequestCount++
if response.RestyResponse, err = hc.request.Send(); nil != err { if response.RestyResponse, err = hc.request.Send(); nil != err {
errType := define.RequestFailTypeSend errType := define.RequestFailTypeSend
if err.Error() == resty.ErrRateLimitExceeded.Error() { if err.Error() == define.ErrRateLimitExceeded.Error() {
// 命中限流 // 命中限流
errType = define.RequestFailTypeRateLimit errType = define.RequestFailTypeRateLimit
} else if _, ok := err.(net.Error); ok { } else if netErr, ok := err.(net.Error); ok {
// 请求超时 if netErr.Timeout() {
errType = define.RequestFailTypeTimeoutError // 请求超时
errType = define.RequestFailTypeTimeoutError
// 重置响应状态码
response.HttpCode = 499
response.HttpCodeStatus = "request timeout"
}
} }
response.FailInfo = &define.ResponseFailInfo{ response.FailInfo = &define.ResponseFailInfo{
Type: errType, Type: errType,
Message: err.Error(), Message: err.Error(),
} }
if errType == define.RequestFailTypeSend { log.RecordDebug("请求发送出现异常", map[string]any{
// 命中限流就不重试了 "err_type": errType,
"err_msg": err.Error(),
}, hc.reqCfg)
if errType == define.RequestFailTypeTimeoutError && !hc.reqOption.ResponseParser.NeedRetry(hc.reqCfg, response) {
// 未配置超时重试
log.RecordDebug("请求超时, 未配置超时重试, 不进行重试", nil, hc.reqCfg)
break break
} }
time.Sleep(time.Duration(hc.reqConfig.RetryRule.RetryTimeInterval) * time.Millisecond) log.RecordDebug("请求发送出现异常, 进行重试", map[string]any{
"err_type": errType,
"err_msg": err.Error(),
"time_interval": time.Duration(hc.reqCfg.RetryRule.RetryTimeInterval) * time.Millisecond,
}, hc.reqCfg)
continue continue
} }
@ -302,14 +344,18 @@ func (hc *HttpClient) requestBackendApi() *define.Response {
Type: define.RequestFailTypeSend, Type: define.RequestFailTypeSend,
Message: "response instance is nil", Message: "response instance is nil",
} }
time.Sleep(time.Duration(hc.reqConfig.RetryRule.RetryTimeInterval) * time.Millisecond) log.RecordDebug("RestyResponse为nil, 准备重试", map[string]any{
"err_type": response.FailInfo.Type,
"time_interval": time.Duration(hc.reqCfg.RetryRule.RetryTimeInterval) * time.Millisecond,
}, hc.reqCfg)
continue continue
} }
// 解析返回信息 // 解析返回信息
hc.fillResponseHeader(response) hc.reqOption.ResponseParser.Parse(hc.reqCfg, response)
hc.fillResponseCookie(response) // 配置了当前code为成功, 或者未配置任何code, 当前code为2xx, 则认为请求成功
hc.fillResponseBody(response) isHttpSuccess := hc.reqOption.ResponseParser.HttpSuccess(hc.reqCfg, response)
if response.HttpCode != http.StatusOK { if !isHttpSuccess {
// 非 成功
errType := define.RequestFailTypeServerError errType := define.RequestFailTypeServerError
if response.HttpCode/100 == 4 { if response.HttpCode/100 == 4 {
// 客户端错误 // 客户端错误
@ -319,15 +365,40 @@ func (hc *HttpClient) requestBackendApi() *define.Response {
Type: errType, Type: errType,
Message: "http code is " + response.HttpCodeStatus + ", not success", Message: "http code is " + response.HttpCodeStatus + ", not success",
} }
time.Sleep(time.Duration(hc.reqConfig.RetryRule.RetryTimeInterval) * time.Millisecond) needRetry := hc.reqOption.ResponseParser.NeedRetry(hc.reqCfg, response)
log.RecordWarn("请求响应的http状态码非成功", map[string]any{
"err_type": errType,
"err_msg": response.RestyResponse.Status(),
"response_http_code": response.HttpCode,
"success_http_code": hc.reqCfg.SuccessHttpCodeList,
"allow_retry": needRetry,
"time_interval": time.Duration(hc.reqCfg.RetryRule.RetryTimeInterval) * time.Millisecond,
}, hc.reqCfg)
if !needRetry {
// 未配置http code重试
break
}
continue continue
} }
if !hc.isCodeSuccess(response) { if !hc.reqOption.ResponseParser.BusinessSuccess(hc.reqCfg, response) {
response.FailInfo = &define.ResponseFailInfo{ response.FailInfo = &define.ResponseFailInfo{
Type: define.RequestFailTypeBusinessError, Type: define.RequestFailTypeBusinessError,
Message: "business code is " + response.Code + ", not success", Message: "business code is " + response.Code + ", not success",
} }
time.Sleep(time.Duration(hc.reqConfig.RetryRule.RetryTimeInterval) * time.Millisecond) needRetry := hc.reqOption.ResponseParser.NeedRetry(hc.reqCfg, response)
log.RecordWarn("请求响应状态码成功, 业务状态码非成功", map[string]any{
"err_type": response.FailInfo.Type,
"err_msg": response.Message,
"response_code": response.Code,
"success_code": hc.reqCfg.SuccessCodeList,
"allow_retry": needRetry,
"time_interval": time.Duration(hc.reqCfg.RetryRule.RetryTimeInterval) * time.Millisecond,
}, hc.reqCfg)
if needRetry {
// 未配置业务code重试
break
}
continue continue
} }
response.IsSuccess = true //设置成功 response.IsSuccess = true //设置成功
@ -338,7 +409,7 @@ func (hc *HttpClient) requestBackendApi() *define.Response {
response.UsedTime = response.RequestFinishTime - response.RequestStartTime response.UsedTime = response.RequestFinishTime - response.RequestStartTime
// 请求完成hook // 请求完成hook
for _, itemAfterResponse := range hc.requestFinishHandler { for _, itemAfterResponse := range hc.requestFinishHandler {
itemAfterResponse(hc.reqConfig, response) itemAfterResponse(hc.reqCfg, response)
} }
return response return response
} }
@ -360,7 +431,7 @@ func (hc *HttpClient) newResponse() *define.Response {
HttpCodeStatus: "", HttpCodeStatus: "",
ResponseDataRule: nil, ResponseDataRule: nil,
Seq: 0, Seq: 0,
RequestStartTime: 0, RequestStartTime: time.Now().UnixMilli(),
RequestFinishTime: 0, RequestFinishTime: 0,
UsedTime: 0, UsedTime: 0,
RestyResponse: nil, RestyResponse: nil,
@ -370,7 +441,7 @@ func (hc *HttpClient) newResponse() *define.Response {
SetCache: false, SetCache: false,
CacheKey: "", CacheKey: "",
CacheValue: "", CacheValue: "",
CacheEnable: nil != hc.cacheInstance && hc.cacheInstance.Enable(), CacheEnable: nil != hc.reqOption.CacheInstance && hc.reqOption.CacheInstance.Enable(),
CacheError: nil, CacheError: nil,
}, },
RequestCount: 0, RequestCount: 0,
@ -378,110 +449,32 @@ func (hc *HttpClient) newResponse() *define.Response {
} }
} }
// fillResponseHeader 填充响应header
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 21:30 2024/6/5
func (hc *HttpClient) fillResponseHeader(response *define.Response) {
response.Header = map[string]any{} // 清空已有数据
response.HttpCode = response.RestyResponse.StatusCode() // http状态码
response.HttpCodeStatus = response.RestyResponse.Status() // http状态码描述
for headerName, headerValue := range response.RestyResponse.Header() {
if len(headerValue) > 0 {
response.Header[headerName] = headerValue[0]
} else {
response.Header[headerName] = ""
}
}
}
// fillResponseCookie 填充cookie
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 21:32 2024/6/5
func (hc *HttpClient) fillResponseCookie(response *define.Response) {
response.Cookie = map[string]any{} // 清空已有数据
for _, cookieValue := range response.RestyResponse.Cookies() {
response.Cookie[cookieValue.Name] = cookieValue.Value
}
}
// fillResponseBody 填充响应body
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 21:38 2024/6/5
func (hc *HttpClient) fillResponseBody(response *define.Response) {
response.Data = string(response.RestyResponse.Body())
response.Code = gjson.Get(response.Data, hc.reqConfig.CodeField).String()
response.Message = gjson.Get(response.Data, hc.reqConfig.MessageField).String()
businessData := gjson.Get(response.Data, hc.reqConfig.DataField)
if businessData.Value() == nil {
// data为空指针, 归一化成空对象
response.Body = map[string]any{}
} else {
if businessData.IsArray() {
// 数组类型的转换
response.Data = fmt.Sprintf(`{"list":` + businessData.String() + "}")
} else {
if businessData.IsObject() {
// 返回的就是对象
response.Data = businessData.String()
} else {
// 返回是普通类型
response.Data = serialize.JSON.MarshalForStringIgnoreError(map[string]any{
"value": businessData.Value(),
})
}
}
_ = serialize.JSON.UnmarshalWithNumber([]byte(response.Data), &response.Body)
}
response.ExtendData = map[string]string{}
gjson.Parse(response.Data).ForEach(func(key, value gjson.Result) bool {
if key.String() == hc.reqConfig.CodeField ||
key.String() == hc.reqConfig.MessageField ||
key.String() == hc.reqConfig.DataField {
return true
}
response.ExtendData[key.String()] = value.String()
return true
})
}
// isHttpCodeSuccess ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 22:48 2024/6/6
func (hc *HttpClient) isCodeSuccess(response *define.Response) bool {
for _, itemSuccessCode := range hc.reqConfig.SuccessCodeList {
if itemSuccessCode == response.Code {
return true
}
}
return false
}
// getCacheResult 获取缓存结果 // getCacheResult 获取缓存结果
// //
// Author : go_developer@163.com<白茶清欢> // Author : go_developer@163.com<白茶清欢>
// //
// Date : 16:04 2024/6/3 // Date : 16:04 2024/6/3
func (hc *HttpClient) getCacheResult() *define.Response { func (hc *HttpClient) getCacheResult() *define.Response {
if nil == hc.cacheInstance || !hc.cacheInstance.Enable() { if nil == hc.reqOption || nil == hc.reqOption.CacheInstance {
log.RecordDebug("接口请求前缓存检测, 未设置缓存实例", map[string]any{}, hc.reqCfg)
return nil
}
if !hc.reqOption.CacheInstance.Enable() {
log.RecordDebug("接口请求前缓存检测, 设置缓存实例, 但未启用缓存功能", map[string]any{}, hc.reqCfg)
return nil return nil
} }
startTime := time.Now().UnixMilli() startTime := time.Now().UnixMilli()
cacheKey := hc.cacheInstance.GetKey(hc.reqConfig) cacheKey := hc.reqOption.CacheInstance.GetKey(hc.reqCfg)
cacheValue := strings.TrimSpace(hc.cacheInstance.GetValue(cacheKey)) cacheValue := strings.TrimSpace(hc.reqOption.CacheInstance.GetValue(cacheKey))
if len(cacheValue) == 0 { if len(cacheValue) == 0 {
log.RecordDebug("接口请求前缓存检测, 未读取到缓存数据", map[string]any{}, hc.reqCfg)
return nil return nil
} }
response := hc.newResponse() response := hc.newResponse()
if err := serialize.JSON.UnmarshalWithNumber([]byte(cacheValue), response); nil != err { if err := serialize.JSON.UnmarshalWithNumber([]byte(cacheValue), response); nil != err {
log.RecordWarn("接口请求前缓存检测, 读取到缓存数据, 数据解析失败, 将跳过缓存, 请求对应接口", map[string]any{
"err_msg": err.Error(),
}, hc.reqCfg)
return nil return nil
} }
response.CacheInfo.IsCache = true // 设置缓存标记 response.CacheInfo.IsCache = true // 设置缓存标记
@ -490,6 +483,7 @@ func (hc *HttpClient) getCacheResult() *define.Response {
response.UsedTime = response.RequestFinishTime - response.RequestStartTime // 耗时 response.UsedTime = response.RequestFinishTime - response.RequestStartTime // 耗时
response.CacheInfo.CacheKey = cacheKey // 缓存key response.CacheInfo.CacheKey = cacheKey // 缓存key
response.CacheInfo.CacheValue = cacheValue // 缓存值 response.CacheInfo.CacheValue = cacheValue // 缓存值
log.RecordDebug("接口请求前缓存检测, 命中缓存, 直接返回缓存数据", map[string]any{}, hc.reqCfg)
return response return response
} }
@ -499,18 +493,37 @@ func (hc *HttpClient) getCacheResult() *define.Response {
// //
// Date : 16:24 2024/6/3 // Date : 16:24 2024/6/3
func (hc *HttpClient) setCacheResult(response *define.Response) (bool, error) { func (hc *HttpClient) setCacheResult(response *define.Response) (bool, error) {
if nil == response || nil == hc.cacheInstance { if nil == response || nil == hc.reqOption.CacheInstance {
log.RecordDebug("接口请求成功后, 缓存设置失败", map[string]any{
"response_is_nil": response == nil,
"cache_instance_is_nil": hc.reqOption.CacheInstance == nil,
}, hc.reqCfg)
return false, nil return false, nil
} }
// 全局未开启或者当前请求不支持缓存 // 全局未开启或者当前请求不支持缓存
if !hc.cacheInstance.Enable() || !hc.cacheInstance.IsAllow(hc.reqConfig, response) { globalCacheEnable := hc.reqOption.CacheInstance.Enable()
currentRequestAllowCache := hc.reqOption.CacheInstance.IsAllow(hc.reqCfg, response)
log.RecordDebug("检测缓存是否允许执行", map[string]any{
"current_cache_enable": currentRequestAllowCache,
"global_cache_enable": globalCacheEnable,
}, hc.reqCfg)
if !globalCacheEnable || !currentRequestAllowCache {
return false, nil return false, nil
} }
cacheKey := hc.cacheInstance.GetKey(hc.reqConfig) cacheKey := hc.reqOption.CacheInstance.GetKey(hc.reqCfg)
cacheValue := serialize.JSON.MarshalForStringIgnoreError(response) cacheValue := serialize.JSON.MarshalForStringIgnoreError(response)
if err := hc.cacheInstance.SetValue(cacheKey, cacheValue); nil != err { if err := hc.reqOption.CacheInstance.SetValue(cacheKey, cacheValue); nil != err {
log.RecordWarn("开启结果缓存, 缓存设置失败", map[string]any{
"current_cache_enable": currentRequestAllowCache,
"global_cache_enable": globalCacheEnable,
"err_msg": err.Error(),
}, hc.reqCfg)
return false, err return false, err
} }
log.RecordDebug("开启结果缓存, 缓存设置成功", map[string]any{
"current_cache_enable": currentRequestAllowCache,
"global_cache_enable": globalCacheEnable,
}, hc.reqCfg)
response.CacheInfo.CacheKey = cacheKey response.CacheInfo.CacheKey = cacheKey
response.CacheInfo.CacheValue = cacheValue response.CacheInfo.CacheValue = cacheValue
return true, nil return true, nil

View File

@ -16,3 +16,8 @@ var (
ErrMethodIsEmpty = errors.New("METHOD_IS_EMPTY") // 请求 方法为空 ErrMethodIsEmpty = errors.New("METHOD_IS_EMPTY") // 请求 方法为空
ErrMethodIsNotSupport = errors.New("METHOD_IS_NOT_SUPPORT") // 请求 method不支持 ErrMethodIsNotSupport = errors.New("METHOD_IS_NOT_SUPPORT") // 请求 method不支持
) )
var (
// ErrRateLimitExceeded v2版本中命中流控的错误信息
ErrRateLimitExceeded = errors.New("rate limit exceeded")
)

View File

@ -7,34 +7,6 @@
// Date : 2024-05-31 14:51 // Date : 2024-05-31 14:51
package define package define
// Http4xxHandler 4xx handler
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 14:54 2024/5/31
type Http4xxHandler func(req *Request, rep *Response)
// Http5xxHandler 5xx handler
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 14:55 2024/5/31
type Http5xxHandler func(req *Request, rep *Response)
// HttpBusinessErrorHandler 接口请求业务错误
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:04 2024/6/1
type HttpBusinessErrorHandler func(req *Request, rep *Response)
// RequestSendErrorHandler 请求发送失败的处理逻辑
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 18:23 2024/6/1
type RequestSendErrorHandler func(req *Request)
// RequestFinishHandler 请求最终完成事件, 不区分成功 OR 失败 // RequestFinishHandler 请求最终完成事件, 不区分成功 OR 失败
// //
// Author : go_developer@163.com<白茶清欢> // Author : go_developer@163.com<白茶清欢>

View File

@ -9,7 +9,6 @@ package define
import ( import (
"context" "context"
"github.com/go-resty/resty/v2"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -19,26 +18,27 @@ import (
// //
// Date : 17:10 2024/5/24 // Date : 17:10 2024/5/24
type Request struct { type Request struct {
Ctx context.Context `json:"-"` // 请求上下文 Ctx context.Context `json:"-"` // 请求上下文
PathParam map[string]string `json:"path_param"` // 替换url中的占位符 PathParam map[string]string `json:"path_param"` // 替换url中的占位符
Body map[string]any `json:"body"` // 请求Body Body map[string]any `json:"body"` // 请求Body
Header map[string]any `json:"header"` // 请求Header Header map[string]any `json:"header"` // 请求Header
Cookie map[string]any `json:"cookie"` // 请求Cookie Cookie map[string]any `json:"cookie"` // 请求Cookie
Query map[string]any `json:"query"` // 请求query Query map[string]any `json:"query"` // 请求query
Static map[string]map[string]any `json:"static"` // 静态参数: location => valName => val Static map[string]map[string]any `json:"static"` // 静态参数: location => valName => val
FullUrl string `json:"full_url"` // 完整的请求URL FullUrl string `json:"full_url"` // 完整的请求URL
ContentType string `json:"content_type"` // 请求类型 ContentType string `json:"content_type"` // 请求类型
Method string `json:"method"` // 请求方法 Method string `json:"method"` // 请求方法
DataField string `json:"data_field"` // 数据字段 DataField string `json:"data_field"` // 数据字段
CodeField string `json:"code_field"` // 业务状态码字段 CodeField string `json:"code_field"` // 业务状态码字段
MessageField string `json:"message_field"` // code描述字段 CodeLocation string `json:"code_location"` // 业务状态码位置
DataReceiver any `json:"-"` // 响应data部分数据解析 MessageField string `json:"message_field"` // code描述字段
SuccessCodeList []string `json:"success_code_list"` // 哪些业务状态码视为成功 DataReceiver any `json:"-"` // 响应data部分数据解析
ConnectTimeout int64 `json:"connect_timeout"` // 连接超时时间: ms SuccessHttpCodeList []int `json:"success_http_code_list"` // 哪些http状态码视为成功, 不配置, 默认2xx
ReadTimeout int64 `json:"read_timeout"` // 读取超时时间 SuccessCodeList []string `json:"success_code_list"` // 哪些业务状态码视为成功
RetryRule *RequestRetryRule `json:"retry_rule"` // 重试规则 ConnectTimeout int64 `json:"connect_timeout"` // 连接超时时间: ms
Logger *zap.Logger `json:"-"` // 日志记录器 ReadTimeout int64 `json:"read_timeout"` // 读取超时时间
RateLimiter resty.RateLimiter `json:"-"` // 流控实例 RetryRule *RequestRetryRule `json:"retry_rule"` // 重试规则
Logger *zap.Logger `json:"-"` // 日志记录器
} }
// RequestRetryRule 重试规则 // RequestRetryRule 重试规则

View File

@ -8,7 +8,7 @@
package define package define
import ( import (
"github.com/go-resty/resty/v2" "resty.dev/v3"
) )
// Response 响应的数据结构定义 // Response 响应的数据结构定义

View File

@ -0,0 +1,55 @@
// Package request ...
//
// Description : request ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-08 14:21
package request
import (
"errors"
"git.zhangdeman.cn/zhangdeman/network/httpclient/abstract"
"resty.dev/v3"
"strings"
)
var (
// WriteBodyInstanceTable 请求类型 => 请求类型写入的实现
WriteBodyInstanceTable = map[string]abstract.IRequestBodyWrite{
"json": &WriteJson{},
"xml": nil,
"x-www-form-urlencoded": &WriteForm{},
}
)
func SetWriteBodyInstance(cType string, instance abstract.IRequestBodyWrite) {
WriteBodyInstanceTable[cType] = instance
}
func DeleteWriteBodyInstance(cType string) {
delete(WriteBodyInstanceTable, cType)
}
// WriteBody 数据写入body
func WriteBody(contentType string, request *resty.Request, bodyData map[string]any) error {
if nil == bodyData {
// body为nil, 无需写入, 否则即使为空map也要正常写入
return nil
}
if nil == request {
return errors.New("request is nil")
}
contentType = strings.TrimSpace(strings.Split(contentType, ";")[0])
if len(contentType) == 0 {
return errors.New("contentType is empty or invalid")
}
contentTypeArr := strings.Split(contentType, "/")
realType := contentTypeArr[len(contentTypeArr)-1]
writeInstance, ok := WriteBodyInstanceTable[realType]
if !ok || writeInstance == nil {
return errors.New(realType + " is not support, writeInstance is nil")
}
writeInstance.Write(request, bodyData)
return nil
}

View File

@ -0,0 +1,33 @@
// Package request ...
//
// Description : request ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-08 14:39
package request
import (
"git.zhangdeman.cn/zhangdeman/serialize"
"github.com/tidwall/gjson"
"resty.dev/v3"
)
// WriteForm application/x-www-form-urlencoded 实现
type WriteForm struct {
}
func (w *WriteForm) Write(request *resty.Request, bodyData map[string]any) error {
if len(bodyData) == 0 {
return nil
}
bodyStr := serialize.JSON.MarshalForStringIgnoreError(bodyData)
formatBodyData := map[string]string{}
jsonObj := gjson.Parse(bodyStr)
jsonObj.ForEach(func(key, value gjson.Result) bool {
formatBodyData[key.String()] = value.String()
return true
})
request.SetFormData(formatBodyData)
return nil
}

View File

@ -0,0 +1,21 @@
// Package request ...
//
// Description : request ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-08 14:21
package request
import (
"resty.dev/v3"
)
// WriteJson application/json写入
type WriteJson struct {
}
func (w *WriteJson) Write(request *resty.Request, bodyData map[string]any) error {
request.SetBody(bodyData)
return nil
}

View File

@ -0,0 +1,46 @@
// Package request ...
//
// Description : request ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-08 14:21
package request
import (
"bytes"
"errors"
"git.zhangdeman.cn/zhangdeman/serialize"
"resty.dev/v3"
"git.zhangdeman.cn/zhangdeman/dynamic-struct/wrapper"
)
// WriteXml application/xml body写入
type WriteXml struct{}
func (w *WriteXml) Write(request *resty.Request, bodyData map[string]any) error {
if nil == bodyData {
return nil
}
var (
err error
bodyBytes []byte
xmlBodyByte []byte
)
if bodyBytes, err = serialize.JSON.MarshalForByte(bodyData); nil != err {
return err
}
instance, err := wrapper.NewJson(string(bodyBytes), &wrapper.Option{XmlName: "RequestBody"})
if nil != err {
return err
}
if nil == instance {
return errors.New("xml generate instance is nil")
}
if xmlBodyByte, err = instance.Marshal("xml"); nil != err {
return err
}
request.SetBody(bytes.NewReader(xmlBodyByte))
return nil
}

View File

@ -0,0 +1,199 @@
// Package implement ...
//
// Description : implement ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-07 14:56
package implement
import (
"errors"
"fmt"
"git.zhangdeman.cn/zhangdeman/consts"
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
"git.zhangdeman.cn/zhangdeman/serialize"
"github.com/tidwall/gjson"
"net/http"
"strings"
)
// Response 响应结果解析
type Response struct {
}
func (r *Response) Parse(reqConfig *define.Request, response *define.Response) error {
r.fillResponseHeader(response)
r.fillResponseCookie(response)
return r.fillResponseBody(reqConfig, response)
}
// fillResponseHeader 填充响应header
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 21:30 2024/6/5
func (r *Response) fillResponseHeader(response *define.Response) {
response.Header = map[string]any{} // 清空已有数据
response.HttpCode = response.RestyResponse.StatusCode() // http状态码
response.HttpCodeStatus = response.RestyResponse.Status() // http状态码描述
for headerName, headerValue := range response.RestyResponse.Header() {
if len(headerValue) > 0 {
response.Header[headerName] = headerValue[0]
} else {
response.Header[headerName] = ""
}
}
}
// fillResponseCookie 填充cookie
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 21:32 2024/6/5
func (r *Response) fillResponseCookie(response *define.Response) {
response.Cookie = map[string]any{} // 清空已有数据
for _, cookieValue := range response.RestyResponse.Cookies() {
response.Cookie[cookieValue.Name] = cookieValue.Value
}
}
// fillResponseBody 填充响应body
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 21:38 2024/6/5
func (r *Response) fillResponseBody(reqCfg *define.Request, response *define.Response) error {
// 解析响应数据类型
responseContentType := response.RestyResponse.Header().Get(consts.HeaderKeyContentType.String())
if responseContentType == "" {
// 返回数据未说明 Content-Type
if response.RestyResponse.StatusCode() != http.StatusOK {
return nil
}
return errors.New("response content type is empty")
}
typeArr := strings.Split(strings.Split(responseContentType, ";")[0], "/")
responseType := "json"
if len(typeArr) > 0 {
responseType = typeArr[len(typeArr)-1]
}
parser := ResponseParserTable[responseType]
if parser == nil {
// 未读取到的, 不支持解析对应的响应数据
return errors.New("response type " + responseType + " is not support")
}
var (
err error
res map[string]any
jsonByte []byte
)
if err = parser.Unmarshal(response.RestyResponse.Bytes(), &res); nil != err {
return errors.New("response parse body error :" + err.Error())
}
if jsonByte, err = parser.MarshalForByte(res); nil != err {
return errors.New("response body Marshal error :" + err.Error())
}
response.Data = string(jsonByte)
if strings.ToLower(reqCfg.CodeLocation) == "header" {
if reqCfg.CodeField == "code" {
response.Code = fmt.Sprintf("%v", response.HttpCode)
response.Message = response.RestyResponse.Status()
} else {
response.Code = response.RestyResponse.Header().Get(reqCfg.CodeField)
response.Message = response.RestyResponse.Header().Get(reqCfg.MessageField)
}
} else {
// 统一认为Body
response.Code = gjson.Get(response.Data, reqCfg.CodeField).String()
response.Message = gjson.Get(response.Data, reqCfg.MessageField).String()
}
businessData := gjson.Get(response.Data, reqCfg.DataField)
if businessData.Value() == nil {
// data为空指针, 归一化成空对象
response.Body = map[string]any{}
} else {
if businessData.IsArray() {
// 数组类型的转换
response.Data = fmt.Sprintf(`{"list":` + businessData.String() + "}")
} else {
if businessData.IsObject() {
// 返回的就是对象
response.Data = businessData.String()
} else {
// 返回是普通类型
response.Data = serialize.JSON.MarshalForStringIgnoreError(map[string]any{
"value": businessData.Value(),
})
}
}
_ = serialize.JSON.UnmarshalWithNumber([]byte(response.Data), &response.Body)
}
response.ExtendData = map[string]string{}
gjson.Parse(response.Data).ForEach(func(key, value gjson.Result) bool {
if key.String() == reqCfg.CodeField ||
key.String() == reqCfg.MessageField ||
key.String() == reqCfg.DataField {
return true
}
response.ExtendData[key.String()] = value.String()
return true
})
return nil
}
// BusinessSuccess ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 22:48 2024/6/6
func (r *Response) BusinessSuccess(reqCfg *define.Request, response *define.Response) bool {
for _, itemSuccessCode := range reqCfg.SuccessCodeList {
if itemSuccessCode == response.Code {
return true
}
}
return false
}
// HttpSuccess ...
func (r *Response) HttpSuccess(reqCfg *define.Request, response *define.Response) bool {
if len(reqCfg.SuccessHttpCodeList) == 0 {
// 没配置, 则 2xx 均视为成功
return response.HttpCode/100 == 2
}
for _, itemHttpCode := range reqCfg.SuccessHttpCodeList {
if itemHttpCode == response.HttpCode {
return true
}
}
return false
}
// NeedRetry 是否需要重试
func (r *Response) NeedRetry(reqCfg *define.Request, response *define.Response) bool {
if nil == reqCfg.RetryRule {
// 未配置重试规则
return false
}
if reqCfg.RetryRule.RetryCount <= 0 {
// 未配置重试次数
return false
}
for _, httpCode := range reqCfg.RetryRule.RetryHttpCodeList {
if response.HttpCode == int(httpCode) {
return true
}
}
for _, businessCode := range reqCfg.RetryRule.RetryBusinessCodeList {
if businessCode == response.Code {
return true
}
}
return false
}

View File

@ -0,0 +1,37 @@
// Package implement ...
//
// Description : implement ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-07 14:53
package implement
import (
"git.zhangdeman.cn/zhangdeman/network/httpclient/abstract"
"git.zhangdeman.cn/zhangdeman/serialize"
)
// ResponseParserTable 响应数据解析器
// 此处为内置解析实现, 未加锁, 未做并发安全处理
// 原因: 无论 Add 还是 Delete, 均应在服务初始化阶段调用, 而在服务完成初始化, 完全启动之后, 应该只有读操作
// 如果强要运行时, 动态操作此配置表, 需在外部调用处自行加锁
// 也可完全自行实现相关解析实例, 在请求时通过 RequestOption 传入, 则控制权完全贵开发者所有
var ResponseParserTable = map[string]abstract.IResponseParser{
"json": serialize.JSON,
"xml": serialize.Xml,
"yml": serialize.Yml,
"toml": serialize.Toml,
"yaml": serialize.Yml,
"text": serialize.JSON,
}
// AddResponseParser 新增一个解析实现
func AddResponseParser(t string, parser abstract.IResponseParser) {
ResponseParserTable[t] = parser
}
// RemoveResponseParser 移除一个解析实现
func RemoveResponseParser(t string) {
delete(ResponseParserTable, t)
}

View File

@ -33,13 +33,17 @@ func Set(l *zap.Logger) {
// //
// Date : 18:19 2025/3/31 // Date : 18:19 2025/3/31
func Get() *zap.Logger { func Get() *zap.Logger {
if nil == logger {
// 返回日志空实现
return zap.NewNop()
}
return logger return logger
} }
var buildHttpLogDataFunc BuildHttpLogDataFunc var buildHttpLogDataFunc BuildHttpLogDataFunc
// BuildHttpLogDataFunc 构建http请求日志数据的方法 // BuildHttpLogDataFunc 构建http请求日志数据的方法
type BuildHttpLogDataFunc func(ctx context.Context, reqCfg *define.Request, response *define.Response) []zap.Field type BuildHttpLogDataFunc func(ctx context.Context, isResponseLog bool, businessData map[string]any, reqCfg *define.Request, response *define.Response) []zap.Field
func SetBuildDataFunc(f BuildHttpLogDataFunc) { func SetBuildDataFunc(f BuildHttpLogDataFunc) {
buildHttpLogDataFunc = f buildHttpLogDataFunc = f
@ -54,8 +58,8 @@ func GetBuildDataFunc() BuildHttpLogDataFunc {
// Author : go_developer@163.com<白茶清欢> // Author : go_developer@163.com<白茶清欢>
// //
// Date : 18:07 2025/3/31 // Date : 18:07 2025/3/31
func Record(ctx context.Context, logInstance *zap.Logger, level consts.LogLevel, msg string, reqCfg *define.Request, response *define.Response) { func Record(level consts.LogLevel, msg string, reqCfg *define.Request, response *define.Response) {
if nil == logInstance { if nil == reqCfg.Logger {
// 未设置日志实例 // 未设置日志实例
return return
} }
@ -64,7 +68,8 @@ func Record(ctx context.Context, logInstance *zap.Logger, level consts.LogLevel,
// 未设置构建日志数据的方法 // 未设置构建日志数据的方法
return return
} }
fields := buildDataFunc(ctx, reqCfg, response) fields := buildDataFunc(reqCfg.Ctx, true, nil, reqCfg, response)
logInstance := reqCfg.Logger
switch level { switch level {
case consts.LogLevelDebug: case consts.LogLevelDebug:
logInstance.Debug(msg, fields...) logInstance.Debug(msg, fields...)
@ -78,3 +83,63 @@ func Record(ctx context.Context, logInstance *zap.Logger, level consts.LogLevel,
logInstance.Panic(msg, fields...) logInstance.Panic(msg, fields...)
} }
} }
// RecordDebug 日志记录
func RecordDebug(msg string, businessData map[string]any, reqCfg *define.Request) {
if nil == reqCfg.Logger {
// 未设置日志实例
return
}
buildDataFunc := GetBuildDataFunc()
if nil == buildDataFunc {
// 未设置构建日志数据的方法
return
}
fields := buildDataFunc(reqCfg.Ctx, false, businessData, reqCfg, nil)
reqCfg.Logger.Debug(msg, fields...)
}
// RecordInfo ...
func RecordInfo(msg string, businessData map[string]any, reqCfg *define.Request) {
if nil == reqCfg.Logger {
// 未设置日志实例
return
}
buildDataFunc := GetBuildDataFunc()
if nil == buildDataFunc {
// 未设置构建日志数据的方法
return
}
fields := buildDataFunc(reqCfg.Ctx, false, businessData, reqCfg, nil)
reqCfg.Logger.Info(msg, fields...)
}
// RecordWarn ...
func RecordWarn(msg string, businessData map[string]any, reqCfg *define.Request) {
if nil == reqCfg.Logger {
// 未设置日志实例
return
}
buildDataFunc := GetBuildDataFunc()
if nil == buildDataFunc {
// 未设置构建日志数据的方法
return
}
fields := buildDataFunc(reqCfg.Ctx, false, businessData, reqCfg, nil)
reqCfg.Logger.Warn(msg, fields...)
}
// RecordError ...
func RecordError(msg string, businessData map[string]any, reqCfg *define.Request) {
if nil == reqCfg.Logger {
// 未设置日志实例
return
}
buildDataFunc := GetBuildDataFunc()
if nil == buildDataFunc {
// 未设置构建日志数据的方法
return
}
fields := buildDataFunc(reqCfg.Ctx, false, businessData, reqCfg, nil)
reqCfg.Logger.Error(msg, fields...)
}

View File

@ -10,7 +10,7 @@ package mesh
import ( import (
"context" "context"
"git.zhangdeman.cn/zhangdeman/consts" "git.zhangdeman.cn/zhangdeman/consts"
"git.zhangdeman.cn/zhangdeman/network/httpclient/cache" "git.zhangdeman.cn/zhangdeman/network/httpclient/abstract"
"git.zhangdeman.cn/zhangdeman/network/httpclient/define" "git.zhangdeman.cn/zhangdeman/network/httpclient/define"
"sync" "sync"
) )
@ -38,7 +38,10 @@ type RequestConfigGroupItem struct {
RequestCfg *define.Request `json:"request_cfg"` // 请求配置 RequestCfg *define.Request `json:"request_cfg"` // 请求配置
FailBehavior *RequestConfigGroupItemFailBehavior `json:"fail_behavior"` // 失败的行为, 不配置, 默认失败break FailBehavior *RequestConfigGroupItemFailBehavior `json:"fail_behavior"` // 失败的行为, 不配置, 默认失败break
FinalFailureAllow bool `json:"final_failure_allow"` // 已经确定当前请求是最终失败了,当前请求是否允许执行 FinalFailureAllow bool `json:"final_failure_allow"` // 已经确定当前请求是最终失败了,当前请求是否允许执行
CacheInstance cache.ICache `json:"-"` // 数据缓存实例 CacheInstance abstract.ICache `json:"-"` // 数据缓存实例
RateLimiter abstract.RateLimiter `json:"-"` // 流控实例
ResponseParser abstract.IResponse `json:"-"` // 响应数据解析
RequestBodyWrite abstract.IRequestBodyWrite `json:"-"` // 请求Body数据写入的实现
Condition any `json:"condition"` // TODO: 请求条件, 特定条件下不执行当前请求 Condition any `json:"condition"` // TODO: 请求条件, 特定条件下不执行当前请求
} }
@ -48,8 +51,7 @@ type RequestConfigGroupItem struct {
// //
// Date : 15:51 2025/3/28 // Date : 15:51 2025/3/28
type RequestConfigGroupItemFailBehavior struct { type RequestConfigGroupItemFailBehavior struct {
Action string `json:"action"` // 请求失败的行为: continue: 继续执行下一个请求, break: 停止执行后续请求 Action string `json:"action"` // 请求失败的行为: continue: 继续执行下一个请求, error: 停止执行后续请求, 并且整体请求失败
FinalFailure bool `json:"final_failure"` // 是否作为最终失败: action = break时, 无论外部是什么值, 永远为true, 当 action = continue时, 该值由外部传入, 原因: 当前接口失败, 可能还需要调用一些后续接口做些逻辑.
} }
// RequestConfigGroupItemParamRule 参数提取规则 // RequestConfigGroupItemParamRule 参数提取规则

View File

@ -95,18 +95,19 @@ func (c *client) Request() *Response {
Errmsg: "", Errmsg: "",
}) })
} }
resByte, err := validate.Run(respByte, fieldList) validateRes, err := validate.Run(respByte, fieldList)
if nil != err { if nil != err {
c.resp.ErrorCode = "-500" c.resp.ErrorCode = "-500"
c.resp.ErrorMessage = err.Error() c.resp.ErrorMessage = err.Error()
return c.resp return c.resp
} }
c.resp.Raw = respByte // 原始返回数据 c.resp.Raw = respByte // 原始返回数据
serialize.JSON.UnmarshalWithNumberIgnoreError(resByte, &c.resp.DataMap) // map结果 c.resp.DataMap, _ = validateRes.Map("json")
// serialize.JSON.UnmarshalWithNumberIgnoreError(resByte, &c.resp.DataMap) // map结果
if nil != c.receiver { if nil != c.receiver {
// 解析到receiver // 解析到receiver
if err = serialize.JSON.UnmarshalWithNumber(resByte, c.receiver); nil != err { if err = validateRes.Transform("json", c.receiver); nil != err {
c.resp.ErrorCode = "-500" c.resp.ErrorCode = "-500"
c.resp.ErrorMessage = err.Error() c.resp.ErrorMessage = err.Error()
return c.resp return c.resp
@ -146,7 +147,11 @@ func (c *client) doRequest(apiList []*RequestConfigGroupItem) bool {
apiCfg.RequestCfg.Header = param[strings.ToLower(consts.RequestDataLocationHeader.String())] // header apiCfg.RequestCfg.Header = param[strings.ToLower(consts.RequestDataLocationHeader.String())] // header
apiCfg.RequestCfg.Cookie = param[strings.ToLower(consts.RequestDataLocationCookie.String())] // cookie apiCfg.RequestCfg.Cookie = param[strings.ToLower(consts.RequestDataLocationCookie.String())] // cookie
apiCfg.RequestCfg.Query = param[strings.ToLower(consts.RequestDataLocationQuery.String())] // query apiCfg.RequestCfg.Query = param[strings.ToLower(consts.RequestDataLocationQuery.String())] // query
if httpClient, err = httpclient.NewHttpClient(apiCfg.RequestCfg, apiCfg.CacheInstance); nil != err { if httpClient, err = httpclient.NewHttpClient(apiCfg.RequestCfg, &httpclient.RequestOption{
CacheInstance: apiCfg.CacheInstance,
RateLimiter: apiCfg.RateLimiter,
ResponseParser: apiCfg.ResponseParser,
}); nil != err {
// 此处获取客户端实例即发生异常, 忽略一切配置, 直接作为全局失败, 后续也不请求了 // 此处获取客户端实例即发生异常, 忽略一切配置, 直接作为全局失败, 后续也不请求了
c.resp.ErrorCode = "-500" c.resp.ErrorCode = "-500"
c.resp.ErrorMessage = err.Error() c.resp.ErrorMessage = err.Error()
@ -173,9 +178,12 @@ func (c *client) doRequest(apiList []*RequestConfigGroupItem) bool {
c.resp.Lock.Lock() c.resp.Lock.Lock()
defer c.resp.Lock.Unlock() defer c.resp.Lock.Unlock()
if !resp.IsSuccess { if !resp.IsSuccess {
// 哪些接口请求失败: 注意,接口请求失败不代表整体失败,接口可能针对失败配置规则 continue
c.resp.FailApiAlias = append(c.resp.FailApiAlias, apiCfg.Alias)
// 判断是否已经是最终失败 // 判断是否已经是最终失败
if apiCfg.FailBehavior.FinalFailure || apiCfg.FailBehavior.Action == FailBehaviorError { if apiCfg.FailBehavior.Action == FailBehaviorError {
c.resp.FinalFailure = true c.resp.FinalFailure = true
c.resp.FailureApiAlias = apiCfg.Alias // 导致最终请求失败的接口
// 判断是否继续, 只能阻断后续分组请求,无法阻断当前租的请求 // 判断是否继续, 只能阻断后续分组请求,无法阻断当前租的请求
isContinue = FailBehaviorContinue == apiCfg.FailBehavior.Action isContinue = FailBehaviorContinue == apiCfg.FailBehavior.Action
} }
@ -191,14 +199,9 @@ func (c *client) doRequest(apiList []*RequestConfigGroupItem) bool {
func (c *client) initApiCfg(apiCfg *RequestConfigGroupItem) { func (c *client) initApiCfg(apiCfg *RequestConfigGroupItem) {
if apiCfg.FailBehavior == nil { if apiCfg.FailBehavior == nil {
apiCfg.FailBehavior = &RequestConfigGroupItemFailBehavior{ apiCfg.FailBehavior = &RequestConfigGroupItemFailBehavior{
Action: FailBehaviorError, // 默认失败终止请求 Action: FailBehaviorError, // 默认失败终止请求
FinalFailure: true, // 默认一旦失败,则争个整个失败
} }
} }
if apiCfg.FailBehavior.Action == FailBehaviorError {
// 配置了请求失败则报错, 一定是导致结果最终失败
apiCfg.FailBehavior.FinalFailure = true
}
// 每一个请求有独立的context // 每一个请求有独立的context
apiCfg.RequestCfg.Ctx = context.WithValue(c.reqCfg.Ctx, "alias", apiCfg.Alias) apiCfg.RequestCfg.Ctx = context.WithValue(c.reqCfg.Ctx, "alias", apiCfg.Alias)
} }
@ -238,7 +241,7 @@ func (c *client) buildRequestParams(apiCfg *RequestConfigGroupItem) (map[string]
return nil, err return nil, err
} }
var d map[string]map[string]any var d map[string]map[string]any
if err = serialize.JSON.UnmarshalWithNumber(buildRes, &d); nil != err { if err = buildRes.Transform("json", &d); nil != err {
return nil, err return nil, err
} }
return d, err return d, err

20
httpclient/option.go Normal file
View File

@ -0,0 +1,20 @@
// Package httpclient ...
//
// Description : httpclient ...
//
// Author : go_developer@163.com<白茶清欢>
//
// Date : 2025-05-07 12:19
package httpclient
import (
"git.zhangdeman.cn/zhangdeman/network/httpclient/abstract"
)
// RequestOption 请求一些选项
type RequestOption struct {
CacheInstance abstract.ICache `json:"-"` // 数据结果缓存实例
RateLimiter abstract.RateLimiter `json:"-"` // 流控实例
RequestBodyWrite abstract.IRequestBodyWrite `json:"-"` // 将请求Body写入请求实例
ResponseParser abstract.IResponse `json:"-"` // 返回结果解析, 不配置使用内置实现
}

View File

@ -8,38 +8,96 @@
package httpclient package httpclient
import ( import (
"encoding/json" "errors"
"fmt"
"git.zhangdeman.cn/zhangdeman/consts" "git.zhangdeman.cn/zhangdeman/consts"
"git.zhangdeman.cn/zhangdeman/network/httpclient/define" "git.zhangdeman.cn/zhangdeman/network/httpclient/define"
"git.zhangdeman.cn/zhangdeman/serialize" requestBoodyWriter "git.zhangdeman.cn/zhangdeman/network/httpclient/implement/request"
"git.zhangdeman.cn/zhangdeman/wrapper" "git.zhangdeman.cn/zhangdeman/wrapper"
"github.com/go-resty/resty/v2"
"github.com/tidwall/gjson"
"net/http" "net/http"
"net/textproto" "net/textproto"
"resty.dev/v3"
"strings" "strings"
) )
// 初始化请求配置, 包括:
// 1. 初始化空指针
// 2. 合并static参数
// 3. 使用 textproto.CanonicalMIMEHeaderKey 统一格式化header key
func initRequestConfig(reqConfig *define.Request) {
if nil == reqConfig.Header {
reqConfig.Header = map[string]any{}
}
if nil == reqConfig.Body {
reqConfig.Body = map[string]any{}
}
if nil == reqConfig.Cookie {
reqConfig.Cookie = map[string]any{}
}
if reqConfig.Query == nil {
reqConfig.Query = map[string]any{}
}
if reqConfig.PathParam == nil {
reqConfig.PathParam = map[string]string{}
}
// 合并静态参数, 传入参数与静态参数存在相同参数, 以静态参数为准
for loc, paramTable := range reqConfig.Static {
switch strings.ToUpper(loc) {
case consts.RequestDataLocationBody.String():
for name, val := range paramTable {
reqConfig.Body[name] = val
}
case consts.RequestDataLocationQuery.String():
for name, val := range paramTable {
reqConfig.Query[name] = val
}
case consts.RequestDataLocationCookie.String():
for name, val := range paramTable {
reqConfig.Cookie[name] = val
}
case consts.RequestDataLocationHeader.String():
for name, val := range paramTable {
reqConfig.Header[name] = val
}
case consts.RequestDataLocationUriPath.String():
for name, val := range paramTable {
reqConfig.PathParam[name] = fmt.Sprintf("%v", val)
}
}
}
formatHeader(reqConfig)
}
// NewRestyClient 获取resty client // NewRestyClient 获取resty client
// //
// Author : go_developer@163.com<白茶清欢> // Author : go_developer@163.com<白茶清欢>
// //
// Date : 15:00 2024/5/31 // Date : 15:00 2024/5/31
func NewRestyClient(reqConfig *define.Request) (*resty.Client, *resty.Request) { func NewRestyClient(reqConfig *define.Request, reqOption *RequestOption) (*resty.Client, *resty.Request, error) {
client := resty.New() client := resty.New()
request := client.R() request := client.R()
if nil == reqConfig { if nil == reqConfig {
return client, request return nil, nil, errors.New("request config is nil")
} }
if nil != reqConfig.RateLimiter { // 限流处理, 增加限流中间件
// 设置流控实例 client.AddRequestMiddleware(func(client *resty.Client, request *resty.Request) error {
client.SetRateLimiter(reqConfig.RateLimiter) if nil == reqOption || nil == reqOption.RateLimiter {
} // 未配置流控
formatHeader(reqConfig) return nil
client.SetAllowGetMethodPayload(true) // 配置 GET 请求允许带 Body }
client.SetJSONMarshaler(json.Marshal) // 序列化方法 if !reqOption.RateLimiter.Allow(reqConfig) {
client.SetJSONEscapeHTML(true) // 处理html实体字符 // 命中流控
client.SetJSONUnmarshaler(serialize.JSON.UnmarshalWithNumber) // 反序列化方法 return define.ErrRateLimitExceeded
}
return nil
})
initRequestConfig(reqConfig) // 初始化 + 格式化配置
client.SetAllowMethodGetPayload(true) // 配置 GET 请求允许带 Body
client.SetAllowMethodDeletePayload(true) // 配置 DELETE 请求允许带 Body
client.SetJSONEscapeHTML(true) // 处理html实体字符
request.SetPathParams(reqConfig.PathParam) // 设置path中的参数 request.SetPathParams(reqConfig.PathParam) // 设置path中的参数
query := map[string]string{} query := map[string]string{}
@ -59,12 +117,8 @@ func NewRestyClient(reqConfig *define.Request) (*resty.Client, *resty.Request) {
} }
request.SetHeaders(header) // 设置header request.SetHeaders(header) // 设置header
request.URL = reqConfig.FullUrl // 请求接口 request.URL = reqConfig.FullUrl // 请求接口
for pathParamName, pathParamValue := range reqConfig.PathParam { // 设置url参数, url path格式需要是 /user/{user_id}/detail格式, {user_id} 会自动替换
if len(pathParamValue) == 0 { request.SetPathParams(reqConfig.PathParam)
continue
}
reqConfig.FullUrl = strings.ReplaceAll(reqConfig.FullUrl, "{#"+pathParamName+"#}", pathParamValue)
}
request.Method = reqConfig.Method // 请求方法 request.Method = reqConfig.Method // 请求方法
cookieList := make([]*http.Cookie, 0) cookieList := make([]*http.Cookie, 0)
for cookieName, cookieValue := range reqConfig.Cookie { for cookieName, cookieValue := range reqConfig.Cookie {
@ -76,9 +130,11 @@ func NewRestyClient(reqConfig *define.Request) (*resty.Client, *resty.Request) {
Value: wrapper.AnyDataType(cookieValue).ToString().Value(), Value: wrapper.AnyDataType(cookieValue).ToString().Value(),
}) })
} }
request.SetCookies(cookieList) // 设置cookie request.SetCookies(cookieList) // 设置cookie
setRestyBody(reqConfig, request) // 设置请求Body if err := setRestyBody(reqConfig, reqOption, request); nil != err {
return client, request return nil, nil, err
}
return client, request, nil
} }
// setRestyBody 设置请求BODY // setRestyBody 设置请求BODY
@ -86,25 +142,17 @@ func NewRestyClient(reqConfig *define.Request) (*resty.Client, *resty.Request) {
// Author : go_developer@163.com<白茶清欢> // Author : go_developer@163.com<白茶清欢>
// //
// Date : 17:18 2024/5/31 // Date : 17:18 2024/5/31
func setRestyBody(reqConfig *define.Request, request *resty.Request) { func setRestyBody(reqConfig *define.Request, requestOption *RequestOption, request *resty.Request) error {
if nil == reqConfig.Body || len(reqConfig.Body) == 0 { if nil == reqConfig.Body {
return return nil
} }
if strings.Contains(strings.ToLower(reqConfig.ContentType), consts.MimeTypeJson) { if nil != requestOption && nil != requestOption.RequestBodyWrite {
request.SetBody(reqConfig.Body) // 外部传入的实现
return requestOption.RequestBodyWrite.Write(request, reqConfig.Body)
return nil
} }
if strings.Contains(strings.ToLower(reqConfig.ContentType), consts.MimeTypeXWWWFormUrlencoded) { // 外部没传入, 使用内置实现, 内置默认实现支持: xml/json/form, 如需其他, 自行扩展
bodyStr := serialize.JSON.MarshalForStringIgnoreError(reqConfig.Body) return requestBoodyWriter.WriteBody(reqConfig.ContentType, request, reqConfig.Body)
bodyData := map[string]string{}
jsonObj := gjson.Parse(bodyStr)
jsonObj.ForEach(func(key, value gjson.Result) bool {
bodyData[key.String()] = value.String()
return true
})
request.SetFormData(bodyData)
}
return
} }
// formatHeader 格式化header // formatHeader 格式化header