330 lines
9.6 KiB
Go
330 lines
9.6 KiB
Go
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)
|
||
}
|