feat(contrib/files): 新增百度图床和58cdn图片上传功能实现

This commit is contained in:
1
2025-12-22 14:57:39 +00:00
parent c19a268b7b
commit 83ee9fba43
14 changed files with 1128 additions and 4 deletions

View File

@@ -0,0 +1,139 @@
package baidu
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"strings"
"time"
)
// 百度图床全局配置
const (
// 上传接口地址
baiduUploadURL = "https://wenku.baidu.com/user/api/editorimg"
// 必需的Referer
baiduReferer = "https://wenku.baidu.com/"
// 固定Cookie可根据实际情况替换
baiduCookie = "BDUSS=3plTHA0aHpRNGI3MmIxTkpmNVpWTTYtLXpVWjlaRjdRQzFsNmxQNlNufkRiWGhvSVFBQUFBJCQAAAAAAAAAAAEAAAB5WXC40tfDzsTPs8cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMPgUGjD4FBoS"
// 请求超时时间
baiduTimeout = 30 * time.Second
)
// BaiduUploadResponse 百度图床接口返回JSON结构
type BaiduUploadResponse struct {
Link string `json:"link"` // 图片链接字段
}
// BaiduImgUpload 百度图床上传函数对齐PHP功能
// filepath: 本地文件路径
// filename: 上传时的文件名对应PHP的setPostFilename
// 返回值: 处理后的图片URL错误信息
func BaiduImgUpload(filepath string, filename string) (string, error) {
// 1. 参数校验
if filepath == "" {
return "", fmt.Errorf("本地文件路径不能为空")
}
if filename == "" {
return "", fmt.Errorf("上传文件名不能为空")
}
// 2. 打开本地文件
file, err := os.Open(filepath)
if err != nil {
return "", fmt.Errorf("打开本地文件失败: %w, 路径: %s", err, filepath)
}
defer file.Close() // 延迟关闭文件,确保资源释放
// 3. 构建multipart/form-data请求体
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
defer bodyWriter.Close() // 延迟关闭writer
// 3.1 添加文件字段对应PHP的'file'字段,设置上传文件名)
fileWriter, err := bodyWriter.CreateFormFile("file", filename)
if err != nil {
return "", fmt.Errorf("创建文件表单字段失败: %w", err)
}
// 复制文件内容到请求体
if _, err := io.Copy(fileWriter, file); err != nil {
return "", fmt.Errorf("复制文件内容到请求体失败: %w", err)
}
// 4. 创建HTTP POST请求
contentType := bodyWriter.FormDataContentType()
req, err := http.NewRequest(http.MethodPost, baiduUploadURL, bodyBuf)
if err != nil {
return "", fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 5. 设置请求头对齐PHP的请求头配置
setBaiduRequestHeaders(req, contentType)
// 6. 发送HTTP请求
client := &http.Client{Timeout: baiduTimeout}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close() // 延迟关闭响应体
// 7. 读取响应内容
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应内容失败: %w", err)
}
// 8. 解析JSON响应
var baiduResp BaiduUploadResponse
if err := json.Unmarshal(respBody, &baiduResp); err != nil {
return "", fmt.Errorf("解析JSON响应失败: %w, 响应内容: %s", err, string(respBody))
}
// 9. 校验响应结果对应PHP的isset($arr['link'])
if baiduResp.Link == "" {
return "", fmt.Errorf("上传失败!接口错误,响应内容: %s", string(respBody))
}
// 10. 替换域名对应PHP的str_replace操作
imgURL := strings.Replace(baiduResp.Link, ".cdn.bcebos.com", ".bj.bcebos.com", 1)
return imgURL, nil
}
// setBaiduRequestHeaders 设置百度图床必需的请求头
func setBaiduRequestHeaders(req *http.Request, contentType string) {
// 设置Content-Type包含multipart boundary
req.Header.Set("Content-Type", contentType)
// 设置RefererPHP中的$referer
req.Header.Set("Referer", baiduReferer)
// 设置CookiePHP中的$cookie
req.Header.Set("Cookie", baiduCookie)
// 补充必要的请求头,提升兼容性
req.Header.Set("Connection", "keep-alive")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36")
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
}
// 示例调用
func main() {
// 本地文件路径(请替换为实际的图片路径)
localFilePath := "./test.jpg"
// 上传时的文件名可自定义对应PHP的setPostFilename
uploadFilename := "custom-test.jpg"
// 调用百度图床上传函数
imgURL, err := BaiduImgUpload(localFilePath, uploadFilename)
if err != nil {
fmt.Printf("百度图床上传失败: %v\n", err)
return
}
fmt.Printf("百度图床上传成功图片URL: %s\n", imgURL)
}

View File

@@ -0,0 +1,3 @@
module blazing/contrib/files/baidu
go 1.25.0

View File

@@ -0,0 +1,329 @@
package main
import (
"bytes"
"crypto/md5"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/rand"
"net/http"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// 全局配置与PHP代码保持一致
const (
// 获取上传地址的接口URL
cdn58GetUploadURL = "https://im.58.com/msg/get_pic_upload_url"
// 固定请求头配置
cdn58Referer = "https://ai.58.com/pc/"
cdn58UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.6261.95 Safari/537.36"
cdn58Origin = "https://ai.58.com"
// 请求超时时间
cdn58Timeout = 30 * time.Second
// 最终图片URL前缀
cdn58ImgPrefix = "https://pic%d.58cdn.com.cn/nowater/im/"
)
// 允许的文件后缀与PHP代码保持一致
var allowedFileExts = map[string]bool{
"jpg": true,
"jpeg": true,
"png": true,
"gif": true,
"bmp": true,
}
// Cdn58UploadInfo 上传信息结构体对应PHP响应中的upload_info
type Cdn58UploadInfo struct {
URL string `json:"url"` // 上传地址
}
// Cdn58Data 响应数据结构体对应PHP响应中的data
type Cdn58Data struct {
UploadInfo []Cdn58UploadInfo `json:"upload_info"`
}
// Cdn58GetUploadURLResponse 获取上传地址的响应结构体
type Cdn58GetUploadURLResponse struct {
ErrorCode int `json:"error_code"`
ErrorMsg string `json:"error_msg"`
Data Cdn58Data `json:"data"`
}
// -------------------------- 对应PHP的私有方法实现 --------------------------
// guid 生成与PHP guid()方法一致的UUID格式字符串
func guid() string {
// 模拟PHP: md5(uniqid(mt_rand(), true))
rand.Seed(time.Now().UnixNano())
uniqID := fmt.Sprintf("%d%d", rand.Int63(), time.Now().UnixNano())
md5Hash := md5.Sum([]byte(uniqID))
guidStr := fmt.Sprintf("%x", md5Hash)
// 拼接为UUID格式8-4-4-4-12
return fmt.Sprintf("%s-%s-4%s-%s-%s",
guidStr[:8],
guidStr[8:12],
guidStr[13:16], // 固定第13位为4符合UUID v4规范
guidStr[16:20],
guidStr[20:32],
)
}
// encrypt 实现与PHP一致的加密逻辑
func encrypt(data string) string {
// 1. base64编码
base64Str := base64.StdEncoding.EncodeToString([]byte(data))
// 2. 统计等号数量
equalCount := strings.Count(base64Str, "=")
// 3. 替换字符并移除等号,拼接等号数量
encodedStr := strings.ReplaceAll(base64Str, "+", "-")
encodedStr = strings.ReplaceAll(encodedStr, "/", "_")
encodedStr = strings.ReplaceAll(encodedStr, "=", "")
encodedStr += strconv.Itoa(equalCount)
// 4. 分成两半,交换前后部分
halfLen := len(encodedStr) / 2
firstHalf := encodedStr[:halfLen]
secondHalf := encodedStr[halfLen:]
return secondHalf + firstHalf
}
// decrypt 实现与PHP一致的解密逻辑虽然核心流程未使用保持完整性
func decrypt(data string) string {
// 1. 分成两半,交换前后部分
halfLen := (len(data) + 1) / 2
firstHalf := data[:halfLen]
secondHalf := data[halfLen:]
data = secondHalf + firstHalf
// 2. 提取末尾的等号数量
equalCount, _ := strconv.Atoi(string(data[len(data)-1]))
data = data[:len(data)-1]
// 3. 还原字符
decodedStr := strings.ReplaceAll(data, "-", "+")
decodedStr = strings.ReplaceAll(data, "_", "/")
// 4. 补充等号
decodedStr += strings.Repeat("=", equalCount)
// 5. base64解码
raw, _ := base64.StdEncoding.DecodeString(decodedStr)
return string(raw)
}
// getMIMEType 根据文件后缀获取MIME类型对应PHP的mime_content_type方法
func getMIMEType(ext string) string {
mimeMap := map[string]string{
"png": "image/png",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"gif": "image/gif",
"bmp": "image/bmp",
}
if mime, ok := mimeMap[strings.ToLower(ext)]; ok {
return mime
}
return "application/octet-stream"
}
// -------------------------- 核心上传逻辑 --------------------------
// Cdn58ImgUpload cdn58图床上传函数完全对齐PHP功能
// filepath: 本地文件路径
// filename: 上传文件名(包含后缀)
// 返回值: 最终图片URL错误信息
func Cdn58ImgUpload(filepath1 string, filename string) (string, error) {
// 1. 提取并校验文件后缀
fileExt := filepath.Ext(filename)
if fileExt != "" {
fileExt = strings.TrimPrefix(fileExt, ".") // 移除前缀"."
}
fileExt = strings.ToLower(fileExt)
if !allowedFileExts[fileExt] {
return "", errors.New("上传失败不支持的文件格式仅支持jpg/jpeg/png/gif/bmp")
}
// 2. 生成UserID
userID := "58Anonymous" + guid()
// 3. 构建user_info并加密对应PHP的user_info数组
userInfo := map[string]string{
"user_id": userID,
"source": "14",
"im_token": userID,
"client_version": "1.0",
"client_type": "pcweb",
"os_type": "Chrome",
"os_version": "122.0.6261.95",
"appid": "10140-mcs@jitmouQrcHs",
"extend_flag": "0",
"unread_index": "1",
"sdk_version": "6432",
"device_id": userID,
"xxzl_smartid": "",
"id58": "CkwAd2e0U3tBNxbRAzQ2Ag==",
}
// 3.1 构建查询字符串对应PHP的http_build_query
queryValues := url.Values{}
for k, v := range userInfo {
queryValues.Set(k, v)
}
userInfoQuery := queryValues.Encode()
// 3.2 加密user_info查询字符串
encryptedUserInfo := encrypt(userInfoQuery)
// 4. 构建post数据并加密对应PHP的post数组
postData := map[string]interface{}{
"sender_id": userID,
"sender_source": 14,
"to_id": "10002",
"to_source": 100,
"file_suffixs": []string{fileExt},
}
// 4.1 JSON编码对应PHP的json_encode
postJSON, err := json.Marshal(postData)
if err != nil {
return "", fmt.Errorf("序列化post数据失败: %w", err)
}
// 4.2 加密post数据
encryptedPost := encrypt(string(postJSON))
// 5. 拼接获取上传地址的URL
getUploadURL := fmt.Sprintf("%s?params=%s&version=j1.0",
cdn58GetUploadURL,
url.QueryEscape(encryptedUserInfo), // URL编码参数
)
// 6. 发送请求获取上传地址对应PHP的get_curl
client := &http.Client{
Timeout: cdn58Timeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // 注意:字段名是 InsecureSkipVerify非 TLSInsecureSkipVerify
},
},
}
req, err := http.NewRequest("POST", getUploadURL, bytes.NewBufferString(encryptedPost))
if err != nil {
return "", fmt.Errorf("创建获取上传地址请求失败: %w", err)
}
// 设置请求头与PHP保持一致
req.Header.Set("Content-Type", "text/plain;charset=UTF-8")
req.Header.Set("Referer", cdn58Referer)
req.Header.Set("User-Agent", cdn58UA)
req.Header.Set("Origin", cdn58Origin)
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("发送获取上传地址请求失败: %w", err)
}
defer resp.Body.Close()
// 解析获取上传地址的响应
var getUploadResp Cdn58GetUploadURLResponse
if err := json.NewDecoder(resp.Body).Decode(&getUploadResp); err != nil {
return "", fmt.Errorf("解析获取上传地址响应失败: %w", err)
}
// 校验响应结果
if getUploadResp.ErrorCode != 0 {
errMsg := "接口错误"
if getUploadResp.ErrorMsg != "" {
errMsg = getUploadResp.ErrorMsg
}
return "", fmt.Errorf("上传失败!%s", errMsg)
}
// 提取上传URL
if len(getUploadResp.Data.UploadInfo) == 0 || getUploadResp.Data.UploadInfo[0].URL == "" {
return "", errors.New("上传失败!未返回上传地址")
}
uploadURL := getUploadResp.Data.UploadInfo[0].URL
// 7. 读取本地文件内容对应PHP的file_get_contents
fileContent, err := os.ReadFile(filepath1)
if err != nil {
return "", fmt.Errorf("读取本地文件失败: %w, 路径: %s", err, filepath1)
}
// 8. 获取MIME类型
mimeType := getMIMEType(fileExt)
// 9. 使用PUT方法上传文件对应PHP的curl_upload方法
putReq, err := http.NewRequest("PUT", uploadURL, bytes.NewBuffer(fileContent))
if err != nil {
return "", fmt.Errorf("创建文件上传请求失败: %w", err)
}
// 设置PUT请求头
putReq.Header.Set("Content-Type", mimeType)
putReq.Header.Set("User-Agent", cdn58UA)
putResp, err := client.Do(putReq)
if err != nil {
return "", fmt.Errorf("发送文件上传请求失败: %w", err)
}
defer putResp.Body.Close()
// 校验HTTP状态码
if putResp.StatusCode != 200 {
return "", fmt.Errorf("上传失败httpCode=%d", putResp.StatusCode)
}
// 10. 截取文件名对应PHP的getSubstr($url, '/nowater/im/', '?')
filenameStart := strings.Index(uploadURL, "/nowater/im/")
if filenameStart == -1 {
return "", errors.New("上传失败!无法从上传地址中截取文件名")
}
filenameStart += len("/nowater/im/")
filenameEnd := strings.Index(uploadURL[filenameStart:], "?")
if filenameEnd == -1 {
filenameEnd = len(uploadURL)
} else {
filenameEnd += filenameStart
}
croppedFilename := uploadURL[filenameStart:filenameEnd]
if croppedFilename == "" {
return "", errors.New("上传失败!截取的文件名为空")
}
// 11. 拼接最终图片URL随机生成1-8的数字
rand.Seed(time.Now().UnixNano())
picNum := rand.Intn(8) + 1 // 生成1-8的随机数
finalImgURL := fmt.Sprintf(cdn58ImgPrefix, picNum) + croppedFilename
return finalImgURL, nil
}
// -------------------------- 示例调用 --------------------------
func main() {
// 配置本地文件路径和上传文件名(请替换为实际值)
localFilePath := "./test.jpg" // 本地图片路径
uploadFilename := "test.jpg" // 上传文件名(包含后缀)
// 调用cdn58上传函数
imgURL, err := Cdn58ImgUpload(localFilePath, uploadFilename)
if err != nil {
fmt.Printf("cdn58图床上传失败: %v\n", err)
return
}
fmt.Printf("cdn58图床上传成功最终图片URL: %s\n", imgURL)
}

View File

@@ -0,0 +1,3 @@
module blazing/contrib/files/cnd58
go 1.25.0

View File

@@ -0,0 +1,139 @@
package baidu
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"strings"
"time"
)
// 百度图床全局配置
const (
// 上传接口地址
baiduUploadURL = "https://wenku.baidu.com/user/api/editorimg"
// 必需的Referer
baiduReferer = "https://wenku.baidu.com/"
// 固定Cookie可根据实际情况替换
baiduCookie = "BDUSS=3plTHA0aHpRNGI3MmIxTkpmNVpWTTYtLXpVWjlaRjdRQzFsNmxQNlNufkRiWGhvSVFBQUFBJCQAAAAAAAAAAAEAAAB5WXC40tfDzsTPs8cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMPgUGjD4FBoS"
// 请求超时时间
baiduTimeout = 30 * time.Second
)
// BaiduUploadResponse 百度图床接口返回JSON结构
type BaiduUploadResponse struct {
Link string `json:"link"` // 图片链接字段
}
// BaiduImgUpload 百度图床上传函数对齐PHP功能
// filepath: 本地文件路径
// filename: 上传时的文件名对应PHP的setPostFilename
// 返回值: 处理后的图片URL错误信息
func BaiduImgUpload(filepath string, filename string) (string, error) {
// 1. 参数校验
if filepath == "" {
return "", fmt.Errorf("本地文件路径不能为空")
}
if filename == "" {
return "", fmt.Errorf("上传文件名不能为空")
}
// 2. 打开本地文件
file, err := os.Open(filepath)
if err != nil {
return "", fmt.Errorf("打开本地文件失败: %w, 路径: %s", err, filepath)
}
defer file.Close() // 延迟关闭文件,确保资源释放
// 3. 构建multipart/form-data请求体
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
defer bodyWriter.Close() // 延迟关闭writer
// 3.1 添加文件字段对应PHP的'file'字段,设置上传文件名)
fileWriter, err := bodyWriter.CreateFormFile("file", filename)
if err != nil {
return "", fmt.Errorf("创建文件表单字段失败: %w", err)
}
// 复制文件内容到请求体
if _, err := io.Copy(fileWriter, file); err != nil {
return "", fmt.Errorf("复制文件内容到请求体失败: %w", err)
}
// 4. 创建HTTP POST请求
contentType := bodyWriter.FormDataContentType()
req, err := http.NewRequest(http.MethodPost, baiduUploadURL, bodyBuf)
if err != nil {
return "", fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 5. 设置请求头对齐PHP的请求头配置
setBaiduRequestHeaders(req, contentType)
// 6. 发送HTTP请求
client := &http.Client{Timeout: baiduTimeout}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close() // 延迟关闭响应体
// 7. 读取响应内容
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应内容失败: %w", err)
}
// 8. 解析JSON响应
var baiduResp BaiduUploadResponse
if err := json.Unmarshal(respBody, &baiduResp); err != nil {
return "", fmt.Errorf("解析JSON响应失败: %w, 响应内容: %s", err, string(respBody))
}
// 9. 校验响应结果对应PHP的isset($arr['link'])
if baiduResp.Link == "" {
return "", fmt.Errorf("上传失败!接口错误,响应内容: %s", string(respBody))
}
// 10. 替换域名对应PHP的str_replace操作
imgURL := strings.Replace(baiduResp.Link, ".cdn.bcebos.com", ".bj.bcebos.com", 1)
return imgURL, nil
}
// setBaiduRequestHeaders 设置百度图床必需的请求头
func setBaiduRequestHeaders(req *http.Request, contentType string) {
// 设置Content-Type包含multipart boundary
req.Header.Set("Content-Type", contentType)
// 设置RefererPHP中的$referer
req.Header.Set("Referer", baiduReferer)
// 设置CookiePHP中的$cookie
req.Header.Set("Cookie", baiduCookie)
// 补充必要的请求头,提升兼容性
req.Header.Set("Connection", "keep-alive")
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36")
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
}
// 示例调用
func main() {
// 本地文件路径(请替换为实际的图片路径)
localFilePath := "./test.jpg"
// 上传时的文件名可自定义对应PHP的setPostFilename
uploadFilename := "custom-test.jpg"
// 调用百度图床上传函数
imgURL, err := BaiduImgUpload(localFilePath, uploadFilename)
if err != nil {
fmt.Printf("百度图床上传失败: %v\n", err)
return
}
fmt.Printf("百度图床上传成功图片URL: %s\n", imgURL)
}

View File

@@ -0,0 +1,3 @@
module blazing/contrib/files/cnd58
go 1.25.0

View File

@@ -0,0 +1,3 @@
module blazing/contrib/files/imgdd
go 1.25.0

View File

@@ -0,0 +1,147 @@
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"time"
)
// 全局配置与PHP代码保持一致
const (
// imgdd上传接口地址
imgddUploadURL = "https://imgdd.com/upload"
// 必需的Referer
imgddReferer = "https://imgdd.com/"
// 请求超时时间
imgddTimeout = 30 * time.Second
)
// ImgddUploadResponse imgdd接口返回JSON结构
// 对应PHP响应中的url和message字段
type ImgddUploadResponse struct {
URL string `json:"url"` // 上传成功返回的图片URL
Message string `json:"message"` // 上传失败返回的提示信息
}
// ImgddImgUpload imgdd图床上传函数完全对齐PHP功能
// filepath: 本地文件路径
// filename: 上传时的自定义文件名对应PHP的setPostFilename
// 返回值: 图片URL错误信息
func ImgddImgUpload(filepath string, filename string) (string, error) {
// 1. 参数校验
if filepath == "" {
return "", errors.New("本地文件路径不能为空")
}
if filename == "" {
return "", errors.New("上传文件名不能为空")
}
// 2. 打开本地文件
file, err := os.Open(filepath)
if err != nil {
return "", fmt.Errorf("打开本地文件失败: %w, 路径: %s", err, filepath)
}
defer file.Close() // 延迟关闭文件,确保资源释放
// 3. 构建multipart/form-data请求体
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
defer bodyWriter.Close() // 延迟关闭writer确保请求体完整
// 3.1 添加文件字段(字段名"image"对应PHP的'image' => $file设置自定义上传文件名
fileWriter, err := bodyWriter.CreateFormFile("image", filename)
if err != nil {
return "", fmt.Errorf("创建文件表单字段失败: %w", err)
}
// 3.2 复制文件内容到请求体
if _, err := io.Copy(fileWriter, file); err != nil {
return "", fmt.Errorf("复制文件内容到请求体失败: %w", err)
}
// 4. 创建HTTP POST请求
contentType := bodyWriter.FormDataContentType() // 自动包含boundary无需手动拼接
req, err := http.NewRequest(http.MethodPost, imgddUploadURL, bodyBuf)
if err != nil {
return "", fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 5. 设置请求头与PHP代码保持一致
setImgddRequestHeaders(req, contentType)
// 6. 发送HTTP请求
client := &http.Client{
Timeout: imgddTimeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // 注意:字段名是 InsecureSkipVerify非 TLSInsecureSkipVerify
},
},
}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close() // 延迟关闭响应体
// 7. 读取响应内容
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应内容失败: %w", err)
}
// 8. 解析JSON响应
var imgddResp ImgddUploadResponse
if err := json.Unmarshal(respBody, &imgddResp); err != nil {
return "", fmt.Errorf("解析JSON响应失败: %w, 响应内容: %s", err, string(respBody))
}
// 9. 按PHP逻辑判断响应结果
if imgddResp.URL != "" {
// 存在url字段上传成功
return imgddResp.URL, nil
} else if imgddResp.Message != "" {
// 存在message字段返回对应错误
return "", fmt.Errorf("上传失败请重试(%s", imgddResp.Message)
} else {
// 既无url也无message接口错误
return "", fmt.Errorf("上传失败!接口错误,响应内容: %s", string(respBody))
}
}
// setImgddRequestHeaders 设置imgdd必需的请求头
func setImgddRequestHeaders(req *http.Request, contentType string) {
// 设置Content-Type包含multipart boundary
req.Header.Set("Content-Type", contentType)
// 设置Referer与PHP一致接口可能做了来源校验
req.Header.Set("Referer", imgddReferer)
// 补充通用请求头,提升兼容性
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36")
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
req.Header.Set("Accept-Encoding", "gzip, deflate, br")
}
// 示例调用
func main() {
// 配置本地文件路径和上传文件名(请替换为实际值)
localFilePath := "./test.jpg" // 本地图片实际路径
uploadFilename := "custom-img.jpg" // 上传时的自定义文件名(可包含后缀)
// 调用imgdd上传函数
imgURL, err := ImgddImgUpload(localFilePath, uploadFilename)
if err != nil {
fmt.Printf("imgdd图床上传失败: %v\n", err)
return
}
fmt.Printf("imgdd图床上传成功图片URL: %s\n", imgURL)
}

View File

@@ -0,0 +1,3 @@
module blazing/contrib/files/locimg
go 1.25.0

View File

@@ -0,0 +1,187 @@
package main
import (
"bytes"
"compress/gzip"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/textproto"
"os"
"time"
)
// 全局配置与PHP代码保持完全一致
const (
// locimg上传接口地址
locimgUploadURL = "https://yunimg.cc/upload/upload.html"
// 必需的Referer
locimgReferer = "https://yunimg.cc/"
// 固定User-Agent与PHP的curl配置一致
locimgUA = "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36"
// 请求超时时间
locimgTimeout = 30 * time.Second
// 文件字段固定MIME类型对应PHP的CURLFile第二个参数
locimgFileMIME = "image/jpeg"
)
// LocimgData 响应数据中的data结构体对应PHP的$arr['data']
type LocimgData struct {
URL string `json:"url"` // 图片URL字段
}
// LocimgUploadResponse locimg接口返回JSON结构对应PHP的响应格式
type LocimgUploadResponse struct {
Data LocimgData `json:"data"` // 成功返回的数据字段
Msg string `json:"msg"` // 失败返回的提示信息
}
// LocimgImgUpload locimg图床上传函数完全对齐PHP功能
// filepath: 本地文件路径
// filename: 上传文件名包含后缀对应PHP的$filename
// 返回值: 最终图片URL错误信息
func LocimgImgUpload(filepath string, filename string) (string, error) {
// 1. 参数校验
if filepath == "" {
return "", errors.New("本地文件路径不能为空")
}
if filename == "" {
return "", errors.New("上传文件名不能为空")
}
// 2. 打开本地文件
file, err := os.Open(filepath)
if err != nil {
return "", fmt.Errorf("打开本地文件失败: %w, 路径: %s", err, filepath)
}
defer file.Close() // 延迟关闭文件,确保资源释放
// 3. 构建multipart/form-data请求体包含文件字段和普通表单字段
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
defer bodyWriter.Close() // 延迟关闭writer确保请求体完整
// 3.1 添加文件字段:"image"修复核心使用textproto.MIMEHeader构建头部
// 步骤1创建textproto.MIMEHeader实例
mimeHeader := textproto.MIMEHeader{}
// 步骤2设置Content-Disposition指定表单字段名和文件名必须用双引号包裹
mimeHeader.Set("Content-Disposition", fmt.Sprintf(`form-data; name="image"; filename="%s"`, filename))
// 步骤3设置Content-Type指定固定MIME类型
mimeHeader.Set("Content-Type", locimgFileMIME)
// 步骤4传入CreatePartmimeHeader会自动转为指针类型
fileWriter, err := bodyWriter.CreatePart(mimeHeader)
if err != nil {
return "", fmt.Errorf("创建文件表单字段失败: %w", err)
}
// 复制文件内容到请求体(保持不变)
if _, err := io.Copy(fileWriter, file); err != nil {
return "", fmt.Errorf("复制文件内容到请求体失败: %w", err)
}
// 3.2 添加普通表单字段:"fileId"对应PHP的'fileId' => $filename
if err := bodyWriter.WriteField("fileId", filename); err != nil {
return "", fmt.Errorf("写入表单字段fileId失败: %w", err)
}
// 4. 创建HTTP POST请求
contentType := bodyWriter.FormDataContentType() // 自动包含boundary无需手动拼接
req, err := http.NewRequest(http.MethodPost, locimgUploadURL, bodyBuf)
if err != nil {
return "", fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 5. 设置请求头完全对齐PHP的curl请求头配置
setLocimgRequestHeaders(req, contentType)
// 6. 配置HTTP客户端支持gzip解码、忽略SSL验证、超时控制
client := &http.Client{
Timeout: locimgTimeout,
Transport: &http.Transport{
// 忽略SSL证书验证对应PHP的CURLOPT_SSL_VERIFYPEER=false/CURLOPT_SSL_VERIFYHOST=false
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // 注意:字段名是 InsecureSkipVerify非 TLSInsecureSkipVerify
},
// 支持gzip解码对应PHP的CURLOPT_ENCODING "gzip"
DisableCompression: false,
},
}
// 7. 发送HTTP请求
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close() // 延迟关闭响应体
// 8. 读取并解码响应内容处理gzip压缩响应
var respBody []byte
if resp.Header.Get("Content-Encoding") == "gzip" {
// 解压gzip响应对应PHP的gzip解码
gzipReader, err := gzip.NewReader(resp.Body)
if err != nil {
return "", fmt.Errorf("创建gzip解压阅读器失败: %w", err)
}
defer gzipReader.Close()
respBody, err = io.ReadAll(gzipReader)
} else {
respBody, err = io.ReadAll(resp.Body)
}
if err != nil {
return "", fmt.Errorf("读取响应内容失败: %w", err)
}
// 9. 解析JSON响应
var locimgResp LocimgUploadResponse
if err := json.Unmarshal(respBody, &locimgResp); err != nil {
return "", fmt.Errorf("解析JSON响应失败: %w, 响应内容: %s", err, string(respBody))
}
// 10. 按PHP逻辑判断响应结果
if locimgResp.Data.URL != "" {
// 存在data.url字段上传成功
return locimgResp.Data.URL, nil
} else if locimgResp.Msg != "" {
// 存在msg字段返回对应错误
return "", fmt.Errorf("上传失败请重试(%s", locimgResp.Msg)
} else {
// 既无data.url也无msg接口错误
return "", fmt.Errorf("上传失败!接口错误,响应内容: %s", string(respBody))
}
}
// setLocimgRequestHeaders 设置locimg必需的请求头完全对齐PHP的curl配置
func setLocimgRequestHeaders(req *http.Request, contentType string) {
// 默认请求头对应PHP的$httpheader数组
req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Encoding", "gzip,deflate,sdch")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.8")
req.Header.Set("Connection", "close") // 对应PHP的Connection: close
req.Header.Set("User-Agent", locimgUA) // 固定User-Agent与PHP一致
// 额外请求头对应PHP的$addheader参数X-Requested-With: XMLHttpRequest
req.Header.Set("X-Requested-With", "XMLHttpRequest")
// 核心请求头
req.Header.Set("Content-Type", contentType) // 包含multipart boundary
req.Header.Set("Referer", locimgReferer) // 来源校验与PHP一致
}
// 示例调用
func main() {
// 配置本地文件路径和上传文件名(请替换为实际值)
localFilePath := "./test.jpg" // 本地图片实际路径
uploadFilename := "custom-locimg.jpg" // 上传时的自定义文件名(包含后缀)
// 调用locimg上传函数
imgURL, err := LocimgImgUpload(localFilePath, uploadFilename)
if err != nil {
fmt.Printf("locimg图床上传失败: %v\n", err)
return
}
fmt.Printf("locimg图床上传成功图片URL: %s\n", imgURL)
}

View File

@@ -7,11 +7,12 @@ require github.com/gogf/gf/v2 v2.8.0
require (
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect

View File

@@ -1,22 +1,30 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogf/gf/v2 v2.8.0 h1:CgNDoLFQCBxQaWOoGMzgU068T+tm0t/eNUgLV2wPJag=
github.com/gogf/gf/v2 v2.8.0/go.mod h1:6iYuZZ+A0ZcH8+4MDS/P0SvTPCvKzRvyAsY1kbkJYJc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -33,12 +41,16 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
@@ -48,11 +60,15 @@ go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,3 @@
module blazing/contrib/files/zhuanzhuan
go 1.25.0

View File

@@ -0,0 +1,148 @@
package zhuanzhuan
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"time"
)
// 全局配置 - 建议通过环境变量或配置文件加载,此处保持原有结构
const (
uploadURL = "https://dsyy.zhuanzhuan.com/uploadFile"
savePath = "./uploads" // 文件保存根路径,需替换为实际路径
timeout = 30 * time.Second
defaultCT = "image/jpeg"
)
// UploadResponse 定义接口返回的JSON结构
type UploadResponse struct {
Status bool `json:"status"`
Data []UploadData `json:"data"`
}
// UploadData 定义返回数据中的单个文件信息
type UploadData struct {
URL string `json:"url"`
}
// UploadFile 上传文件到指定接口
// fileKey: 文件标识(不含后缀),接口会拼接 .jpg 查找本地文件
// 返回值: 上传成功后的文件URL错误信息
func UploadFile(fileKey string) (string, error) {
// 1. 校验参数
if fileKey == "" {
return "", fmt.Errorf("fileKey 不能为空")
}
// 2. 拼接本地文件路径
fileName := fmt.Sprintf("%s.jpg", fileKey)
localPath := filepath.Join(savePath, fileName)
// 3. 检查并打开文件
file, err := os.Open(localPath)
if err != nil {
return "", fmt.Errorf("打开本地文件失败: %w, 路径: %s", err, localPath)
}
defer file.Close() // 延迟关闭文件,确保资源释放
// 4. 构建multipart/form-data请求体
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
defer bodyWriter.Close() // 延迟关闭writer简化代码
// 4.1 添加表单字段picDirType
if err := bodyWriter.WriteField("picDirType", "1"); err != nil {
return "", fmt.Errorf("写入表单字段 picDirType 失败: %w", err)
}
// 4.2 添加文件字段
fileWriter, err := bodyWriter.CreateFormFile("uploadfiles", fileName)
if err != nil {
return "", fmt.Errorf("创建文件表单字段失败: %w", err)
}
// 复制文件内容到请求体
if _, err := io.Copy(fileWriter, file); err != nil {
return "", fmt.Errorf("复制文件内容到请求体失败: %w", err)
}
// 5. 创建HTTP POST请求
contentType := bodyWriter.FormDataContentType() // 自动包含boundary无需手动拼接
req, err := http.NewRequest(http.MethodPost, uploadURL, bodyBuf)
if err != nil {
return "", fmt.Errorf("创建HTTP请求失败: %w", err)
}
// 6. 设置请求头对齐原有Java版本请求头
setRequestHeaders(req, contentType)
// 7. 发送HTTP请求
client := &http.Client{Timeout: timeout}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("发送HTTP请求失败: %w", err)
}
defer resp.Body.Close() // 延迟关闭响应体
// 8. 读取响应内容
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应内容失败: %w", err)
}
// 9. 解析JSON响应
var uploadResp UploadResponse
if err := json.Unmarshal(respBody, &uploadResp); err != nil {
return "", fmt.Errorf("解析JSON响应失败: %w, 响应内容: %s", err, string(respBody))
}
// 10. 校验响应结果并提取URL
if err := validateUploadResponse(uploadResp, respBody); err != nil {
return "", err
}
return uploadResp.Data[0].URL, nil
}
// setRequestHeaders 设置HTTP请求头抽离为独立函数提升可读性
func setRequestHeaders(req *http.Request, contentType string) {
req.Header.Set("Connection", "keep-alive")
req.Header.Set("Accept", "*/*")
req.Header.Set("Origin", "https://m.zhuanzhuan.com")
req.Header.Set("User-Agent", "Mozilla/5.0")
req.Header.Set("Content-Type", contentType)
req.Header.Set("Referer", "https://m.zhuanzhuan.com/ec/")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
req.Header.Set("Accept-Encoding", "gzip,deflate")
}
// validateUploadResponse 校验上传响应结果,抽离为独立函数
func validateUploadResponse(resp UploadResponse, respBody []byte) error {
if !resp.Status {
return fmt.Errorf("接口返回失败状态, 响应内容: %s", string(respBody))
}
if len(resp.Data) == 0 {
return fmt.Errorf("接口返回数据为空, 响应内容: %s", string(respBody))
}
if resp.Data[0].URL == "" {
return fmt.Errorf("接口返回URL为空, 响应内容: %s", string(respBody))
}
return nil
}
// 示例调用
func main() {
// 替换为实际的文件标识(不含.jpg后缀
fileKey := "test-file-key"
url, err := UploadFile(fileKey)
if err != nil {
fmt.Printf("文件上传失败: %v\n", err)
return
}
fmt.Printf("文件上传成功访问URL: %s\n", url)
}