Compare commits
71 Commits
1c57dcf5af
...
master
Author | SHA1 | Date | |
---|---|---|---|
0e6bd9e669 | |||
0b2546217c | |||
42b9f26a9a | |||
5ddab034f3 | |||
a56528f2ee | |||
7b1a36a7c3 | |||
21060e4913 | |||
70d044e3a3 | |||
8ddb1f72b2 | |||
1403693fda | |||
1abe6c1c29 | |||
4767aeb504 | |||
bfbcecb498 | |||
2cb3d44ec9 | |||
e02ee1fef9 | |||
ba47891d3c | |||
6dcd072570 | |||
f558f78e1c | |||
1fe39e0547 | |||
38f97bc69f | |||
b335d7c2bb | |||
741c9c8ea0 | |||
088670c7d4 | |||
94fdab4e36 | |||
f1a65c1ed8 | |||
1ca3c2d094 | |||
370d98f2a8 | |||
db679f05e9 | |||
19992ab41c | |||
9bd3030cb8 | |||
78f6b61d65 | |||
5592e0a45c | |||
f780551ba6 | |||
84cf228b5d | |||
e8e02d9848 | |||
ecf2bd6b55 | |||
04927ae811 | |||
1a55576e10 | |||
834ce55f26 | |||
60dd279663 | |||
dbd9ab31a1 | |||
2d08e9a629 | |||
06587ace43 | |||
98b96051e7 | |||
fd767af279 | |||
420fa27f5c | |||
b6c908a08a | |||
655b6d3a21 | |||
53ed0fe04f | |||
7ebc610c2e | |||
b33afff40c | |||
f696c0e871 | |||
311f896f0f | |||
e8532e4b24 | |||
b59cef44ca | |||
6a5aefafae | |||
1d8ee18a03 | |||
5d790ff1e7 | |||
f14401a39f | |||
17703d2521 | |||
abe6e9da6b | |||
b9bd189ade | |||
94cbccc047 | |||
d9da985da5 | |||
93a855d1ae | |||
99f3229b88 | |||
ae373db179 | |||
f005012b36 | |||
4e538b59e0 | |||
96632368a8 | |||
5b296b74b0 |
37
go.mod
37
go.mod
@ -1,16 +1,47 @@
|
||||
module git.zhangdeman.cn/zhangdeman/network
|
||||
|
||||
go 1.20
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
git.zhangdeman.cn/zhangdeman/exception v0.0.0-20250207091724-ca151fbc1f06
|
||||
github.com/ddliu/go-httpclient v0.7.1
|
||||
git.zhangdeman.cn/gateway/validate v0.0.0-20250506091017-1d2dfd10f600
|
||||
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250425024726-cc17224cb995
|
||||
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250504055908-8d68e6106ea9
|
||||
git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250321102712-1cbfbe959740
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
|
||||
github.com/mssola/user_agent v0.6.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
go.uber.org/zap v1.27.0
|
||||
resty.dev/v3 v3.0.0-beta.2
|
||||
)
|
||||
|
||||
require (
|
||||
git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250429065800-fc340b9417cf // 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/util v0.0.0-20240618042405-6ee2c904644e // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect
|
||||
github.com/creasty/defaults v1.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mozillazg/go-pinyin v0.20.0 // 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/stretchr/testify v1.10.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
98
go.sum
98
go.sum
@ -1,15 +1,74 @@
|
||||
git.zhangdeman.cn/zhangdeman/exception v0.0.0-20230819024237-f674702ad28d h1:XqjAykNMm/ai1+CebqcxBKXmIQqZNyYmTeIufLTXQJc=
|
||||
git.zhangdeman.cn/zhangdeman/exception v0.0.0-20230819024237-f674702ad28d/go.mod h1:Voc8J4ordx7nuMWpgACXXZULQy7ZIuBzcEIoS8VnDIw=
|
||||
git.zhangdeman.cn/zhangdeman/exception v0.0.0-20250207091724-ca151fbc1f06 h1:XsjGMkBCi93h56oCg5Lrz5zVpUxify/CQVhQU9+qLWM=
|
||||
git.zhangdeman.cn/zhangdeman/exception v0.0.0-20250207091724-ca151fbc1f06/go.mod h1:Voc8J4ordx7nuMWpgACXXZULQy7ZIuBzcEIoS8VnDIw=
|
||||
github.com/ddliu/go-httpclient v0.7.1 h1:COWYBalfbaFNe6e0eQU38++vCD5kzLh1H1RFs3xcn9g=
|
||||
github.com/ddliu/go-httpclient v0.7.1/go.mod h1:uwipe9x9SYGk4JhBemO7+dD87QbiY224y0DLB9OY0Ik=
|
||||
git.zhangdeman.cn/gateway/validate v0.0.0-20250506091017-1d2dfd10f600 h1:o+ggli1YCXiiB/SwlPqb34DePkP6mh+KqhQ/lYhBISE=
|
||||
git.zhangdeman.cn/gateway/validate v0.0.0-20250506091017-1d2dfd10f600/go.mod h1:DHrM753rWJ7Dvrm7CqWq1Q05jMhiGPNMKXYcKp11BHg=
|
||||
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250425024726-cc17224cb995 h1:LmPRAf0AsxRVFPibdpZR89ajlsz8hof2IvMMyTqiEq4=
|
||||
git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250425024726-cc17224cb995/go.mod h1:5p8CEKGBxi7qPtTXDI3HDmqKAfIm5i/aBWdrbkbdNjc=
|
||||
git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250429065800-fc340b9417cf h1:xCPM3U6i62UvLo9VNvDP45Ue3dPl7ratHu1rSEJRE2k=
|
||||
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-20250506090824-b0db389ca3d7 h1:fBf+lN63axb8zYwqCVbexzPm8x9W4aWxzzIJIXIxS2U=
|
||||
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/go.mod h1:VHb9qmhaPDAQDcS6vUiDCamYjZ4R5lD1XtVsh55KsMI=
|
||||
git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20250504055908-8d68e6106ea9 h1:/GLQaFoLb+ciHOtAS2BIyPNnf4O5ME3AC5PUaJY9kfs=
|
||||
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/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/go.mod h1:1ct92dbVc49pmXusA/iGfcQUJzcYmJ+cjAhgc3sDv1I=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ=
|
||||
github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg=
|
||||
github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYKk=
|
||||
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
|
||||
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/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/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
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/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/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
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/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
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/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/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mozillazg/go-pinyin v0.20.0 h1:BtR3DsxpApHfKReaPO1fCqF4pThRwH9uwvXzm+GnMFQ=
|
||||
github.com/mozillazg/go-pinyin v0.20.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
|
||||
github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4=
|
||||
github.com/mssola/user_agent v0.6.0/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=
|
||||
github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg=
|
||||
github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
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/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/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec=
|
||||
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
|
||||
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@ -17,5 +76,28 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
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=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
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=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
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/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=
|
||||
|
17
httpclient/README.md
Normal file
17
httpclient/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# httpclient
|
||||
|
||||
## 库作用
|
||||
|
||||
- 对第三方接口发起请求
|
||||
|
||||
## 依赖基础库
|
||||
|
||||
- [resty](https://github.com/go-resty/resty)
|
||||
|
||||
## 配置说明
|
||||
|
||||
## 默认配置
|
||||
|
||||
- AllowGetMethodPayload : 允许GET请求带Body, 默认值 **`true`** , 如需关闭, 可通过 HttpClient.GetRestyClient().SetAllowGetMethodPayload(false) 关闭
|
||||
|
||||
## 使用方式
|
36
httpclient/abstract/ICache.go
Normal file
36
httpclient/abstract/ICache.go
Normal file
@ -0,0 +1,36 @@
|
||||
// Package abstract ...
|
||||
//
|
||||
// Description : cache ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2024-06-03 15:49
|
||||
package abstract
|
||||
|
||||
import (
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
|
||||
)
|
||||
|
||||
// ICache 缓存定义
|
||||
type ICache interface {
|
||||
// Enable 是否启用缓存(总开关)
|
||||
Enable() bool
|
||||
// CacheTime 缓存时长, 单位 : s , 默认 1800, 最小值 90, 设置失效时间, 会上下波动60s, 避免缓存集中失效
|
||||
CacheTime() int64
|
||||
// IsAllow 针对当前请求数据和状态, 是否允许缓存
|
||||
IsAllow(reqCfg *define.Request, response *define.Response) bool
|
||||
// GetKey 获取缓存key
|
||||
GetKey(reqCfg *define.Request) string
|
||||
// GetValue 获取缓存值
|
||||
GetValue(cacheKey string) string
|
||||
// SetValue 设置缓存
|
||||
SetValue(cacheKey string, cacheValue string) error
|
||||
// TTL 缓存剩余生命周期(单位: s)
|
||||
TTL(cacheKey string) int64
|
||||
// PreHeatConfig 缓存预热配置
|
||||
PreHeatConfig() *define.CachePreHeatConfig
|
||||
// Lock 设置缓存时加锁
|
||||
Lock(lockKey string) error
|
||||
// Unlock 完成缓存设置时, 释放锁
|
||||
Unlock(lockKey string) error
|
||||
}
|
15
httpclient/abstract/IRateLimiter.go
Normal file
15
httpclient/abstract/IRateLimiter.go
Normal 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
|
||||
}
|
16
httpclient/abstract/IRequestBodyWrite.go
Normal file
16
httpclient/abstract/IRequestBodyWrite.go
Normal 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
|
||||
}
|
32
httpclient/abstract/IResponse.go
Normal file
32
httpclient/abstract/IResponse.go
Normal 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
|
||||
}
|
530
httpclient/client.go
Normal file
530
httpclient/client.go
Normal file
@ -0,0 +1,530 @@
|
||||
// Package httpclient ...
|
||||
//
|
||||
// Description : httpclient ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2024-05-31 15:22
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient/implement"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.zhangdeman.cn/zhangdeman/consts"
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient/log"
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient/validate"
|
||||
"git.zhangdeman.cn/zhangdeman/serialize"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
// NewHttpClient 获取http client
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 15:27 2024/5/31
|
||||
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 {
|
||||
reqConfig.Logger = log.Get() // 未单独指定日志实例, 则使用全局日志实例
|
||||
}
|
||||
// 验证配置正确性以及初始化默认值
|
||||
if err := validate.RequestConfig(reqConfig); 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 {
|
||||
for loc, valMap := range reqConfig.Static {
|
||||
if len(valMap) == 0 {
|
||||
continue
|
||||
}
|
||||
l := strings.ToUpper(loc)
|
||||
switch l {
|
||||
case consts.RequestDataLocationHeader.String():
|
||||
if reqConfig.Header == nil {
|
||||
reqConfig.Header = make(map[string]any)
|
||||
}
|
||||
for k, v := range valMap {
|
||||
reqConfig.Header[k] = v
|
||||
}
|
||||
case consts.RequestDataLocationCookie.String():
|
||||
if reqConfig.Cookie == nil {
|
||||
reqConfig.Cookie = make(map[string]any)
|
||||
}
|
||||
for k, v := range valMap {
|
||||
reqConfig.Cookie[k] = v
|
||||
}
|
||||
case consts.RequestDataLocationBody.String():
|
||||
if reqConfig.Body == nil {
|
||||
reqConfig.Body = make(map[string]any)
|
||||
}
|
||||
for k, v := range valMap {
|
||||
reqConfig.Body[k] = v
|
||||
}
|
||||
case consts.RequestDataLocationQuery.String():
|
||||
if reqConfig.Query == nil {
|
||||
reqConfig.Query = make(map[string]any)
|
||||
}
|
||||
for k, v := range valMap {
|
||||
reqConfig.Query[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if reqConfig.Header == nil {
|
||||
reqConfig.Header = make(map[string]any)
|
||||
}
|
||||
if ua, exist := reqConfig.Header[consts.HeaderKeyUserAgent.String()]; !exist || nil == ua || fmt.Sprintf("%v", ua) == "" {
|
||||
reqConfig.Header[consts.HeaderKeyUserAgent.String()] = "resty-v3@network/httpclient"
|
||||
}
|
||||
restyClient, restyRequest, err := NewRestyClient(reqConfig, reqOption)
|
||||
if nil != err {
|
||||
if nil != restyClient {
|
||||
_ = restyClient.Close()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer restyClient.Close()
|
||||
|
||||
hc := &HttpClient{
|
||||
Client: restyClient,
|
||||
request: restyRequest,
|
||||
reqOption: reqOption,
|
||||
reqCfg: reqConfig,
|
||||
requestFinishHandler: make([]define.RequestFinishHandler, 0),
|
||||
}
|
||||
hc.OnRequestFinish(func(req *define.Request, rep *define.Response) {
|
||||
if rep.IsSuccess {
|
||||
// 请求成功
|
||||
log.Record(consts.LogLevelInfo, "接口请求成功", req, rep)
|
||||
return
|
||||
}
|
||||
// 请求失败
|
||||
log.Record(consts.LogLevelError, "接口请求失败", req, rep)
|
||||
})
|
||||
return hc, nil
|
||||
}
|
||||
|
||||
// HttpClient 请求客户端
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 15:27 2024/5/31
|
||||
type HttpClient struct {
|
||||
*resty.Client
|
||||
request *resty.Request
|
||||
reqOption *RequestOption
|
||||
reqCfg *define.Request
|
||||
requestFinishHandler []define.RequestFinishHandler
|
||||
}
|
||||
|
||||
// OnRequestFinish 请求完成事件
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 18:36 2024/6/1
|
||||
func (hc *HttpClient) OnRequestFinish(handlerList ...define.RequestFinishHandler) {
|
||||
hc.requestFinishHandler = append(hc.requestFinishHandler, handlerList...)
|
||||
}
|
||||
|
||||
// GetRestyClient 获取 resty client
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 15:57 2024/5/31
|
||||
func (hc *HttpClient) GetRestyClient() *resty.Client {
|
||||
return hc.Client
|
||||
}
|
||||
|
||||
// Request 发送请求
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 15:52 2024/5/31
|
||||
func (hc *HttpClient) Request() *define.Response {
|
||||
var (
|
||||
cacheResult *define.Response
|
||||
)
|
||||
|
||||
if cacheResult = hc.getCacheResult(); nil == cacheResult {
|
||||
// 未命中缓存, 直接请求后端接口, 无需检测预热等逻辑
|
||||
return hc.requestBackendApi()
|
||||
}
|
||||
// 上面若命中缓存, 则后续缓存实例不可能为nil, 无需判断
|
||||
// 判断是否开启预热
|
||||
cachePreHeatConfig := hc.reqOption.CacheInstance.PreHeatConfig()
|
||||
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() {
|
||||
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 {
|
||||
log.RecordDebug("接口请求命中缓存, 强制执行缓存预热, 忽略其他策略配置", map[string]any{
|
||||
"cache_pre_heat_config": cachePreHeatConfig,
|
||||
}, hc.reqCfg)
|
||||
_ = hc.requestBackendApi()
|
||||
return
|
||||
}
|
||||
// 将百分比的配置归一化成最小剩余时间的配置
|
||||
if cachePreHeatConfig.MinPercent > 0 {
|
||||
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 {
|
||||
cachePreHeatConfig.MinTTL = expectMinTTL
|
||||
}
|
||||
}
|
||||
if cachePreHeatConfig.MinTTL <= 0 {
|
||||
// 未配置最小剩余时间
|
||||
log.RecordDebug("接口请求命中缓存, 未配置预热市场策略, 不执行预热", map[string]any{
|
||||
"min_ttl": cachePreHeatConfig.MinTTL,
|
||||
}, hc.reqCfg)
|
||||
return
|
||||
}
|
||||
ttl := hc.reqOption.CacheInstance.TTL(cacheResult.CacheInfo.CacheKey)
|
||||
if ttl < 0 {
|
||||
// 不存在或者未设置有效期
|
||||
log.RecordDebug("接口请求命中缓存, 当前缓存结果不存在或未设置有效期, 不执行预热", map[string]any{
|
||||
"min_ttl": cachePreHeatConfig.MinTTL,
|
||||
"note": "预热时间至少在缓存过期前10s触发预热, 以保证足够时间进行预热, 以及不会因为预热尚未完成, 但是大量流量涌入, 进而导致流量穿透",
|
||||
}, hc.reqCfg)
|
||||
return
|
||||
}
|
||||
|
||||
if ttl > cachePreHeatConfig.MinTTL {
|
||||
log.RecordDebug("接口请求命中缓存, 缓存结果有效期剩余时长大于配置阈值, 不执行预热", map[string]any{
|
||||
"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)
|
||||
}()
|
||||
}()
|
||||
// 命中缓存必然请求成功, 直接记录成功日志即可
|
||||
log.Record(consts.LogLevelInfo, "接口请求成功:命中缓存", hc.reqCfg, cacheResult)
|
||||
return cacheResult
|
||||
}
|
||||
|
||||
// requestBackendApi 请求后端接口
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 18:47 2024/10/9
|
||||
func (hc *HttpClient) requestBackendApi() *define.Response {
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
var response *define.Response
|
||||
// +1 是因为正常便会请求一次, 正常请求1次 + 重试次数 = 请求总次数
|
||||
for i := 0; i < hc.reqCfg.RetryRule.RetryCount+1; i++ {
|
||||
if i > 0 && nil != response {
|
||||
// 说明是重试, 记录上一次的请求信息
|
||||
response.RequestFinishTime = time.Now().UnixMilli()
|
||||
response.UsedTime = response.RequestFinishTime - response.RequestStartTime
|
||||
for _, itemAfterResponse := range hc.requestFinishHandler {
|
||||
itemAfterResponse(hc.reqCfg, response)
|
||||
}
|
||||
// 非首次请求, 说明是重试, 暂停指定时间间隔
|
||||
time.Sleep(time.Duration(hc.reqCfg.RetryRule.RetryTimeInterval) * time.Millisecond)
|
||||
}
|
||||
response = hc.newResponse()
|
||||
response.Seq = i
|
||||
response.RequestCount = i + 1
|
||||
if response.RestyResponse, err = hc.request.Send(); nil != err {
|
||||
errType := define.RequestFailTypeSend
|
||||
if err.Error() == define.ErrRateLimitExceeded.Error() {
|
||||
// 命中限流
|
||||
errType = define.RequestFailTypeRateLimit
|
||||
} else if netErr, ok := err.(net.Error); ok {
|
||||
if netErr.Timeout() {
|
||||
// 请求超时
|
||||
errType = define.RequestFailTypeTimeoutError
|
||||
// 重置响应状态码
|
||||
response.HttpCode = 499
|
||||
response.HttpCodeStatus = "request timeout"
|
||||
}
|
||||
}
|
||||
response.FailInfo = &define.ResponseFailInfo{
|
||||
Type: errType,
|
||||
Message: err.Error(),
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if nil == response.RestyResponse {
|
||||
response.FailInfo = &define.ResponseFailInfo{
|
||||
Type: define.RequestFailTypeSend,
|
||||
Message: "response instance is nil",
|
||||
}
|
||||
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
|
||||
}
|
||||
// 解析返回信息
|
||||
hc.reqOption.ResponseParser.Parse(hc.reqCfg, response)
|
||||
// 配置了当前code为成功, 或者未配置任何code, 当前code为2xx, 则认为请求成功
|
||||
isHttpSuccess := hc.reqOption.ResponseParser.HttpSuccess(hc.reqCfg, response)
|
||||
if !isHttpSuccess {
|
||||
// 非 成功
|
||||
errType := define.RequestFailTypeServerError
|
||||
if response.HttpCode/100 == 4 {
|
||||
// 客户端错误
|
||||
errType = define.RequestFailTypeClientRequestInvalidError
|
||||
}
|
||||
response.FailInfo = &define.ResponseFailInfo{
|
||||
Type: errType,
|
||||
Message: "http code is " + response.HttpCodeStatus + ", not success",
|
||||
}
|
||||
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
|
||||
}
|
||||
if !hc.reqOption.ResponseParser.BusinessSuccess(hc.reqCfg, response) {
|
||||
response.FailInfo = &define.ResponseFailInfo{
|
||||
Type: define.RequestFailTypeBusinessError,
|
||||
Message: "business code is " + response.Code + ", not success",
|
||||
}
|
||||
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
|
||||
}
|
||||
response.IsSuccess = true //设置成功
|
||||
response.CacheInfo.SetCache, response.CacheInfo.CacheError = hc.setCacheResult(response) // 设置缓存
|
||||
break
|
||||
}
|
||||
response.RequestFinishTime = time.Now().UnixMilli()
|
||||
response.UsedTime = response.RequestFinishTime - response.RequestStartTime
|
||||
// 请求完成hook
|
||||
for _, itemAfterResponse := range hc.requestFinishHandler {
|
||||
itemAfterResponse(hc.reqCfg, response)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
// newResponse 默认返回数据
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 17:44 2024/6/1
|
||||
func (hc *HttpClient) newResponse() *define.Response {
|
||||
return &define.Response{
|
||||
Header: map[string]any{},
|
||||
Cookie: map[string]any{},
|
||||
Body: map[string]any{},
|
||||
Code: "",
|
||||
Message: "",
|
||||
Data: "",
|
||||
HttpCode: 0,
|
||||
HttpCodeStatus: "",
|
||||
ResponseDataRule: nil,
|
||||
Seq: 0,
|
||||
RequestStartTime: time.Now().UnixMilli(),
|
||||
RequestFinishTime: 0,
|
||||
UsedTime: 0,
|
||||
RestyResponse: nil,
|
||||
IsSuccess: false,
|
||||
CacheInfo: &define.ResponseCacheInfo{
|
||||
IsCache: false,
|
||||
SetCache: false,
|
||||
CacheKey: "",
|
||||
CacheValue: "",
|
||||
CacheEnable: nil != hc.reqOption.CacheInstance && hc.reqOption.CacheInstance.Enable(),
|
||||
CacheError: nil,
|
||||
},
|
||||
RequestCount: 0,
|
||||
FailInfo: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// getCacheResult 获取缓存结果
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 16:04 2024/6/3
|
||||
func (hc *HttpClient) getCacheResult() *define.Response {
|
||||
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
|
||||
}
|
||||
startTime := time.Now().UnixMilli()
|
||||
cacheKey := hc.reqOption.CacheInstance.GetKey(hc.reqCfg)
|
||||
cacheValue := strings.TrimSpace(hc.reqOption.CacheInstance.GetValue(cacheKey))
|
||||
if len(cacheValue) == 0 {
|
||||
log.RecordDebug("接口请求前缓存检测, 未读取到缓存数据", map[string]any{}, hc.reqCfg)
|
||||
return nil
|
||||
}
|
||||
response := hc.newResponse()
|
||||
if err := serialize.JSON.UnmarshalWithNumber([]byte(cacheValue), response); nil != err {
|
||||
log.RecordWarn("接口请求前缓存检测, 读取到缓存数据, 数据解析失败, 将跳过缓存, 请求对应接口", map[string]any{
|
||||
"err_msg": err.Error(),
|
||||
}, hc.reqCfg)
|
||||
return nil
|
||||
}
|
||||
response.CacheInfo.IsCache = true // 设置缓存标记
|
||||
response.RequestStartTime = startTime // 开始时间
|
||||
response.RequestFinishTime = time.Now().UnixMilli() // 结束时间
|
||||
response.UsedTime = response.RequestFinishTime - response.RequestStartTime // 耗时
|
||||
response.CacheInfo.CacheKey = cacheKey // 缓存key
|
||||
response.CacheInfo.CacheValue = cacheValue // 缓存值
|
||||
log.RecordDebug("接口请求前缓存检测, 命中缓存, 直接返回缓存数据", map[string]any{}, hc.reqCfg)
|
||||
return response
|
||||
}
|
||||
|
||||
// setCacheResult ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 16:24 2024/6/3
|
||||
func (hc *HttpClient) setCacheResult(response *define.Response) (bool, error) {
|
||||
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
|
||||
}
|
||||
// 全局未开启或者当前请求不支持缓存
|
||||
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
|
||||
}
|
||||
cacheKey := hc.reqOption.CacheInstance.GetKey(hc.reqCfg)
|
||||
cacheValue := serialize.JSON.MarshalForStringIgnoreError(response)
|
||||
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
|
||||
}
|
||||
log.RecordDebug("开启结果缓存, 缓存设置成功", map[string]any{
|
||||
"current_cache_enable": currentRequestAllowCache,
|
||||
"global_cache_enable": globalCacheEnable,
|
||||
}, hc.reqCfg)
|
||||
response.CacheInfo.CacheKey = cacheKey
|
||||
response.CacheInfo.CacheValue = cacheValue
|
||||
return true, nil
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
// Package httpclient 基础请求库
|
||||
//
|
||||
// Author: go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Description: 基础常量定义
|
||||
//
|
||||
// File: define.go
|
||||
//
|
||||
// Version: 1.0.0
|
||||
//
|
||||
// Date: 2022/05/01 19:56:48
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"git.zhangdeman.cn/zhangdeman/exception"
|
||||
"github.com/ddliu/go-httpclient"
|
||||
)
|
||||
|
||||
const (
|
||||
// ContentTypeFormData form-data 请求
|
||||
ContentTypeFormData = "form-data"
|
||||
// ContentTypeFormURLEncoded x-www-form-urlencoded 请求
|
||||
ContentTypeFormURLEncoded = "application/x-www-form-urlencoded"
|
||||
// ContentTypeJSON json的请求方式
|
||||
ContentTypeJSON = "application/json"
|
||||
// ContentTypeDefault 默认的请求方式
|
||||
ContentTypeDefault = ContentTypeJSON
|
||||
)
|
||||
|
||||
const (
|
||||
// BodyTypeJson json数据
|
||||
BodyTypeJson = "json"
|
||||
// BodyTypeXML xml数据
|
||||
BodyTypeXML = "xml"
|
||||
// BodyTypeYaml yaml数据
|
||||
BodyTypeYaml = "yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultConnectTimeout 默认连接超时时间, 毫秒
|
||||
DefaultConnectTimeout = 1000
|
||||
// DefaultReadTimeout 默认读取超时时间, 毫秒
|
||||
DefaultReadTimeout = 1000
|
||||
// DefaultResponseCodeField 默认code字段
|
||||
DefaultResponseCodeField = "code"
|
||||
// DefaultResponseMessageField 默认message字段
|
||||
DefaultResponseMessageField = "message"
|
||||
// DefaultResponseDataField 默认数据字段
|
||||
DefaultResponseDataField = "data"
|
||||
// DefaultFailMessage 接口响应失败
|
||||
DefaultFailMessage = "api request fail"
|
||||
// DefaultSuccessMessage 接口响应成功
|
||||
DefaultSuccessMessage = "api request success"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultSuccessCode 默认成功状态码
|
||||
DefaultSuccessCode = []string{"0", "200"}
|
||||
// DefaultSuccessHttpCode 默认成功http状态码
|
||||
DefaultSuccessHttpCode = []string{"200"}
|
||||
)
|
||||
|
||||
const (
|
||||
// ResponseCodeFieldLocationBody 响应状态码在body
|
||||
ResponseCodeFieldLocationBody = "body"
|
||||
// ResponseCodeFieldLocationHeader 在header
|
||||
ResponseCodeFieldLocationHeader = "header"
|
||||
// ResponseCodeFieldLocationDefault 响应状态码位置默认值
|
||||
ResponseCodeFieldLocationDefault = ResponseCodeFieldLocationBody
|
||||
// ResponseBodyAsData 会用整体的body作为数据
|
||||
ResponseBodyAsData = "__response__body__"
|
||||
)
|
||||
|
||||
// ApiRequestConfig api请求的配置
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Date: 2022/05/01 20:14:18
|
||||
type ApiRequestConfig struct {
|
||||
Method string `json:"method"` // 请求方法
|
||||
Domain string `json:"domain"` // 请求域名
|
||||
URI string `json:"uri"` // 请求的路由
|
||||
ResponseCodeField string `json:"response_code_field"` // 业务状态码字段
|
||||
ResponseCodeFieldLocation string `json:"response_code_field_location"` // 响应的业务状态码位置, 默认 : body . 可配置 body / header
|
||||
ResponseMessageField string `json:"response_message_field"` // 业务状态码描述的字段
|
||||
ResponseDataField string `json:"response_data_field"` // 业务数据的字段
|
||||
SuccessCodeList []string `json:"success_code_list"` // 代表请求成功的code列表
|
||||
SuccessHttpCodeList []string `json:"success_http_code_list"` // 代表成功的http code列表
|
||||
Parameter map[string]interface{} `json:"parameter"` // 传入的请求参数
|
||||
CommonHeader map[string]string `json:"common_header"` // 全部请求都要传的header
|
||||
Body []byte `json:"-"` // 请求体
|
||||
FullURL string `json:"full_url"` // 完整请求地址
|
||||
Timeout Timeout `json:"timeout"` // 超时配置
|
||||
ContentType string `json:"content_type"` // 请求方法
|
||||
}
|
||||
|
||||
// Timeout 超时配置
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 15:02 2022/5/2
|
||||
type Timeout struct {
|
||||
Connect int `json:"connect"` // 连接超时
|
||||
Read int `json:"read"` // 读取超时时间
|
||||
}
|
||||
|
||||
// ApiResponse 接口响应结果
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// Date: 2022/05/01 20:25:39
|
||||
type ApiResponse struct {
|
||||
RequestConfig *ApiRequestConfig `json:"request_config"` // 请求配置
|
||||
Response *httpclient.Response `json:"response"` // 响应体
|
||||
Exception exception.IException `json:"-"` // 异常信息
|
||||
StartRequestTime int64 `json:"start_request_time"` // 开始请求时间, 纳秒
|
||||
FinishRequestTime int64 `json:"finish_request_time"` // 完成请求时间,纳秒
|
||||
Code string `json:"code"` // 业务状态码
|
||||
Message string `json:"message"` // 状态码描述
|
||||
Data string `json:"data"` // 响应数据
|
||||
}
|
20
httpclient/define/cache.go
Normal file
20
httpclient/define/cache.go
Normal file
@ -0,0 +1,20 @@
|
||||
// Package define ...
|
||||
//
|
||||
// Description : define ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2024-10-09 18:39
|
||||
package define
|
||||
|
||||
// CachePreHeatConfig 缓存预热配置, MinPercent / MinTTL 同时配置, 则任意一个满足, 均进行预热
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 18:40 2024/10/9
|
||||
type CachePreHeatConfig struct {
|
||||
Enable bool `json:"enable"` // 缓存预热是否可用
|
||||
MinPercent int64 `json:"min_percent"` // 最小百分比, 剩余有效期低于此百分比进行预热
|
||||
MinTTL int64 `json:"min_ttl"` // 最小剩余生命周期, 低于此百分比进行预热
|
||||
Force bool `json:"force"` // 启用预热的情况下, 强制预热, 会忽略 MinPercent / MinTTL 的配置
|
||||
}
|
23
httpclient/define/error.go
Normal file
23
httpclient/define/error.go
Normal file
@ -0,0 +1,23 @@
|
||||
// Package define ...
|
||||
//
|
||||
// Description : define ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2024-05-31 16:02
|
||||
package define
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrRequestConfigNil = errors.New("REQUEST_CONFIG_NIL") // 请求配置 nil
|
||||
ErrFullUrlEmpty = errors.New("FULL_URL_EMPTY") // 没传 full_url
|
||||
ErrFullUrlInvalid = errors.New("FULL_URL_Invalid") // 请求 full_url 不是 http 或者 https 开头
|
||||
ErrMethodIsEmpty = errors.New("METHOD_IS_EMPTY") // 请求 方法为空
|
||||
ErrMethodIsNotSupport = errors.New("METHOD_IS_NOT_SUPPORT") // 请求 method不支持
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrRateLimitExceeded v2版本中命中流控的错误信息
|
||||
ErrRateLimitExceeded = errors.New("rate limit exceeded")
|
||||
)
|
15
httpclient/define/event.go
Normal file
15
httpclient/define/event.go
Normal file
@ -0,0 +1,15 @@
|
||||
// Package define ...
|
||||
//
|
||||
// Description : define ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2024-05-31 14:51
|
||||
package define
|
||||
|
||||
// RequestFinishHandler 请求最终完成事件, 不区分成功 OR 失败
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 18:34 2024/6/1
|
||||
type RequestFinishHandler func(req *Request, rep *Response)
|
66
httpclient/define/request.go
Normal file
66
httpclient/define/request.go
Normal file
@ -0,0 +1,66 @@
|
||||
// Package define ...
|
||||
//
|
||||
// Description : define ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2024-05-24 17:09
|
||||
package define
|
||||
|
||||
import (
|
||||
"context"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Request 请求配置
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 17:10 2024/5/24
|
||||
type Request struct {
|
||||
Ctx context.Context `json:"-"` // 请求上下文
|
||||
PathParam map[string]string `json:"path_param"` // 替换url中的占位符
|
||||
Body map[string]any `json:"body"` // 请求Body
|
||||
Header map[string]any `json:"header"` // 请求Header
|
||||
Cookie map[string]any `json:"cookie"` // 请求Cookie
|
||||
Query map[string]any `json:"query"` // 请求query
|
||||
Static map[string]map[string]any `json:"static"` // 静态参数: location => valName => val
|
||||
FullUrl string `json:"full_url"` // 完整的请求URL
|
||||
ContentType string `json:"content_type"` // 请求类型
|
||||
Method string `json:"method"` // 请求方法
|
||||
DataField string `json:"data_field"` // 数据字段
|
||||
CodeField string `json:"code_field"` // 业务状态码字段
|
||||
CodeLocation string `json:"code_location"` // 业务状态码位置
|
||||
MessageField string `json:"message_field"` // code描述字段
|
||||
DataReceiver any `json:"-"` // 响应data部分数据解析
|
||||
SuccessHttpCodeList []int `json:"success_http_code_list"` // 哪些http状态码视为成功, 不配置, 默认2xx
|
||||
SuccessCodeList []string `json:"success_code_list"` // 哪些业务状态码视为成功
|
||||
ConnectTimeout int64 `json:"connect_timeout"` // 连接超时时间: ms
|
||||
ReadTimeout int64 `json:"read_timeout"` // 读取超时时间
|
||||
RetryRule *RequestRetryRule `json:"retry_rule"` // 重试规则
|
||||
Logger *zap.Logger `json:"-"` // 日志记录器
|
||||
}
|
||||
|
||||
// RequestRetryRule 重试规则
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 12:26 2024/5/31
|
||||
type RequestRetryRule struct {
|
||||
RetryCount int `json:"retry_count"` // 重试次数
|
||||
RetryTimeInterval int64 `json:"retry_time_interval"` // 重试的时间间隔 1 - 10 之间, 单位毫秒
|
||||
RetryHttpCodeList []int64 `json:"retry_http_code_list"` // 哪些http状态码需要重试
|
||||
RetryBusinessCodeList []string `json:"retry_business_code_list"` // 哪些业务状态码需要重试
|
||||
}
|
||||
|
||||
const (
|
||||
DefaultConnectTimeout = 1000 // 默认连接超时时间
|
||||
DefaultReadTimeout = 1000 // 默认连接读取时间
|
||||
DefaultCodeField = "code" // 默认业务状态码字段
|
||||
DefaultMessageField = "message" // 默认状态码描述字段
|
||||
DefaultDataField = "data" // 默认数据字段
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultSuccessCodeList = []string{"0"} // 默认成功业务状态码
|
||||
)
|
73
httpclient/define/response.go
Normal file
73
httpclient/define/response.go
Normal file
@ -0,0 +1,73 @@
|
||||
// Package define ...
|
||||
//
|
||||
// Description : define ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2024-05-31 12:34
|
||||
package define
|
||||
|
||||
import (
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
// Response 响应的数据结构定义
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 12:34 2024/5/31
|
||||
type Response struct {
|
||||
Query map[string]any `json:"query"` // 请求query, 模拟自循环的请求
|
||||
Header map[string]any `json:"header"` // 响应header
|
||||
Cookie map[string]any `json:"cookie"` // 响应cookie
|
||||
Data string `json:"data"` // 响应body
|
||||
Code string `json:"code"` // 业务状态码
|
||||
Message string `json:"message"` // 业务状态码描述
|
||||
Body map[string]any `json:"body"` // 响应数据
|
||||
ExtendData map[string]string `json:"extend_data"` // 除去 code / message / data 之外的其他数据
|
||||
HttpCode int `json:"http_code"` // http状态码
|
||||
HttpCodeStatus string `json:"http_code_status"` // http状态码描述
|
||||
ResponseDataRule map[string]any `json:"response_data_rule"` // 返回数据的验证规则
|
||||
Seq int `json:"seq"` // 第几次请求
|
||||
RequestStartTime int64 `json:"request_start_time"` // 请求开始时间 : ms
|
||||
RequestFinishTime int64 `json:"request_finish_time"` // 请求完成时间 : ms
|
||||
UsedTime int64 `json:"used_time"` // 请求耗时 : ms
|
||||
RestyResponse *resty.Response `json:"-"` // 请求返回
|
||||
IsSuccess bool `json:"is_success"` // 是否请求成功
|
||||
RequestCount int `json:"request_count"` // 请求次数
|
||||
FailInfo *ResponseFailInfo `json:"fail_info"` // 请求失败信息记录
|
||||
CacheInfo *ResponseCacheInfo `json:"cache_info"` // 缓存信息
|
||||
}
|
||||
|
||||
// ResponseFailInfo 失败信息
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 17:48 2024/6/1
|
||||
type ResponseFailInfo struct {
|
||||
Type string `json:"type"` // 失败类型
|
||||
Message string `json:"message"` // 失败信息
|
||||
}
|
||||
|
||||
// ResponseCacheInfo 缓存信息
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 22:26 2024/6/14
|
||||
type ResponseCacheInfo struct {
|
||||
IsCache bool `json:"is_cache"` // 是否命中缓存
|
||||
SetCache bool `json:"set_cache"` // 是否设置缓存
|
||||
CacheKey string `json:"cache_key"` // 缓存key
|
||||
CacheValue string `json:"cache_value"` // 缓存值
|
||||
CacheEnable bool `json:"cache_enable"` // 是否允许缓存
|
||||
CacheError error `json:"-"` // 缓存是否异常
|
||||
}
|
||||
|
||||
const (
|
||||
RequestFailTypeRateLimit = "SEND_REQUEST_RATE_LIMIT" // 发送请求即失败, 命中限流
|
||||
RequestFailTypeSend = "SEND_REQUEST_FAIL" // 发送请求即失败, 问题出现在客户端
|
||||
RequestFailTypeClientRequestInvalidError = "CLIENT_REQUEST_INVALID" // 请求非法
|
||||
RequestFailTypeTimeoutError = "CLIENT_REQUEST_TIMEOUT_ERROR" // 请求失败, 请求超时
|
||||
RequestFailTypeServerError = "SERVER_DEAL_ERROR" // 服务端处理失败, 对应 http code 5xx
|
||||
RequestFailTypeBusinessError = "SERVICE_BUSINESS_ERROR" // 返回状态码为200, 但是业务状态码非成功
|
||||
)
|
@ -1,27 +0,0 @@
|
||||
// Package httpclient ...
|
||||
//
|
||||
// Author: go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Description: 定义异常
|
||||
//
|
||||
// File: exception.go
|
||||
//
|
||||
// Version: 1.0.0
|
||||
//
|
||||
// Date: 2022/05/01 20:40:08
|
||||
package httpclient
|
||||
|
||||
const (
|
||||
// ResponseCodeNotFound 响应结果获取不到状态码字段
|
||||
ResponseCodeNotFound = 404000000002
|
||||
// ResponseDataNotFound 响应结果获取不到数据字段
|
||||
ResponseDataNotFound = 404000000004
|
||||
// SendRequestError 请求发送出现异常
|
||||
SendRequestError = 400000000002
|
||||
// ReadResponseBodyError 读取响应数据体出现异常
|
||||
ReadResponseBodyError = 400000000003
|
||||
// RequestMethodNotSupport 请求方法不支持
|
||||
RequestMethodNotSupport = 405000000001
|
||||
// ResponseHttpCodeIsNotSuccess 响应的http状态码非成功
|
||||
ResponseHttpCodeIsNotSuccess = 500000000001
|
||||
)
|
55
httpclient/implement/request/write_body.go
Normal file
55
httpclient/implement/request/write_body.go
Normal 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
|
||||
}
|
33
httpclient/implement/request/write_form.go
Normal file
33
httpclient/implement/request/write_form.go
Normal 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
|
||||
}
|
21
httpclient/implement/request/write_json.go
Normal file
21
httpclient/implement/request/write_json.go
Normal 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
|
||||
}
|
46
httpclient/implement/request/write_xml.go
Normal file
46
httpclient/implement/request/write_xml.go
Normal 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
|
||||
}
|
199
httpclient/implement/response.go
Normal file
199
httpclient/implement/response.go
Normal 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
|
||||
}
|
37
httpclient/implement/response_parser.go
Normal file
37
httpclient/implement/response_parser.go
Normal 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)
|
||||
}
|
145
httpclient/log/log.go
Normal file
145
httpclient/log/log.go
Normal file
@ -0,0 +1,145 @@
|
||||
// Package log ...
|
||||
//
|
||||
// Description : httpclient ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2025-03-31 18:02
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.zhangdeman.cn/zhangdeman/consts"
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
logger *zap.Logger
|
||||
)
|
||||
|
||||
// Set 设置日志实例
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 18:02 2025/3/31
|
||||
func Set(l *zap.Logger) {
|
||||
logger = l
|
||||
}
|
||||
|
||||
// Get 获取日志实例
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 18:19 2025/3/31
|
||||
func Get() *zap.Logger {
|
||||
if nil == logger {
|
||||
// 返回日志空实现
|
||||
return zap.NewNop()
|
||||
}
|
||||
return logger
|
||||
}
|
||||
|
||||
var buildHttpLogDataFunc BuildHttpLogDataFunc
|
||||
|
||||
// BuildHttpLogDataFunc 构建http请求日志数据的方法
|
||||
type BuildHttpLogDataFunc func(ctx context.Context, isResponseLog bool, businessData map[string]any, reqCfg *define.Request, response *define.Response) []zap.Field
|
||||
|
||||
func SetBuildDataFunc(f BuildHttpLogDataFunc) {
|
||||
buildHttpLogDataFunc = f
|
||||
}
|
||||
|
||||
func GetBuildDataFunc() BuildHttpLogDataFunc {
|
||||
return buildHttpLogDataFunc
|
||||
}
|
||||
|
||||
// Record 日志记录
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 18:07 2025/3/31
|
||||
func Record(level consts.LogLevel, msg string, reqCfg *define.Request, response *define.Response) {
|
||||
if nil == reqCfg.Logger {
|
||||
// 未设置日志实例
|
||||
return
|
||||
}
|
||||
buildDataFunc := GetBuildDataFunc()
|
||||
if nil == buildDataFunc {
|
||||
// 未设置构建日志数据的方法
|
||||
return
|
||||
}
|
||||
fields := buildDataFunc(reqCfg.Ctx, true, nil, reqCfg, response)
|
||||
logInstance := reqCfg.Logger
|
||||
switch level {
|
||||
case consts.LogLevelDebug:
|
||||
logInstance.Debug(msg, fields...)
|
||||
case consts.LogLevelInfo:
|
||||
logInstance.Info(msg, fields...)
|
||||
case consts.LogLevelWarn:
|
||||
logInstance.Warn(msg, fields...)
|
||||
case consts.LogLevelError:
|
||||
logInstance.Error(msg, fields...)
|
||||
case consts.LogLevelPanic:
|
||||
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...)
|
||||
}
|
111
httpclient/mesh/define.go
Normal file
111
httpclient/mesh/define.go
Normal file
@ -0,0 +1,111 @@
|
||||
// Package mesh ...
|
||||
//
|
||||
// Description : define ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2025-03-28 14:12
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.zhangdeman.cn/zhangdeman/consts"
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient/abstract"
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// RequestConfig 聚合请求
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 14:14 2025/3/28
|
||||
type RequestConfig struct {
|
||||
Ctx context.Context `json:"-"` // 上下文
|
||||
CommonParam map[string]map[string]any `json:"common_param"` // 公共参数
|
||||
Group [][]*RequestConfigGroupItem `json:"group"` // 请求分组
|
||||
ResultRule []*RequestConfigResultRule `json:"result_rule"` // 结果提取规则, 多个接口的结果构造成一个返回结果
|
||||
}
|
||||
|
||||
// RequestConfigGroupItem 分组请求每一项的结构
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 14:16 2025/3/28
|
||||
type RequestConfigGroupItem struct {
|
||||
Alias string `json:"alias"` // 请求别名
|
||||
ParamRuleList []*RequestConfigGroupItemParamRule `json:"param_rule_list"` // 参数规则列表
|
||||
RequestCfg *define.Request `json:"request_cfg"` // 请求配置
|
||||
FailBehavior *RequestConfigGroupItemFailBehavior `json:"fail_behavior"` // 失败的行为, 不配置, 默认失败break
|
||||
FinalFailureAllow bool `json:"final_failure_allow"` // 已经确定当前请求是最终失败了,当前请求是否允许执行
|
||||
CacheInstance abstract.ICache `json:"-"` // 数据缓存实例
|
||||
RateLimiter abstract.RateLimiter `json:"-"` // 流控实例
|
||||
ResponseParser abstract.IResponse `json:"-"` // 响应数据解析
|
||||
RequestBodyWrite abstract.IRequestBodyWrite `json:"-"` // 请求Body数据写入的实现
|
||||
Condition any `json:"condition"` // TODO: 请求条件, 特定条件下不执行当前请求
|
||||
}
|
||||
|
||||
// RequestConfigGroupItemFailBehavior 失败的行为
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 15:51 2025/3/28
|
||||
type RequestConfigGroupItemFailBehavior struct {
|
||||
Action string `json:"action"` // 请求失败的行为: continue: 继续执行下一个请求, error: 停止执行后续请求, 并且整体请求失败
|
||||
}
|
||||
|
||||
// RequestConfigGroupItemParamRule 参数提取规则
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 14:23 2025/3/28
|
||||
type RequestConfigGroupItemParamRule struct {
|
||||
Location consts.RequestDataLocation `json:"location"` // 参数位置
|
||||
Path string `json:"path"` // 参数设置的路径
|
||||
Type consts.DataType `json:"type"` // 参数类型
|
||||
SourceAlias string `json:"source_alias"` // 数据源请求别名 __COMMON__ 代表从公共参数读取数据
|
||||
SourceResultLocation string `json:"source_result_location"` // 数据源请求结果位置
|
||||
SourceResultPath string `json:"source_result_path"` // 数据源数据, 从结果中获取的路径
|
||||
DefaultValue string `json:"default_value"` // 默认值
|
||||
}
|
||||
|
||||
// RequestConfigResultRule 返回值构建规则
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 14:28 2025/3/28
|
||||
type RequestConfigResultRule struct {
|
||||
RequestAlias string `json:"request_alias"` // 请求别名
|
||||
RequestResultLocation consts.ResponseDataLocation `json:"request_result_location"` // 请求结果位置
|
||||
RequestResultPath string `json:"request_result_path"` // 请求结果路径
|
||||
Location consts.ResponseDataLocation `json:"location"` // 返回值位置
|
||||
DataPath string `json:"data_path"` // 返回值的路径
|
||||
DataType consts.DataType `json:"data_type"` // 返回值类型
|
||||
}
|
||||
|
||||
// Response 聚合请求响应结果
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 14:40 2025/3/28
|
||||
type Response struct {
|
||||
FinalFailure bool `json:"final_failure"` // 是否最终失败
|
||||
FailureApiAlias string `json:"failure_api_alias"` // 导致最终失败的接口别名
|
||||
IsSuccess bool `json:"is_success"` // 请求是否成功
|
||||
ErrorCode string `json:"error_code"` // 错误码
|
||||
ErrorMessage string `json:"error_message"` // 错误信息
|
||||
FailApiAlias []string `json:"fail_api_alias"` // 失败的接口别名
|
||||
Raw []byte `json:"raw"` // 返回结果的raw数据(按照result_rule格式化后的结果)
|
||||
DataMap map[string]any `json:"data_map"` // 返回结果的map格式(按照result_rule格式化后的结果)
|
||||
AliasResultTable map[string]*define.Response `json:"alias_result_table"` // 每一个请求的返回结果 key: 请求别名 value: 请求返回数据
|
||||
Lock *sync.RWMutex `json:"-"` // 数据锁, public, 外部拿到结果需要做一席并发读写操作, 可以直接复用这把锁
|
||||
}
|
||||
|
||||
const (
|
||||
FailBehaviorContinue = "continue" // 失败后继续请求
|
||||
FailBehaviorError = "error" // 失败后终止请求
|
||||
)
|
||||
|
||||
const (
|
||||
CommonServiceAlias = "__COMMON__"
|
||||
)
|
248
httpclient/mesh/mesh.go
Normal file
248
httpclient/mesh/mesh.go
Normal file
@ -0,0 +1,248 @@
|
||||
// Package mesh ...
|
||||
//
|
||||
// Description : mesh ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2025-03-28 14:11
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.zhangdeman.cn/gateway/validate"
|
||||
"git.zhangdeman.cn/zhangdeman/consts"
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient"
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
|
||||
"git.zhangdeman.cn/zhangdeman/serialize"
|
||||
)
|
||||
|
||||
func Request(req *RequestConfig, receiver any) *Response {
|
||||
if nil == req.Ctx {
|
||||
req.Ctx = context.Background()
|
||||
}
|
||||
c := &client{
|
||||
resp: &Response{
|
||||
IsSuccess: false,
|
||||
ErrorCode: "",
|
||||
ErrorMessage: "",
|
||||
Raw: nil,
|
||||
DataMap: nil,
|
||||
AliasResultTable: make(map[string]*define.Response),
|
||||
Lock: &sync.RWMutex{},
|
||||
},
|
||||
reqCfg: req,
|
||||
receiver: receiver,
|
||||
}
|
||||
c.resp.AliasResultTable[CommonServiceAlias] = &define.Response{
|
||||
Query: c.reqCfg.CommonParam[consts.RequestDataLocationQuery.String()],
|
||||
Header: c.reqCfg.CommonParam[consts.RequestDataLocationHeader.String()],
|
||||
Cookie: c.reqCfg.CommonParam[consts.RequestDataLocationCookie.String()],
|
||||
Data: "",
|
||||
Code: "",
|
||||
Message: "",
|
||||
Body: c.reqCfg.CommonParam[consts.RequestDataLocationBody.String()],
|
||||
ExtendData: nil,
|
||||
HttpCode: 0,
|
||||
HttpCodeStatus: "",
|
||||
ResponseDataRule: nil,
|
||||
Seq: 0,
|
||||
RequestStartTime: 0,
|
||||
RequestFinishTime: 0,
|
||||
UsedTime: 0,
|
||||
RestyResponse: nil,
|
||||
IsSuccess: false,
|
||||
RequestCount: 0,
|
||||
FailInfo: nil,
|
||||
CacheInfo: nil,
|
||||
}
|
||||
return c.Request()
|
||||
}
|
||||
|
||||
type client struct {
|
||||
ctx context.Context
|
||||
resp *Response
|
||||
reqCfg *RequestConfig
|
||||
receiver any
|
||||
}
|
||||
|
||||
func (c *client) Request() *Response {
|
||||
for _, itemGroup := range c.reqCfg.Group {
|
||||
if !c.doRequest(itemGroup) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if c.resp.FinalFailure {
|
||||
// 请求失败
|
||||
return c.resp
|
||||
}
|
||||
// 请求成功, 构建返回结果
|
||||
respByte := serialize.JSON.MarshalForByteIgnoreError(c.resp.AliasResultTable)
|
||||
fieldList := make([]validate.StructField, 0)
|
||||
for _, item := range c.reqCfg.ResultRule {
|
||||
pathArr := strings.Split(item.DataPath, ".")
|
||||
fieldList = append(fieldList, validate.StructField{
|
||||
JsonTag: pathArr[len(pathArr)-1],
|
||||
Type: item.DataType,
|
||||
Required: false,
|
||||
RuleList: nil,
|
||||
DefaultValue: "",
|
||||
SourcePath: fmt.Sprintf("%v.%v.%v", item.RequestAlias, item.RequestResultLocation.String(), item.RequestResultPath),
|
||||
TargetPath: fmt.Sprintf("%v.%v", item.Location, item.DataPath),
|
||||
Errmsg: "",
|
||||
})
|
||||
}
|
||||
validateRes, err := validate.Run(respByte, fieldList)
|
||||
if nil != err {
|
||||
c.resp.ErrorCode = "-500"
|
||||
c.resp.ErrorMessage = err.Error()
|
||||
return c.resp
|
||||
}
|
||||
c.resp.Raw = respByte // 原始返回数据
|
||||
c.resp.DataMap, _ = validateRes.Map("json")
|
||||
// serialize.JSON.UnmarshalWithNumberIgnoreError(resByte, &c.resp.DataMap) // map结果
|
||||
|
||||
if nil != c.receiver {
|
||||
// 解析到receiver
|
||||
if err = validateRes.Transform("json", c.receiver); nil != err {
|
||||
c.resp.ErrorCode = "-500"
|
||||
c.resp.ErrorMessage = err.Error()
|
||||
return c.resp
|
||||
}
|
||||
}
|
||||
c.resp.IsSuccess = true
|
||||
return c.resp
|
||||
}
|
||||
|
||||
// doRequest 返回是否继续
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 15:30 2025/3/28
|
||||
func (c *client) doRequest(apiList []*RequestConfigGroupItem) bool {
|
||||
if len(apiList) == 0 {
|
||||
return true
|
||||
}
|
||||
// 初始化请求配置, 并获取 http client 实例
|
||||
httpClientList := make([]*httpclient.HttpClient, 0)
|
||||
for _, apiCfg := range apiList {
|
||||
var (
|
||||
err error
|
||||
httpClient *httpclient.HttpClient
|
||||
param map[string]map[string]any
|
||||
)
|
||||
// 初始化一下请求
|
||||
c.initApiCfg(apiCfg)
|
||||
// 构造生成请求参数
|
||||
if param, err = c.buildRequestParams(apiCfg); nil != err {
|
||||
// 构造请求参数失败, 直接返回
|
||||
c.resp.ErrorCode = "-500"
|
||||
c.resp.ErrorMessage = err.Error()
|
||||
return false
|
||||
}
|
||||
apiCfg.RequestCfg.Body = param[strings.ToLower(consts.RequestDataLocationBody.String())] // body
|
||||
apiCfg.RequestCfg.Header = param[strings.ToLower(consts.RequestDataLocationHeader.String())] // header
|
||||
apiCfg.RequestCfg.Cookie = param[strings.ToLower(consts.RequestDataLocationCookie.String())] // cookie
|
||||
apiCfg.RequestCfg.Query = param[strings.ToLower(consts.RequestDataLocationQuery.String())] // query
|
||||
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.ErrorMessage = err.Error()
|
||||
return false
|
||||
}
|
||||
httpClientList = append(httpClientList, httpClient)
|
||||
}
|
||||
isContinue := true
|
||||
wg := &sync.WaitGroup{}
|
||||
for idx, apiCfg := range apiList {
|
||||
wg.Add(1)
|
||||
go func(clientIdx int, apiCfg *RequestConfigGroupItem) {
|
||||
defer func() {
|
||||
if err := recover(); nil != err {
|
||||
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
if c.resp.FinalFailure && !apiCfg.FinalFailureAllow {
|
||||
// 已经最终失败, 并且最终失败后, 当前接口已经不允许请求, 不在进行请求
|
||||
return
|
||||
}
|
||||
resp := httpClientList[clientIdx].Request()
|
||||
c.resp.Lock.Lock()
|
||||
defer c.resp.Lock.Unlock()
|
||||
if !resp.IsSuccess {
|
||||
// 哪些接口请求失败: 注意,接口请求失败不代表整体失败,接口可能针对失败配置规则 continue
|
||||
c.resp.FailApiAlias = append(c.resp.FailApiAlias, apiCfg.Alias)
|
||||
// 判断是否已经是最终失败
|
||||
if apiCfg.FailBehavior.Action == FailBehaviorError {
|
||||
c.resp.FinalFailure = true
|
||||
c.resp.FailureApiAlias = apiCfg.Alias // 导致最终请求失败的接口
|
||||
// 判断是否继续, 只能阻断后续分组请求,无法阻断当前租的请求
|
||||
isContinue = FailBehaviorContinue == apiCfg.FailBehavior.Action
|
||||
}
|
||||
}
|
||||
// 记录请求的信息
|
||||
c.resp.AliasResultTable[apiCfg.Alias] = resp
|
||||
}(idx, apiCfg)
|
||||
}
|
||||
wg.Wait()
|
||||
return isContinue
|
||||
}
|
||||
|
||||
func (c *client) initApiCfg(apiCfg *RequestConfigGroupItem) {
|
||||
if apiCfg.FailBehavior == nil {
|
||||
apiCfg.FailBehavior = &RequestConfigGroupItemFailBehavior{
|
||||
Action: FailBehaviorError, // 默认失败终止请求
|
||||
}
|
||||
}
|
||||
// 每一个请求有独立的context
|
||||
apiCfg.RequestCfg.Ctx = context.WithValue(c.reqCfg.Ctx, "alias", apiCfg.Alias)
|
||||
}
|
||||
|
||||
// buildRequestParams 构建请求参数 location => path: value
|
||||
func (c *client) buildRequestParams(apiCfg *RequestConfigGroupItem) (map[string]map[string]any, error) {
|
||||
sourceData := map[string]any{}
|
||||
for aliasName, itemRes := range c.resp.AliasResultTable {
|
||||
sourceData[aliasName] = map[string]any{
|
||||
strings.ToLower(consts.ResponseDataLocationBody.String()): itemRes.Body[apiCfg.RequestCfg.DataField],
|
||||
strings.ToLower(consts.ResponseDataLocationHeader.String()): itemRes.Header,
|
||||
strings.ToLower(consts.ResponseDataLocationCookie.String()): itemRes.Cookie,
|
||||
strings.ToLower(consts.RequestDataLocationQuery.String()): itemRes.Query,
|
||||
}
|
||||
}
|
||||
sourceDataByte := serialize.JSON.MarshalForByteIgnoreError(sourceData)
|
||||
validateRuleList := make([]validate.StructField, 0)
|
||||
for _, itemParam := range apiCfg.ParamRuleList {
|
||||
arr := strings.Split(itemParam.Path, ".")
|
||||
sourcePath := itemParam.SourceResultPath
|
||||
sourcePath = fmt.Sprintf("%v.%v.%v", itemParam.SourceAlias, strings.ToLower(itemParam.SourceResultLocation), itemParam.SourceResultPath)
|
||||
targetPath := fmt.Sprintf("%v.%v", strings.ToLower(itemParam.Location.String()), itemParam.Path)
|
||||
validateRuleList = append(validateRuleList, validate.StructField{
|
||||
JsonTag: arr[len(arr)-1],
|
||||
Type: itemParam.Type,
|
||||
Required: false,
|
||||
RuleList: nil,
|
||||
DefaultValue: itemParam.DefaultValue,
|
||||
SourcePath: sourcePath,
|
||||
TargetPath: targetPath,
|
||||
Errmsg: "",
|
||||
})
|
||||
}
|
||||
|
||||
buildRes, err := validate.Run(sourceDataByte, validateRuleList)
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
var d map[string]map[string]any
|
||||
if err = buildRes.Transform("json", &d); nil != err {
|
||||
return nil, err
|
||||
}
|
||||
return d, err
|
||||
}
|
20
httpclient/option.go
Normal file
20
httpclient/option.go
Normal 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:"-"` // 返回结果解析, 不配置使用内置实现
|
||||
}
|
@ -1,350 +0,0 @@
|
||||
// Package httpclient ...
|
||||
//
|
||||
// Author: go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Description:
|
||||
//
|
||||
// File: request.go
|
||||
//
|
||||
// Version: 1.0.0
|
||||
//
|
||||
// Date: 2022/05/01 21:25:03
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.zhangdeman.cn/zhangdeman/exception"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ddliu/go-httpclient"
|
||||
)
|
||||
|
||||
// Request 发送请求
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2022/05/01 21:26:02
|
||||
func Request(apiConfig *ApiRequestConfig, header map[string]string, param map[string]interface{}) *ApiResponse {
|
||||
apiConfig.Method = strings.ToUpper(apiConfig.Method)
|
||||
switch apiConfig.Method {
|
||||
case http.MethodGet:
|
||||
return GET(apiConfig, header, param)
|
||||
case http.MethodPost:
|
||||
return POST(apiConfig, header, param)
|
||||
case http.MethodPut:
|
||||
return PUT(apiConfig, header, param)
|
||||
case http.MethodDelete:
|
||||
return DELETE(apiConfig, header, param)
|
||||
case http.MethodConnect:
|
||||
return Connect(apiConfig, header, param)
|
||||
case http.MethodOptions:
|
||||
return OPTION(apiConfig, header, param)
|
||||
case http.MethodTrace:
|
||||
return Trace(apiConfig, header, param)
|
||||
case http.MethodPatch:
|
||||
return Patch(apiConfig, header, param)
|
||||
default:
|
||||
return &ApiResponse{
|
||||
RequestConfig: apiConfig,
|
||||
Exception: exception.New(RequestMethodNotSupport, 0, map[string]string{"method": apiConfig.Method}, apiConfig.Method+" : request method is not support"),
|
||||
Response: nil,
|
||||
StartRequestTime: time.Now().UnixNano(),
|
||||
FinishRequestTime: time.Now().UnixNano(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GET ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date: 2022/05/01 21:29:09
|
||||
func GET(apiConfig *ApiRequestConfig, header map[string]string, param map[string]interface{}) *ApiResponse {
|
||||
apiConfig.Method = http.MethodGet
|
||||
buildRequestURLAndParam(apiConfig, header, param)
|
||||
return send(apiConfig, header)
|
||||
}
|
||||
|
||||
// POST post请求
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2022/05/01 21:31:36
|
||||
func POST(apiConfig *ApiRequestConfig, header map[string]string, param map[string]interface{}) *ApiResponse {
|
||||
apiConfig.Method = http.MethodPost
|
||||
buildRequestURLAndParam(apiConfig, header, param)
|
||||
return send(apiConfig, header)
|
||||
}
|
||||
|
||||
// PUT put请求
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2022/05/01 21:31:52
|
||||
func PUT(apiConfig *ApiRequestConfig, header map[string]string, param map[string]interface{}) *ApiResponse {
|
||||
apiConfig.Method = http.MethodPut
|
||||
buildRequestURLAndParam(apiConfig, header, param)
|
||||
return send(apiConfig, header)
|
||||
}
|
||||
|
||||
// DELETE delete请求
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2022/05/01 21:32:08
|
||||
func DELETE(apiConfig *ApiRequestConfig, header map[string]string, param map[string]interface{}) *ApiResponse {
|
||||
apiConfig.Method = http.MethodDelete
|
||||
buildRequestURLAndParam(apiConfig, header, param)
|
||||
return send(apiConfig, header)
|
||||
}
|
||||
|
||||
// OPTION option请求
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2022/05/01 21:32:18
|
||||
func OPTION(apiConfig *ApiRequestConfig, header map[string]string, param map[string]interface{}) *ApiResponse {
|
||||
apiConfig.Method = http.MethodOptions
|
||||
buildRequestURLAndParam(apiConfig, header, param)
|
||||
return send(apiConfig, header)
|
||||
}
|
||||
|
||||
// Patch patch请求
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2022/05/01 21:36:12
|
||||
func Patch(apiConfig *ApiRequestConfig, header map[string]string, param map[string]interface{}) *ApiResponse {
|
||||
apiConfig.Method = http.MethodPatch
|
||||
buildRequestURLAndParam(apiConfig, header, param)
|
||||
return send(apiConfig, header)
|
||||
}
|
||||
|
||||
// Trace trace请求
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2022/05/01 21:36:24
|
||||
func Trace(apiConfig *ApiRequestConfig, header map[string]string, param map[string]interface{}) *ApiResponse {
|
||||
apiConfig.Method = http.MethodTrace
|
||||
buildRequestURLAndParam(apiConfig, header, param)
|
||||
return send(apiConfig, header)
|
||||
}
|
||||
|
||||
// Connect connect请求
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2022/05/01 21:36:39
|
||||
func Connect(apiConfig *ApiRequestConfig, header map[string]string, param map[string]interface{}) *ApiResponse {
|
||||
apiConfig.Method = http.MethodConnect
|
||||
buildRequestURLAndParam(apiConfig, header, param)
|
||||
return send(apiConfig, header)
|
||||
}
|
||||
|
||||
// getHttpClient 获取httpclient实例
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 22:27 2022/5/1
|
||||
func getHttpClient(apiConfig *ApiRequestConfig, header map[string]string) *httpclient.HttpClient {
|
||||
fullHeader := make(map[string]string)
|
||||
for name, val := range apiConfig.CommonHeader {
|
||||
fullHeader[name] = val
|
||||
}
|
||||
for name, val := range header {
|
||||
fullHeader[name] = val
|
||||
}
|
||||
if len(apiConfig.ContentType) == 0 {
|
||||
apiConfig.ContentType = ContentTypeDefault
|
||||
}
|
||||
fullHeader["content-type"] = apiConfig.ContentType
|
||||
return httpclient.NewHttpClient().WithHeaders(fullHeader).
|
||||
WithOption(httpclient.OPT_CONNECTTIMEOUT_MS, apiConfig.Timeout.Connect).
|
||||
WithOption(httpclient.OPT_TIMEOUT_MS, apiConfig.Timeout.Read)
|
||||
}
|
||||
|
||||
// buildRequestURLAndParam 构建完整请求URL与请求参数
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 21:55 2022/5/1
|
||||
func buildRequestURLAndParam(apiConfig *ApiRequestConfig, header map[string]string, param map[string]interface{}) {
|
||||
apiConfig.Method = strings.ToUpper(apiConfig.Method)
|
||||
if apiConfig.Timeout.Connect == 0 {
|
||||
apiConfig.Timeout.Connect = DefaultConnectTimeout
|
||||
}
|
||||
if apiConfig.Timeout.Read == 0 {
|
||||
apiConfig.Timeout.Read = DefaultConnectTimeout
|
||||
}
|
||||
formatParam := make(map[string]interface{})
|
||||
for paramName, paramValue := range param {
|
||||
uriTemplate := "{" + paramName + "}"
|
||||
if strings.Contains(apiConfig.URI, uriTemplate) {
|
||||
apiConfig.URI = strings.ReplaceAll(apiConfig.URI, uriTemplate, fmt.Sprintf("%v", paramValue))
|
||||
continue
|
||||
}
|
||||
formatParam[paramName] = paramValue
|
||||
}
|
||||
apiConfig.Parameter = formatParam
|
||||
paramPair := make([]string, 0)
|
||||
switch apiConfig.Method {
|
||||
case http.MethodPost:
|
||||
apiConfig.Body, _ = json.Marshal(formatParam)
|
||||
default:
|
||||
for paramName, paramValue := range formatParam {
|
||||
paramPair = append(paramPair, fmt.Sprintf("%v=%v", paramName, paramValue))
|
||||
}
|
||||
if len(paramPair) > 0 {
|
||||
apiConfig.URI = apiConfig.URI + "?" + strings.Join(paramPair, "&")
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(apiConfig.Domain, "/") {
|
||||
apiConfig.Domain = strings.TrimRight(apiConfig.Domain, "/")
|
||||
}
|
||||
if !strings.HasPrefix(apiConfig.URI, "/") {
|
||||
apiConfig.URI = "/" + apiConfig.URI
|
||||
}
|
||||
apiConfig.FullURL = apiConfig.Domain + apiConfig.URI
|
||||
}
|
||||
|
||||
// send 发送请求
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 14:53 2022/5/2
|
||||
func send(apiConfig *ApiRequestConfig, header map[string]string) *ApiResponse {
|
||||
var (
|
||||
client *httpclient.HttpClient
|
||||
err error
|
||||
responseByte []byte
|
||||
)
|
||||
|
||||
// 填充默认配置
|
||||
if len(apiConfig.ResponseCodeFieldLocation) == 0 {
|
||||
apiConfig.ResponseCodeFieldLocation = ResponseCodeFieldLocationDefault
|
||||
}
|
||||
if len(apiConfig.ResponseCodeField) == 0 {
|
||||
apiConfig.ResponseCodeField = DefaultResponseCodeField
|
||||
}
|
||||
if len(apiConfig.ResponseMessageField) == 0 {
|
||||
apiConfig.ResponseMessageField = DefaultResponseMessageField
|
||||
}
|
||||
if len(apiConfig.ResponseDataField) == 0 {
|
||||
apiConfig.ResponseDataField = DefaultResponseDataField
|
||||
}
|
||||
if len(apiConfig.SuccessCodeList) == 0 {
|
||||
apiConfig.SuccessCodeList = DefaultSuccessCode
|
||||
}
|
||||
if len(apiConfig.SuccessHttpCodeList) == 0 {
|
||||
apiConfig.SuccessHttpCodeList = DefaultSuccessHttpCode
|
||||
}
|
||||
|
||||
response := &ApiResponse{
|
||||
RequestConfig: apiConfig,
|
||||
Response: nil,
|
||||
Exception: nil,
|
||||
StartRequestTime: time.Now().UnixNano(),
|
||||
FinishRequestTime: 0,
|
||||
Code: "",
|
||||
Message: "",
|
||||
Data: "",
|
||||
}
|
||||
|
||||
defer func() {
|
||||
response.FinishRequestTime = time.Now().UnixNano()
|
||||
}()
|
||||
|
||||
client = getHttpClient(apiConfig, header)
|
||||
if response.Response, err = client.Do(apiConfig.Method, apiConfig.FullURL, nil, bytes.NewReader(apiConfig.Body)); nil != err {
|
||||
response.Exception = exception.New(SendRequestError, 0, map[string]string{"real_reason": err.Error()}, "http request send fail : "+err.Error())
|
||||
return response
|
||||
}
|
||||
if responseByte, err = io.ReadAll(response.Response.Body); nil != err {
|
||||
response.Exception = exception.New(ReadResponseBodyError, response.Response.StatusCode, map[string]string{"real_reason": err.Error()}, "response body read fail : "+err.Error())
|
||||
return response
|
||||
}
|
||||
// 判断http状态码是否为成功
|
||||
isHttpSuccess := false
|
||||
responseHttpCode := fmt.Sprintf("%v", response.Response.StatusCode)
|
||||
for _, itemSuccessHttpCode := range response.RequestConfig.SuccessHttpCodeList {
|
||||
if responseHttpCode == itemSuccessHttpCode {
|
||||
isHttpSuccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// http请求失败
|
||||
if !isHttpSuccess {
|
||||
response.Exception = exception.New(ResponseHttpCodeIsNotSuccess, response.Response.StatusCode, map[string]string{
|
||||
"real_reason": responseHttpCode + " : response http code is not success",
|
||||
}, responseHttpCode+" : response http code is not success")
|
||||
return response
|
||||
}
|
||||
// 提取响应错误码
|
||||
if response.RequestConfig.ResponseCodeFieldLocation == ResponseCodeFieldLocationHeader {
|
||||
if strings.ToLower(response.RequestConfig.ResponseCodeField) == "http_code" {
|
||||
response.Code = responseHttpCode
|
||||
} else {
|
||||
response.Code = response.Response.Header.Get(response.RequestConfig.ResponseCodeField)
|
||||
}
|
||||
} else {
|
||||
businessCode := gjson.GetBytes(responseByte, response.RequestConfig.ResponseCodeField)
|
||||
if businessCode.Exists() {
|
||||
response.Code = businessCode.String()
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否提取到响应状态码
|
||||
if len(response.Code) == 0 {
|
||||
response.Exception = exception.New(ResponseCodeNotFound, response.Response.StatusCode, map[string]string{
|
||||
"real_reason": "parse response business code fail",
|
||||
}, fmt.Sprintf("business code location : %v, business code name : %v, parse business code fail", response.RequestConfig.ResponseCodeFieldLocation, response.RequestConfig.ResponseCodeField))
|
||||
return response
|
||||
}
|
||||
// 提取响应文案
|
||||
response.Message = gjson.GetBytes(responseByte, response.RequestConfig.ResponseMessageField).String()
|
||||
|
||||
// 判断响应状态码是否成功
|
||||
isBusinessCodeSuccess := false
|
||||
for _, itemSuccessCode := range response.RequestConfig.SuccessCodeList {
|
||||
if itemSuccessCode == response.Code {
|
||||
isBusinessCodeSuccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(response.Message) == 0 {
|
||||
if isBusinessCodeSuccess {
|
||||
response.Message = DefaultSuccessMessage
|
||||
} else {
|
||||
response.Message = DefaultFailMessage
|
||||
}
|
||||
}
|
||||
|
||||
if !isBusinessCodeSuccess {
|
||||
response.Exception = exception.New(ResponseCodeNotFound, response.Response.StatusCode, map[string]string{}, response.Message)
|
||||
return response
|
||||
}
|
||||
|
||||
// 提取响应数据
|
||||
if response.RequestConfig.ResponseDataField == ResponseBodyAsData {
|
||||
response.Data = string(responseByte)
|
||||
} else {
|
||||
responseData := gjson.GetBytes(responseByte, response.RequestConfig.ResponseDataField)
|
||||
if !responseData.Exists() {
|
||||
response.Exception = exception.New(ResponseDataNotFound, response.Response.StatusCode, map[string]string{}, "response data not found, data field name : "+response.RequestConfig.ResponseDataField)
|
||||
return response
|
||||
}
|
||||
response.Data = responseData.String()
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
173
httpclient/resty.go
Normal file
173
httpclient/resty.go
Normal file
@ -0,0 +1,173 @@
|
||||
// Package httpclient ...
|
||||
//
|
||||
// Description : httpclient ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2024-05-31 14:59
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.zhangdeman.cn/zhangdeman/consts"
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
|
||||
requestBoodyWriter "git.zhangdeman.cn/zhangdeman/network/httpclient/implement/request"
|
||||
"git.zhangdeman.cn/zhangdeman/wrapper"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"resty.dev/v3"
|
||||
"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
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 15:00 2024/5/31
|
||||
func NewRestyClient(reqConfig *define.Request, reqOption *RequestOption) (*resty.Client, *resty.Request, error) {
|
||||
client := resty.New()
|
||||
request := client.R()
|
||||
if nil == reqConfig {
|
||||
return nil, nil, errors.New("request config is nil")
|
||||
}
|
||||
// 限流处理, 增加限流中间件
|
||||
client.AddRequestMiddleware(func(client *resty.Client, request *resty.Request) error {
|
||||
if nil == reqOption || nil == reqOption.RateLimiter {
|
||||
// 未配置流控
|
||||
return nil
|
||||
}
|
||||
if !reqOption.RateLimiter.Allow(reqConfig) {
|
||||
// 命中流控
|
||||
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中的参数
|
||||
query := map[string]string{}
|
||||
for queryName, queryValue := range reqConfig.Query {
|
||||
if nil == queryValue {
|
||||
continue
|
||||
}
|
||||
query[queryName] = wrapper.AnyDataType(queryValue).ToString().Value()
|
||||
}
|
||||
request.SetQueryParams(query) // 设置query参数
|
||||
header := map[string]string{}
|
||||
for headerName, headerValue := range reqConfig.Header {
|
||||
if nil == headerValue {
|
||||
continue
|
||||
}
|
||||
header[headerName] = wrapper.AnyDataType(headerValue).ToString().Value()
|
||||
}
|
||||
request.SetHeaders(header) // 设置header
|
||||
request.URL = reqConfig.FullUrl // 请求接口
|
||||
// 设置url参数, url path格式需要是 /user/{user_id}/detail格式, {user_id} 会自动替换
|
||||
request.SetPathParams(reqConfig.PathParam)
|
||||
request.Method = reqConfig.Method // 请求方法
|
||||
cookieList := make([]*http.Cookie, 0)
|
||||
for cookieName, cookieValue := range reqConfig.Cookie {
|
||||
if nil == cookieValue {
|
||||
continue
|
||||
}
|
||||
cookieList = append(cookieList, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: wrapper.AnyDataType(cookieValue).ToString().Value(),
|
||||
})
|
||||
}
|
||||
request.SetCookies(cookieList) // 设置cookie
|
||||
if err := setRestyBody(reqConfig, reqOption, request); nil != err {
|
||||
return nil, nil, err
|
||||
}
|
||||
return client, request, nil
|
||||
}
|
||||
|
||||
// setRestyBody 设置请求BODY
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 17:18 2024/5/31
|
||||
func setRestyBody(reqConfig *define.Request, requestOption *RequestOption, request *resty.Request) error {
|
||||
if nil == reqConfig.Body {
|
||||
return nil
|
||||
}
|
||||
if nil != requestOption && nil != requestOption.RequestBodyWrite {
|
||||
// 外部传入的实现
|
||||
requestOption.RequestBodyWrite.Write(request, reqConfig.Body)
|
||||
return nil
|
||||
}
|
||||
// 外部没传入, 使用内置实现, 内置默认实现支持: xml/json/form, 如需其他, 自行扩展
|
||||
return requestBoodyWriter.WriteBody(reqConfig.ContentType, request, reqConfig.Body)
|
||||
}
|
||||
|
||||
// formatHeader 格式化header
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 15:18 2024/5/31
|
||||
func formatHeader(requestConfig *define.Request) {
|
||||
if nil == requestConfig {
|
||||
return
|
||||
}
|
||||
formatHeaderData := make(map[string]any)
|
||||
for headerName, headerVal := range requestConfig.Header {
|
||||
// 规范化处理 header 名称
|
||||
formatHeaderData[textproto.CanonicalMIMEHeaderKey(headerName)] = headerVal
|
||||
}
|
||||
requestConfig.Header = formatHeaderData
|
||||
}
|
144
httpclient/validate/request_config.go
Normal file
144
httpclient/validate/request_config.go
Normal file
@ -0,0 +1,144 @@
|
||||
// Package validate ...
|
||||
//
|
||||
// Description : validate ...
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 2024-05-31 16:18
|
||||
package validate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.zhangdeman.cn/zhangdeman/consts"
|
||||
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RequestConfig 验证请求配置
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 16:18 2024/5/31
|
||||
func RequestConfig(reqConfig *define.Request) error {
|
||||
if nil == reqConfig {
|
||||
return define.ErrRequestConfigNil
|
||||
}
|
||||
rc := &requestConfig{}
|
||||
if err := rc.validateFullUrl(reqConfig); nil != err {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type requestConfig struct {
|
||||
}
|
||||
|
||||
// initDefaultConfig 初始化默认配置
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 16:25 2024/5/31
|
||||
func (rc *requestConfig) initDefaultConfig(reqConfig *define.Request) {
|
||||
if nil == reqConfig.RetryRule {
|
||||
reqConfig.RetryRule = &define.RequestRetryRule{
|
||||
RetryCount: 0,
|
||||
RetryTimeInterval: 5,
|
||||
RetryHttpCodeList: make([]int64, 0),
|
||||
RetryBusinessCodeList: make([]string, 0),
|
||||
}
|
||||
}
|
||||
if nil == reqConfig.Header {
|
||||
reqConfig.Header = map[string]any{}
|
||||
}
|
||||
if len(reqConfig.ContentType) > 0 {
|
||||
reqConfig.Header[consts.HeaderKeyContentType.String()] = reqConfig.ContentType
|
||||
} else {
|
||||
if val, exist := reqConfig.Header[consts.HeaderKeyContentType.String()]; exist {
|
||||
if val == nil || fmt.Sprintf("%v", val) == "" {
|
||||
// 没配置Content-Type, 默认JSON
|
||||
reqConfig.Header[consts.HeaderKeyContentType.String()] = consts.MimeTypeJson
|
||||
}
|
||||
} else {
|
||||
// 没配置Content-Type, 默认JSON
|
||||
reqConfig.Header[consts.HeaderKeyContentType.String()] = consts.MimeTypeJson
|
||||
}
|
||||
}
|
||||
if nil == reqConfig.Cookie {
|
||||
reqConfig.Cookie = map[string]any{}
|
||||
}
|
||||
if reqConfig.ConnectTimeout <= 0 {
|
||||
reqConfig.ConnectTimeout = define.DefaultConnectTimeout
|
||||
}
|
||||
if reqConfig.ReadTimeout <= 0 {
|
||||
reqConfig.ReadTimeout = define.DefaultReadTimeout
|
||||
}
|
||||
reqConfig.CodeField = strings.TrimSpace(reqConfig.CodeField)
|
||||
reqConfig.MessageField = strings.TrimSpace(reqConfig.MessageField)
|
||||
reqConfig.DataField = strings.TrimSpace(reqConfig.DataField)
|
||||
if len(reqConfig.CodeField) == 0 {
|
||||
reqConfig.CodeField = define.DefaultCodeField
|
||||
}
|
||||
if len(reqConfig.MessageField) == 0 {
|
||||
reqConfig.MessageField = define.DefaultMessageField
|
||||
}
|
||||
if len(reqConfig.DataField) == 0 {
|
||||
reqConfig.DataField = define.DefaultDataField
|
||||
}
|
||||
if len(reqConfig.SuccessCodeList) == 0 {
|
||||
reqConfig.SuccessCodeList = define.DefaultSuccessCodeList
|
||||
}
|
||||
}
|
||||
|
||||
// validateFullUrl 验证full url
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 16:17 2024/5/31
|
||||
func (rc *requestConfig) validateFullUrl(reqConfig *define.Request) error {
|
||||
// 验证 full url
|
||||
reqConfig.FullUrl = strings.TrimSpace(reqConfig.FullUrl)
|
||||
if len(reqConfig.FullUrl) == 0 {
|
||||
return define.ErrFullUrlEmpty
|
||||
}
|
||||
if _, err := url.Parse(reqConfig.FullUrl); nil != err {
|
||||
return define.ErrFullUrlInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateMethod 验证method
|
||||
//
|
||||
// Author : go_developer@163.com<白茶清欢>
|
||||
//
|
||||
// Date : 16:22 2024/5/31
|
||||
func (rc *requestConfig) validateMethod(reqConfig *define.Request) error {
|
||||
// 验证Method
|
||||
reqConfig.Method = strings.ToUpper(reqConfig.Method)
|
||||
if len(reqConfig.Method) == 0 {
|
||||
return define.ErrMethodIsEmpty
|
||||
}
|
||||
supportMethodList := []string{
|
||||
http.MethodGet,
|
||||
http.MethodHead,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch,
|
||||
http.MethodDelete,
|
||||
http.MethodConnect,
|
||||
http.MethodOptions,
|
||||
http.MethodTrace,
|
||||
}
|
||||
isSupportMethod := false
|
||||
for _, item := range supportMethodList {
|
||||
if item == reqConfig.Method {
|
||||
isSupportMethod = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isSupportMethod {
|
||||
return define.ErrMethodIsNotSupport
|
||||
}
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user