Files
cpdaily_xiaozhushou/backed/internal/service/cpdaily/image.go
T
becomer 2e2c161770 feat: init cpdaily-xiaozhushou with blue-green deploy
- backed/: Go Fiber backend with cpdaily integration
- frondend/: Vue 3 + Vite frontend
- Dockerfile: multi-stage build (Node → Go → Alpine)
- deploy/docker-compose.yml: production compose file
- .woodpecker/build.yml: CI/CD with blue-green zero-downtime deploy
- /health endpoint for deployment health checks
- Port 8884/8885 blue-green swap via 1Panel OpenResty
2026-07-03 15:37:55 +08:00

191 lines
5.7 KiB
Go

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