2e2c161770
- 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
191 lines
5.7 KiB
Go
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})
|
|
}
|