package cpdaily import ( "bytes" "crypto/tls" "encoding/json" "fmt" "io" "mime/multipart" "time" "github.com/go-resty/resty/v2" ) // CpImageUploadPolicy 今日校园 OBS 上传策略 type CpImageUploadPolicy struct { AccessId string `json:"accessid"` FileName string `json:"fileName"` Policy string `json:"policy"` Host string `json:"host"` Signature string `json:"signature"` } // getImageUrlPrefix 根据任务类型返回图片上传的 URL 前缀 func getImageUrlPrefix(taskType TaskType) string { switch taskType { case TaskTypeSleepCheck: return "/wec-counselor-attendance-apps/student/attendance" case TaskTypeSign: return "/wec-counselor-sign-apps/stu" default: return "/wec-counselor-sign-apps/stu" } } // GetUploadPolicy 从今日校园获取 OBS 上传策略(前端用,浏览器直传到 OBS) func GetUploadPolicy(cookieMap map[string]string, host string, taskType TaskType) (*CpImageUploadPolicy, error) { client := newImgClient() cookieStr := cookiesToStr(cookieMap) urlPrefix := getImageUrlPrefix(taskType) var apiPath string if taskType == TaskTypeSleepCheck { apiPath = "/getUploadPolicy" } else { apiPath = "/obs/getUploadPolicy" } fullUrl := host + urlPrefix + apiPath resp, err := client.R(). SetHeader("User-Agent", "Mozilla/5.0 (Linux; Android 15; Redmi Note 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.81 Mobile Safari/537.36 cpdaily/9.9.6 wisedu/9.9.6"). SetHeader("CpdailyClientType", "CPDAILY"). SetHeader("Content-Type", "application/json; charset=UTF-8"). SetHeader("Cookie", cookieStr). SetBody(map[string]interface{}{"fileType": 1}). Post(fullUrl) if err != nil { return nil, fmt.Errorf("获取上传策略失败: %w", err) } defer resp.RawBody().Close() body, _ := io.ReadAll(resp.RawBody()) var result struct { ErrCode int `json:"errCode"` ErrMsg string `json:"errMsg"` Datas *CpImageUploadPolicy `json:"datas"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("解析上传策略失败: %w", err) } if result.ErrCode != 0 || result.Datas == nil { return nil, fmt.Errorf("获取上传策略错误(%d): %s", result.ErrCode, result.ErrMsg) } return result.Datas, nil } // uploadToOBS 将文件上传到今日校园的 OBS func uploadToOBS(policy *CpImageUploadPolicy, fileData []byte, contentType string) error { client := resty.New().SetTimeout(30 * time.Second).SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) if contentType == "" { contentType = "image/jpeg" } // 手动构造 multipart form body := &bytes.Buffer{} writer := multipart.NewWriter(body) writer.WriteField("x-obs-acl", "public-read") writer.WriteField("AccessKeyId", policy.AccessId) writer.WriteField("key", policy.FileName) writer.WriteField("signature", policy.Signature) writer.WriteField("policy", policy.Policy) writer.WriteField("content-type", contentType) part, _ := writer.CreateFormFile("file", "file") part.Write(fileData) writer.Close() resp, err := client.R(). SetHeader("Content-Type", writer.FormDataContentType()). SetBody(body.Bytes()). Post(policy.Host) if err != nil { return fmt.Errorf("上传文件到 OBS 失败: %w", err) } if resp.StatusCode() > 299 { return fmt.Errorf("上传文件到 OBS 返回错误(%d): %s", resp.StatusCode(), string(resp.Body())) } return nil } // GetPreviewUrl 从今日校园获取图片预览 URL func GetPreviewUrl(cookieMap map[string]string, host string, taskType TaskType, fileKey string) (string, error) { client := newImgClient() cookieStr := cookiesToStr(cookieMap) urlPrefix := getImageUrlPrefix(taskType) var apiPath string switch taskType { case TaskTypeSleepCheck: apiPath = "/previewAttachment" case TaskTypeSign: apiPath = "/sign/previewAttachment" default: apiPath = "/sign/previewAttachment" } fullUrl := host + urlPrefix + apiPath resp, err := client.R(). SetHeader("User-Agent", "Mozilla/5.0 (Linux; Android 15; Redmi Note 8 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/131.0.6778.81 Mobile Safari/537.36 cpdaily/9.9.6 wisedu/9.9.6"). SetHeader("CpdailyClientType", "CPDAILY"). SetHeader("Content-Type", "application/json; charset=UTF-8"). SetHeader("Cookie", cookieStr). SetBody(map[string]interface{}{"ossKey": fileKey}). Post(fullUrl) if err != nil { return "", fmt.Errorf("获取预览 URL 失败: %w", err) } defer resp.RawBody().Close() body, _ := io.ReadAll(resp.RawBody()) var result struct { ErrCode int `json:"errCode"` ErrMsg string `json:"errMsg"` Datas json.RawMessage `json:"datas"` } if err := json.Unmarshal(body, &result); err != nil { return "", fmt.Errorf("解析预览 URL 响应失败: %w", err) } if result.ErrCode != 0 { return "", fmt.Errorf("获取预览 URL 错误(%d): %s", result.ErrCode, result.ErrMsg) } var previewUrl string if err := json.Unmarshal(result.Datas, &previewUrl); err != nil { return "", fmt.Errorf("解析预览 URL 数据失败: %w", err) } return previewUrl, nil } // UploadImage 完整的图片上传流程:获取策略 → 上传 OBS → 获取预览 URL func UploadImage(cookieMap map[string]string, host string, taskType TaskType, fileData []byte, contentType string) (string, error) { policy, err := GetUploadPolicy(cookieMap, host, taskType) if err != nil { return "", err } if err := uploadToOBS(policy, fileData, contentType); err != nil { return "", err } previewUrl, err := GetPreviewUrl(cookieMap, host, taskType, policy.FileName) if err != nil { return "", err } return previewUrl, nil } func newImgClient() *resty.Client { return resty.New(). SetTimeout(30*time.Second). SetDoNotParseResponse(true). SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) }