diff --git a/httpclient/define.go b/httpclient.bak/define.go similarity index 100% rename from httpclient/define.go rename to httpclient.bak/define.go diff --git a/httpclient/exception.go b/httpclient.bak/exception.go similarity index 100% rename from httpclient/exception.go rename to httpclient.bak/exception.go diff --git a/httpclient/request.go b/httpclient.bak/request.go similarity index 100% rename from httpclient/request.go rename to httpclient.bak/request.go 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..3b1da7f --- /dev/null +++ b/httpclient/cache/ICache.go @@ -0,0 +1,66 @@ +// Package cache ... +// +// Description : cache ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2024-06-03 15:49 +package cache + +import "git.zhangdeman.cn/gateway/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..6de1ff5 --- /dev/null +++ b/httpclient/client.go @@ -0,0 +1,431 @@ +// Package httpclient ... +// +// Description : httpclient ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2024-05-31 15:22 +package httpclient + +import ( + "fmt" + "git.zhangdeman.cn/gateway/httpclient/cache" + "git.zhangdeman.cn/gateway/httpclient/define" + "git.zhangdeman.cn/gateway/httpclient/validate" + "git.zhangdeman.cn/zhangdeman/serialize" + "github.com/go-resty/resty/v2" + "github.com/tidwall/gjson" + "net/http" + "strings" + "time" +) + +// 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 err := validate.RequestConfig(reqConfig); nil != err { + return nil, err + } + 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, + } + 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++ { + 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", + } + for _, itemAfterResponse := range hc.requestFinishHandler { + itemAfterResponse(hc.reqConfig, response) + } + 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", + } + for _, itemAfterResponse := range hc.requestFinishHandler { + itemAfterResponse(hc.reqConfig, response) + } + 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 + } + + 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]string{}, + Cookie: map[string]string{}, + 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]string{} // 清空已有数据 + 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]string{} // 清空已有数据 + 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/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..e59378f --- /dev/null +++ b/httpclient/define/error.go @@ -0,0 +1,17 @@ +// 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 开头 + 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..e300e2e --- /dev/null +++ b/httpclient/define/request.go @@ -0,0 +1,56 @@ +// Package define ... +// +// Description : define ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2024-05-24 17:09 +package define + +// Request 请求配置 +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 17:10 2024/5/24 +type Request struct { + PathParam map[string]string `json:"path_param"` // 替换url中的占位符 + Body map[string]any `json:"body"` // 请求Body + Header map[string]string `json:"header"` // 请求Header + Cookie map[string]string `json:"cookie"` // 请求Cookie + Query map[string]string `json:"query"` // 请求query + 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 interface{} `json:"-"` // 响应data部分数据解析 + SuccessCodeList []string `json:"success_code_list"` // 哪些业务状态码视为成功 + ConnectTimeout int64 `json:"connect_timeout"` // 连接超时时间: ms + ReadTimeout int64 `json:"read_timeout"` // 读取超时时间 + RetryRule *RequestRetryRule `json:"retry_rule"` // 重试规则 +} + +// 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..5761589 --- /dev/null +++ b/httpclient/define/response.go @@ -0,0 +1,70 @@ +// 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 { + Header map[string]string `json:"header"` // 响应header + Cookie map[string]string `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/resty.go b/httpclient/resty.go new file mode 100644 index 0000000..a7eded1 --- /dev/null +++ b/httpclient/resty.go @@ -0,0 +1,102 @@ +// Package httpclient ... +// +// Description : httpclient ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2024-05-31 14:59 +package httpclient + +import ( + "encoding/json" + "git.zhangdeman.cn/gateway/httpclient/define" + "git.zhangdeman.cn/zhangdeman/consts" + "git.zhangdeman.cn/zhangdeman/serialize" + "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中的参数 + request.SetQueryParams(reqConfig.Query) // 设置query参数 + request.SetHeaders(reqConfig.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 { + cookieList = append(cookieList, &http.Cookie{ + Name: cookieName, + Value: cookieValue, + }) + } + 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]string) + for headerName, headerVal := range requestConfig.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..7b8e608 --- /dev/null +++ b/httpclient/validate/request_config.go @@ -0,0 +1,131 @@ +// Package validate ... +// +// Description : validate ... +// +// Author : go_developer@163.com<白茶清欢> +// +// Date : 2024-05-31 16:18 +package validate + +import ( + "git.zhangdeman.cn/gateway/httpclient/define" + "net/http" + "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]string{} + } + if len(reqConfig.ContentType) > 0 { + reqConfig.Header["Content-Type"] = reqConfig.ContentType + } + if nil == reqConfig.Cookie { + reqConfig.Cookie = map[string]string{} + } + 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 !strings.HasPrefix(reqConfig.FullUrl, "http://") && !strings.HasPrefix(reqConfig.FullUrl, "https://") { + 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.ErrFullUrlEmpty + } + 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 +}