增加 自定义导航栏模块

This commit is contained in:
2026-04-17 03:12:28 +08:00
parent 792f547dc3
commit ec508e6a32
18 changed files with 1274 additions and 653 deletions

View File

@@ -0,0 +1,80 @@
package services
import (
"NetworkAuth/models"
"strings"
"gorm.io/gorm"
)
const portalNavigationAdminPath = "admin"
const portalNavigationAdminSort = 999
// NormalizePortalNavigation 规范化门户导航数据
// 统一清理首尾空白,并处理首页与排序约束
func NormalizePortalNavigation(item *models.PortalNavigation) {
item.Name = strings.TrimSpace(item.Name)
item.Path = strings.TrimSpace(item.Path)
if item.Sort < 0 {
item.Sort = 0
}
if item.IsHome {
item.IsHidden = false
}
}
// IsPortalNavigationAdminEntry 判断是否为管理员入口
// 管理员入口属于系统保留导航项,不允许修改基础信息
func IsPortalNavigationAdminEntry(item models.PortalNavigation) bool {
return strings.EqualFold(strings.TrimSpace(item.Path), portalNavigationAdminPath)
}
// LockPortalNavigationProtectedFields 锁定系统保留导航字段
// 管理员入口仅允许调整隐藏状态,其余字段保持系统固定值
func LockPortalNavigationProtectedFields(item *models.PortalNavigation, exists models.PortalNavigation) {
switch IsPortalNavigationAdminEntry(exists) {
case true:
item.Name = "管理员登录"
item.Path = portalNavigationAdminPath
item.Sort = portalNavigationAdminSort
item.IsHome = false
item.IsExternal = false
default:
return
}
}
// SavePortalNavigation 保存门户导航
// 当当前记录被设置为门户首页时,会自动取消其他记录的首页状态
func SavePortalNavigation(db *gorm.DB, item *models.PortalNavigation, exists ...models.PortalNavigation) error {
if len(exists) > 0 {
LockPortalNavigationProtectedFields(item, exists[0])
}
NormalizePortalNavigation(item)
return db.Transaction(func(tx *gorm.DB) error {
if item.IsHome {
query := tx.Model(&models.PortalNavigation{}).Where("is_home = ?", true)
if item.ID > 0 {
query = query.Where("id <> ?", item.ID)
}
if err := query.Update("is_home", false).Error; err != nil {
return err
}
}
switch {
case item.ID == 0:
return tx.Create(item).Error
default:
return tx.Model(&models.PortalNavigation{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
"name": item.Name,
"path": item.Path,
"sort": item.Sort,
"is_home": item.IsHome,
"is_hidden": item.IsHidden,
"is_external": item.IsExternal,
}).Error
}
})
}

View File

@@ -4,64 +4,119 @@ import (
"bytes"
"compress/flate"
"compress/gzip"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"reflect"
"net/url"
"strings"
"time"
"unsafe"
"github.com/andybalholm/brotli"
"github.com/go-resty/resty/v2"
req "github.com/imroc/req/v3"
"github.com/skycheung803/go-bypasser"
)
type RestyClient struct {
client *resty.Client
client *resty.Client
reqClient *req.Client
ctx context.Context
baseURL string
defaultHeaders map[string]string
proxyStr string
timeout time.Duration
}
func (request *RestyClient) Resty() *resty.Client {
return request.client
}
// NewClient 创建一个基于 uTLS 指纹与 HTTP/2 指纹的 Resty 客户端
// baseURL 不为空则设置默认 BaseURLproxyStr 不为空则启用 HTTP 代理(仅 HTTP/1.1
// persistCookies 启用持久化 CookiefollowRedirect 启用重定向跟随timeout 设置超时时间0 或负数则默认 60 秒)
// NewClient 创建一个基于 go-bypasser(req/v3) 的客户端
// 对外继续保留 Resty 风格接口,但底层请求不再走 resty.Transport。
func NewClient(baseURL string, proxyStr string, persistCookies bool, timeout int) *RestyClient {
rc := resty.New()
if baseURL != "" {
rc.SetBaseURL(baseURL)
}
if persistCookies {
jar, _ := cookiejar.New(nil)
rc.SetCookieJar(jar)
}
// 设置请求超时时间,如果传入 0 或负数则默认 60 秒
if timeout <= 0 {
timeout = 60
}
rc.SetTimeout(time.Duration(timeout) * time.Second)
timeoutDuration := time.Duration(timeout) * time.Second
// 统一设置客户端默认请求头(调用级 headers 可覆盖),字段按字母顺序排列
rc.SetHeader("accept", "*/*")
rc.SetHeader("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6")
rc.SetHeader("connection", "keep-alive")
rc.SetHeader("pragma", "no-cache")
rc.SetHeader("priority", "u=1,i")
rc.SetHeader("sec-ch-ua", "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"")
rc.SetHeader("sec-ch-ua-mobile", "?0")
rc.SetHeader("sec-ch-ua-platform", "\"macOS\"")
rc.SetHeader("sec-fetch-dest", "empty")
rc.SetHeader("sec-fetch-mode", "cors")
rc.SetHeader("sec-fetch-site", "same-origin")
rc.SetHeader("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36")
defaultHeaders := map[string]string{
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"connection": "keep-alive",
"pragma": "no-cache",
"priority": "u=1,i",
"sec-ch-ua": "\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
}
// 初始化 go-bypasser 替代原有的 spoofed-round-tripper
stateClient := resty.New().
SetTimeout(timeoutDuration).
SetHeaders(defaultHeaders)
if baseURL != "" {
stateClient.SetBaseURL(baseURL)
}
var sharedJar http.CookieJar
if persistCookies {
jar, _ := cookiejar.New(nil)
sharedJar = jar
stateClient.SetCookieJar(sharedJar)
}
baseReqClient := mustNewReqClient(proxyStr, timeoutDuration, baseURL, defaultHeaders, sharedJar)
return &RestyClient{
client: stateClient,
reqClient: baseReqClient,
ctx: context.Background(),
baseURL: baseURL,
defaultHeaders: defaultHeaders,
proxyStr: proxyStr,
timeout: timeoutDuration,
}
}
func (request *RestyClient) WithContext(ctx context.Context) *RestyClient {
if ctx == nil {
ctx = context.Background()
}
return &RestyClient{
client: request.client,
reqClient: request.reqClient,
ctx: ctx,
baseURL: request.baseURL,
defaultHeaders: request.defaultHeaders,
proxyStr: request.proxyStr,
timeout: request.timeout,
}
}
// SetPersistentHeader 设置持久化 Header。
// 除 Cookie 外,其余 Header 会同步到 req 客户端的 common headers。
func (request *RestyClient) SetPersistentHeader(key string, value string) {
if request.defaultHeaders == nil {
request.defaultHeaders = make(map[string]string)
}
lowerKey := strings.ToLower(key)
request.defaultHeaders[lowerKey] = value
if request.client != nil {
request.client.SetHeader(key, value)
}
if request.reqClient != nil && lowerKey != "cookie" {
request.reqClient.SetCommonHeader(key, value)
}
}
func mustNewReqClient(proxyStr string, timeout time.Duration, baseURL string, defaultHeaders map[string]string, jar http.CookieJar) *req.Client {
opts := []bypasser.BypasserOption{
bypasser.WithInsecureSkipVerify(true),
}
@@ -74,155 +129,287 @@ func NewClient(baseURL string, proxyStr string, persistCookies bool, timeout int
panic(err)
}
rc.SetTransport(&sanitizeTransport{t: bypass.Transport})
return &RestyClient{client: rc}
}
// sanitizeTransport 包装 http.RoundTripper 以修复底层库可能违背 Go 接口约定的行为
type sanitizeTransport struct {
t http.RoundTripper
}
func (s *sanitizeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := s.t.RoundTrip(req)
// net/http 规定 RoundTripper 要么返回有效的 resp 和 nil error要么返回 nil resp 和有效的 error。
// 某些第三方库(如部分 tls-client 封装)在遇到网络小问题时会同时返回 resp 和 err。
// 这会导致 net/http 打印 "RoundTripper returned a response & error; ignoring response" 并强制丢弃响应。
// 在这里我们进行修正:如果已经拿到了响应(哪怕是不完整的),我们优先保留响应并将 err 置空,让上层通过读取 Body 自行发现错误。
if resp != nil && err != nil {
err = nil
}
return resp, err
}
// fillResponseBody 使用反射强制填充响应体
// 当 Resty 因为重定向策略错误而提前返回时,它可能不会读取 Body
// 此方法手动读取 RawResponse.Body 并回填到 resty.Response 的私有 body 字段中
func (request *RestyClient) fillResponseBody(resp *resty.Response) {
if resp == nil || resp.RawResponse == nil {
return
}
// 如果已经有 body 内容,则不处理
if len(resp.Body()) > 0 {
return
rt, ok := bypass.Transport.(*bypasser.StandardRoundTripper)
if !ok || rt.Client == nil {
panic("go-bypasser did not return a StandardRoundTripper client")
}
// 读取底层 Body
bodyBytes, err := io.ReadAll(resp.RawResponse.Body)
client := rt.Client
client.SetTimeout(timeout)
client.SetRedirectPolicy(req.DefaultRedirectPolicy())
if baseURL != "" {
client.SetBaseURL(baseURL)
}
for k, v := range defaultHeaders {
if strings.ToLower(k) == "cookie" {
continue
}
client.SetCommonHeader(k, v)
}
if jar != nil {
client.SetCookieJar(jar)
}
return client
}
func (request *RestyClient) newRequestClient(redirectCount int) *req.Client {
client := request.reqClient.Clone()
if request.baseURL != "" {
client.SetBaseURL(request.baseURL)
}
client.SetTimeout(request.timeout)
switch {
case redirectCount == 0:
client.SetRedirectPolicy(req.NoRedirectPolicy())
case redirectCount > 0:
client.SetRedirectPolicy(req.MaxRedirectPolicy(redirectCount))
default:
client.SetRedirectPolicy(req.DefaultRedirectPolicy())
}
return client
}
func (request *RestyClient) resolveRequestURL(path string) *url.URL {
if path == "" {
return nil
}
parsedURL, err := url.Parse(path)
if err != nil {
return
return nil
}
resp.RawResponse.Body.Close()
// 重置 Body 以便后续可能得读取
resp.RawResponse.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// 使用反射设置私有字段 body
v := reflect.ValueOf(resp).Elem()
f := v.FieldByName("body")
if f.IsValid() {
// 必须使用 UnsafeAddr 获取未导出字段的地址
rf := reflect.NewAt(f.Type(), unsafe.Pointer(f.UnsafeAddr())).Elem()
rf.SetBytes(bodyBytes)
if parsedURL.IsAbs() {
return parsedURL
}
if request.baseURL == "" {
return parsedURL
}
// 设置 size 字段
s := v.FieldByName("size")
if s.IsValid() {
rs := reflect.NewAt(s.Type(), unsafe.Pointer(s.UnsafeAddr())).Elem()
rs.SetInt(int64(len(bodyBytes)))
baseURL, err := url.Parse(request.baseURL)
if err != nil {
return parsedURL
}
return baseURL.ResolveReference(parsedURL)
}
func cloneCookie(cookie *http.Cookie) *http.Cookie {
if cookie == nil {
return nil
}
copied := *cookie
return &copied
}
func isSafeHTTPCookieValue(value string) bool {
if value == "" {
return true
}
for _, r := range value {
if r < 0x21 || r > 0x7e {
return false
}
switch r {
case '"', ';', '\\', ',':
return false
}
}
return true
}
func parseRawCookieHeader(raw string) []*http.Cookie {
if raw == "" {
return nil
}
var cookies []*http.Cookie
for _, part := range strings.Split(raw, ";") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
name, value, ok := strings.Cut(part, "=")
if !ok {
continue
}
name = strings.TrimSpace(name)
value = strings.TrimSpace(value)
if name == "" {
continue
}
cookies = append(cookies, &http.Cookie{Name: name, Value: value})
}
return cookies
}
func buildCookieHeader(cookies []*http.Cookie) string {
if len(cookies) == 0 {
return ""
}
parts := make([]string, 0, len(cookies))
for _, cookie := range cookies {
if cookie == nil || cookie.Name == "" {
continue
}
parts = append(parts, fmt.Sprintf("%s=%s", cookie.Name, cookie.Value))
}
return strings.Join(parts, "; ")
}
// decodeCompressedBody 按响应头解压正文,兼容项目里依赖明文 HTML/JSON 的解析逻辑。
func decodeCompressedBody(body []byte, contentEncoding string) ([]byte, error) {
encoding := strings.ToLower(strings.TrimSpace(contentEncoding))
switch {
case encoding == "", encoding == "identity":
return body, nil
case strings.Contains(encoding, "gzip"):
reader, err := gzip.NewReader(bytes.NewReader(body))
if err != nil {
return nil, err
}
defer reader.Close()
return io.ReadAll(reader)
case strings.Contains(encoding, "deflate"):
reader := flate.NewReader(bytes.NewReader(body))
defer reader.Close()
return io.ReadAll(reader)
case strings.Contains(encoding, "br"):
reader := brotli.NewReader(bytes.NewReader(body))
return io.ReadAll(reader)
default:
return body, nil
}
}
// makeReq 构造带可选请求头的 resty.Request
// 功能:基于客户端创建请求对象,并在传入 headers 时进行设置
// 返回:带有请求头的请求对象
func (request *RestyClient) makeReq(headers map[string]string, cookies []*http.Cookie) *resty.Request {
req := request.client.R()
func (request *RestyClient) prepareCookies(path string, requestCookies []*http.Cookie) ([]*http.Cookie, string) {
cookieMap := make(map[string]*http.Cookie)
order := make([]string, 0)
rawCookieNames := make(map[string]struct{})
appendCookie := func(cookie *http.Cookie) {
if cookie == nil || cookie.Name == "" {
return
}
if _, exists := cookieMap[cookie.Name]; !exists {
order = append(order, cookie.Name)
}
cloned := cloneCookie(cookie)
cookieMap[cookie.Name] = cloned
if cloned != nil && !isSafeHTTPCookieValue(cloned.Value) {
rawCookieNames[cloned.Name] = struct{}{}
}
}
parsedURL := request.resolveRequestURL(path)
if request.client != nil && request.client.GetClient() != nil && request.client.GetClient().Jar != nil && parsedURL != nil {
for _, cookie := range request.client.GetClient().Jar.Cookies(parsedURL) {
appendCookie(cookie)
}
}
if request.client != nil {
for _, cookie := range request.client.Cookies {
appendCookie(cookie)
}
}
for _, cookie := range requestCookies {
appendCookie(cookie)
}
rawCookies := parseRawCookieHeader(request.defaultHeaders["cookie"])
for _, cookie := range rawCookies {
if cookie == nil || cookie.Name == "" {
continue
}
rawCookieNames[cookie.Name] = struct{}{}
if _, exists := cookieMap[cookie.Name]; !exists {
order = append(order, cookie.Name)
}
cookieMap[cookie.Name] = cookie
}
mergedCookies := make([]*http.Cookie, 0, len(order))
for _, name := range order {
if cookie := cookieMap[name]; cookie != nil {
mergedCookies = append(mergedCookies, cloneCookie(cookie))
}
}
if len(rawCookieNames) == 0 {
return mergedCookies, ""
}
return mergedCookies, buildCookieHeader(mergedCookies)
}
func (request *RestyClient) buildReqRequest(client *req.Client, path string, headers map[string]string, cookies []*http.Cookie) *req.Request {
r := client.R().SetContext(request.ctx)
if len(headers) > 0 {
req = req.SetHeaders(headers)
r.SetHeaders(headers)
}
if len(cookies) > 0 {
req = req.SetCookies(cookies)
mergedCookies, rawCookieHeader := request.prepareCookies(path, cookies)
if rawCookieHeader != "" {
r.SetHeader("Cookie", rawCookieHeader)
} else if len(mergedCookies) > 0 {
r.SetCookies(mergedCookies...)
}
return req
return r
}
// doWithEncodingFallback 封装请求发送并在出现压缩相关错误时进行一次降级重试
// 逻辑:首次请求失败且错误包含 gzip/zstd/brotli/magic number mismatch 时,设置 accept-encoding 为 identity 重试一次
func (request *RestyClient) doWithEncodingFallback(headers map[string]string, cookies []*http.Cookie, allowRedirect bool, do func(*resty.Request) (*resty.Response, error)) (*resty.Response, error) {
req := request.makeReq(headers, cookies)
if allowRedirect {
request.client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10))
} else {
// 使用 http.ErrUseLastResponse 确保 302 响应被返回且 Body 可读,而不是报错
request.client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}))
}
resp, err := do(req)
// 尝试补救响应体(特别是当重定向被禁用导致报错时)
request.fillResponseBody(resp)
if err == nil {
return resp, nil
}
s := err.Error()
if strings.Contains(s, "gzip: invalid header") || strings.Contains(s, "magic number mismatch") || strings.Contains(s, "zstd") || strings.Contains(s, "brotli") {
h2 := map[string]string{}
for k, v := range headers {
if strings.ToLower(k) != "accept-encoding" {
h2[k] = v
}
}
h2["Accept-Encoding"] = "identity"
req2 := request.makeReq(h2, cookies)
resp2, err2 := do(req2)
request.fillResponseBody(resp2)
if err2 == nil {
return resp2, nil
}
}
return resp, err
}
// decodeResponse 处理响应解压与 JSON 解析
// 功能:自动识别 gzip 压缩并解压;在 result 非空时按 JSON 解析到 result
// 返回:解析错误(成功时为 nil
func (request *RestyClient) decodeResponse(resp *resty.Response, result interface{}) error {
func (request *RestyClient) adaptReqResponse(path string, method string, data any, headers map[string]string, cookies []*http.Cookie, resp *req.Response) (*resty.Response, error) {
if resp == nil {
return nil, nil
}
body, err := resp.ToBytes()
if err != nil && resp.Response == nil {
return nil, err
}
rawResponse := resp.Response
if rawResponse != nil {
decodedBody, decodeErr := decodeCompressedBody(body, rawResponse.Header.Get("Content-Encoding"))
if decodeErr != nil {
return nil, decodeErr
}
body = decodedBody
rawResponse.Body = io.NopCloser(bytes.NewReader(body))
rawResponse.Header.Del("Content-Encoding")
rawResponse.ContentLength = int64(len(body))
}
restyReq := request.client.R()
restyReq.Method = method
restyReq.URL = path
restyReq.Body = data
restyReq.Header = make(http.Header)
for k, v := range headers {
restyReq.Header.Set(k, v)
}
mergedCookies, rawCookieHeader := request.prepareCookies(path, cookies)
if rawCookieHeader != "" {
restyReq.Header.Set("Cookie", rawCookieHeader)
} else {
restyReq.Cookies = mergedCookies
}
restyResp := &resty.Response{
Request: restyReq,
RawResponse: rawResponse,
}
restyResp.SetBody(body)
return restyResp, err
}
func (request *RestyClient) decodeResponse(resp *resty.Response, result any) error {
if resp == nil || result == nil {
return nil
}
body := resp.Body()
if len(body) == 0 {
return nil
}
ct := strings.ToLower(resp.Header().Get("Content-Type"))
ce := strings.ToLower(resp.Header().Get("Content-Encoding"))
body := resp.Body()
if strings.Contains(ce, "gzip") && len(body) > 0 {
gr, gerr := gzip.NewReader(bytes.NewReader(body))
if gerr == nil {
defer gr.Close()
if dec, derr := io.ReadAll(gr); derr == nil {
body = dec
resp.SetBody(body)
}
}
} else if strings.Contains(ce, "deflate") && len(body) > 0 {
// 处理 deflate 压缩
dr := flate.NewReader(bytes.NewReader(body))
defer dr.Close()
if dec, derr := io.ReadAll(dr); derr == nil {
body = dec
resp.SetBody(body)
}
} else if strings.Contains(ce, "br") && len(body) > 0 {
// 处理 brotli 压缩
br := brotli.NewReader(bytes.NewReader(body))
if dec, derr := io.ReadAll(br); derr == nil {
body = dec
resp.SetBody(body) // 将解压后的 body 写回 response
}
}
if result != nil && (strings.Contains(ct, "application/json") || json.Valid(body)) {
if strings.Contains(ct, "application/json") || json.Valid(body) {
if err := json.Unmarshal(body, result); err != nil {
return err
}
@@ -230,114 +417,111 @@ func (request *RestyClient) decodeResponse(resp *resty.Response, result interfac
return nil
}
// RestyGet 发送 GET 请求
func (request *RestyClient) RestyGet(path string, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
return r.Get(path)
})
if resp == nil && err != nil {
return nil, err
func (request *RestyClient) execute(method string, path string, data any, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
client := request.newRequestClient(redirectCount)
doRequest := func(extraHeaders map[string]string) (*resty.Response, error) {
r := request.buildReqRequest(client, path, extraHeaders, cookies)
if data != nil {
r.SetBody(data)
}
var (
resp *req.Response
err error
)
switch method {
case http.MethodGet:
resp, err = r.Get(path)
case http.MethodPost:
resp, err = r.Post(path)
case http.MethodPut:
resp, err = r.Put(path)
case http.MethodPatch:
resp, err = r.Patch(path)
case http.MethodDelete:
resp, err = r.Delete(path)
case http.MethodHead:
resp, err = r.Head(path)
case http.MethodOptions:
resp, err = r.Options(path)
default:
return nil, fmt.Errorf("unsupported method: %s", method)
}
restyResp, adaptErr := request.adaptReqResponse(path, method, data, extraHeaders, cookies, resp)
if err != nil && errors.Is(err, http.ErrUseLastResponse) {
err = nil
}
if adaptErr != nil && err == nil {
err = adaptErr
}
return restyResp, err
}
if err := request.decodeResponse(resp, result); err != nil {
return nil, err
resp, err := doRequest(headers)
if err == nil && resp != nil && strings.Contains(strings.ToLower(resp.Header().Get("Content-Encoding")), "zstd") {
err = fmt.Errorf("zstd body requires identity fallback")
}
if err == nil {
if decodeErr := request.decodeResponse(resp, result); decodeErr != nil {
return nil, decodeErr
}
return resp, nil
}
return resp, err
errStr := err.Error()
if strings.Contains(errStr, "gzip") || strings.Contains(errStr, "magic number mismatch") || strings.Contains(errStr, "zstd") || strings.Contains(errStr, "brotli") || strings.Contains(errStr, "flate") {
h2 := map[string]string{}
for k, v := range headers {
if strings.ToLower(k) != "accept-encoding" {
h2[k] = v
}
}
h2["Accept-Encoding"] = "identity"
resp2, err2 := doRequest(h2)
if err2 == nil {
if decodeErr := request.decodeResponse(resp2, result); decodeErr != nil {
return nil, decodeErr
}
return resp2, nil
}
if resp2 != nil {
return resp2, err2
}
}
if resp != nil {
return resp, err
}
return nil, err
}
// RestyPost 发送 POST 请求
func (request *RestyClient) RestyPost(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(data).Post(path)
})
if resp == nil && err != nil {
return nil, err
}
if err := request.decodeResponse(resp, result); err != nil {
return nil, err
}
return resp, err
func (request *RestyClient) RestyGet(path string, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
return request.execute(http.MethodGet, path, nil, result, headers, cookies, redirectCount)
}
// RestyPut 发送 PUT 请求
// 功能:发送 PUT支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON
// 返回:响应对象与错误信息
func (request *RestyClient) RestyPut(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(data).Put(path)
})
if resp == nil && err != nil {
return nil, err
}
if err := request.decodeResponse(resp, result); err != nil {
return nil, err
}
return resp, err
func (request *RestyClient) RestyPost(path string, data any, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
return request.execute(http.MethodPost, path, data, result, headers, cookies, redirectCount)
}
// RestyPatch 发送 PATCH 请求
// 功能:发送 PATCH支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON
// 返回:响应对象与错误信息
func (request *RestyClient) RestyPatch(path string, data any, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
return r.SetBody(data).Patch(path)
})
if resp == nil && err != nil {
return nil, err
}
if err := request.decodeResponse(resp, result); err != nil {
return nil, err
}
return resp, err
func (request *RestyClient) RestyPut(path string, data any, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
return request.execute(http.MethodPut, path, data, result, headers, cookies, redirectCount)
}
// RestyDelete 发送 DELETE 请求
// 功能:发送 DELETE支持请求级 headers 覆盖客户端默认,自动识别 gzip 并解析 JSON
// 返回:响应对象与错误信息
func (request *RestyClient) RestyDelete(path string, result interface{}, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
return r.Delete(path)
})
if resp == nil && err != nil {
return nil, err
}
if err := request.decodeResponse(resp, result); err != nil {
return nil, err
}
return resp, err
func (request *RestyClient) RestyPatch(path string, data any, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
return request.execute(http.MethodPatch, path, data, result, headers, cookies, redirectCount)
}
// RestyHead 发送 HEAD 请求
// 功能:发送 HEAD支持请求级 headers 覆盖客户端默认HEAD 通常无正文
// 返回:响应对象与错误信息
func (request *RestyClient) RestyHead(path string, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
return r.Head(path)
})
if resp == nil && err != nil {
return nil, err
}
return resp, err
func (request *RestyClient) RestyDelete(path string, result any, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
return request.execute(http.MethodDelete, path, nil, result, headers, cookies, redirectCount)
}
// RestyOptions 发送 OPTIONS 请求
// 功能:发送 OPTIONS支持请求级 headers 覆盖客户端默认
// 返回:响应对象与错误信息
func (request *RestyClient) RestyOptions(path string, headers map[string]string, cookies []*http.Cookie, allowRedirect bool) (*resty.Response, error) {
resp, err := request.doWithEncodingFallback(headers, cookies, allowRedirect, func(r *resty.Request) (*resty.Response, error) {
return r.Options(path)
})
if resp == nil && err != nil {
return nil, err
}
return resp, err
func (request *RestyClient) RestyHead(path string, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
return request.execute(http.MethodHead, path, nil, nil, headers, cookies, redirectCount)
}
func (request *RestyClient) RestyOptions(path string, headers map[string]string, cookies []*http.Cookie, redirectCount int) (*resty.Response, error) {
return request.execute(http.MethodOptions, path, nil, nil, headers, cookies, redirectCount)
}