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