173 lines
5.0 KiB
Go
173 lines
5.0 KiB
Go
package pkg
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"mime/multipart"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
)
|
||
|
||
// PostMultipart 发送 multipart/form-data 请求
|
||
// url: 请求地址
|
||
// data: map[string]interface{} 类型的数据,支持以下类型:
|
||
// - 普通字段:string, int, float64, bool 等
|
||
// - 文件字段:*os.File, []byte, 或实现了 io.Reader 接口的类型,需配合文件名使用
|
||
// - 文件路径:使用 "file_path" 字段标记,如:map[string]interface{}{"field_name": map[string]interface{}{"file_path": "/path/to/file"}}
|
||
// - 简化文件:map[string]interface{}{"field_name": map[string]interface{}{"file": fileObj, "filename": "custom.png"}}
|
||
//
|
||
// result: 响应 JSON 将解析到此指针对象
|
||
func PostMultipart(url string, data map[string]interface{}, result interface{}) error {
|
||
// 创建缓冲区和 multipart writer
|
||
body := &bytes.Buffer{}
|
||
writer := multipart.NewWriter(body)
|
||
|
||
// 遍历所有字段
|
||
for fieldName, fieldValue := range data {
|
||
switch v := fieldValue.(type) {
|
||
case string:
|
||
// 普通字符串字段
|
||
if err := writer.WriteField(fieldName, v); err != nil {
|
||
writer.Close()
|
||
return fmt.Errorf("写入字段 %s 失败: %w", fieldName, err)
|
||
}
|
||
|
||
case int, int32, int64, float32, float64, bool:
|
||
// 基本类型转字符串
|
||
if err := writer.WriteField(fieldName, fmt.Sprintf("%v", v)); err != nil {
|
||
writer.Close()
|
||
return fmt.Errorf("写入字段 %s 失败: %w", fieldName, err)
|
||
}
|
||
|
||
case *os.File:
|
||
// *os.File 类型,自动获取文件名
|
||
if err := addFilePart(writer, fieldName, v.Name(), v); err != nil {
|
||
writer.Close()
|
||
return err
|
||
}
|
||
|
||
case []byte:
|
||
// 字节数组,需要提供文件名
|
||
return fmt.Errorf("字段 %s 为 []byte 类型,请使用 map[string]interface{} 格式提供文件名: {\"data\": 字节内容, \"filename\": \"文件名\"}", fieldName)
|
||
|
||
case map[string]interface{}:
|
||
// 复杂文件描述
|
||
if err := handleFileMap(writer, fieldName, v); err != nil {
|
||
writer.Close()
|
||
return err
|
||
}
|
||
|
||
default:
|
||
// 其他类型转为字符串
|
||
if err := writer.WriteField(fieldName, fmt.Sprintf("%v", v)); err != nil {
|
||
writer.Close()
|
||
return fmt.Errorf("写入字段 %s 失败: %w", fieldName, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 关闭 writer 以写入结束边界
|
||
if err := writer.Close(); err != nil {
|
||
return fmt.Errorf("关闭 multipart writer 失败: %w", err)
|
||
}
|
||
|
||
// 创建请求
|
||
req, err := http.NewRequest("POST", url, body)
|
||
if err != nil {
|
||
return fmt.Errorf("创建请求失败: %w", err)
|
||
}
|
||
|
||
// 设置 Content-Type,包含边界分隔符
|
||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||
|
||
// 发送请求
|
||
client := &http.Client{
|
||
Timeout: 30 * time.Second,
|
||
}
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
return fmt.Errorf("请求失败: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 读取响应体
|
||
respBody, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return fmt.Errorf("读取响应失败: %w", err)
|
||
}
|
||
|
||
// 检查 HTTP 状态码
|
||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||
return fmt.Errorf("HTTP 错误: %d, 响应: %s", resp.StatusCode, string(respBody))
|
||
}
|
||
|
||
// 解析 JSON 到 result 指针
|
||
if err := json.Unmarshal(respBody, result); err != nil {
|
||
return fmt.Errorf("JSON 解析失败: %w, 原始响应: %s", err, string(respBody))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// addFilePart 添加文件部分
|
||
func addFilePart(writer *multipart.Writer, fieldName, filename string, reader io.Reader) error {
|
||
part, err := writer.CreateFormFile(fieldName, filename)
|
||
if err != nil {
|
||
return fmt.Errorf("创建文件字段 %s 失败: %w", fieldName, err)
|
||
}
|
||
|
||
if _, err := io.Copy(part, reader); err != nil {
|
||
return fmt.Errorf("复制文件 %s 内容失败: %w", fieldName, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// handleFileMap 处理文件映射
|
||
func handleFileMap(writer *multipart.Writer, fieldName string, fileMap map[string]interface{}) error {
|
||
// 支持两种格式:
|
||
// 1. {"file_path": "/path/to/file"}
|
||
// 2. {"file": io.Reader, "filename": "custom_name.ext"}
|
||
|
||
// 格式1: 文件路径
|
||
if filePath, ok := fileMap["file_path"].(string); ok {
|
||
file, err := os.Open(filePath)
|
||
if err != nil {
|
||
return fmt.Errorf("打开文件 %s 失败: %w", filePath, err)
|
||
}
|
||
defer file.Close()
|
||
return addFilePart(writer, fieldName, filepath.Base(filePath), file)
|
||
}
|
||
|
||
// 格式2: 直接提供文件和文件名
|
||
if fileReader, ok := fileMap["file"]; ok {
|
||
// 获取文件名
|
||
filename := "file"
|
||
if fn, ok := fileMap["filename"].(string); ok && fn != "" {
|
||
filename = fn
|
||
}
|
||
|
||
var reader io.Reader
|
||
switch r := fileReader.(type) {
|
||
case io.Reader:
|
||
reader = r
|
||
case []byte:
|
||
reader = bytes.NewReader(r)
|
||
case *os.File:
|
||
reader = r
|
||
if filename == "file" {
|
||
filename = filepath.Base(r.Name())
|
||
}
|
||
default:
|
||
return fmt.Errorf("文件字段 %s 不支持的类型: %T", fieldName, fileReader)
|
||
}
|
||
|
||
return addFilePart(writer, fieldName, filename, reader)
|
||
}
|
||
|
||
return fmt.Errorf("文件字段 %s 格式错误,需要 file_path 或 file+filename", fieldName)
|
||
}
|