diff --git a/backed/internal/service/cpdaily/leave_xsgz.go b/backed/internal/service/cpdaily/leave_xsgz.go index 8604e17..2fdbe25 100644 --- a/backed/internal/service/cpdaily/leave_xsgz.go +++ b/backed/internal/service/cpdaily/leave_xsgz.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "encoding/json" "fmt" + "log/slog" "math/big" "net/url" "regexp" @@ -110,13 +111,84 @@ func XsgzLogin(ctx context.Context, schoolID, casTicket string) (*XsgzSession, e return nil, fmt.Errorf("登录失败: %s", msg) } - return &XsgzSession{ + sess := &XsgzSession{ Token: result.Data.AccessToken, RoleID: result.Data.UserInfo.RoleId, RoleName: result.Data.UserInfo.RoleName, RealName: result.Data.UserInfo.RealName, baseURL: base, client: client, + } + + // roleId 不能只依赖 cas/callback 的响应。前端把两者分开处理: + // getOAuthToken: 只取 data.data.access_token(不管 roleId) + // getUserInfo: POST /auth/user → data.role.roleId → setCurrentRole + // 再由 axios 拦截器把 roleId 塞进每个请求头。 + // 若此处继续只读 cas 响应的 userInfo,一旦该校不返回该嵌套结构, + // RoleID 就会为空,所有后续请求都缺 roleId 头。 + // 因此这里主动补一次 /auth/user,拿不到也不阻断(保持向后兼容)。 + if info, iErr := sess.fetchUserRole(ctx); iErr != nil { + slog.Warn("xsgz 拉取用户角色失败,沿用 cas 响应中的 roleId", + "roleId", sess.RoleID, "error", iErr) + } else if info != nil { + if info.RoleID != "" { + sess.RoleID = info.RoleID + } + if info.RoleName != "" { + sess.RoleName = info.RoleName + } + if info.RealName != "" { + sess.RealName = info.RealName + } + } + + if sess.RoleID == "" { + return nil, fmt.Errorf("未能获取角色信息(roleId 为空),请刷新页面后重试") + } + return sess, nil +} + +// xsgzUserInfo /auth/user 返回的角色信息(前端 getUserInfo)。 +type xsgzUserInfo struct { + RoleID string + RoleName string + RealName string +} + +// fetchUserRole 对应前端 GET/POST /api/auth/user,取 data.role.roleId。 +// +// 前端因此把 roleId 存在 sessionStorage 并注入每个请求头。 +func (s *XsgzSession) fetchUserRole(ctx context.Context) (*xsgzUserInfo, error) { + body, err := s.do(ctx, "POST", "/api/auth/user", nil, nil) + if err != nil { + return nil, err + } + var result struct { + Code int `json:"code"` + Data struct { + Role struct { + RoleID string `json:"roleId"` + RoleName string `json:"roleName"` + } `json:"role"` + RealName string `json:"realName"` + Xm string `json:"xm"` + } `json:"data"` + Message string `json:"message"` + } + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("解析用户信息失败: %w", err) + } + if result.Code != 0 { + return nil, fmt.Errorf("获取用户信息失败: %s", result.Message) + } + name := result.Data.RealName + if name == "" { + name = result.Data.Xm + } + return &xsgzUserInfo{ + RoleID: result.Data.Role.RoleID, + RoleName: result.Data.Role.RoleName, + RealName: name, }, nil } diff --git a/backed/internal/service/cpdaily/leave_xsgz_submit_test.go b/backed/internal/service/cpdaily/leave_xsgz_submit_test.go index 0a7b913..a073c23 100644 --- a/backed/internal/service/cpdaily/leave_xsgz_submit_test.go +++ b/backed/internal/service/cpdaily/leave_xsgz_submit_test.go @@ -360,3 +360,64 @@ func TestXsgzFetchLeaveDetailError(t *testing.T) { t.Fatal("code!=0 应报错") } } + +// TestXsgzFetchUserRoleContract /auth/user 取 data.role.roleId。 +// +// 前端不依赖 cas/callback 返回 roleId: +// +// getOAuthToken 只存 access_token +// getUserInfo → POST /auth/user → data.role.roleId → 注入请求头 +func TestXsgzFetchUserRoleContract(t *testing.T) { + var gotPath, gotMethod string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gotPath, gotMethod = r.URL.Path, r.Method + _, _ = w.Write([]byte(`{"code":0,"data":{ + "realName":"张三", + "role":{"roleId":"role-777","roleName":"学生"} + }}`)) + })) + defer srv.Close() + + sess := newXsgzTestSession(t, srv) + info, err := sess.fetchUserRole(context.Background()) + if err != nil { + t.Fatalf("获取用户角色失败: %v", err) + } + if gotPath != "/api/auth/user" { + t.Fatalf("路径错误: %s", gotPath) + } + if gotMethod != http.MethodPost { + t.Fatalf("方法应为 POST,实际 %s", gotMethod) + } + if info.RoleID != "role-777" || info.RoleName != "学生" || info.RealName != "张三" { + t.Fatalf("解析异常: %+v", info) + } +} + +// TestXsgzFetchUserRoleFallbackXm realName 缺失时回退 xm。 +func TestXsgzFetchUserRoleFallbackXm(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"code":0,"data":{"xm":"李四","role":{"roleId":"r1"}}}`)) + })) + defer srv.Close() + + info, err := newXsgzTestSession(t, srv).fetchUserRole(context.Background()) + if err != nil { + t.Fatalf("失败: %v", err) + } + if info.RealName != "李四" { + t.Fatalf("应回退 xm,实际 %q", info.RealName) + } +} + +// TestXsgzFetchUserRoleError 非 0 code 必须报错。 +func TestXsgzFetchUserRoleError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"code":401,"message":"未登录"}`)) + })) + defer srv.Close() + + if _, err := newXsgzTestSession(t, srv).fetchUserRole(context.Background()); err == nil { + t.Fatal("code!=0 应报错") + } +}