154670742c
此前完全没有时间段判断:用户在非打卡时间点执行,会把注定被学校端 拒绝的请求发出去,白扣次数且报错含义不清。 用真实用户(学号 2364400108)拉 detailSignInstance 确认字段: rateSignDate = "2026-10-25 (周日)" rateTaskBeginTime = "18:30" rateTaskEndTime = "23:00" singleTaskBeginTime/EndTime 单次任务的时间段 currentTime = "2026-10-19 11:31:21" 该任务实际打卡时段是 10-25 18:30~23:00,而当时是 10-19,明显时段外。 新增(internal/service/cpdaily/task.go): - SignWindow + ParseSignWindow:解析上述字段,singleTask* 存在时优先用它; 解析失败一律 Parsed=false,调用方放行,避免因时间格式差异误杀 - CheckSignWindow:明确在窗口外才返回错误;起止时刻本身算在窗口内 - ErrOutsideSignWindow + signWindowError:可 errors.Is 识别, 但 Error() 只输出用户可读文案,不暴露内部标记 接入 handler(执行打卡流程):取到详情后、填充表单前校验; 拦截时写任务状态为 fail、记录日志、发 task_error 事件并带 outsideWindow 标记, 不扣减次数。 测试用实测真实字段值:解析正确性、时段外拦截、时段内放行、 边界(开始/结束时刻放行、结束后一分钟拦截)、信息缺失不拦截、 singleTask* 优先级。
117 lines
3.5 KiB
Go
117 lines
3.5 KiB
Go
package cpdaily
|
||
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// 真实的 detailSignInstance rows 片段(学号 2364400108 实测)。
|
||
func realRows() map[string]interface{} {
|
||
return map[string]interface{}{
|
||
"taskName": "2026(1)节假日返校签到",
|
||
"rateSignDate": "2026-10-25 (周日)",
|
||
"rateTaskBeginTime": "18:30",
|
||
"rateTaskEndTime": "23:00",
|
||
"currentTime": "2026-10-19 11:31:21",
|
||
"signStatus": "2",
|
||
"taskType": float64(6),
|
||
}
|
||
}
|
||
|
||
func TestParseSignWindowReal(t *testing.T) {
|
||
w := ParseSignWindow(realRows())
|
||
if !w.Parsed {
|
||
t.Fatal("应能解析出时间段")
|
||
}
|
||
if w.Start.IsZero() || w.End.IsZero() {
|
||
t.Fatal("起止时间不应为零值")
|
||
}
|
||
t.Logf("窗口: %s ~ %s (date=%s)", w.Start, w.End, w.Date)
|
||
|
||
wantStart := time.Date(2026, 10, 25, 18, 30, 0, 0, time.Local)
|
||
wantEnd := time.Date(2026, 10, 25, 23, 0, 0, 0, time.Local)
|
||
if !w.Start.Equal(wantStart) {
|
||
t.Fatalf("开始时间错: got %s want %s", w.Start, wantStart)
|
||
}
|
||
if !w.End.Equal(wantEnd) {
|
||
t.Fatalf("结束时间错: got %s want %s", w.End, wantEnd)
|
||
}
|
||
}
|
||
|
||
func TestCheckSignWindowReal(t *testing.T) {
|
||
rows := realRows()
|
||
|
||
// 实测当时时间:10-19 11:31,早于 10-25 18:30 → 应拦截
|
||
now := time.Date(2026, 10, 19, 11, 31, 21, 0, time.Local)
|
||
err := CheckSignWindow(rows, now)
|
||
if err == nil {
|
||
t.Fatal("不在时间段内应返回错误")
|
||
}
|
||
if !errors.Is(err, ErrOutsideSignWindow) {
|
||
t.Fatalf("错误应可用 errors.Is 识别,实际: %v", err)
|
||
}
|
||
t.Logf("拦截提示: %v", err)
|
||
|
||
// 时段内:应放行
|
||
inWin := time.Date(2026, 10, 25, 20, 0, 0, 0, time.Local)
|
||
if err := CheckSignWindow(rows, inWin); err != nil {
|
||
t.Fatalf("时段内不应拦截: %v", err)
|
||
}
|
||
|
||
// 边界:开始时刻与结束时刻本身应算在窗口内
|
||
if err := CheckSignWindow(rows, wantTime(2026, 10, 25, 18, 30)); err != nil {
|
||
t.Fatalf("开始时刻应放行: %v", err)
|
||
}
|
||
if err := CheckSignWindow(rows, wantTime(2026, 10, 25, 23, 0)); err != nil {
|
||
t.Fatalf("结束时刻应放行: %v", err)
|
||
}
|
||
// 刚过结束一分钟:应拦截
|
||
if err := CheckSignWindow(rows, wantTime(2026, 10, 25, 23, 1)); err == nil {
|
||
t.Fatal("超时后应拦截")
|
||
}
|
||
}
|
||
|
||
func wantTime(y int, m time.Month, d, hh, mm int) time.Time {
|
||
return time.Date(y, m, d, hh, mm, 0, 0, time.Local)
|
||
}
|
||
|
||
// 时间信息缺失时不应拦截,避免误杀。
|
||
func TestCheckSignWindowNoInfo(t *testing.T) {
|
||
cases := []map[string]interface{}{
|
||
nil,
|
||
{},
|
||
{"taskName": "x"},
|
||
{"rateSignDate": "2026-10-25 (周日)"}, // 缺 begin/end
|
||
{"rateTaskBeginTime": "18:30", "rateTaskEndTime": "23:00"}, // 缺日期
|
||
{"rateSignDate": "bad-date", "rateTaskBeginTime": "x", "rateTaskEndTime": "y"},
|
||
}
|
||
for i, rows := range cases {
|
||
if err := CheckSignWindow(rows, time.Now()); err != nil {
|
||
t.Fatalf("case %d 信息不足时不应拦截: %v", i, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// singleTask* 存在时优先使用(单次任务精确区间)。
|
||
func TestParseSignWindowSingleTaskPriority(t *testing.T) {
|
||
rows := map[string]interface{}{
|
||
"rateSignDate": "2026-10-25 (周日)",
|
||
"rateTaskBeginTime": "18:30",
|
||
"rateTaskEndTime": "23:00",
|
||
"singleTaskBeginTime": "2026-10-19 08:00:00",
|
||
"singleTaskEndTime": "2026-10-19 12:00:00",
|
||
}
|
||
w := ParseSignWindow(rows)
|
||
if !w.Parsed {
|
||
t.Fatal("应解析成功")
|
||
}
|
||
if w.Start.Hour() != 8 || w.Start.Day() != 19 {
|
||
t.Fatalf("应优先使用 singleTaskBeginTime,实际 %s", w.Start)
|
||
}
|
||
// 在该区间内应放行
|
||
if err := CheckSignWindow(rows, wantTime(2026, 10, 19, 10, 0)); err != nil {
|
||
t.Fatalf("单次任务区间内应放行: %v", err)
|
||
}
|
||
}
|