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