341 lines
7.9 KiB
Go
341 lines
7.9 KiB
Go
package client
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"mime"
|
||
"mime/multipart"
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
type HTTPClient struct {
|
||
Client *http.Client
|
||
BaseURL string
|
||
Headers map[string]string
|
||
}
|
||
|
||
// NewHTTPClient 创建新的HTTP客户端实例
|
||
func NewHTTPClient(baseURL string, timeout time.Duration) *HTTPClient {
|
||
return &HTTPClient{
|
||
Client: &http.Client{
|
||
Timeout: timeout,
|
||
},
|
||
BaseURL: baseURL,
|
||
Headers: make(map[string]string),
|
||
}
|
||
}
|
||
|
||
// SetHeader 设置请求头
|
||
func (c *HTTPClient) SetHeader(key, value string) {
|
||
c.Headers[key] = value
|
||
}
|
||
|
||
// Get 发送GET请求,支持查询参数
|
||
func (c *HTTPClient) Get(path string, queryParams ...map[string]string) ([]byte, error) {
|
||
// 构建完整URL
|
||
fullURL := c.BaseURL + path
|
||
|
||
// 处理查询参数
|
||
if len(queryParams) > 0 {
|
||
// 创建URL对象
|
||
u, err := url.Parse(fullURL)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("解析URL失败: %w", err)
|
||
}
|
||
|
||
// 添加查询参数
|
||
q := u.Query()
|
||
for key, value := range queryParams[0] {
|
||
q.Add(key, value)
|
||
}
|
||
u.RawQuery = q.Encode()
|
||
|
||
fullURL = u.String()
|
||
}
|
||
|
||
// 创建请求
|
||
req, err := http.NewRequest("GET", fullURL, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 设置请求头
|
||
for k, v := range c.Headers {
|
||
req.Header.Set(k, v)
|
||
}
|
||
|
||
// 发送请求
|
||
resp, err := c.Client.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 检查状态码
|
||
if resp.StatusCode != http.StatusOK {
|
||
// 尝试读取响应体以获取更多错误信息
|
||
return nil, &HTTPError{
|
||
StatusCode: resp.StatusCode,
|
||
Message: resp.Status,
|
||
}
|
||
}
|
||
|
||
return io.ReadAll(resp.Body)
|
||
}
|
||
|
||
// PostJSON 发送JSON格式POST请求
|
||
func (c *HTTPClient) PostJSON(path string, data interface{}) ([]byte, error) {
|
||
// 拼接URL
|
||
fullURL := c.BaseURL + path
|
||
jsonData, err := json.Marshal(data)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
fmt.Println("request url, body is :", fullURL, string(jsonData))
|
||
req, err := http.NewRequest("POST", fullURL, bytes.NewBuffer(jsonData))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 设置请求头
|
||
req.Header.Set("Content-Type", "application/json")
|
||
for k, v := range c.Headers {
|
||
req.Header.Set(k, v)
|
||
}
|
||
|
||
resp, err := c.Client.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return nil, &HTTPError{
|
||
StatusCode: resp.StatusCode,
|
||
Message: resp.Status,
|
||
}
|
||
}
|
||
|
||
return io.ReadAll(resp.Body)
|
||
}
|
||
|
||
// UploadFiles 上传多个文件并支持额外表单字段
|
||
// files 参数现在支持每个字段名对应多个文件路径
|
||
func (c *HTTPClient) UploadFiles(
|
||
path string,
|
||
filesPrefix string,
|
||
files map[string][]string, // 字段名 -> 多个文件路径
|
||
formFields map[string]string, // 额外表单字段
|
||
) ([]byte, error) {
|
||
fullURL := c.BaseURL + path
|
||
|
||
// 创建multipart writer
|
||
body := &bytes.Buffer{}
|
||
writer := multipart.NewWriter(body)
|
||
|
||
// 添加文件部分 - 支持每个字段名多个文件
|
||
for fieldName, filePaths := range files {
|
||
for _, filePath := range filePaths {
|
||
file, err := os.Open(filePath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("打开文件 %s 失败: %w", filePath, err)
|
||
}
|
||
defer file.Close()
|
||
|
||
relativePath, err := removePathPrefix(filePath, filesPrefix)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
part, err := writer.CreateFormFile(fieldName, url.PathEscape(relativePath))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("为文件 %s 创建表单字段失败: %w", filePath, err)
|
||
}
|
||
|
||
_, err = io.Copy(part, file)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("复制文件 %s 内容失败: %w", filePath, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加额外表单字段
|
||
for key, value := range formFields {
|
||
err := writer.WriteField(key, value)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("添加表单字段 %s 失败: %w", key, err)
|
||
}
|
||
}
|
||
|
||
// 关闭writer以完成multipart消息
|
||
err := writer.Close()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("关闭multipart writer失败: %w", err)
|
||
}
|
||
|
||
// 创建请求
|
||
req, err := http.NewRequest("POST", fullURL, body)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 设置内容类型(必须包含boundary)
|
||
contentType := writer.FormDataContentType()
|
||
req.Header.Set("Content-Type", contentType)
|
||
|
||
// 添加自定义头
|
||
for k, v := range c.Headers {
|
||
req.Header.Set(k, v)
|
||
}
|
||
|
||
// 发送请求
|
||
resp, err := c.Client.Do(req)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 检查状态码
|
||
if resp.StatusCode != http.StatusOK {
|
||
// 尝试读取错误响应体
|
||
errorBody, _ := io.ReadAll(resp.Body)
|
||
errorMsg := string(errorBody)
|
||
if errorMsg == "" {
|
||
errorMsg = resp.Status
|
||
}
|
||
|
||
return nil, &HTTPError{
|
||
StatusCode: resp.StatusCode,
|
||
Message: errorMsg,
|
||
}
|
||
}
|
||
|
||
return io.ReadAll(resp.Body)
|
||
}
|
||
|
||
func removePathPrefix(fullPath, prefix string) (string, error) {
|
||
// 使用系统文件分隔符标准化路径
|
||
prefix = filepath.Clean(prefix) + string(filepath.Separator)
|
||
fullPath = filepath.Clean(fullPath)
|
||
|
||
// 检查路径是否有指定的前缀
|
||
if strings.HasPrefix(fullPath, prefix) {
|
||
// 返回去除前缀的路径
|
||
return fullPath[len(prefix):], nil
|
||
}
|
||
// 如果不匹配,返回原始路径(或者可以根据需要返回错误)
|
||
return "", fmt.Errorf("错误的filepath和prefix, filepath:%v, prefix:%v", fullPath, prefix)
|
||
}
|
||
|
||
func (c *HTTPClient) encodeFilepath(paths []string) []string {
|
||
var result []string
|
||
for _, path := range paths {
|
||
// 编码文件路径,使用urlencoded编码
|
||
encodedPath := url.PathEscape(path)
|
||
result = append(result, encodedPath)
|
||
}
|
||
fmt.Println("encodeFilepath result: ", result)
|
||
return result
|
||
}
|
||
|
||
// DownloadFile 下载文件到本地路径(支持大文件流式处理)
|
||
// localPath:保存文件的本地路径(包含文件名)
|
||
// queryParams:可选查询参数
|
||
func (c *HTTPClient) DownloadFile(path, localPath string, queryParams ...map[string]string) (string, error) {
|
||
// 构建完整URL
|
||
fullURL := c.BaseURL + path
|
||
|
||
// 处理查询参数
|
||
if len(queryParams) > 0 {
|
||
u, err := url.Parse(fullURL)
|
||
if err != nil {
|
||
return "", fmt.Errorf("解析URL失败: %w", err)
|
||
}
|
||
|
||
q := u.Query()
|
||
for key, value := range queryParams[0] {
|
||
q.Add(key, value)
|
||
}
|
||
u.RawQuery = q.Encode()
|
||
fullURL = u.String()
|
||
}
|
||
|
||
// 创建请求
|
||
req, err := http.NewRequest("GET", fullURL, nil)
|
||
if err != nil {
|
||
return "", fmt.Errorf("创建请求失败: %w", err)
|
||
}
|
||
|
||
// 设置请求头
|
||
for k, v := range c.Headers {
|
||
req.Header.Set(k, v)
|
||
}
|
||
|
||
// 发送请求
|
||
resp, err := c.Client.Do(req)
|
||
if err != nil {
|
||
return "", fmt.Errorf("请求失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 检查状态码 (接受2xx状态码)
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
// 尝试读取部分错误信息(限制长度防止内存溢出)
|
||
return "", &HTTPError{
|
||
StatusCode: resp.StatusCode,
|
||
Message: resp.Status,
|
||
}
|
||
}
|
||
|
||
os.MkdirAll(localPath, 0755)
|
||
filename := extractFilenameFromDisposition(resp.Header.Get("Content-Disposition"))
|
||
if filename == "" {
|
||
filename = filepath.Join(localPath, "file.zip")
|
||
} else {
|
||
filename = filepath.Join(localPath, filename)
|
||
}
|
||
// 创建本地文件
|
||
outFile, err := os.Create(filename)
|
||
if err != nil {
|
||
return "", fmt.Errorf("创建本地文件失败: %w", err)
|
||
}
|
||
defer outFile.Close()
|
||
|
||
// 流式拷贝(避免大文件内存溢出)
|
||
_, err = io.Copy(outFile, resp.Body)
|
||
if err != nil {
|
||
// 删除可能不完整的文件
|
||
_ = os.Remove(localPath)
|
||
return "", fmt.Errorf("下载文件写入失败: %w", err)
|
||
}
|
||
|
||
return filename, nil
|
||
}
|
||
|
||
func extractFilenameFromDisposition(contentDisposition string) string {
|
||
// 尝试使用标准库的 mime.ParseMediaType
|
||
if _, params, err := mime.ParseMediaType(contentDisposition); err == nil {
|
||
if filename, exists := params["filename"]; exists && filename != "" {
|
||
return filename
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// HTTPError 自定义HTTP错误
|
||
type HTTPError struct {
|
||
StatusCode int
|
||
Message string
|
||
}
|
||
|
||
func (e *HTTPError) Error() string {
|
||
return e.Message
|
||
}
|