diff --git a/go.mod b/go.mod index 83e67d0..0001b2d 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,71 @@ 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-20250331140536-3477e2ec106d + git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250328040304-7e4a6f9f148c + git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd + git.zhangdeman.cn/zhangdeman/wrapper v0.0.0-20250321102712-1cbfbe959740 github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 + github.com/go-resty/resty/v2 v2.16.5 github.com/mssola/user_agent v0.6.0 github.com/tidwall/gjson v1.18.0 + go.uber.org/zap v1.27.0 ) require ( + git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250323125506-1b217c0e4fee // indirect + git.zhangdeman.cn/zhangdeman/easylock v0.0.0-20230731062340-983985c12eda // indirect + git.zhangdeman.cn/zhangdeman/easymap v0.0.0-20241101082529-28a6c68e38a4 // indirect + git.zhangdeman.cn/zhangdeman/gin v0.0.0-20250228104311-2fd9195b77e7 // indirect + git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250329080213-587a9bbe9dbc // indirect + git.zhangdeman.cn/zhangdeman/logger v0.0.0-20250305064240-fee53ce64dc9 // indirect + git.zhangdeman.cn/zhangdeman/op_type v0.0.0-20240122104027-4928421213c0 // indirect + git.zhangdeman.cn/zhangdeman/util v0.0.0-20240618042405-6ee2c904644e // indirect + git.zhangdeman.cn/zhangdeman/websocket v0.0.0-20241125101541-c5ea194c9c1e // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 // indirect + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/creasty/defaults v1.8.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.0.0 // indirect + github.com/gin-gonic/gin v1.10.0 // 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/goccy/go-json v0.10.5 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect + github.com/lestrrat-go/strftime v1.1.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mcuadros/go-defaults v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mozillazg/go-pinyin v0.20.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sbabiv/xml2map v1.2.1 // indirect + github.com/spaolacci/murmur3 v1.1.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 + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.15.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 675beab..91adb1b 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,137 @@ -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-20250329080338-97d87fe06967 h1:ksdHEPciNlqq/h/OSpF3YyiXZIvgcCP5Vo7rTFy0/Cs= +git.zhangdeman.cn/gateway/validate v0.0.0-20250329080338-97d87fe06967/go.mod h1:Bwb9ks9pUp0Em+7CDTCUfnI+o6jRxMLifSHICsWmWXU= +git.zhangdeman.cn/gateway/validate v0.0.0-20250331140536-3477e2ec106d h1:rEnbN/j9/xqDAqdjAwIxE1UxoQoZVBl5Qdj+R/zEdCQ= +git.zhangdeman.cn/gateway/validate v0.0.0-20250331140536-3477e2ec106d/go.mod h1:Bwb9ks9pUp0Em+7CDTCUfnI+o6jRxMLifSHICsWmWXU= +git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250328040304-7e4a6f9f148c h1:cl3gQGXQpJ8ugDs0C/hQLfcvF4lGBm5BeABLvROFDoM= +git.zhangdeman.cn/zhangdeman/consts v0.0.0-20250328040304-7e4a6f9f148c/go.mod h1:IXXaZkb7vGzGnGM5RRWrASAuwrVSNxuoe0DmeXx5g6k= +git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250323125506-1b217c0e4fee h1:ufokPLk1nUKk+N5fDm/uD82OKqQn0EP/CMN6lK+wslM= +git.zhangdeman.cn/zhangdeman/dynamic-struct v0.0.0-20250323125506-1b217c0e4fee/go.mod h1:HwmL/Vtn3T9T4w6Gh8RlXRQ5o/3YNGqgLIuwgTJB0Mg= +git.zhangdeman.cn/zhangdeman/easylock v0.0.0-20230731062340-983985c12eda h1:bMD6r9gjRy7cO+T4zRQVYAesgIblBdTnhzT1vN5wjvI= +git.zhangdeman.cn/zhangdeman/easylock v0.0.0-20230731062340-983985c12eda/go.mod h1:dT0rmHcJ9Z9IqWeMIt7YzR88nKkNV2V3dfG0j9Q6lK0= +git.zhangdeman.cn/zhangdeman/easymap v0.0.0-20241101082529-28a6c68e38a4 h1:s6d4b6yY+NaK1AzoBD1pxqsuygEHQz0Oie86c45geDw= +git.zhangdeman.cn/zhangdeman/easymap v0.0.0-20241101082529-28a6c68e38a4/go.mod h1:V4Dfg1v/JVIZGEKCm6/aehs8hK+Xow1dkL1yiQymXlQ= +git.zhangdeman.cn/zhangdeman/gin v0.0.0-20250228104311-2fd9195b77e7 h1:koL8c0do1mOLFY+wLMqSpojgHSwVRbV6sSsJVKo9WfA= +git.zhangdeman.cn/zhangdeman/gin v0.0.0-20250228104311-2fd9195b77e7/go.mod h1:T2Q8Wcq98yTuSSaEPZVAZfs0DMSxeXMN10GOQCha5g4= +git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250329080213-587a9bbe9dbc h1:jqaoktpx+gLiBLGpW/X03ZZ+oIsrQIlvimLSaHQo0iA= +git.zhangdeman.cn/zhangdeman/json_filter v0.0.0-20250329080213-587a9bbe9dbc/go.mod h1:KuVC2+wQ4dXMgqy4RF+wrsDxa+FWYuc7x17Y+/2b1YE= +git.zhangdeman.cn/zhangdeman/logger v0.0.0-20250305064240-fee53ce64dc9 h1:HifwSBUtAxrKppYOZfKXI+iEtcTHh2+/zVeNsrIPN+M= +git.zhangdeman.cn/zhangdeman/logger v0.0.0-20250305064240-fee53ce64dc9/go.mod h1:0QtFBqrH3FDygCcsgsP/bK5lHSq0qpAulU9HyxtG1pM= +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-20241223084948-de2e49144fcd h1:q7GG14qgXKB4MEXQFOe7/UYebsqMfPaSX80TcPdOosI= +git.zhangdeman.cn/zhangdeman/serialize v0.0.0-20241223084948-de2e49144fcd/go.mod h1:+D6uPSljwHywjVY5WSBY4TRVMj26TN5f5cFGEYMldjs= +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/websocket v0.0.0-20241125101541-c5ea194c9c1e h1:YE2Gi+M03UDImIpWa3I7jzSesyfu2RL8x/4ONs5v0oE= +git.zhangdeman.cn/zhangdeman/websocket v0.0.0-20241125101541-c5ea194c9c1e/go.mod h1:L/7JugxKZL3JP9JP/XDvPAPz0FQXG1u181Su1+u/d1c= +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/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 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.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= +github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +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/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/gofuzz v1.0.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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= +github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +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/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/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= +github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= +github.com/lestrrat-go/strftime v1.1.0 h1:gMESpZy44/4pXLO/m+sL0yBd1W6LjgjrrD4a68Gapyg= +github.com/lestrrat-go/strftime v1.1.0/go.mod h1:uzeIB52CeUJenCo1syghlugshMysrqUT51HlxphXVeI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc= +github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY= +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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +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/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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 +139,39 @@ 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= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +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/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= +golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 h1:sY2a+y0j4iDrajJcorb+a0hJIQ6uakU5gybjfLWHlXo= +gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376/go.mod h1:BHKOc1m5wm8WwQkMqYBoo4vNxhmF7xg8+xhG8L+Cy3M= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/httpclient/README.md b/httpclient/README.md new file mode 100644 index 0000000..93e147c --- /dev/null +++ b/httpclient/README.md @@ -0,0 +1,17 @@ +# httpclient + +## 库作用 + +- 对第三方接口发起请求 + +## 依赖基础库 + +- [resty](https://github.com/go-resty/resty) + +## 配置说明 + +## 默认配置 + +- AllowGetMethodPayload : 允许GET请求带Body, 默认值 **`true`** , 如需关闭, 可通过 HttpClient.GetRestyClient().SetAllowGetMethodPayload(false) 关闭 + +## 使用方式 diff --git a/httpclient/cache/ICache.go b/httpclient/cache/ICache.go new file mode 100644 index 0000000..d931357 --- /dev/null +++ b/httpclient/cache/ICache.go @@ -0,0 +1,68 @@ +// Package cache ... +// +// Description : cache ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2024-06-03 15:49 +package cache + +import ( + "git.zhangdeman.cn/zhangdeman/network/httpclient/define" +) + +// ICache 缓存定义 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:49 2024/6/3 +type ICache interface { + // Enable 是否启用缓存(总开关) + // + // Author : go_developer@163.com<白茶清欢> + // + // Date : 15:50 2024/6/3 + Enable() bool + // CacheTime 缓存时长, 单位 : s , 默认 1800, 最小值 90, 设置失效时间, 会上下波动60s, 避免缓存集中失效 + // + // Author : go_developer@163.com<白茶清欢> + // + // Date : 15:50 2024/6/3 + CacheTime() int64 + // IsAllow 针对当前请求数据和状态, 是否允许缓存 + // + // Author : go_developer@163.com<白茶清欢> + // + // Date : 15:52 2024/6/3 + IsAllow(reqCfg *define.Request, response *define.Response) bool + // GetKey 获取缓存key + // + // Author : go_developer@163.com<白茶清欢> + // + // Date : 15:51 2024/6/3 + GetKey(reqCfg *define.Request) string + // GetValue 获取缓存值 + // + // Author : zhangdeman001@ke.com<张德满> + // + // Date : 16:01 2024/6/3 + GetValue(cacheKey string) string + // SetValue 设置缓存 + // + // Author : go_developer@163.com<白茶清欢> + // + // Date : 16:46 2024/6/3 + SetValue(cacheKey string, cacheValue string) error + // TTL 缓存剩余生命周期(单位: s) + // + // Author : go_developer@163.com<白茶清欢> + // + // Date : 18:39 2024/10/9 + TTL(cacheKey string) int64 + // PreHeatConfig 缓存预热配置 + // + // Author : go_developer@163.com<白茶清欢> + // + // Date : 18:42 2024/10/9 + PreHeatConfig() *define.CachePreHeatConfig +} diff --git a/httpclient/client.go b/httpclient/client.go new file mode 100644 index 0000000..9c29a74 --- /dev/null +++ b/httpclient/client.go @@ -0,0 +1,493 @@ +// Package httpclient ... +// +// Description : httpclient ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2024-05-31 15:22 +package httpclient + +import ( + "fmt" + "net/http" + "strings" + "time" + + "git.zhangdeman.cn/zhangdeman/consts" + "git.zhangdeman.cn/zhangdeman/network/httpclient/cache" + "git.zhangdeman.cn/zhangdeman/network/httpclient/define" + "git.zhangdeman.cn/zhangdeman/network/httpclient/log" + "git.zhangdeman.cn/zhangdeman/network/httpclient/validate" + "git.zhangdeman.cn/zhangdeman/serialize" + "github.com/go-resty/resty/v2" + "github.com/tidwall/gjson" +) + +// NewHttpClient 获取http client +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:27 2024/5/31 +func NewHttpClient(reqConfig *define.Request, cacheInstance cache.ICache) (*HttpClient, error) { + if nil == reqConfig.Logger { + reqConfig.Logger = log.Get() // 未单独指定日志实例, 则使用全局日志实例 + } + // 验证配置正确性以及初始化默认值 + if err := validate.RequestConfig(reqConfig); nil != err { + return nil, err + } + 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 + } + } + } + } + restyClient, restyRequest := NewRestyClient(reqConfig) + hc := &HttpClient{ + Client: restyClient, + request: restyRequest, + reqConfig: reqConfig, + http4xxHandler: make([]define.Http4xxHandler, 0), + http5xxHandler: make([]define.Http5xxHandler, 0), + httpBusinessErrorHandler: make([]define.HttpBusinessErrorHandler, 0), + requestFinishHandler: make([]define.RequestFinishHandler, 0), + cacheInstance: cacheInstance, + } + hc.OnRequestFinish(func(req *define.Request, rep *define.Response) { + if rep.IsSuccess { + // 请求成功 + log.Record(req.Ctx, req.Logger, consts.LogLevelInfo, "接口请求成功", req, rep) + return + } + // 请求失败 + log.Record(req.Ctx, req.Logger, 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 + reqConfig *define.Request + http4xxHandler []define.Http4xxHandler + http5xxHandler []define.Http5xxHandler + httpBusinessErrorHandler []define.HttpBusinessErrorHandler + requestSendErrorHandler []define.RequestSendErrorHandler + requestFinishHandler []define.RequestFinishHandler + cacheInstance cache.ICache +} + +// OnResponse4xx 4xx处理逻辑 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 18:20 2024/6/1 +func (hc *HttpClient) OnResponse4xx(handlerList ...define.Http4xxHandler) { + hc.http4xxHandler = append(hc.http4xxHandler, handlerList...) +} + +// OnResponse5xx 5xx处理逻辑 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 18:21 2024/6/1 +func (hc *HttpClient) OnResponse5xx(handlerList ...define.Http5xxHandler) { + hc.http5xxHandler = append(hc.http5xxHandler, handlerList...) +} + +// OnResponseBusinessError 业务错误出路逻辑 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 18:22 2024/6/1 +func (hc *HttpClient) OnResponseBusinessError(handlerList ...define.HttpBusinessErrorHandler) { + hc.httpBusinessErrorHandler = append(hc.httpBusinessErrorHandler, handlerList...) +} + +// OnRequestFinish 请求完成事件 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 18:36 2024/6/1 +func (hc *HttpClient) OnRequestFinish(handlerList ...define.RequestFinishHandler) { + hc.requestFinishHandler = append(hc.requestFinishHandler, handlerList...) +} + +// getRequestValidateMiddleware 请求验证的Middleware +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:40 2024/5/31 +func (hc *HttpClient) getRequestValidateMiddleware() resty.RequestMiddleware { + return func(client *resty.Client, request *resty.Request) error { + return nil + } +} + +// getResponseValidateMiddleware 获取响应数据验证的middleware +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:42 2024/5/31 +func (hc *HttpClient) getResponseValidateMiddleware() resty.ResponseMiddleware { + return func(client *resty.Client, response *resty.Response) error { + return nil + } +} + +// SetRestyClient 设置client +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:54 2024/5/31 +func (hc *HttpClient) SetRestyClient(restyClient *resty.Client) { + hc.Client = restyClient +} + +// GetRestyClient 获取 resty client +// +// 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 { + // 判断是否开启预热 + inputCachePreHeatConfig := hc.cacheInstance.PreHeatConfig() + var cachePreHeatConfig define.CachePreHeatConfig + serialize.JSON.TransitionIgnoreError(inputCachePreHeatConfig, &cachePreHeatConfig) + if !cachePreHeatConfig.Enable || (cachePreHeatConfig.MinTTL <= 0 && cachePreHeatConfig.MinPercent <= 0 && !cachePreHeatConfig.Force) { + // 无预热配置或未启用预热或者未设置预热规则 + return cacheResult + } + go func() { + // 判断是否触发预热 + if cachePreHeatConfig.Force { + _ = hc.requestBackendApi() + return + } + // 将百分比的配置归一化成最小剩余时间的配置 + if cachePreHeatConfig.MinPercent > 0 { + expectMinTTL := hc.cacheInstance.CacheTime() * cachePreHeatConfig.MinPercent / 100 + if cachePreHeatConfig.MinTTL == 0 || cachePreHeatConfig.MinTTL > expectMinTTL { + cachePreHeatConfig.MinTTL = expectMinTTL + } + } + if cachePreHeatConfig.MinTTL <= 0 { + // 未配置最小剩余时间 + return + } + ttl := hc.cacheInstance.TTL(cacheResult.CacheInfo.CacheKey) + if ttl < 0 { + // 不存在或者未设置有效期 + return + } + + if ttl <= cachePreHeatConfig.MinTTL { + // 配置了最小剩余时间,并且key剩余有效期小于最小剩余时间 + _ = hc.requestBackendApi() + } + }() + } + + return hc.requestBackendApi() + +} + +// requestBackendApi 请求后端接口 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 18:47 2024/10/9 +func (hc *HttpClient) requestBackendApi() *define.Response { + hc.Client.OnBeforeRequest(hc.getRequestValidateMiddleware()) // 请求参数验证中间件必注册 + hc.Client.OnAfterResponse(hc.getResponseValidateMiddleware()) // 响应验证中间件必注册 + var ( + err error + ) + + response := hc.newResponse() + for i := 0; i < hc.reqConfig.RetryRule.RetryCount+1; i++ { + if response.Seq > 0 { + // 说明是重试, 记录上一次的请求信息 + response.RequestFinishTime = time.Now().UnixMilli() + response.UsedTime = response.RequestFinishTime - response.RequestStartTime + for _, itemAfterResponse := range hc.requestFinishHandler { + itemAfterResponse(hc.reqConfig, response) + } + } + response.RequestStartTime = time.Now().UnixMilli() // 每次重置请求时间 + response.RequestFinishTime = 0 // 清空完成时间 + response.Seq++ + response.RequestCount++ + if response.RestyResponse, err = hc.request.Send(); nil != err { + response.FailInfo = &define.ResponseFailInfo{ + Type: define.RequestFailTypeSend, + Message: "response instance is nil", + } + time.Sleep(time.Duration(hc.reqConfig.RetryRule.RetryTimeInterval) * time.Millisecond) + continue + } + + if nil == response.RestyResponse { + response.FailInfo = &define.ResponseFailInfo{ + Type: define.RequestFailTypeSend, + Message: "response instance is nil", + } + time.Sleep(time.Duration(hc.reqConfig.RetryRule.RetryTimeInterval) * time.Millisecond) + continue + } + // 解析返回信息 + hc.fillResponseHeader(response) + hc.fillResponseCookie(response) + hc.fillResponseBody(response) + if response.HttpCode != http.StatusOK { + response.FailInfo = &define.ResponseFailInfo{ + Type: define.RequestFailTypeServerError, + Message: "http code is " + response.HttpCodeStatus + ", not success", + } + time.Sleep(time.Duration(hc.reqConfig.RetryRule.RetryTimeInterval) * time.Millisecond) + continue + } + if !hc.isCodeSuccess(response) { + response.FailInfo = &define.ResponseFailInfo{ + Type: define.RequestFailTypeBusinessError, + Message: "business code is " + response.Code + ", not success", + } + time.Sleep(time.Duration(hc.reqConfig.RetryRule.RetryTimeInterval) * time.Millisecond) + 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.reqConfig, 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: 0, + RequestFinishTime: 0, + UsedTime: 0, + RestyResponse: nil, + IsSuccess: false, + CacheInfo: &define.ResponseCacheInfo{ + IsCache: false, + SetCache: false, + CacheKey: "", + CacheValue: "", + CacheEnable: nil != hc.cacheInstance && hc.cacheInstance.Enable(), + CacheError: nil, + }, + RequestCount: 0, + FailInfo: nil, + } +} + +// fillResponseHeader 填充响应header +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 21:30 2024/6/5 +func (hc *HttpClient) fillResponseHeader(response *define.Response) { + response.Header = map[string]any{} // 清空已有数据 + response.HttpCode = response.RestyResponse.StatusCode() // http状态码 + response.HttpCodeStatus = response.RestyResponse.Status() // http状态码描述 + for headerName, headerValue := range response.RestyResponse.Header() { + if len(headerValue) > 0 { + response.Header[headerName] = headerValue[0] + } else { + response.Header[headerName] = "" + } + } +} + +// fillResponseCookie 填充cookie +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 21:32 2024/6/5 +func (hc *HttpClient) fillResponseCookie(response *define.Response) { + response.Cookie = map[string]any{} // 清空已有数据 + for _, cookieValue := range response.RestyResponse.Cookies() { + response.Cookie[cookieValue.Name] = cookieValue.Value + } +} + +// fillResponseBody 填充响应body +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 21:38 2024/6/5 +func (hc *HttpClient) fillResponseBody(response *define.Response) { + response.Data = string(response.RestyResponse.Body()) + response.Code = gjson.Get(response.Data, hc.reqConfig.CodeField).String() + response.Message = gjson.Get(response.Data, hc.reqConfig.MessageField).String() + businessData := gjson.Get(response.Data, hc.reqConfig.DataField) + if businessData.Value() == nil { + // data为空指针, 归一化成空对象 + response.Body = map[string]any{} + } else { + if businessData.IsArray() { + // 数组类型的转换 + response.Data = fmt.Sprintf(`{"list":` + businessData.String() + "}") + } else { + if businessData.IsObject() { + // 返回的就是对象 + response.Data = businessData.String() + } else { + // 返回是普通类型 + response.Data = serialize.JSON.MarshalForStringIgnoreError(map[string]any{ + "value": businessData.Value(), + }) + } + } + _ = serialize.JSON.UnmarshalWithNumber([]byte(response.Data), &response.Body) + } + + response.ExtendData = map[string]string{} + gjson.Parse(response.Data).ForEach(func(key, value gjson.Result) bool { + if key.String() == hc.reqConfig.CodeField || + key.String() == hc.reqConfig.MessageField || + key.String() == hc.reqConfig.DataField { + return true + } + response.ExtendData[key.String()] = value.String() + return true + }) +} + +// isHttpCodeSuccess ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 22:48 2024/6/6 +func (hc *HttpClient) isCodeSuccess(response *define.Response) bool { + for _, itemSuccessCode := range hc.reqConfig.SuccessCodeList { + if itemSuccessCode == response.Code { + return true + } + } + return false +} + +// getCacheResult 获取缓存结果 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 16:04 2024/6/3 +func (hc *HttpClient) getCacheResult() *define.Response { + if nil == hc.cacheInstance || !hc.cacheInstance.Enable() { + return nil + } + startTime := time.Now().UnixMilli() + cacheKey := hc.cacheInstance.GetKey(hc.reqConfig) + cacheValue := strings.TrimSpace(hc.cacheInstance.GetValue(cacheKey)) + if len(cacheValue) == 0 { + return nil + } + response := hc.newResponse() + if err := serialize.JSON.UnmarshalWithNumber([]byte(cacheValue), response); nil != err { + 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 // 缓存值 + 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.cacheInstance { + return false, nil + } + // 全局未开启或者当前请求不支持缓存 + if !hc.cacheInstance.Enable() || !hc.cacheInstance.IsAllow(hc.reqConfig, response) { + return false, nil + } + cacheKey := hc.cacheInstance.GetKey(hc.reqConfig) + cacheValue := serialize.JSON.MarshalForStringIgnoreError(response) + if err := hc.cacheInstance.SetValue(cacheKey, cacheValue); nil != err { + return false, err + } + response.CacheInfo.CacheKey = cacheKey + response.CacheInfo.CacheValue = cacheValue + return true, nil +} diff --git a/httpclient/define.go b/httpclient/define.go deleted file mode 100644 index 23c78e4..0000000 --- a/httpclient/define.go +++ /dev/null @@ -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"` // 响应数据 -} diff --git a/httpclient/define/cache.go b/httpclient/define/cache.go new file mode 100644 index 0000000..24c74a3 --- /dev/null +++ b/httpclient/define/cache.go @@ -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 的配置 +} diff --git a/httpclient/define/error.go b/httpclient/define/error.go new file mode 100644 index 0000000..bee6f77 --- /dev/null +++ b/httpclient/define/error.go @@ -0,0 +1,18 @@ +// 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不支持 +) diff --git a/httpclient/define/event.go b/httpclient/define/event.go new file mode 100644 index 0000000..c77f51e --- /dev/null +++ b/httpclient/define/event.go @@ -0,0 +1,43 @@ +// Package define ... +// +// Description : define ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2024-05-31 14:51 +package define + +// Http4xxHandler 4xx handler +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 14:54 2024/5/31 +type Http4xxHandler func(req *Request, rep *Response) + +// Http5xxHandler 5xx handler +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 14:55 2024/5/31 +type Http5xxHandler func(req *Request, rep *Response) + +// HttpBusinessErrorHandler 接口请求业务错误 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 18:04 2024/6/1 +type HttpBusinessErrorHandler func(req *Request, rep *Response) + +// RequestSendErrorHandler 请求发送失败的处理逻辑 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 18:23 2024/6/1 +type RequestSendErrorHandler func(req *Request) + +// RequestFinishHandler 请求最终完成事件, 不区分成功 OR 失败 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 18:34 2024/6/1 +type RequestFinishHandler func(req *Request, rep *Response) diff --git a/httpclient/define/request.go b/httpclient/define/request.go new file mode 100644 index 0000000..70ea06b --- /dev/null +++ b/httpclient/define/request.go @@ -0,0 +1,64 @@ +// 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"` // 业务状态码字段 + MessageField string `json:"message_field"` // code描述字段 + DataReceiver any `json:"-"` // 响应data部分数据解析 + 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"} // 默认成功业务状态码 +) diff --git a/httpclient/define/response.go b/httpclient/define/response.go new file mode 100644 index 0000000..2c3e13a --- /dev/null +++ b/httpclient/define/response.go @@ -0,0 +1,71 @@ +// Package define ... +// +// Description : define ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2024-05-31 12:34 +package define + +import ( + "github.com/go-resty/resty/v2" +) + +// 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 ( + RequestFailTypeSend = "SEND_REQUEST_FAIL" // 发送请求即失败, 问题出现在客户端 + RequestFailTypeClientError = "CLIENT_REQUEST_ERROR" // 请求失败, 原因出在客户端, 对应http code 4xx + RequestFailTypeServerError = "SERVER_DEAL_ERROR" // 服务端处理失败, 对应 http code 5xx + RequestFailTypeBusinessError = "SERVICE_BUSINESS_ERROR" // 返回状态码为200, 但是业务状态码非成功 +) diff --git a/httpclient/exception.go b/httpclient/exception.go deleted file mode 100644 index 3cfa15f..0000000 --- a/httpclient/exception.go +++ /dev/null @@ -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 -) diff --git a/httpclient/log/log.go b/httpclient/log/log.go new file mode 100644 index 0000000..0029787 --- /dev/null +++ b/httpclient/log/log.go @@ -0,0 +1,80 @@ +// 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 { + return logger +} + +var buildHttpLogDataFunc BuildHttpLogDataFunc + +// BuildHttpLogDataFunc 构建http请求日志数据的方法 +type BuildHttpLogDataFunc func(ctx context.Context, 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(ctx context.Context, logInstance *zap.Logger, level consts.LogLevel, msg string, reqCfg *define.Request, response *define.Response) { + if nil == logInstance { + // 未设置日志实例 + return + } + buildDataFunc := GetBuildDataFunc() + if nil == buildDataFunc { + // 未设置构建日志数据的方法 + return + } + fields := buildDataFunc(ctx, reqCfg, response) + 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...) + } +} diff --git a/httpclient/mesh/define.go b/httpclient/mesh/define.go new file mode 100644 index 0000000..6539f33 --- /dev/null +++ b/httpclient/mesh/define.go @@ -0,0 +1,109 @@ +// 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/cache" + "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 cache.ICache `json:"-"` // 数据缓存实例 + 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: 继续执行下一个请求, break: 停止执行后续请求 + FinalFailure bool `json:"final_failure"` // 是否作为最终失败: action = break时, 无论外部是什么值, 永远为true, 当 action = continue时, 该值由外部传入, 原因: 当前接口失败, 可能还需要调用一些后续接口做些逻辑. +} + +// 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__" +) diff --git a/httpclient/mesh/mesh.go b/httpclient/mesh/mesh.go new file mode 100644 index 0000000..daf39d3 --- /dev/null +++ b/httpclient/mesh/mesh.go @@ -0,0 +1,245 @@ +// 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: "", + }) + } + resByte, err := validate.Run(respByte, fieldList) + if nil != err { + c.resp.ErrorCode = "-500" + c.resp.ErrorMessage = err.Error() + return c.resp + } + c.resp.Raw = respByte // 原始返回数据 + serialize.JSON.UnmarshalWithNumberIgnoreError(resByte, &c.resp.DataMap) // map结果 + + if nil != c.receiver { + // 解析到receiver + if err = serialize.JSON.UnmarshalWithNumber(resByte, 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, apiCfg.CacheInstance); 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 { + // 判断是否已经是最终失败 + if apiCfg.FailBehavior.FinalFailure || apiCfg.FailBehavior.Action == FailBehaviorError { + c.resp.FinalFailure = true + // 判断是否继续, 只能阻断后续分组请求,无法阻断当前租的请求 + 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, // 默认失败终止请求 + FinalFailure: true, // 默认一旦失败,则争个整个失败 + } + } + if apiCfg.FailBehavior.Action == FailBehaviorError { + // 配置了请求失败则报错, 一定是导致结果最终失败 + apiCfg.FailBehavior.FinalFailure = true + } + // 每一个请求有独立的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 = serialize.JSON.UnmarshalWithNumber(buildRes, &d); nil != err { + return nil, err + } + return d, err +} diff --git a/httpclient/request.go b/httpclient/request.go deleted file mode 100644 index c25d32b..0000000 --- a/httpclient/request.go +++ /dev/null @@ -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 -} diff --git a/httpclient/resty.go b/httpclient/resty.go new file mode 100644 index 0000000..faaf57c --- /dev/null +++ b/httpclient/resty.go @@ -0,0 +1,121 @@ +// Package httpclient ... +// +// Description : httpclient ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2024-05-31 14:59 +package httpclient + +import ( + "encoding/json" + "git.zhangdeman.cn/zhangdeman/consts" + "git.zhangdeman.cn/zhangdeman/network/httpclient/define" + "git.zhangdeman.cn/zhangdeman/serialize" + "git.zhangdeman.cn/zhangdeman/wrapper" + "github.com/go-resty/resty/v2" + "github.com/tidwall/gjson" + "net/http" + "net/textproto" + "strings" +) + +// NewRestyClient 获取resty client +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 15:00 2024/5/31 +func NewRestyClient(reqConfig *define.Request) (*resty.Client, *resty.Request) { + client := resty.New() + request := client.R() + if nil == reqConfig { + return client, request + } + formatHeader(reqConfig) + client.SetAllowGetMethodPayload(true) // 配置 GET 请求允许带 Body + client.SetJSONMarshaler(json.Marshal) // 序列化方法 + client.SetJSONEscapeHTML(true) // 处理html实体字符 + client.SetJSONUnmarshaler(serialize.JSON.UnmarshalWithNumber) // 反序列化方法 + + 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 // 请求接口 + for pathParamName, pathParamValue := range reqConfig.PathParam { + if len(pathParamValue) == 0 { + continue + } + reqConfig.FullUrl = strings.ReplaceAll(reqConfig.FullUrl, "{#"+pathParamName+"#}", pathParamValue) + } + 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 + setRestyBody(reqConfig, request) // 设置请求Body + return client, request +} + +// setRestyBody 设置请求BODY +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 17:18 2024/5/31 +func setRestyBody(reqConfig *define.Request, request *resty.Request) { + if nil == reqConfig.Body || len(reqConfig.Body) == 0 { + return + } + if strings.Contains(strings.ToLower(reqConfig.ContentType), consts.MimeTypeJson) { + request.SetBody(reqConfig.Body) + return + } + if strings.Contains(strings.ToLower(reqConfig.ContentType), consts.MimeTypeXWWWFormUrlencoded) { + bodyStr := serialize.JSON.MarshalForStringIgnoreError(reqConfig.Body) + bodyData := map[string]string{} + jsonObj := gjson.Parse(bodyStr) + jsonObj.ForEach(func(key, value gjson.Result) bool { + bodyData[key.String()] = value.String() + return true + }) + request.SetFormData(bodyData) + } + return +} + +// formatHeader 格式化header +// +// 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 +} diff --git a/httpclient/validate/request_config.go b/httpclient/validate/request_config.go new file mode 100644 index 0000000..31c242a --- /dev/null +++ b/httpclient/validate/request_config.go @@ -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 +}