销假范围标记改用 leave_detail 精确查询,避免分页上限导致漏拦截

上一版 recordRequiresInArea 拉 50 条列表再线性查找留两条隐患:
记录超过 50 条时找不到 → 误判「不要求」→ 漏掉前端那道拦截。

核对前端源码后改为按 leaveId 精确查询(LeaveDetailView 的 ve()):
  s.listType==="apply" && (d.leaveId = s.data.id, n = await Ft(d))
  c.value = { ...n }
  "cancellation-apply-in-area": c.value?.cancellationApplyInArea === true
                             || c.value?.cancellationApplyInArea === "是"
其中 Ft 即 studentForm 的 he → GET /leave/student_action/leave_detail。

改动:
- 新增 XsgzFetchLeaveDetail(GET leave_detail?leaveId=...,code!=0 报错);
- recordRequiresInArea 改为「详情优先、列表回退、都失败则保守 false」,
  既消除分页隐患,也保留对详情接口异常的容错。

测试:详情接口路径与参数名、bool true / 字符串「是」两种标记解析、
非 0 code 必须报错(不能静默当成空详情)。
This commit is contained in:
becomer
2026-10-17 18:20:28 +08:00
parent ab4f29a474
commit 7c2b9aa3a7
3 changed files with 100 additions and 3 deletions
+11 -3
View File
@@ -265,14 +265,22 @@ func streamCtx() (context.Context, context.CancelFunc) {
// recordRequiresInArea 判断该销假记录是否要求在指定地理范围内申请。
//
// 前端从列表数据取该标记并作为 prop 传给提交弹窗:
// 前端从详情对象取该标记并作为 prop 传给提交弹窗:
//
// "cancellation-apply-in-area": c.value?.cancellationApplyInArea === true
// || c.value?.cancellationApplyInArea === "是"
//
// 后端不复用前端传值(客户端可篡改,且前端可能未传),改为按项目重新查询,
// 取不到时保守返回 false(不额外拦截,交由学校端最终校验)。
// 其中 c.value 来自 leave_detail(listType==="apply")。
// 后端不复用前端传值(客户端可篡改,且前端可能未传),改为按 leaveId 精确重查;
// 详情查不到时回退到列表查找;两者都失败则保守返回 false(不额外拦截,
// 交由学校端最终校验)。
func recordRequiresInArea(ctx context.Context, sess *cpd.XsgzSession, projectID, leaveID string) bool {
if d, err := sess.XsgzFetchLeaveDetail(ctx, leaveID); err != nil {
slog.Warn("xsgz 查询请假详情失败,回退列表查找", "leaveId", leaveID, "error", err)
} else if d != nil {
return d.RequiresInArea()
}
recs, _, err := sess.XsgzFetchLeavePage(ctx, 1, 50)
if err != nil {
slog.Warn("xsgz 查询请假记录失败,跳过销假范围强制校验", "leaveId", leaveID, "error", err)
@@ -180,6 +180,33 @@ func (r XsgzLeaveRecord) RequiresInArea() bool {
}
}
// XsgzFetchLeaveDetail 查询单条请假详情(只读)。
//
// 对应前端 GET /api/leave/student_action/leave_detail?leaveId=...
// (studentForm 的 he/Ft;LeaveDetailView 在 listType==="apply" 时用它填充详情对象,
// 并在其中读取 cancellationApplyInArea 传给销假弹窗)。
//
// 相比拉列表再线性查找,这里按 leaveId 精确查询,不受分页上限影响。
func (s *XsgzSession) XsgzFetchLeaveDetail(ctx context.Context, leaveID string) (*XsgzLeaveRecord, error) {
body, err := s.do(ctx, "GET", "/api/leave/student_action/leave_detail",
map[string]string{"leaveId": leaveID}, nil)
if err != nil {
return nil, err
}
var result struct {
Code int `json:"code"`
Data *XsgzLeaveRecord `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)
}
return result.Data, nil
}
// XsgzFetchLeavePage 查询请假记录列表(只读)。
func (s *XsgzSession) XsgzFetchLeavePage(ctx context.Context, page, limit int) ([]XsgzLeaveRecord, int, error) {
if page <= 0 {
@@ -298,3 +298,65 @@ func TestXsgzSubmitEmptyFieldsOmitted(t *testing.T) {
}
}
}
// TestXsgzFetchLeaveDetailContract 详情接口:路径、参数名、标记解析。
func TestXsgzFetchLeaveDetailContract(t *testing.T) {
var gotPath, gotLeaveID string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotLeaveID = r.URL.Query().Get("leaveId")
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"code":0,"data":{
"id":"apply-1","projectId":"proj-1","projectName":"病假",
"status":"休假中","cancellationApplyInArea":true
}}`))
}))
defer srv.Close()
sess := newXsgzTestSession(t, srv)
d, err := sess.XsgzFetchLeaveDetail(context.Background(), "apply-1")
if err != nil {
t.Fatalf("查询详情失败: %v", err)
}
if gotPath != "/api/leave/student_action/leave_detail" {
t.Fatalf("路径错误: %s", gotPath)
}
// 前端用 params 传递,键名必须是 leaveId
if gotLeaveID != "apply-1" {
t.Fatalf("leaveId 查询参数错误: %q", gotLeaveID)
}
if d == nil || d.ID != "apply-1" || d.ProjectID != "proj-1" {
t.Fatalf("详情解析异常: %+v", d)
}
if !d.RequiresInArea() {
t.Fatal("cancellationApplyInArea=true 应判定为需要范围内申请")
}
}
// TestXsgzFetchLeaveDetailStringYes 该标记也可能是字符串「是」。
func TestXsgzFetchLeaveDetailStringYes(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"code":0,"data":{"id":"a","cancellationApplyInArea":"是"}}`))
}))
defer srv.Close()
d, err := newXsgzTestSession(t, srv).XsgzFetchLeaveDetail(context.Background(), "a")
if err != nil {
t.Fatalf("查询详情失败: %v", err)
}
if !d.RequiresInArea() {
t.Fatal("字符串「是」应判定为需要范围内申请")
}
}
// TestXsgzFetchLeaveDetailError 非 0 code 必须报错,不能当成空详情。
func TestXsgzFetchLeaveDetailError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"code":400,"message":"记录不存在"}`))
}))
defer srv.Close()
if _, err := newXsgzTestSession(t, srv).XsgzFetchLeaveDetail(context.Background(), "x"); err == nil {
t.Fatal("code!=0 应报错")
}
}