销假范围强制校验对齐前端:读 cancellationApplyInArea 决定是否拦截
上一版(355557b)算出 inArea 就上报,但没有判断「该校是否要求在范围内」,
等于把前端的一道拦截丢了。前端源码(LeaveDetailView-BveoXCrD.js):
const v = computed(() => a.actionType==="CANCELAPPLY" && a.cancellationApplyInArea)
if (v.value && !T?.canPosition) { warning("不在销假申请范围内,无法销假"); return }
该标记的来源也一并核实(同文件传入 StudentActionDialog 的 props):
"application-id": c.value?.id || s.data?.id
"project-id": c.value?.project?.id || c.value?.projectId
"cancellation-apply-in-area":
c.value?.cancellationApplyInArea === true
|| c.value?.cancellationApplyInArea === "是"
顺带确认了 originalId 与 projectId 的取值与我们现有实现一致
(ToLeaveItems 的 ID=r.ID、TypeWid=r.ProjectID)。
改动:
- XsgzLeaveRecord 增加 CancellationApplyInArea(用 interface{} 兼容
bool 与 "是" 两种返回),并加 RequiresInArea() 做等价归一化;
- 新增 recordRequiresInArea:后端自行重查记录判定,不复用前端传值
(可篡改、且可能未传);查不到时保守返回 false,不额外拦截;
- 销假流程改为:该校要求范围内且实际不在范围内 → 明确报错并拒绝提交,
与前端一致;未配置范围时 inArea 视为 true。
测试:RequiresInArea 覆盖 bool true/false、"是"/"否"、"true" 字符串、
nil、数字等输入。
This commit is contained in:
@@ -545,29 +545,36 @@ func executeXsgzLeaveCancel(c *fiber.Ctx, user *model.SysUser, req *ExecuteLeave
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 销假地理范围校验(对齐前端 LeaveCancelLocation):
|
// 销假地理范围校验(严格对齐前端 LeaveCancelLocation):
|
||||||
// 仅当该校要求「必须在范围内申请」时才强制,否则只上报实际结果。
|
// v = actionType==="CANCELAPPLY" && cancellationApplyInArea
|
||||||
// 前端:v = actionType==="CANCELAPPLY" && cancellationApplyInArea
|
// if (v && !canPosition) { warning("不在销假申请范围内,无法销假"); return }
|
||||||
// if (v && !canPosition) { 拒绝提交 }
|
// 即:只有该校要求「必须在范围内申请」时才拦截;否则照常提交,
|
||||||
inArea := false
|
// 但仍把真实计算结果放进 inArea 上报给学校端。
|
||||||
|
inArea := true // 未配置范围视为不受限(前端:!r.value.length 时 return true)
|
||||||
if ranges, rErr := sess.XsgzFetchLocationRange(ctx, req.ProjectID); rErr != nil {
|
if ranges, rErr := sess.XsgzFetchLocationRange(ctx, req.ProjectID); rErr != nil {
|
||||||
// 范围查询失败不阻断:可能该校未配置范围,按不限制处理
|
|
||||||
slog.Warn("xsgz 查询销假范围失败,按不限制处理", "userId", user.ID, "error", rErr)
|
slog.Warn("xsgz 查询销假范围失败,按不限制处理", "userId", user.ID, "error", rErr)
|
||||||
inArea = true
|
} else if len(ranges) > 0 {
|
||||||
} else if len(ranges) == 0 {
|
|
||||||
inArea = true // 前端:范围为空时 canPosition 直接为 true
|
|
||||||
} else {
|
|
||||||
lng, lngErr := strconv.ParseFloat(req.Longitude, 64)
|
lng, lngErr := strconv.ParseFloat(req.Longitude, 64)
|
||||||
lat, latErr := strconv.ParseFloat(req.Latitude, 64)
|
lat, latErr := strconv.ParseFloat(req.Latitude, 64)
|
||||||
if lngErr == nil && latErr == nil {
|
if lngErr != nil || latErr != nil {
|
||||||
inArea = cpd.InLocationRange(ranges, lng, lat)
|
slog.Warn("xsgz 销假坐标不可解析,跳过范围判断",
|
||||||
} else {
|
|
||||||
// 无坐标无法判断,交由学校端校验
|
|
||||||
slog.Warn("xsgz 销假缺少有效坐标,跳过范围判断",
|
|
||||||
"userId", user.ID, "lng", req.Longitude, "lat", req.Latitude)
|
"userId", user.ID, "lng", req.Longitude, "lat", req.Latitude)
|
||||||
|
} else {
|
||||||
|
inArea = cpd.InLocationRange(ranges, lng, lat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 该校若要求必须在范围内,且实际不在范围内,则与前端一致地拒绝提交
|
||||||
|
requiresInArea := recordRequiresInArea(ctx, sess, req.ProjectID, req.LeaveID)
|
||||||
|
if requiresInArea && !inArea {
|
||||||
|
msg := "当前位置不在学校要求的销假范围内,无法提交销假,请到校后再试"
|
||||||
|
slog.Warn("xsgz 销假位置不在许可范围内,拒绝提交",
|
||||||
|
"userId", user.ID, "leaveId", req.LeaveID, "inArea", inArea)
|
||||||
|
saveLog(false, msg)
|
||||||
|
sendEvent("task_error", map[string]interface{}{"status": "error", "message": msg})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
body, err := sess.XsgzSubmitCancel(ctx, cpd.XsgzSubmitPayload{
|
body, err := sess.XsgzSubmitCancel(ctx, cpd.XsgzSubmitPayload{
|
||||||
ProjectID: req.ProjectID,
|
ProjectID: req.ProjectID,
|
||||||
OriginalID: req.LeaveID,
|
OriginalID: req.LeaveID,
|
||||||
|
|||||||
@@ -262,3 +262,26 @@ func leaveNewApproversToLeaveApprovers(list []cpd.LeaveNewApprover) []cpd.LeaveA
|
|||||||
func streamCtx() (context.Context, context.CancelFunc) {
|
func streamCtx() (context.Context, context.CancelFunc) {
|
||||||
return context.WithTimeout(context.Background(), 3*time.Minute)
|
return context.WithTimeout(context.Background(), 3*time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordRequiresInArea 判断该销假记录是否要求在指定地理范围内申请。
|
||||||
|
//
|
||||||
|
// 前端从列表数据取该标记并作为 prop 传给提交弹窗:
|
||||||
|
//
|
||||||
|
// "cancellation-apply-in-area": c.value?.cancellationApplyInArea === true
|
||||||
|
// || c.value?.cancellationApplyInArea === "是"
|
||||||
|
//
|
||||||
|
// 后端不复用前端传值(客户端可篡改,且前端可能未传),改为按项目重新查询,
|
||||||
|
// 取不到时保守返回 false(不额外拦截,交由学校端最终校验)。
|
||||||
|
func recordRequiresInArea(ctx context.Context, sess *cpd.XsgzSession, projectID, leaveID string) bool {
|
||||||
|
recs, _, err := sess.XsgzFetchLeavePage(ctx, 1, 50)
|
||||||
|
if err != nil {
|
||||||
|
slog.Warn("xsgz 查询请假记录失败,跳过销假范围强制校验", "leaveId", leaveID, "error", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, r := range recs {
|
||||||
|
if r.ID == leaveID {
|
||||||
|
return r.RequiresInArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -160,6 +160,24 @@ type XsgzLeaveRecord struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Approved bool `json:"approved"`
|
Approved bool `json:"approved"`
|
||||||
Cancelled bool `json:"cancelled"`
|
Cancelled bool `json:"cancelled"`
|
||||||
|
// CancellationApplyInArea 该校/该项目是否要求销假必须在范围内申请。
|
||||||
|
// 前端按 `=== true || === "是"` 归一化(见 LeaveDetailView),
|
||||||
|
// 因此这里用 interface{} 兼容 bool 与字符串两种返回。
|
||||||
|
CancellationApplyInArea interface{} `json:"cancellationApplyInArea"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiresInArea 归一化 cancellationApplyInArea,等价于前端:
|
||||||
|
//
|
||||||
|
// c.value?.cancellationApplyInArea === true || c.value?.cancellationApplyInArea === "是"
|
||||||
|
func (r XsgzLeaveRecord) RequiresInArea() bool {
|
||||||
|
switch v := r.CancellationApplyInArea.(type) {
|
||||||
|
case bool:
|
||||||
|
return v
|
||||||
|
case string:
|
||||||
|
return v == "是"
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// XsgzFetchLeavePage 查询请假记录列表(只读)。
|
// XsgzFetchLeavePage 查询请假记录列表(只读)。
|
||||||
|
|||||||
@@ -131,3 +131,26 @@ func TestPointInPolygonDegenerate(t *testing.T) {
|
|||||||
t.Fatal("两点不应命中")
|
t.Fatal("两点不应命中")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestXsgzLeaveRecordRequiresInArea 归一化前端 `=== true || === "是"` 的语义。
|
||||||
|
func TestXsgzLeaveRecordRequiresInArea(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
val interface{}
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"布尔 true", true, true},
|
||||||
|
{"布尔 false", false, false},
|
||||||
|
{"字符串 是", "是", true},
|
||||||
|
{"字符串 否", "否", false},
|
||||||
|
{"字符串 true", "true", false},
|
||||||
|
{"nil", nil, false},
|
||||||
|
{"数字", float64(1), false},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
rec := XsgzLeaveRecord{CancellationApplyInArea: c.val}
|
||||||
|
if got := rec.RequiresInArea(); got != c.want {
|
||||||
|
t.Fatalf("%s: RequiresInArea()=%v, want %v", c.name, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user