Files
bl/common/contrib/files/cdn58/cdn58.go

330 lines
9.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

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