package controllers import ( "testing" "time" "server/models" ) // TestOaAttendPeriodRange 月 / 季 / 年区间换算(含闰年 2 月)。 func TestOaAttendPeriodRange(t *testing.T) { cases := []struct { period, anchor, start, end string }{ {"month", "2026-11-14", "2026-11-01", "2026-11-30"}, {"month", "2024-02-10", "2024-02-01", "2024-02-29"}, {"quarter", "2026-11-14", "2026-10-01", "2026-12-31"}, {"quarter", "2026-01-05", "2026-01-01", "2026-03-31"}, {"year", "2026-07-01", "2026-01-01", "2026-12-31"}, } for _, item := range cases { start, end, label, ok := oaAttendPeriodRange(item.period, item.anchor) if !ok { t.Fatalf("%s %s 解析失败", item.period, item.anchor) } if start != item.start || end != item.end { t.Errorf("%s %s 期望 %s~%s,得到 %s~%s", item.period, item.anchor, item.start, item.end, start, end) } if label == "" { t.Errorf("%s %s 缺少周期标题", item.period, item.anchor) } } if _, _, _, ok := oaAttendPeriodRange("month", "2026-13-01"); ok { t.Error("非法日期应返回 ok=false") } } // TestOaAttendAggregateCards 季度卡与年度卡由月度卡累加得出。 func TestOaAttendAggregateCards(t *testing.T) { months := make([]oaAttendCard, 0, 12) for month := 1; month <= 12; month++ { card := oaAttendCard{Scheduled: 10, Matched: 8, Clocked: 9} oaAttendFillCard(&card) months = append(months, card) } quarters, yearCard := oaAttendAggregateCards(months, 2026) if len(quarters) != 4 { t.Fatalf("期望 4 个季度卡,得到 %d", len(quarters)) } if quarters[0].Scheduled != 30 || quarters[0].Matched != 24 { t.Errorf("Q1 期望 应出勤 30 / 匹配 24,得到 %d / %d", quarters[0].Scheduled, quarters[0].Matched) } if quarters[0].Missing != 6 || quarters[0].AttendanceRate != 80 { t.Errorf("Q1 期望 缺卡 6 / 出勤率 80,得到 %d / %d", quarters[0].Missing, quarters[0].AttendanceRate) } if yearCard.Scheduled != 120 || yearCard.Matched != 96 || yearCard.AttendanceRate != 80 { t.Errorf("年度卡累计异常:%+v", yearCard) } } // TestOaShiftWindow 班次时间窗:普通班次与跨天班次都应换算成分钟区间。 func TestOaShiftWindow(t *testing.T) { start, end, ok := oaShiftWindow("09:00", "18:00") if !ok || start != 540 || end != 1080 { t.Fatalf("09:00-18:00 期望 540-1080,得到 %d-%d ok=%v", start, end, ok) } start, end, ok = oaShiftWindow("22:00", "06:00") if !ok || start != 1320 || end != 1800 { t.Fatalf("22:00-06:00 期望 1320-1800,得到 %d-%d ok=%v", start, end, ok) } if _, _, ok := oaShiftWindow("", "18:00"); ok { t.Error("缺少上班时间时应返回 ok=false") } } // TestOaAttendClockInStatus 签到判定:正常 / 迟到 / 过早(不在时段)/ 无排班。 func TestOaAttendClockInStatus(t *testing.T) { matched := &models.OaAttendance{ WorkStart: "09:00", WorkEnd: "18:00", MatchStatus: models.OaAttendMatchOK, } cases := []struct { now string status int8 }{ {"08:55", models.OaAttendClockNormal}, // 容差内早到 {"09:05", models.OaAttendClockNormal}, // 容差内晚到 {"09:30", models.OaAttendClockLate}, // 迟到 {"07:30", models.OaAttendClockOffTime}, // 提前超过 60 分钟 } for _, item := range cases { now, err := time.ParseInLocation("2006-01-02 15:04", "2026-11-14 "+item.now, time.Local) if err != nil { t.Fatalf("解析时间失败: %v", err) } status, _ := oaAttendClockInStatus(matched, now) if status != item.status { t.Errorf("%s 期望状态 %d,得到 %d", item.now, item.status, status) } } // 未匹配排班:不阻断打卡,状态记为「无排班」 missing := &models.OaAttendance{MatchStatus: models.OaAttendMatchMissing} now := time.Date(2026, 11, 14, 9, 0, 0, 0, time.Local) if status, msg := oaAttendClockInStatus(missing, now); status != models.OaAttendClockNoShift || msg == "" { t.Errorf("未匹配排班期望状态 %d 且有提醒文案,得到 %d %q", models.OaAttendClockNoShift, status, msg) } } // TestOaAttendClockOutStatus 签退判定:早退与跨天班次凌晨签退。 func TestOaAttendClockOutStatus(t *testing.T) { row := &models.OaAttendance{ WorkStart: "09:00", WorkEnd: "18:00", MatchStatus: models.OaAttendMatchOK, } early := time.Date(2026, 11, 14, 17, 0, 0, 0, time.Local) if status, msg := oaAttendClockOutStatus(row, early); status != models.OaAttendClockLate || msg == "" { t.Errorf("17:00 签退期望早退,得到 %d %q", status, msg) } night := &models.OaAttendance{ WorkStart: "22:00", WorkEnd: "06:00", MatchStatus: models.OaAttendMatchOK, } // 跨天班次:次日 06:10 签退应视为正常 nextMorning := time.Date(2026, 11, 15, 6, 10, 0, 0, time.Local) if status, msg := oaAttendClockOutStatus(night, nextMorning); status != models.OaAttendClockNormal || msg != "" { t.Errorf("06:10 跨天签退期望正常,得到 %d %q", status, msg) } } // TestOaAttendDayResult 当日考勤结果口径:缺卡 > 无排班 > 迟到 > 早退 > 时段异常 > 未签退 > 正常。 func TestOaAttendDayResult(t *testing.T) { if _, text := oaAttendDayResult("", 0, "", 0, models.OaAttendMatchMissing); text != "缺卡" { t.Errorf("未签到应为缺卡,得到 %q", text) } if _, text := oaAttendDayResult("09:00", models.OaAttendClockOffTime, "18:00", models.OaAttendClockNormal, models.OaAttendMatchOK); text != "时段异常" { t.Errorf("时段异常期望「时段异常」,得到 %q", text) } if _, text := oaAttendDayResult("09:00", models.OaAttendClockNormal, "18:00", models.OaAttendClockNormal, models.OaAttendMatchOK); text != "正常" { t.Errorf("正常打卡期望「正常」,得到 %q", text) } if _, text := oaAttendDayResult("09:30", models.OaAttendClockLate, "", 0, models.OaAttendMatchOK); text != "迟到" { t.Errorf("迟到期望「迟到」,得到 %q", text) } if _, text := oaAttendDayResult("09:00", models.OaAttendClockNormal, "17:00", models.OaAttendClockLate, models.OaAttendMatchOK); text != "早退" { t.Errorf("早退期望「早退」,得到 %q", text) } if _, text := oaAttendDayResult("09:00", models.OaAttendClockNoShift, "18:00", models.OaAttendClockNoShift, models.OaAttendMatchMissing); text != "无排班" { t.Errorf("无排班打卡期望「无排班」,得到 %q", text) } if _, text := oaAttendDayResult("09:00", models.OaAttendClockNormal, "", 0, models.OaAttendMatchOK); text != "未签退" { t.Errorf("缺签退期望「未签退」,得到 %q", text) } }