Compare commits

...

10 Commits

13 changed files with 230 additions and 67 deletions

View File

@ -7,7 +7,9 @@
// Date : 2025-05-07 21:13
package abstract
import "git.zhangdeman.cn/zhangdeman/network/httpclient/define"
// RateLimiter v2 流控口约束, v3移除了, 人工补齐
type RateLimiter interface {
Allow() bool
Allow(reqCfg *define.Request) bool
}

View File

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

View File

@ -10,7 +10,6 @@ package httpclient
import (
"fmt"
"git.zhangdeman.cn/zhangdeman/network/httpclient/implement"
"git.zhangdeman.cn/zhangdeman/wrapper"
"net"
"strings"
"time"
@ -99,7 +98,13 @@ func NewHttpClient(reqConfig *define.Request, reqOption *RequestOption) (*HttpCl
if ua, exist := reqConfig.Header[consts.HeaderKeyUserAgent.String()]; !exist || nil == ua || fmt.Sprintf("%v", ua) == "" {
reqConfig.Header[consts.HeaderKeyUserAgent.String()] = "resty-v3@network/httpclient"
}
restyClient, restyRequest := NewRestyClient(reqConfig, reqOption)
restyClient, restyRequest, err := NewRestyClient(reqConfig, reqOption)
if nil != err {
if nil != restyClient {
_ = restyClient.Close()
}
return nil, err
}
defer restyClient.Close()
hc := &HttpClient{
@ -235,8 +240,8 @@ func (hc *HttpClient) Request() *define.Response {
"remaining_ttl": ttl,
}, hc.reqCfg)
// 配置了最小剩余时间,并且key剩余有效期小于最小剩余时间
// 预热加锁, 并发请求触发预热, 仅触发一个即可, 使用接口key + query参数做key, 按照一般约定, 写请求不会做缓存, 只有读请求会
lockKey := wrapper.String(hc.reqCfg.FullUrl + serialize.JSON.MarshalForStringIgnoreError(hc.reqCfg.Query)).Md5().Value
// 预热加锁, 并发请求触发预热, 仅触发一个即可, 使用接口缓存key + LOCK做锁的key {{CACHE_KEY}}_LOCK, 按照一般约定, 写请求不会做缓存, 只有读请求会
lockKey := hc.reqOption.CacheInstance.GetKey(hc.reqCfg) + "_LOCK"
if err := hc.reqOption.CacheInstance.Lock(lockKey); err != nil {
log.RecordWarn("接口请求命中缓存, 缓存结果有效期大于剩余时长小于配置阈值, 触发预热, 加锁失败, 未执行预热", map[string]any{
"min_ttl": cachePreHeatConfig.MinTTL,
@ -450,7 +455,7 @@ func (hc *HttpClient) newResponse() *define.Response {
//
// Date : 16:04 2024/6/3
func (hc *HttpClient) getCacheResult() *define.Response {
if nil == hc.reqOption.CacheInstance {
if nil == hc.reqOption || nil == hc.reqOption.CacheInstance {
log.RecordDebug("接口请求前缓存检测, 未设置缓存实例", map[string]any{}, hc.reqCfg)
return nil
}

View File

@ -7,34 +7,6 @@
// 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<白茶清欢>

View File

@ -30,6 +30,7 @@ type Request struct {
Method string `json:"method"` // 请求方法
DataField string `json:"data_field"` // 数据字段
CodeField string `json:"code_field"` // 业务状态码字段
CodeLocation string `json:"code_location"` // 业务状态码位置
MessageField string `json:"message_field"` // code描述字段
DataReceiver any `json:"-"` // 响应data部分数据解析
SuccessHttpCodeList []int `json:"success_http_code_list"` // 哪些http状态码视为成功, 不配置, 默认2xx

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import (
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
"git.zhangdeman.cn/zhangdeman/serialize"
"github.com/tidwall/gjson"
"net/http"
"strings"
)
@ -67,6 +68,9 @@ func (r *Response) fillResponseBody(reqCfg *define.Request, response *define.Res
responseContentType := response.RestyResponse.Header().Get(consts.HeaderKeyContentType.String())
if responseContentType == "" {
// 返回数据未说明 Content-Type
if response.RestyResponse.StatusCode() != http.StatusOK {
return nil
}
return errors.New("response content type is empty")
}
typeArr := strings.Split(strings.Split(responseContentType, ";")[0], "/")
@ -94,8 +98,20 @@ func (r *Response) fillResponseBody(reqCfg *define.Request, response *define.Res
return errors.New("response body Marshal error :" + err.Error())
}
response.Data = string(jsonByte)
response.Code = gjson.Get(response.Data, reqCfg.CodeField).String()
response.Message = gjson.Get(response.Data, reqCfg.MessageField).String()
if strings.ToLower(reqCfg.CodeLocation) == "header" {
if reqCfg.CodeField == "code" {
response.Code = fmt.Sprintf("%v", response.HttpCode)
response.Message = response.RestyResponse.Status()
} else {
response.Code = response.RestyResponse.Header().Get(reqCfg.CodeField)
response.Message = response.RestyResponse.Header().Get(reqCfg.MessageField)
}
} else {
// 统一认为Body
response.Code = gjson.Get(response.Data, reqCfg.CodeField).String()
response.Message = gjson.Get(response.Data, reqCfg.MessageField).String()
}
businessData := gjson.Get(response.Data, reqCfg.DataField)
if businessData.Value() == nil {
// data为空指针, 归一化成空对象

View File

@ -41,6 +41,7 @@ type RequestConfigGroupItem struct {
CacheInstance abstract.ICache `json:"-"` // 数据缓存实例
RateLimiter abstract.RateLimiter `json:"-"` // 流控实例
ResponseParser abstract.IResponse `json:"-"` // 响应数据解析
RequestBodyWrite abstract.IRequestBodyWrite `json:"-"` // 请求Body数据写入的实现
Condition any `json:"condition"` // TODO: 请求条件, 特定条件下不执行当前请求
}

View File

@ -13,7 +13,8 @@ import (
// RequestOption 请求一些选项
type RequestOption struct {
CacheInstance abstract.ICache `json:"-"` // 数据结果缓存实例
RateLimiter abstract.RateLimiter `json:"-"` // 流控实例
ResponseParser abstract.IResponse `json:"-"` // 返回结果解析, 不配置使用内置实现
CacheInstance abstract.ICache `json:"-"` // 数据结果缓存实例
RateLimiter abstract.RateLimiter `json:"-"` // 流控实例
RequestBodyWrite abstract.IRequestBodyWrite `json:"-"` // 将请求Body写入请求实例
ResponseParser abstract.IResponse `json:"-"` // 返回结果解析, 不配置使用内置实现
}

View File

@ -8,12 +8,12 @@
package httpclient
import (
"errors"
"fmt"
"git.zhangdeman.cn/zhangdeman/consts"
"git.zhangdeman.cn/zhangdeman/network/httpclient/define"
"git.zhangdeman.cn/zhangdeman/serialize"
requestBoodyWriter "git.zhangdeman.cn/zhangdeman/network/httpclient/implement/request"
"git.zhangdeman.cn/zhangdeman/wrapper"
"github.com/tidwall/gjson"
"net/http"
"net/textproto"
"resty.dev/v3"
@ -75,25 +75,25 @@ func initRequestConfig(reqConfig *define.Request) {
// Author : go_developer@163.com<白茶清欢>
//
// Date : 15:00 2024/5/31
func NewRestyClient(reqConfig *define.Request, reqOption *RequestOption) (*resty.Client, *resty.Request) {
func NewRestyClient(reqConfig *define.Request, reqOption *RequestOption) (*resty.Client, *resty.Request, error) {
client := resty.New()
request := client.R()
if nil == reqConfig {
return client, request
return nil, nil, errors.New("request config is nil")
}
// 限流处理, 增加限流中间件
client.AddRequestMiddleware(func(client *resty.Client, request *resty.Request) error {
if nil != reqOption && nil != reqOption.RateLimiter {
if nil == reqOption || nil == reqOption.RateLimiter {
// 未配置流控
return nil
}
if !reqOption.RateLimiter.Allow() {
if !reqOption.RateLimiter.Allow(reqConfig) {
// 命中流控
return define.ErrRateLimitExceeded
}
return nil
})
initRequestConfig(reqConfig) // 初始化 + 格式化配置
client.SetAllowMethodGetPayload(true) // 配置 GET 请求允许带 Body
client.SetAllowMethodDeletePayload(true) // 配置 DELETE 请求允许带 Body
@ -130,35 +130,29 @@ func NewRestyClient(reqConfig *define.Request, reqOption *RequestOption) (*resty
Value: wrapper.AnyDataType(cookieValue).ToString().Value(),
})
}
request.SetCookies(cookieList) // 设置cookie
setRestyBody(reqConfig, request) // 设置请求Body
return client, request
request.SetCookies(cookieList) // 设置cookie
if err := setRestyBody(reqConfig, reqOption, request); nil != err {
return nil, nil, err
}
return client, request, nil
}
// setRestyBody 设置请求BODY TODO: 支持xml / yml等
// 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
func setRestyBody(reqConfig *define.Request, requestOption *RequestOption, request *resty.Request) error {
if nil == reqConfig.Body {
return nil
}
if strings.Contains(strings.ToLower(reqConfig.ContentType), consts.MimeTypeJson) {
request.SetBody(reqConfig.Body)
return
if nil != requestOption && nil != requestOption.RequestBodyWrite {
// 外部传入的实现
requestOption.RequestBodyWrite.Write(request, reqConfig.Body)
return nil
}
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
// 外部没传入, 使用内置实现, 内置默认实现支持: xml/json/form, 如需其他, 自行扩展
return requestBoodyWriter.WriteBody(reqConfig.ContentType, request, reqConfig.Body)
}
// formatHeader 格式化header