package main import ( "fmt" "strconv" "strings" "time" ) // 5 段 cron:分 时 日 月 周。支持 * 、数字、范围 a-b、步长 */n 与 a-b/n、逗号列表。 // 日(day-of-month) 与 周(day-of-week) 均受限时采用标准 cron 的「或」语义。 type cronSpec struct { min, hour, dom, mon, dow map[int]bool domAll, dowAll bool } func parseCron(spec string) (*cronSpec, error) { fields := strings.Fields(spec) if len(fields) != 5 { return nil, fmt.Errorf("cron 需要 5 个字段(分 时 日 月 周),收到 %d 个", len(fields)) } c := &cronSpec{} var err error if c.min, err = parseCronField(fields[0], 0, 59); err != nil { return nil, fmt.Errorf("分钟字段: %w", err) } if c.hour, err = parseCronField(fields[1], 0, 23); err != nil { return nil, fmt.Errorf("小时字段: %w", err) } if c.dom, err = parseCronField(fields[2], 1, 31); err != nil { return nil, fmt.Errorf("日字段: %w", err) } if c.mon, err = parseCronField(fields[3], 1, 12); err != nil { return nil, fmt.Errorf("月字段: %w", err) } if c.dow, err = parseCronField(fields[4], 0, 7); err != nil { return nil, fmt.Errorf("星期字段: %w", err) } if c.dow[7] { c.dow[0] = true } c.domAll = len(c.dom) == 31 c.dowAll = len(c.dow) == 8 return c, nil } func parseCronField(f string, min, max int) (map[int]bool, error) { set := map[int]bool{} for _, part := range strings.Split(f, ",") { part = strings.TrimSpace(part) if part == "" { return nil, fmt.Errorf("存在空字段") } step := 1 if i := strings.Index(part, "/"); i >= 0 { n, err := strconv.Atoi(part[i+1:]) if err != nil || n <= 0 { return nil, fmt.Errorf("无效步长 %q", part) } step = n part = part[:i] if part == "" { part = "*" } } lo, hi := 0, 0 if part == "*" { lo, hi = min, max } else if rng := strings.SplitN(part, "-", 2); len(rng) == 2 { var err error if lo, err = strconv.Atoi(rng[0]); err != nil { return nil, fmt.Errorf("无效值 %q", part) } if hi, err = strconv.Atoi(rng[1]); err != nil { return nil, fmt.Errorf("无效值 %q", part) } } else { v, err := strconv.Atoi(part) if err != nil { return nil, fmt.Errorf("无效值 %q", part) } lo, hi = v, v } if lo < min || hi > max || lo > hi { return nil, fmt.Errorf("值超出范围 [%d,%d]: %q", min, max, part) } for v := lo; v <= hi; v += step { set[v] = true } } return set, nil } // cronNextString 返回 cron 表达式相对当前时间的下次触发时间(供配置表单实时预览) func cronNextString(expr string) (string, error) { spec, err := parseCron(expr) if err != nil { return "", err } return cronNext(spec, time.Now()).Format("2006-01-02 15:04"), nil } // cronNext 返回 spec 在 after 之后的下一次触发时间(分钟精度,最多向前搜索一年) func cronNext(spec *cronSpec, after time.Time) time.Time { t := after.Truncate(time.Minute).Add(time.Minute) limit := t.AddDate(1, 0, 0) for t.Before(limit) { if !spec.mon[int(t.Month())] { t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()).AddDate(0, 1, 0) continue } domOK := spec.dom[t.Day()] dowOK := spec.dow[int(t.Weekday())] dayOK := domOK && dowOK if !spec.domAll && !spec.dowAll { dayOK = domOK || dowOK } if !dayOK { t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()).AddDate(0, 0, 1) continue } if !spec.hour[t.Hour()] { t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location()).Add(time.Hour) continue } if spec.min[t.Minute()] { return t } t = t.Add(time.Minute) } return limit }