销假范围标记改用 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:
@@ -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 应报错")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user