feat(contrib/files): 新增百度图床和58cdn图片上传功能实现
This commit is contained in:
139
common/contrib/files/baidu/baidu.go
Normal file
139
common/contrib/files/baidu/baidu.go
Normal 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)
|
||||
// 设置Referer(PHP中的$referer)
|
||||
req.Header.Set("Referer", baiduReferer)
|
||||
// 设置Cookie(PHP中的$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)
|
||||
}
|
||||
3
common/contrib/files/baidu/go.mod
Normal file
3
common/contrib/files/baidu/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module blazing/contrib/files/baidu
|
||||
|
||||
go 1.25.0
|
||||
329
common/contrib/files/cdn58/cdn58.go
Normal file
329
common/contrib/files/cdn58/cdn58.go
Normal 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)
|
||||
}
|
||||
3
common/contrib/files/cdn58/go.mod
Normal file
3
common/contrib/files/cdn58/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module blazing/contrib/files/cnd58
|
||||
|
||||
go 1.25.0
|
||||
139
common/contrib/files/img/cdn58.go
Normal file
139
common/contrib/files/img/cdn58.go
Normal 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)
|
||||
// 设置Referer(PHP中的$referer)
|
||||
req.Header.Set("Referer", baiduReferer)
|
||||
// 设置Cookie(PHP中的$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)
|
||||
}
|
||||
3
common/contrib/files/img/go.mod
Normal file
3
common/contrib/files/img/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module blazing/contrib/files/cnd58
|
||||
|
||||
go 1.25.0
|
||||
3
common/contrib/files/imgdd/go.mod
Normal file
3
common/contrib/files/imgdd/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module blazing/contrib/files/imgdd
|
||||
|
||||
go 1.25.0
|
||||
147
common/contrib/files/imgdd/imgdd.go
Normal file
147
common/contrib/files/imgdd/imgdd.go
Normal 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)
|
||||
}
|
||||
3
common/contrib/files/locimg/go.mod
Normal file
3
common/contrib/files/locimg/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module blazing/contrib/files/locimg
|
||||
|
||||
go 1.25.0
|
||||
187
common/contrib/files/locimg/locimg.go
Normal file
187
common/contrib/files/locimg/locimg.go
Normal 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:传入CreatePart(mimeHeader会自动转为指针类型)
|
||||
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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
3
common/contrib/files/zhuanzhuan/go.mod
Normal file
3
common/contrib/files/zhuanzhuan/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module blazing/contrib/files/zhuanzhuan
|
||||
|
||||
go 1.25.0
|
||||
148
common/contrib/files/zhuanzhuan/zhuanzhuan.go
Normal file
148
common/contrib/files/zhuanzhuan/zhuanzhuan.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user