Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7180eebba |
@@ -353,17 +353,47 @@ func CompleteGuide(c *fiber.Ctx) error {
|
||||
return utils.Success(c, nil)
|
||||
}
|
||||
|
||||
// syncUserProfile 同步 cpdaily 用户信息到数据库
|
||||
// syncUserProfile 同步 cpdaily 用户信息到数据库。
|
||||
// 登录态失效(cookie 过期)时自动续期并重试一次,避免用户信息长期同步不上。
|
||||
func syncUserProfile(user *model.SysUser, cookieStr string, wecloudUrl string) {
|
||||
syncUserProfileWith(user, cookieStr, wecloudUrl, true)
|
||||
}
|
||||
|
||||
// syncUserProfileWith allowRefresh=false 时不做 cookie 续期,
|
||||
// 供续期流程内部调用,避免「续期 → 同步 → 再续期」的递归风暴。
|
||||
func syncUserProfileWith(user *model.SysUser, cookieStr string, wecloudUrl string, allowRefresh bool) {
|
||||
cookieMap := cpd.ParseCookieJSON(cookieStr)
|
||||
profile, err := cpd.FetchUserProfile(cookieMap, wecloudUrl)
|
||||
if err != nil {
|
||||
slog.Warn("同步用户信息失败", "userId", user.ID, "error", err)
|
||||
|
||||
fetch := func(cm map[string]string) (*cpd.CpdailyUserProfile, error) {
|
||||
return cpd.FetchUserProfile(cm, wecloudUrl)
|
||||
}
|
||||
|
||||
var profile *cpd.CpdailyUserProfile
|
||||
if allowRefresh {
|
||||
_, err := withCookieRefresh(user, cookieMap, wecloudUrl, func(cm map[string]string) error {
|
||||
var e error
|
||||
profile, e = fetch(cm)
|
||||
return e
|
||||
})
|
||||
if err != nil {
|
||||
slog.Warn("同步用户信息失败", "userId", user.ID, "error", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
p, err := fetch(cookieMap)
|
||||
if err != nil {
|
||||
slog.Warn("同步用户信息失败(禁续期)", "userId", user.ID, "error", err)
|
||||
return
|
||||
}
|
||||
profile = p
|
||||
}
|
||||
if profile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
updates := syncProfileToUser(profile)
|
||||
updates["cookie"] = cookieStr // 确保 cookie 最新
|
||||
// 同步结果不覆盖 cookie:cookie 由调用方(登录/续期流程)负责维护,
|
||||
// 避免异步同步与续期流程并发写 cookie 造成相互覆盖。
|
||||
database.DB.Model(&user).Updates(updates)
|
||||
slog.Info("用户信息同步成功", "userId", user.ID, "name", profile.Name)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
@@ -46,16 +47,28 @@ func updateRawPassword(userID uint, rawPassword string) error {
|
||||
return saveRawPassword(userID, rawPassword)
|
||||
}
|
||||
|
||||
// isCookieExpired 判断错误是否由 cpdaily cookie 过期引起
|
||||
// isCookieExpired 判断错误是否由 cpdaily 登录态失效(cookie 过期)引起。
|
||||
// 两种来源:
|
||||
// 1. cpd.ErrCookieExpired:响应体是 HTML(被重定向到统一身份认证登录页),
|
||||
// 这是最常见、也最容易被漏判的情形(原文案匹配不到 "invalid character '<'");
|
||||
// 2. 接口以 JSON 形式返回的「未登录 / 登录已过期」。
|
||||
func isCookieExpired(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, cpd.ErrCookieExpired) {
|
||||
return true
|
||||
}
|
||||
msg := err.Error()
|
||||
return strings.Contains(msg, "未登录") ||
|
||||
strings.Contains(msg, "登录已过期")
|
||||
}
|
||||
|
||||
// isCpdailyAuthError 语义化别名:今日校园登录态失效。
|
||||
func isCpdailyAuthError(err error) bool {
|
||||
return isCookieExpired(err)
|
||||
}
|
||||
|
||||
// refreshCpdailyCookie 使用数据库中的 raw_password 重新登录 cpdaily 并更新 cookie
|
||||
// 密码错误时返回 ErrPasswordChanged
|
||||
func refreshCpdailyCookie(user *model.SysUser) error {
|
||||
@@ -141,12 +154,12 @@ func refreshCpdailyCookie(user *model.SysUser) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 同步用户信息(异步)
|
||||
go syncUserProfile(user, cookieStr, school.WecloudUrl)
|
||||
|
||||
// 更新传入用户的 cookie(引用),上层重试时使用
|
||||
// 先更新传入用户的 cookie(引用),上层重试与后续同步均使用最新值
|
||||
user.Cookie = cookieStr
|
||||
|
||||
// 同步用户信息(异步,禁续期:避免「续期 → 同步 → 再续期」递归)
|
||||
go syncUserProfileWith(user, cookieStr, school.WecloudUrl, false)
|
||||
|
||||
slog.Info("刷新cookie: 成功", "userId", user.ID)
|
||||
return nil
|
||||
}
|
||||
@@ -185,3 +198,33 @@ func refreshCookieByLongTermToken(user *model.SysUser) (string, error) {
|
||||
user.Cookie = cookieStr
|
||||
return cookieStr, nil
|
||||
}
|
||||
|
||||
// withCookieRefresh 执行一次今日校园调用;若因登录态失效失败,则尝试续期 cookie 后重试一次。
|
||||
// - 返回 (调用结果, 续期后的 cookieMap, error)。cookieMap 有变化时调用方可用于后续调用。
|
||||
// - 续期失败(如 MFA 学校无法自动续期、密码已改)时,返回错误里带上 ErrCookieExpired,
|
||||
// 由上层转成「登录凭证已过期,请重新登录」引导用户重登。
|
||||
//
|
||||
// 说明:MFA 学校登录后签发的是短期 JWT,到期会要求重新登录,本就无需自动续期;
|
||||
// 普通学校(非 MFA)的 cookie 会在此处用长期凭证/密码自动续期,避免用户被莫名踢出。
|
||||
func withCookieRefresh(user *model.SysUser, cookieMap map[string]string, host string,
|
||||
fn func(cm map[string]string) error) (map[string]string, error) {
|
||||
|
||||
err := fn(cookieMap)
|
||||
if err == nil || !isCpdailyAuthError(err) {
|
||||
return cookieMap, err
|
||||
}
|
||||
|
||||
slog.Info("检测到今日校园登录态失效,尝试自动续期 cookie", "userId", user.ID, "host", host)
|
||||
if refreshErr := refreshCpdailyCookie(user); refreshErr != nil {
|
||||
// 续期失败:保留原始错误语义,让上层引导重新登录
|
||||
slog.Warn("cookie 自动续期失败", "userId", user.ID, "error", refreshErr)
|
||||
return cookieMap, fmt.Errorf("%w: %v", cpd.ErrCookieExpired, refreshErr)
|
||||
}
|
||||
|
||||
// 用续期后的 cookie 重试一次
|
||||
newMap := cpd.ParseCookieJSON(user.Cookie)
|
||||
if retryErr := fn(newMap); retryErr != nil {
|
||||
return newMap, retryErr
|
||||
}
|
||||
return newMap, nil
|
||||
}
|
||||
|
||||
@@ -37,31 +37,25 @@ func GetPendingLeaves(c *fiber.Ctx) error {
|
||||
host := getSchoolHost(user.SchoolID)
|
||||
|
||||
// 应用套件分流:新套(leave-new)走独立链路
|
||||
if schoolLeaveFlavor(&user, cookieMap) == cpd.LeaveFlavorNew {
|
||||
items, err := fetchLeaveNewPendingList(cookieMap, host, &user)
|
||||
if err != nil {
|
||||
slog.Error("获取待销假列表失败(新套)", "userId", userID, "error", err)
|
||||
if isCookieExpired(err) {
|
||||
return utils.Fail(c, 401, "登录凭证已过期,请重新登录")
|
||||
}
|
||||
return utils.Fail(c, 503, "获取待销假列表失败: "+err.Error())
|
||||
}
|
||||
return utils.Success(c, items)
|
||||
}
|
||||
isNewFlavor := schoolLeaveFlavor(&user, cookieMap) == cpd.LeaveFlavorNew
|
||||
|
||||
items, err := cpd.FetchLeaveList(cookieMap, host)
|
||||
if err != nil && isCookieExpired(err) {
|
||||
slog.Info("获取待销假列表cookie过期,尝试刷新", "userId", userID)
|
||||
if refreshErr := refreshCpdailyCookie(&user); refreshErr == nil {
|
||||
cookieMap = cpd.ParseCookieJSON(user.Cookie)
|
||||
items, err = cpd.FetchLeaveList(cookieMap, host)
|
||||
} else if errors.Is(refreshErr, ErrPasswordChanged) {
|
||||
err = ErrPasswordChanged
|
||||
var items []cpd.LeaveItem
|
||||
_, err := withCookieRefresh(&user, cookieMap, host, func(cm map[string]string) error {
|
||||
if isNewFlavor {
|
||||
// 套件判定用的是会话开始时解析的 cookie;续期后重新判定以保持准确
|
||||
if schoolLeaveFlavor(&user, cm) == cpd.LeaveFlavorNew {
|
||||
var e error
|
||||
items, e = fetchLeaveNewPendingList(cm, host, &user)
|
||||
return e
|
||||
}
|
||||
}
|
||||
}
|
||||
var e error
|
||||
items, e = cpd.FetchLeaveList(cm, host)
|
||||
return e
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("获取待销假列表失败", "userId", userID, "error", err)
|
||||
if errors.Is(err, ErrPasswordChanged) {
|
||||
if isCpdailyAuthError(err) {
|
||||
return utils.Fail(c, 401, "登录凭证已过期,请重新登录")
|
||||
}
|
||||
return utils.Fail(c, 503, "获取待销假列表失败: "+err.Error())
|
||||
|
||||
@@ -36,7 +36,20 @@ func GetTaskTitles(c *fiber.Ctx) error {
|
||||
cookieMap := cpd.ParseCookieJSON(user.Cookie)
|
||||
host := getSchoolHost(user.SchoolID)
|
||||
|
||||
tasks := cpd.FetchAllRecentTasks(cookieMap, host)
|
||||
// 登录态失效时自动续期并重试一次;仍失败则明确提示重新登录
|
||||
var tasks []cpd.TodayTaskInfo
|
||||
_, err := withCookieRefresh(&user, cookieMap, host, func(cm map[string]string) error {
|
||||
var fetchErr error
|
||||
tasks, fetchErr = cpd.FetchAllRecentTasks(cm, host)
|
||||
return fetchErr
|
||||
})
|
||||
if err != nil {
|
||||
if isCpdailyAuthError(err) {
|
||||
return utils.Fail(c, 401, "登录凭证已过期,请重新登录")
|
||||
}
|
||||
return utils.Fail(c, 503, "获取打卡任务失败: "+err.Error())
|
||||
}
|
||||
|
||||
return utils.Success(c, tasks)
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ func GetUploadPolicy(cookieMap map[string]string, host string, taskType TaskType
|
||||
ErrMsg string `json:"errMsg"`
|
||||
Datas *CpImageUploadPolicy `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析上传策略失败: %w", err)
|
||||
}
|
||||
if result.ErrCode != 0 || result.Datas == nil {
|
||||
@@ -175,7 +175,7 @@ func GetPreviewUrl(cookieMap map[string]string, host string, taskType TaskType,
|
||||
Message string `json:"message"`
|
||||
Datas json.RawMessage `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return "", fmt.Errorf("解析预览 URL 响应失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
|
||||
@@ -186,7 +186,7 @@ func FetchLeaveList(cookieMap map[string]string, host string) ([]LeaveItem, erro
|
||||
Rows []map[string]interface{} `json:"rows"`
|
||||
} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析待销假列表失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
@@ -273,7 +273,7 @@ func FetchLeaveDetail(cookieMap map[string]string, host, leaveID string) (*Leave
|
||||
} `json:"reportPlaces"`
|
||||
} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析请假详情失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
@@ -342,7 +342,7 @@ func FetchReportApprovers(cookieMap map[string]string, host string, leaveCodeNum
|
||||
} `json:"approveNodes"`
|
||||
} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析销假审批人失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
code, _ := result.Code.Int64()
|
||||
@@ -434,7 +434,7 @@ func SubmitLeaveReport(cookieMap map[string]string, host string, form map[string
|
||||
Code json.Number `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return fmt.Errorf("解析销假响应失败: %w, body: %s", err, string(body))
|
||||
}
|
||||
code, _ := result.Code.Int64()
|
||||
|
||||
@@ -42,7 +42,7 @@ func CheckOnceLeave(cookieMap map[string]string, host string) error {
|
||||
Code json.Number `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return fmt.Errorf("解析请假资格响应失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
code, _ := result.Code.Int64()
|
||||
@@ -77,7 +77,7 @@ func FetchLeaveTypes(cookieMap map[string]string, host string) ([]LeaveType, err
|
||||
} `json:"leaveType"`
|
||||
} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析请假类型失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
code, _ := result.Code.Int64()
|
||||
@@ -144,7 +144,7 @@ func FetchLeaveTypeCfg(cookieMap map[string]string, host string, code int) (*Lea
|
||||
LimitDay interface{} `json:"limitDay"`
|
||||
} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析请假类型配置失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
codeVal, _ := result.Code.Int64()
|
||||
@@ -332,7 +332,7 @@ func SubmitLeaveApply(cookieMap map[string]string, host string, form map[string]
|
||||
Code json.Number `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return fmt.Errorf("解析请假响应失败: %w, body: %s", err, string(body))
|
||||
}
|
||||
code, _ := result.Code.Int64()
|
||||
|
||||
@@ -126,7 +126,7 @@ type leaveNewResponse struct {
|
||||
// leaveNewResultErr 解析通用响应:code != "0" 时返回带 message 的错误
|
||||
func leaveNewResultErr(body []byte, action string) (*leaveNewResponse, error) {
|
||||
var result leaveNewResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("%s响应解析失败: %w (body: %.200s)", action, err, string(body))
|
||||
}
|
||||
if result.Code != "0" {
|
||||
@@ -184,7 +184,7 @@ func FetchLeaveAppFlavor(cookieMap map[string]string, host string) (LeaveAppFlav
|
||||
Code string `json:"code"`
|
||||
Datas []LeaveAppInfo `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil || result.Code != "0" {
|
||||
if err := unmarshalBody(body, &result); err != nil || result.Code != "0" {
|
||||
slog.Warn("解析辅导猫应用列表失败", "error", err, "body", string(body))
|
||||
return LeaveFlavorUnknown, ""
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func PingLeaveNew(cookieMap map[string]string, host string) bool {
|
||||
return false
|
||||
}
|
||||
var result leaveNewResponse
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return false
|
||||
}
|
||||
return result.Code == "0"
|
||||
@@ -251,7 +251,7 @@ func FetchLeaveNewTypes(cookieMap map[string]string, host string) ([]LeaveNewTyp
|
||||
TotalSize interface{} `json:"totalSize"`
|
||||
} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析请假类型失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
for _, r := range result.Datas.Rows {
|
||||
@@ -296,7 +296,7 @@ func FetchLeaveNewTypeDetail(cookieMap map[string]string, host string, typeWid i
|
||||
TypeApplyDesc string `json:"typeApplyDesc"`
|
||||
} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析请假类型配置失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
e := result.Datas.ExtendInfoBean
|
||||
@@ -361,7 +361,7 @@ func FetchLeaveNewApproveFlow(cookieMap map[string]string, host string, typeWid
|
||||
FlowData map[string]interface{} `json:"flowData"`
|
||||
} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析请假审批人失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
return &LeaveNewFlow{
|
||||
@@ -386,7 +386,7 @@ func FetchLeaveNewCcSearch(cookieMap map[string]string, host, searchKey string)
|
||||
var result struct {
|
||||
Datas []LeaveNewApprover `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析抄送人失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
return result.Datas, nil
|
||||
@@ -404,7 +404,7 @@ func FetchLeaveNewLastCc(cookieMap map[string]string, host string) ([]LeaveNewAp
|
||||
var result struct {
|
||||
Datas []LeaveNewApprover `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析上次抄送人失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
return result.Datas, nil
|
||||
@@ -464,7 +464,7 @@ func FetchLeaveNewList(cookieMap map[string]string, host string, pageNumber, pag
|
||||
Rows []map[string]interface{} `json:"rows"`
|
||||
} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析请假记录失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
list := make([]LeaveNewItem, 0, len(result.Datas.Rows))
|
||||
@@ -540,7 +540,7 @@ func FetchLeaveNewDetail(cookieMap map[string]string, host, leaveID string) (*Le
|
||||
Rows []map[string]interface{} `json:"rows"`
|
||||
} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析请假详情失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
if len(result.Datas.Rows) == 0 {
|
||||
@@ -613,7 +613,7 @@ func FetchLeaveNewReportPlaces(cookieMap map[string]string, host string) ([]Leav
|
||||
} `json:"rows"`
|
||||
} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析规定销假位置失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
list := make([]LeaveNewReportPlace, 0, len(result.Datas.Rows))
|
||||
@@ -645,7 +645,7 @@ func FetchLeaveNewReportApprovers(cookieMap map[string]string, host, leaveID str
|
||||
var result struct {
|
||||
Datas []LeaveNewApprover `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析销假确认人失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
return result.Datas, nil
|
||||
@@ -745,7 +745,7 @@ func SubmitLeaveNewReport(cookieMap map[string]string, host string, form map[str
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return fmt.Errorf("解析销假响应失败: %w, body: %s", err, string(body))
|
||||
}
|
||||
slog.Info("leave-new/report 响应", "code", result.Code, "message", result.Message, "body", string(body))
|
||||
@@ -888,7 +888,7 @@ func SubmitLeaveNewApply(cookieMap map[string]string, host string, form map[stri
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return fmt.Errorf("解析请假响应失败: %w, body: %s", err, string(body))
|
||||
}
|
||||
slog.Info("leave-new/apply 响应", "code", result.Code, "message", result.Message, "body", string(body))
|
||||
@@ -947,7 +947,7 @@ func GetLeaveNewUploadPolicy(cookieMap map[string]string, host string) (*LeaveNe
|
||||
var result struct {
|
||||
Datas *LeaveNewUploadPolicy `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析上传策略失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
if result.Datas == nil || result.Datas.FileName == "" {
|
||||
@@ -970,7 +970,7 @@ func GetLeaveNewPreviewURL(cookieMap map[string]string, host, obsKey string) (st
|
||||
var result struct {
|
||||
Datas string `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return "", fmt.Errorf("解析预览 URL 失败: %w (body: %.200s)", err, string(body))
|
||||
}
|
||||
if result.Datas == "" {
|
||||
|
||||
@@ -57,7 +57,7 @@ func fetchFromWecloud(wecloudUrl string, cookieMap map[string]string) (*CpdailyU
|
||||
MobilePhone string `json:"mobilePhone"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析 wecloud 用户信息失败: %w", err)
|
||||
}
|
||||
if result.ErrCode != 0 || result.Data == nil {
|
||||
|
||||
@@ -17,6 +17,44 @@ import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
// ========== 登录态失效的统一识别 ==========
|
||||
|
||||
// ErrCookieExpired 表示今日校园的登录态(cookie)已失效:
|
||||
// 请求被重定向到统一身份认证登录页,返回 HTML 而非接口 JSON。
|
||||
// handler 层据此触发 cookie 续期(raw_password 重登 或 长期凭证换取),
|
||||
// 续期失败则引导用户重新登录,而不是把「解析失败」这种模糊错误抛给用户。
|
||||
var ErrCookieExpired = fmt.Errorf("cpdaily_cookie_expired")
|
||||
|
||||
// looksLikeHTML 判断响应体是否为一个 HTML 页面(而非接口 JSON)。
|
||||
// 登录态失效时,今日校园会把请求 302 到 authserver 登录页,响应体是 HTML。
|
||||
func looksLikeHTML(body []byte) bool {
|
||||
head := body
|
||||
if len(head) > 512 {
|
||||
head = head[:512]
|
||||
}
|
||||
trimmed := strings.TrimSpace(string(head))
|
||||
if trimmed == "" {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "<") {
|
||||
return true
|
||||
}
|
||||
lower := strings.ToLower(trimmed)
|
||||
return strings.Contains(lower, "<!doctype html") ||
|
||||
strings.Contains(lower, "<html")
|
||||
}
|
||||
|
||||
// unmarshalBody 解析接口 JSON 响应体。
|
||||
// 若响应体是 HTML(登录态失效被重定向到认证页),返回 ErrCookieExpired,
|
||||
// 以便上层触发 cookie 续期或引导重新登录,而不是抛出难以理解的
|
||||
// "invalid character '<' looking for beginning of value"。
|
||||
func unmarshalBody(body []byte, v interface{}) error {
|
||||
if looksLikeHTML(body) {
|
||||
return ErrCookieExpired
|
||||
}
|
||||
return json.Unmarshal(body, v)
|
||||
}
|
||||
|
||||
// ========== HTTP 客户端 ==========
|
||||
|
||||
var httpClient *resty.Client
|
||||
|
||||
@@ -97,7 +97,7 @@ func (r *submitCaptchaResolver) GetValidationInfo() (*ValidationInfo, error) {
|
||||
body, _ := io.ReadAll(resp.RawBody())
|
||||
|
||||
var apiResp apiResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
if err := unmarshalBody(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("解析验证信息响应失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ func (r *submitCaptchaResolver) createCaptcha(info *ValidationInfo) (*CpSubmitCa
|
||||
body, _ := io.ReadAll(resp.RawBody())
|
||||
|
||||
var apiResp apiResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
if err := unmarshalBody(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("解析创建验证码响应失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ func (r *submitCaptchaResolver) submitCaptcha(info *ValidationInfo, captchaInfo
|
||||
body, _ := io.ReadAll(resp.RawBody())
|
||||
|
||||
var apiResp apiResponse
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
if err := unmarshalBody(body, &apiResp); err != nil {
|
||||
return "", fmt.Errorf("解析提交验证码响应失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ func fetchTasksByEndpoint(cookieMap map[string]string, host string, taskType Tas
|
||||
ErrMsg string `json:"errMsg"`
|
||||
Datas map[string]interface{} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析任务列表响应失败: %w", err)
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
@@ -241,8 +241,10 @@ func parseTasksFromDatas(datas map[string]interface{}) []TodayTaskInfo {
|
||||
return tasks
|
||||
}
|
||||
|
||||
// FetchAllRecentTasks 并发查询 sign + sleep_check 两种任务类型,聚合去重后返回
|
||||
func FetchAllRecentTasks(cookieMap map[string]string, host string) []TodayTaskInfo {
|
||||
// FetchAllRecentTasks 并发查询 sign + sleep_check 两种任务类型,聚合去重后返回。
|
||||
// 若两类查询都失败则返回错误(供上层识别登录态失效并续期重试);
|
||||
// 只要有一类成功就返回结果(另一类失败仅记录日志)。
|
||||
func FetchAllRecentTasks(cookieMap map[string]string, host string) ([]TodayTaskInfo, error) {
|
||||
type result struct {
|
||||
tasks []TodayTaskInfo
|
||||
err error
|
||||
@@ -267,9 +269,18 @@ func FetchAllRecentTasks(cookieMap map[string]string, host string) []TodayTaskIn
|
||||
|
||||
seen := map[string]bool{}
|
||||
var all []TodayTaskInfo
|
||||
var firstErr error
|
||||
successCount := 0
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
r := <-ch
|
||||
if r.err != nil {
|
||||
if firstErr == nil {
|
||||
firstErr = r.err
|
||||
}
|
||||
} else {
|
||||
successCount++
|
||||
}
|
||||
for _, t := range r.tasks {
|
||||
key := t.TaskName + "|" + t.TaskType
|
||||
if !seen[key] {
|
||||
@@ -279,10 +290,15 @@ func FetchAllRecentTasks(cookieMap map[string]string, host string) []TodayTaskIn
|
||||
}
|
||||
}
|
||||
|
||||
if len(all) == 0 {
|
||||
return []TodayTaskInfo{}
|
||||
// 两类都失败:返回错误(cookie 失效等场景由上层续期重试)
|
||||
if successCount == 0 && firstErr != nil {
|
||||
return nil, firstErr
|
||||
}
|
||||
return all
|
||||
|
||||
if len(all) == 0 {
|
||||
return []TodayTaskInfo{}, nil
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
func fetchTasksByYearMonth(cookieMap map[string]string, host string, taskType TaskType, year, month string) ([]TodayTaskInfo, error) {
|
||||
yearMonth := fmt.Sprintf("%s-%s", year, month)
|
||||
@@ -439,7 +455,7 @@ func fetchTaskDetailByWid(cookieMap map[string]string, host string, taskType Tas
|
||||
ErrMsg string `json:"errMsg"`
|
||||
Datas map[string]interface{} `json:"datas"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析任务详情响应失败: %w", err)
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
@@ -1429,7 +1445,7 @@ func SubmitSign(cookieMap map[string]string, host string, taskType TaskType, sub
|
||||
Code json.Number `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
if err := unmarshalBody(body, &result); err != nil {
|
||||
return fmt.Errorf("解析签到响应失败: %w, body: %s", err, string(body))
|
||||
}
|
||||
code, _ := result.Code.Int64()
|
||||
|
||||
Reference in New Issue
Block a user