7cbbbff3ea
ci/woodpecker/tag/build Pipeline was successful
从线上签到 H5 拿到源码(counselor-front.campusphere.net/...
wec-counselor-sign-apps/prod/prod_v2.1.1.1357_4DnD/stu/mobile/js/app.js),
按其中实现重写判定,不再依赖推测。
H5 关键实现:
xt = { sign: 1, noSign: 2, leave: 5 } // signStatus 枚举
Rt = { noStart, running, finished } // 任务阶段
$t(): isLoop 时把 signDate.substring(0,10) 与时刻拼接,否则用时刻本身
Gt(): current > end → finished
current <= begin → noStart
否则 → running
handlerButtonClick: if (signed && !isAllowUpdate) toast("更新签到开关已关闭")
updateSign: !updateLimit || leftNum 才允许提交,否则提示"已超出最大更新次数"
对补签的结论:签到 H5 全量 JS 中不存在 supply / 补签 字样。
所谓"补签"在 H5 里就是"更新签到"——已签到后再次提交,
受 isAllowUpdate 与 updateLimit/leftNum 控制。
查寝详情另有 supplyTime 字段(实测为空串),但签到 H5 未使用它。
改动:
- SignWindow/ParseSignWindow:singleTask*(查寝,完整日期时间)优先,
否则用 rateSignDate + rateTaskBeginTime/EndTime(sign、return_register);
解析失败一律 Parsed=false 放行,避免时间格式差异误杀
- EvalTaskPhase:复现 Gt(),含"等于开始时刻算 noStart"的边界
- SignUpdateBlocked:已签到且 isAllowUpdate=false → 拦截;
updateLimit=true 且 leftNum<=0 → 拦截(次数用尽)
- handler 在执行前先查"已签到不允许更新",再查时间段,
两者拦截时都写 fail 状态、记日志、发 task_error 事件且不扣次数
- 时间字段实测:sign/return_register 用 rateSignDate+时刻,
sleep_check 用 singleTaskBeginTime/EndTime 完整时间
测试覆盖:两种时间格式解析、时段内/外/边界、信息缺失不拦截、
signStatus 枚举各值、次数限制、EvalTaskPhase 三态与边界。
250 lines
7.8 KiB
Go
250 lines
7.8 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)
|
||
}
|
||
}
|
||
|
||
// 查寝(sleep_check)实测字段:singleTask* 是完整日期时间。
|
||
func sleepCheckRows() map[string]interface{} {
|
||
return map[string]interface{}{
|
||
"rateSignDate": "2026-10-19 (周一)",
|
||
"rateTaskBeginTime": "22:30",
|
||
"rateTaskEndTime": "23:30",
|
||
"singleTaskBeginTime": "2026-10-19 22:30",
|
||
"singleTaskEndTime": "2026-10-19 23:30",
|
||
"iotEndTime": "23:30",
|
||
"supplyTime": "",
|
||
"signStatus": "0",
|
||
"currentTime": "2026-10-19 11:37:32",
|
||
}
|
||
}
|
||
|
||
func TestParseSignWindowSleepCheck(t *testing.T) {
|
||
w := ParseSignWindow(sleepCheckRows())
|
||
if !w.Parsed {
|
||
t.Fatal("查寝应能解析出时间段")
|
||
}
|
||
wantStart := wantTime(2026, 10, 19, 22, 30)
|
||
wantEnd := wantTime(2026, 10, 19, 23, 30)
|
||
if !w.Start.Equal(wantStart) || !w.End.Equal(wantEnd) {
|
||
t.Fatalf("解析错: %s ~ %s", w.Start, w.End)
|
||
}
|
||
// 当日上午 → 应拦截
|
||
if err := CheckSignWindow(sleepCheckRows(), wantTime(2026, 10, 19, 11, 37)); err == nil {
|
||
t.Fatal("上午应拦截")
|
||
}
|
||
// 时段内 → 放行
|
||
if err := CheckSignWindow(sleepCheckRows(), wantTime(2026, 10, 19, 23, 0)); err != nil {
|
||
t.Fatalf("时段内应放行: %v", err)
|
||
}
|
||
}
|
||
|
||
// return_register 与 sign 共用 rateSignDate + rateTaskBeginTime 组合格式。
|
||
func TestParseSignWindowReturnRegister(t *testing.T) {
|
||
rows := map[string]interface{}{
|
||
"rateSignDate": "2026-10-25 (周日)",
|
||
"rateTaskBeginTime": "18:30",
|
||
"rateTaskEndTime": "23:00",
|
||
"signStatus": "2",
|
||
"singleTaskBeginTime": nil,
|
||
"singleTaskEndTime": nil,
|
||
}
|
||
w := ParseSignWindow(rows)
|
||
if !w.Parsed {
|
||
t.Fatal("返校签到应能解析")
|
||
}
|
||
if !w.Start.Equal(wantTime(2026, 10, 25, 18, 30)) {
|
||
t.Fatalf("解析错: %s", w.Start)
|
||
}
|
||
}
|
||
|
||
// signStatus 枚举实测自 H5:xt = { sign:1, noSign:2, leave:5 }
|
||
func TestSignUpdateBlocked(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
rows map[string]interface{}
|
||
blocked bool
|
||
}{
|
||
{
|
||
name: "未签到(signStatus=2) → 不拦截",
|
||
rows: map[string]interface{}{"signStatus": "2", "isAllowUpdate": false},
|
||
blocked: false,
|
||
},
|
||
{
|
||
name: "已签到且禁止更新 → 拦截",
|
||
rows: map[string]interface{}{"signStatus": "1", "isAllowUpdate": false},
|
||
blocked: true,
|
||
},
|
||
{
|
||
name: "已签到但允许更新 → 不拦截",
|
||
rows: map[string]interface{}{"signStatus": "1", "isAllowUpdate": true},
|
||
blocked: false,
|
||
},
|
||
{
|
||
name: "已签到、无 isAllowUpdate 字段 → 不拦截(保守)",
|
||
rows: map[string]interface{}{"signStatus": float64(1)},
|
||
blocked: false,
|
||
},
|
||
{
|
||
name: "已签到、有次数限制但还剩 → 不拦截",
|
||
rows: map[string]interface{}{"signStatus": "1", "isAllowUpdate": true, "updateLimit": true, "leftNum": 2},
|
||
blocked: false,
|
||
},
|
||
{
|
||
name: "已签到、次数用尽 → 拦截",
|
||
rows: map[string]interface{}{"signStatus": "1", "isAllowUpdate": true, "updateLimit": true, "leftNum": 0},
|
||
blocked: true,
|
||
},
|
||
{
|
||
name: "请假(signStatus=5) → 不拦截",
|
||
rows: map[string]interface{}{"signStatus": "5"},
|
||
blocked: false,
|
||
},
|
||
{
|
||
name: "nil rows → 不拦截",
|
||
rows: nil,
|
||
blocked: false,
|
||
},
|
||
}
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
msg, blocked := SignUpdateBlocked(c.rows)
|
||
if blocked != c.blocked {
|
||
t.Fatalf("blocked=%v want %v (msg=%q)", blocked, c.blocked, msg)
|
||
}
|
||
if blocked && msg == "" {
|
||
t.Fatal("拦截时应给出提示文案")
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// EvalTaskPhase 复现 H5 的 Gt():>end → finished,<=begin → noStart,否则 running。
|
||
func TestEvalTaskPhase(t *testing.T) {
|
||
w := ParseSignWindow(realRows())
|
||
if got := EvalTaskPhase(w, wantTime(2026, 10, 19, 11, 0)); got != TaskPhaseNoStart {
|
||
t.Fatalf("应为 noStart,实际 %q", got)
|
||
}
|
||
if got := EvalTaskPhase(w, wantTime(2026, 10, 25, 20, 0)); got != TaskPhaseRunning {
|
||
t.Fatalf("应为 running,实际 %q", got)
|
||
}
|
||
if got := EvalTaskPhase(w, wantTime(2026, 10, 26, 1, 0)); got != TaskPhaseFinished {
|
||
t.Fatalf("应为 finished,实际 %q", got)
|
||
}
|
||
// 边界:等于开始时 → noStart(H5 用 isSameOrBefore)
|
||
if got := EvalTaskPhase(w, wantTime(2026, 10, 25, 18, 30)); got != TaskPhaseNoStart {
|
||
t.Fatalf("等于开始时刻应为 noStart(H5 isSameOrBefore),实际 %q", got)
|
||
}
|
||
}
|