geoGo/pkg/Multi.go

173 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}