Files
cpdaily_xiaozhushou/backed/internal/service/cpdaily/leave_xsgz_locus_test.go
T
becomer ab4f29a474 销假范围强制校验对齐前端:读 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、数字等输入。
2026-10-17 18:19:27 +08:00

157 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package cpdaily
import "testing"
// 销假地理范围判断,对齐前端 LeaveCancelLocation 的 c():
// Circle/poi 比球面距离,Polygon/Rectangle 用点在多边形内。
type pt = struct {
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
}
// TestInLocationRangeEmpty 范围为空时不限制(前端:!r.value.length 时 return true)。
func TestInLocationRangeEmpty(t *testing.T) {
if !InLocationRange(nil, 117.12, 36.19) {
t.Fatal("范围为空应视为不限制")
}
if !InLocationRange([]XsgzLocationRange{}, 117.12, 36.19) {
t.Fatal("空切片应视为不限制")
}
}
// TestInLocationRangeCircle 圆形范围:半径内 true,半径外 false。
func TestInLocationRangeCircle(t *testing.T) {
// 山东农业大学附近,半径 500 米
center := XsgzLocationRange{
CompatibleType: "Circle",
Longitude: 117.12,
Latitude: 36.19,
Radius: 500,
}
// 圆心
if !InLocationRange([]XsgzLocationRange{center}, 117.12, 36.19) {
t.Fatal("圆心应在范围内")
}
// 约 111 米外(纬度 0.001 度 ≈ 111 米)
if !InLocationRange([]XsgzLocationRange{center}, 117.12, 36.191) {
t.Fatal("111 米外应在半径内")
}
// 约 1.1 公里外(0.01 度 ≈ 1111 米),应超出
if InLocationRange([]XsgzLocationRange{center}, 117.12, 36.20) {
t.Fatal("1.1 公里外应超出 500 米半径")
}
}
// TestInLocationRangePoi poi 类型按圆形处理。
func TestInLocationRangePoi(t *testing.T) {
r := XsgzLocationRange{
CompatibleType: "poi",
Longitude: 117.12,
Latitude: 36.19,
Radius: 200,
}
if !InLocationRange([]XsgzLocationRange{r}, 117.12, 36.19) {
t.Fatal("poi 中心应在范围内")
}
if InLocationRange([]XsgzLocationRange{r}, 117.12, 36.25) {
t.Fatal("远处应在范围外")
}
}
// TestInLocationRangePolygon 多边形:内部 true,外部 false。
func TestInLocationRangePolygon(t *testing.T) {
// 一个经纬度平面上的正方形,覆盖 117.10~117.14 × 36.17~36.21
poly := XsgzLocationRange{
CompatibleType: "Polygon",
Points: []pt{
{Longitude: 117.10, Latitude: 36.17},
{Longitude: 117.14, Latitude: 36.17},
{Longitude: 117.14, Latitude: 36.21},
{Longitude: 117.10, Latitude: 36.21},
},
}
ranges := []XsgzLocationRange{poly}
if !InLocationRange(ranges, 117.12, 36.19) {
t.Fatal("中心点应在多边形内")
}
if InLocationRange(ranges, 117.20, 36.19) {
t.Fatal("右侧外部点不应在多边形内")
}
if InLocationRange(ranges, 117.12, 36.30) {
t.Fatal("上方外部点不应在多边形内")
}
}
// TestInLocationRangeRectangle 矩形按多边形处理。
func TestInLocationRangeRectangle(t *testing.T) {
rect := XsgzLocationRange{
CompatibleType: "Rectangle",
Points: []pt{
{Longitude: 117.10, Latitude: 36.17},
{Longitude: 117.14, Latitude: 36.17},
{Longitude: 117.14, Latitude: 36.21},
{Longitude: 117.10, Latitude: 36.21},
},
}
if !InLocationRange([]XsgzLocationRange{rect}, 117.13, 36.18) {
t.Fatal("矩形内点应命中")
}
if InLocationRange([]XsgzLocationRange{rect}, 117.15, 36.18) {
t.Fatal("矩形外点不应命中")
}
}
// TestInLocationRangeMultiple 任一范围命中即可。
func TestInLocationRangeMultiple(t *testing.T) {
ranges := []XsgzLocationRange{
{CompatibleType: "Circle", Longitude: 117.12, Latitude: 36.19, Radius: 100},
{CompatibleType: "Circle", Longitude: 117.50, Latitude: 36.50, Radius: 100},
}
if !InLocationRange(ranges, 117.50, 36.50) {
t.Fatal("第二个圆应命中")
}
if InLocationRange(ranges, 118.00, 37.00) {
t.Fatal("都不应命中")
}
}
// TestPointInPolygonDegenerate 少于 3 个点时不构成多边形。
func TestPointInPolygonDegenerate(t *testing.T) {
if pointInPolygon(0, 0, nil) {
t.Fatal("空点集不应命中")
}
one := []pt{{Longitude: 0, Latitude: 0}}
if pointInPolygon(0, 0, one) {
t.Fatal("单点不应命中")
}
two := []pt{{Longitude: 0, Latitude: 0}, {Longitude: 1, Latitude: 1}}
if pointInPolygon(0.5, 0.5, two) {
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)
}
}
}