修 cookie 自动续期后仍失效时把内部错误码暴露给用户
现象(用户反馈): 获取签到表单失败: 获取当日任务列表失败: 解析任务列表响应失败: cpdaily_cookie_expired 期望:能自动续期就续期;续不了就明确引导重新登录(前端据此跳登录页)。 实际:既没提示重新登录,还把内部错误码 cpdaily_cookie_expired 直接显示出来。 根因:自动续期成功、但用新 cookie 重试仍判登录态失效时, 重新拿到的错误(仍是 ErrCookieExpired)被原样返回。 而 needReLoginSignal() 只认 ErrPasswordChanged / ErrNeedReLogin, 不认识 ErrCookieExpired,于是: - errMsg 保持 "前缀 + cpdaily_cookie_expired"; - needReLogin=false,前端不会引导重新登录。 这属于「续期拿到了凭证,但凭证其实无效」的情形(长期凭证过期、 学校侧强制下线等),语义上等同于必须重新登录。 改动: 1. withCookieRefresh:续期后重试仍报登录态失效 → 升级为 ErrNeedReLogin, 这是所有 withCookieRefresh 调用方的统一入口,一处修全覆盖; 2. 签到执行路径:续期后重试仍过期同样升级,并加注释说明; 3. 新增 userFacingError(prefix, err):输出前统一兜底, 任何残留的登录态失效语义都转成「登录凭证已过期,请重新登录」, 普通错误保留原文。即使某条链路漏做升级,也不会再泄露内部错误码; 4. 「获取签到表单」与「签到提交」两处失败文案改用 userFacingError, needReLogin 一并由它给出(前端据此跳登录页)。 测试:原始 ErrCookieExpired、多层 %w 包装、ErrNeedReLogin、 JSON 文本「未登录」均转成引导重登文案且不含内部错误码; 普通错误原样保留;nil 不触发重登。
This commit is contained in:
@@ -244,7 +244,39 @@ func withCookieRefresh(user *model.SysUser, cookieMap map[string]string, host st
|
||||
// 用续期后的 cookie 重试一次
|
||||
newMap := cpd.ParseCookieJSON(user.Cookie)
|
||||
if retryErr := fn(newMap); retryErr != nil {
|
||||
// 续期成功但重试仍判登录态失效:说明拿到的凭证依然无效
|
||||
// (长期凭证过期、学校侧强制下线等),等同于需要用户重新登录。
|
||||
// 若不在这里升级,原始 ErrCookieExpired 会一路透传,
|
||||
// 最终在界面上显示 cpdaily_cookie_expired 这种内部错误码。
|
||||
if isCpdailyAuthError(retryErr) {
|
||||
slog.Warn("续期后重试仍提示登录态失效,判定需重新登录",
|
||||
"userId", user.ID, "host", host, "error", retryErr)
|
||||
return newMap, fmt.Errorf("%w: %v", ErrNeedReLogin, retryErr)
|
||||
}
|
||||
return newMap, retryErr
|
||||
}
|
||||
return newMap, nil
|
||||
}
|
||||
|
||||
// userFacingError 把内部错误转成可直接展示给用户的文案。
|
||||
//
|
||||
// 背景:ErrCookieExpired 的文本是 `cpdaily_cookie_expired`,
|
||||
// Go 的 `%w` 包装会把它拼进错误链外层(如
|
||||
// "获取签到表单失败: cpdaily_cookie_expired"),
|
||||
// 最终原样出现在界面上,用户完全看不懂。
|
||||
//
|
||||
// 统一在输出前做一次兜底:任何残留的登录态失效语义都转成
|
||||
// 「登录凭证已过期,请重新登录」,并保留原始前缀便于定位。
|
||||
// 这样即使某条调用链漏做了 wrapNeedReLogin,也不会把内部错误码暴露出去。
|
||||
func userFacingError(prefix string, err error) (msg string, needReLogin bool) {
|
||||
if err == nil {
|
||||
return prefix, false
|
||||
}
|
||||
if needReLoginSignal(err) || isCookieExpired(err) {
|
||||
if prefix == "" {
|
||||
return "登录凭证已过期,请重新登录", true
|
||||
}
|
||||
return prefix + "登录凭证已过期,请重新登录", true
|
||||
}
|
||||
return prefix + err.Error(), false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
cpd "cpdaily-new/internal/service/cpdaily"
|
||||
)
|
||||
|
||||
// userFacingError:保证内部错误码 cpdaily_cookie_expired 不会原样展示给用户。
|
||||
//
|
||||
// 线上问题:用户拿到 "获取签到表单失败: 获取当日任务列表失败: 解析任务列表响应失败:
|
||||
// cpdaily_cookie_expired",既看不懂也没引导重新登录。
|
||||
func TestUserFacingErrorHidesInternalCode(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
prefix string
|
||||
err error
|
||||
wantContains string
|
||||
wantReLogin bool
|
||||
wantNotSecret string
|
||||
}{
|
||||
{
|
||||
name: "原始 ErrCookieExpired",
|
||||
prefix: "获取签到表单失败: ",
|
||||
err: cpd.ErrCookieExpired,
|
||||
wantContains: "登录凭证已过期,请重新登录",
|
||||
wantReLogin: true,
|
||||
wantNotSecret: "cpdaily_cookie_expired",
|
||||
},
|
||||
{
|
||||
name: "多层包装后仍能识别",
|
||||
prefix: "获取签到表单失败: ",
|
||||
err: fmt.Errorf("获取当日任务列表失败: %w", fmt.Errorf("解析任务列表响应失败: %w", cpd.ErrCookieExpired)),
|
||||
wantContains: "登录凭证已过期,请重新登录",
|
||||
wantReLogin: true,
|
||||
wantNotSecret: "cpdaily_cookie_expired",
|
||||
},
|
||||
{
|
||||
name: "ErrNeedReLogin",
|
||||
prefix: "签到失败(晚查寝): ",
|
||||
err: fmt.Errorf("%w: 续期失败", ErrNeedReLogin),
|
||||
wantContains: "登录凭证已过期,请重新登录",
|
||||
wantReLogin: true,
|
||||
wantNotSecret: "cpdaily_cookie_expired",
|
||||
},
|
||||
{
|
||||
name: "JSON 形式未登录",
|
||||
prefix: "获取签到表单失败: ",
|
||||
err: errors.New("接口返回: 未登录"),
|
||||
wantContains: "登录凭证已过期,请重新登录",
|
||||
wantReLogin: true,
|
||||
wantNotSecret: "",
|
||||
},
|
||||
{
|
||||
name: "普通错误原样保留",
|
||||
prefix: "获取签到表单失败: ",
|
||||
err: errors.New("网络超时"),
|
||||
wantContains: "网络超时",
|
||||
wantReLogin: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
msg, needLogin := userFacingError(c.prefix, c.err)
|
||||
if needLogin != c.wantReLogin {
|
||||
t.Fatalf("needReLogin=%v, want %v", needLogin, c.wantReLogin)
|
||||
}
|
||||
if !strings.Contains(msg, c.wantContains) {
|
||||
t.Fatalf("文案 %q 应包含 %q", msg, c.wantContains)
|
||||
}
|
||||
if c.wantNotSecret != "" && strings.Contains(msg, c.wantNotSecret) {
|
||||
t.Fatalf("文案 %q 不应包含内部错误码 %q", msg, c.wantNotSecret)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// nil 错误不应触发重新登录。
|
||||
func TestUserFacingErrorNil(t *testing.T) {
|
||||
msg, need := userFacingError("前缀: ", nil)
|
||||
if need {
|
||||
t.Fatal("nil 不应要求重新登录")
|
||||
}
|
||||
if msg != "前缀: " {
|
||||
t.Fatalf("应原样返回前缀,实际 %q", msg)
|
||||
}
|
||||
}
|
||||
@@ -667,22 +667,27 @@ func executeSignTask(c *fiber.Ctx) error {
|
||||
if refreshErr := refreshCpdailyCookie(&user); refreshErr == nil {
|
||||
cookieMap = cpd.ParseCookieJSON(user.Cookie)
|
||||
detail, err = cpd.FetchTodayTaskDetail(cookieMap, host, cpd.TaskType(task.TaskType), task.Title)
|
||||
// 续期成功但仍过期:不能让原始 ErrCookieExpired 透传到用户面前
|
||||
// (否则界面显示 cpdaily_cookie_expired 这种内部错误码)。
|
||||
// 说明自动续期拿到的凭证依然无效,等同于需要用户重新登录。
|
||||
if err != nil && isCookieExpired(err) {
|
||||
slog.Warn("自动续期后仍提示登录态失效,判定需重新登录", "taskId", id, "error", err)
|
||||
err = wrapNeedReLogin(err)
|
||||
}
|
||||
} else {
|
||||
err = wrapNeedReLogin(refreshErr)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
errMsg := "获取签到表单失败: " + err.Error()
|
||||
if needReLoginSignal(err) {
|
||||
errMsg = "登录凭证已过期,请重新登录"
|
||||
}
|
||||
// 统一兜底:避免 cpdaily_cookie_expired 等内部错误码直接展示给用户
|
||||
errMsg, needLoginNow := userFacingError("获取签到表单失败: ", err)
|
||||
slog.Error("获取表单失败", "taskId", id, "error", err)
|
||||
database.DB.Model(&task).Updates(map[string]interface{}{
|
||||
"task_status": "fail",
|
||||
"error_message": errMsg,
|
||||
})
|
||||
saveLog(false, errMsg, taskTitle(task))
|
||||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": errMsg, "needReLogin": needReLoginSignal(err)})
|
||||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": errMsg, "needReLogin": needLoginNow})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -765,17 +770,16 @@ func executeSignTask(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
errMsg := fmt.Sprintf("签到失败(%s): %s", taskTitle(task), err.Error())
|
||||
if needReLoginSignal(err) {
|
||||
errMsg = "登录凭证已过期,请重新登录"
|
||||
}
|
||||
// 同上一处:签到失败也要避免内部错误码外泄
|
||||
errMsg, needLoginNow := userFacingError(
|
||||
fmt.Sprintf("签到失败(%s): ", taskTitle(task)), err)
|
||||
slog.Error("签到失败", "taskId", id, "error", err)
|
||||
database.DB.Model(&task).Updates(map[string]interface{}{
|
||||
"task_status": "fail",
|
||||
"error_message": errMsg,
|
||||
})
|
||||
saveLog(false, errMsg, taskTitle(task))
|
||||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": errMsg, "needReLogin": needReLoginSignal(err)})
|
||||
sendEvent("task_error", map[string]interface{}{"status": "error", "message": errMsg, "needReLogin": needLoginNow})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user