回退 4f9027b:实测证明返校登记上传端点不存在该路径

用真实用户 cookie(学号 2364400108,桂林信息科技学院)实测:

  /wec-counselor-leave-return-apps/stu/obsOperate/obs/getUploadPolicy → 404
  /wec-counselor-leave-return-apps/stu/obs/getUploadPolicy             → 404
  /wec-counselor-leave-return-apps/stu/leave/obs/getUploadPolicy       → 404
  /wec-counselor-attendance-apps/student/attendance/getUploadPolicy    → 200
  /wec-counselor-sign-apps/stu/obs/getUploadPolicy                     → 200

4f9027b 中推断的 leave-return 上传策略路径根本不存在,属臆造。
issue 的真正成因尚未确定,不应保留臆造修复。

同时确认:sign 与 sleep_check 用真实 cookie 走完整链路
(取策略 → 上传 OBS → 取预览 URL)均成功,OBS 返回 204;
return_register 走现有路径得到的是 cookie_expired(HTML 登录页),
与用户看到的 400 现象不一致,需前端侧信息才能继续定位。
This commit is contained in:
becomer
2026-10-18 18:57:08 +08:00
parent 4f9027bb0f
commit 91b8d7bfd4
4 changed files with 6 additions and 175 deletions
-44
View File
@@ -1015,29 +1015,6 @@ func GetUploadPolicy(c *fiber.Ctx) error {
return utils.Success(c, policy)
}
// 返校登记:属于 leave-return 模块,上传策略端点与签到/查寝/请假均不同。
// 若不单独处理会落到 getImageUrlPrefix 的 default(sign-apps 路径),
// OBS 因桶/签名不匹配返回 400,前端提示「图片上传到 OBS 失败(400)」。
if req.TaskType == string(cpd.TaskTypeReturnRegister) {
policy, err := cpd.GetLeaveReturnUploadPolicy(cookieMap, host)
if err != nil && isCookieExpired(err) {
if refreshErr := refreshCpdailyCookie(&user); refreshErr == nil {
cookieMap = cpd.ParseCookieJSON(user.Cookie)
policy, err = cpd.GetLeaveReturnUploadPolicy(cookieMap, host)
} else {
err = wrapNeedReLogin(refreshErr)
}
}
if err != nil {
slog.Error("获取上传策略失败(返校登记)", "userId", userID, "error", err)
if needReLoginSignal(err) {
return utils.Fail(c, 401, "登录凭证已过期,请重新登录")
}
return utils.Fail(c, 503, "获取上传策略失败: "+err.Error())
}
return utils.Success(c, policy)
}
policy, err := cpd.GetUploadPolicy(cookieMap, host, cpd.TaskType(req.TaskType))
if err != nil && isCookieExpired(err) {
slog.Info("获取上传策略cookie过期,尝试刷新", "userId", userID)
@@ -1103,27 +1080,6 @@ func GetPreviewUrl(c *fiber.Ctx) error {
return utils.Success(c, fiber.Map{"previewUrl": previewUrl})
}
// 返校登记:预览 URL 同样走 leave-return 模块,不能落到 sign-apps
if req.TaskType == string(cpd.TaskTypeReturnRegister) {
previewUrl, err := cpd.GetLeaveReturnPreviewURL(cookieMap, host, req.FileKey)
if err != nil && isCookieExpired(err) {
if refreshErr := refreshCpdailyCookie(&user); refreshErr == nil {
cookieMap = cpd.ParseCookieJSON(user.Cookie)
previewUrl, err = cpd.GetLeaveReturnPreviewURL(cookieMap, host, req.FileKey)
} else {
err = wrapNeedReLogin(refreshErr)
}
}
if err != nil {
slog.Error("获取预览 URL 失败(返校登记)", "userId", userID, "error", err)
if needReLoginSignal(err) {
return utils.Fail(c, 401, "登录凭证已过期,请重新登录")
}
return utils.Fail(c, 503, "获取预览 URL 失败: "+err.Error())
}
return utils.Success(c, fiber.Map{"previewUrl": previewUrl})
}
previewUrl, err := cpd.GetPreviewUrl(cookieMap, host, cpd.TaskType(req.TaskType), req.FileKey)
if err != nil && isCookieExpired(err) {
slog.Info("获取预览URL cookie过期,尝试刷新", "userId", userID)
-27
View File
@@ -7,11 +7,9 @@ import (
"encoding/json"
"fmt"
"io"
"log/slog"
"mime/multipart"
"net/textproto"
"net/url"
"sort"
"strings"
"time"
@@ -38,15 +36,7 @@ func getImageUrlPrefix(taskType TaskType) string {
return "/wec-counselor-leave-apps/leave"
case TaskTypeSign:
return "/wec-counselor-sign-apps/stu"
case TaskTypeReturnRegister:
// 返校登记属于独立模块,走 leave-return;
// 注意:其上传策略/预览端点与其它模块不同,实际调用在
// GetLeaveReturnUploadPolicy / GetLeaveReturnPreviewURL,
// 不应依赖本函数拼接。
return leaveReturnAPIBase
default:
// 未知类型的兜底仍是签到路径(历史行为),
// 但调用方应显式判断类型,避免静默用错模块导致 OBS 400。
return "/wec-counselor-sign-apps/stu"
}
}
@@ -90,23 +80,6 @@ func GetUploadPolicy(cookieMap map[string]string, host string, taskType TaskType
return nil, fmt.Errorf("获取上传策略错误(%d): %s", result.ErrCode, result.ErrMsg)
}
// 诊断:formData 为空会导致前端回退到自行拼装字段的旧分支,
// 而那条路径会被 OBS 以 400/AccessDenied 拒绝。
// 记录实际字段名与个数(不含值,避免泄露签名),便于线上定位。
if result.Datas == nil || len(result.Datas.FormData) == 0 {
slog.Warn("上传策略未返回 formData,前端将回退到默认字段拼装",
"taskType", taskType,
"bodySnippet", truncateForLog(string(body), 400))
} else {
keys := make([]string, 0, len(result.Datas.FormData))
for k := range result.Datas.FormData {
keys = append(keys, k)
}
sort.Strings(keys)
slog.Info("上传策略已获取",
"taskType", taskType, "formDataKeys", keys, "host", result.Datas.Host)
}
return result.Datas, nil
}
@@ -35,12 +35,12 @@ type LeaveReturnPlace struct {
// LeaveReturnTask 待办返校登记(一名学生在一期离返校任务中的返校登记)
type LeaveReturnTask struct {
TaskWid int `json:"taskWid"`
StuWid int `json:"stuWid"`
LeaveName string `json:"leaveName"` // 如「2026年中秋节离返校」
StartTime string `json:"startTime"` // 假期开始
EndTime string `json:"endTime"` // 假期结束
SignType int `json:"signType"` // 1=范围内 2=范围外(提交时按坐标计算)
TaskWid int `json:"taskWid"`
StuWid int `json:"stuWid"`
LeaveName string `json:"leaveName"` // 如「2026年中秋节离返校」
StartTime string `json:"startTime"` // 假期开始
EndTime string `json:"endTime"` // 假期结束
SignType int `json:"signType"` // 1=范围内 2=范围外(提交时按坐标计算)
Disclaimer string `json:"disclaimer"` // 承诺书文案(透传给前端展示)
}
@@ -237,64 +237,3 @@ func SubmitLeaveReturnRegister(cookieMap map[string]string, host string, taskWid
}
return nil
}
// GetLeaveReturnUploadPolicy 获取「返校登记」模块的图片上传策略。
//
// 背景:返校登记属于 leave-return 模块(/wec-counselor-leave-return-apps),
// 与签到(sign-apps)、查寝(attendance-apps)、请假(leave-apps)都不同。
// 早期 getImageUrlPrefix 没有该分支,taskType=return_register 会落到 default,
// 错误地去 sign-apps 拿上传策略;OBS 侧因签名/桶不匹配返回 400,
// 前端显示「图片上传到 OBS 失败(400),请重新选择照片上传」。
//
// 路径参照同族 leave-new 模块的 /stu/obsOperate/obs/getUploadPolicy,
// 并复用 leaveReturnPost 的请求头与错误处理(同一网关,Header 一致)。
func GetLeaveReturnUploadPolicy(cookieMap map[string]string, host string) (*CpImageUploadPolicy, error) {
url := host + leaveReturnAPIBase + "/stu/obsOperate/obs/getUploadPolicy"
body, err := leaveReturnPost(cookieMap, url, map[string]interface{}{"fileType": 1})
if err != nil {
return nil, fmt.Errorf("获取返校登记上传策略失败: %w", err)
}
var result struct {
Datas *CpImageUploadPolicy `json:"datas"`
}
if err := unmarshalBody(body, &result); err != nil {
return nil, fmt.Errorf("解析返校登记上传策略失败: %w (body: %.200s)", err, string(body))
}
if result.Datas == nil || result.Datas.FileName == "" {
return nil, fmt.Errorf("返校登记上传策略为空(body: %.200s)", string(body))
}
slog.Info("返校登记上传策略已获取",
"host", result.Datas.Host, "formDataCount", len(result.Datas.FormData),
"fileName", result.Datas.FileName)
return result.Datas, nil
}
// GetLeaveReturnPreviewURL 获取返校登记图片的预览 URL。
//
// 与 GetLeaveReturnUploadPolicy 同因:return_register 原先落到 sign-apps 的
// /sign/previewAttachment,路径与模块不匹配会取不到预览。
// 参照 leave-new 的 /stu/obsOperate/obs/previewAttachment。
func GetLeaveReturnPreviewURL(cookieMap map[string]string, host, obsKey string) (string, error) {
url := host + leaveReturnAPIBase + "/stu/obsOperate/obs/previewAttachment"
body, err := leaveReturnPost(cookieMap, url, map[string]interface{}{"obsKey": obsKey})
if err != nil {
return "", fmt.Errorf("获取返校登记预览 URL 失败: %w", err)
}
var result struct {
Datas string `json:"datas"`
}
if err := unmarshalBody(body, &result); err != nil {
// 部分学校把预览地址放在 message 里,做个兜底
var alt struct {
Message string `json:"message"`
}
if json.Unmarshal(body, &alt) == nil && strings.HasPrefix(alt.Message, "http") {
return alt.Message, nil
}
return "", fmt.Errorf("解析返校登记预览 URL 失败: %w (body: %.200s)", err, string(body))
}
if result.Datas == "" {
return "", fmt.Errorf("返校登记预览 URL 为空(body: %.200s)", string(body))
}
return result.Datas, nil
}
@@ -1,37 +0,0 @@
package cpdaily
import (
"strings"
"testing"
)
// 返校登记是独立模块,不能复用签到的图片路径。
// 线上现象:user 19865075523 编辑返校签到表单上传图片报「OBS失败(400)」,
// 根因是 return_register 落到 getImageUrlPrefix 的 default(sign-apps),
// 拿到的上传策略属于错误的桶/签名。
func TestReturnRegisterUsesOwnModule(t *testing.T) {
got := getImageUrlPrefix(TaskTypeReturnRegister)
if got != leaveReturnAPIBase {
t.Fatalf("return_register 前缀应为 %q,实际 %q", leaveReturnAPIBase, got)
}
if strings.Contains(got, "sign-apps") {
t.Fatal("返校登记不应使用 sign-apps 前缀")
}
if !strings.Contains(leaveReturnAPIBase, "leave-return") {
t.Fatalf("leaveReturnAPIBase 异常: %q", leaveReturnAPIBase)
}
}
// 其它类型的既有映射不能被破坏。
func TestImageUrlPrefixOthersUnchanged(t *testing.T) {
cases := map[TaskType]string{
TaskTypeSign: "/wec-counselor-sign-apps/stu",
TaskTypeSleepCheck: "/wec-counselor-attendance-apps/student/attendance",
TaskTypeLeave: "/wec-counselor-leave-apps/leave",
}
for tt, want := range cases {
if got := getImageUrlPrefix(tt); got != want {
t.Fatalf("%s 前缀应为 %q,实际 %q", tt, want, got)
}
}
}