package timerx import ( "errors" "time" "github.com/robfig/cron/v3" ) // 计算该任务下次执行时间 // @param job *JobData 任务数据 // @param t time.Time 当前时间 // @return time.Time 下次执行时间 // @return error 错误信息 func GetNextTime(t time.Time, job JobData) (*time.Time, error) { if err := validateJobData(job); err != nil { return nil, err } var next *time.Time var err error switch job.JobType { case JobTypeEveryMonth: next, err = calculateNextMonthTime(t, job) case JobTypeEveryWeek: next, err = calculateNextWeekTime(t, job) case JobTypeEveryDay: next, err = calculateNextDayTime(t, job) case JobTypeEveryHour: next, err = calculateNextHourTime(t, job) case JobTypeEveryMinute: next, err = calculateNextMinuteTime(t, job) case JobTypeInterval: next, err = calculateNextInterval(t, job) case JobTypeCron: next, err = calculateNextCronTime(t, job) default: return nil, errors.New("未知的任务类型: " + string(job.JobType)) } if err != nil { return nil, err } return next, nil } // 参数校验 func validateJobData(job JobData) error { switch job.JobType { case JobTypeEveryMonth: if job.Day < 1 || job.Day > 31 { return ErrMonthDay } case JobTypeEveryWeek: if job.Weekday < time.Sunday || job.Weekday > time.Saturday { return ErrWeekday } case JobTypeEveryDay: if job.Hour < 0 || job.Hour > 23 { return ErrHour } case JobTypeEveryHour: if job.Minute < 0 || job.Minute > 59 { return ErrMinute } case JobTypeEveryMinute: if job.Second < 0 || job.Second > 59 { return ErrSecond } case JobTypeInterval: if job.IntervalTime <= 0 { return ErrIntervalTime } if job.BaseTime.IsZero() { return ErrBaseTime } case JobTypeCron: if job.CronExpression == "" { return ErrCronExpression } if job.CronSchedule == nil { return ErrCronParser } _, err := calculateNextCronTime(time.Now(), job) if err != nil { return err } } if job.Hour < 0 || job.Hour > 23 { return ErrHour } if job.Minute < 0 || job.Minute > 59 { return ErrMinute } if job.Second < 0 || job.Second > 59 { return ErrSecond } return nil } // 计算间隔任务下一次执行时间 // 固定基准时间,因为在不同的实例中需要对齐基准点 func calculateNextInterval(t time.Time, job JobData) (*time.Time, error) { if job.BaseTime.IsZero() { return nil, ErrBaseTime } if job.IntervalTime <= 0 { return nil, ErrIntervalTime } // 计算从基准时间到当前时间经过了多少个间隔 elapsed := t.Sub(job.BaseTime) intervals := elapsed / job.IntervalTime // 计算下一个执行时间 next := job.BaseTime.Add((intervals + 1) * job.IntervalTime) // 需要整的 next = next.Round(job.IntervalTime) // 确保下次执行时间不早于当前时间 if next.Before(t) || next.Equal(t) { next = next.Add(job.IntervalTime) } return &next, nil } func calculateNextMonthTime(t time.Time, job JobData) (*time.Time, error) { // 尝试当月是否有这个天数 // time.Date(2025, 2, 30, 0, 0, 0, 0, t.Location()) => 2025-03-02 00:00:00 +0800 CST 当月不足往后补 currentMonthTime := time.Date(t.Year(), t.Month(), job.Day, job.Hour, job.Minute, job.Second, 0, t.Location()) // 如果日期无效(比如2月30号),则调整到该月最后一天 if currentMonthTime.Day() != job.Day { // 获取该月的最后一天(0日就是上个月最后一天) // time.Date(2025,2,0,0,0,0,0,time.Local) => 2025-01-31 00:00:00 +0800 CST lastDay := time.Date(t.Year(), t.Month()+1, 0, 0, 0, 0, 0, t.Location()).Day() if job.Day > lastDay { currentMonthTime = time.Date(t.Year(), t.Month(), lastDay, job.Hour, job.Minute, job.Second, 0, t.Location()) } } if currentMonthTime.After(t) { return ¤tMonthTime, nil } // 计算下个月的同一天 nextMonth := t.Month() + 1 year := t.Year() if nextMonth > 12 { nextMonth = 1 year++ } nextMonthTime := time.Date(year, nextMonth, job.Day, job.Hour, job.Minute, job.Second, 0, t.Location()) // 如果日期无效,调整到下个月的最后一天 if nextMonthTime.Day() != job.Day { lastDay := time.Date(year, nextMonth+1, 0, 0, 0, 0, 0, t.Location()).Day() if job.Day > lastDay { nextMonthTime = time.Date(year, nextMonth, lastDay, job.Hour, job.Minute, job.Second, 0, t.Location()) } } return &nextMonthTime, nil } func calculateNextWeekTime(t time.Time, job JobData) (*time.Time, error) { currentWeekday := t.Weekday() targetWeekday := job.Weekday // 计算距离目标星期几的天数 daysToAdd := int(targetWeekday - currentWeekday) if daysToAdd < 0 { daysToAdd += 7 } // 本周的目标时间 thisWeekTime := time.Date(t.Year(), t.Month(), t.Day()+daysToAdd, job.Hour, job.Minute, job.Second, 0, t.Location()) if thisWeekTime.After(t) { return &thisWeekTime, nil } // 下周的目标时间 nextWeekTime := time.Date(t.Year(), t.Month(), t.Day()+daysToAdd+7, job.Hour, job.Minute, job.Second, 0, t.Location()) return &nextWeekTime, nil } func calculateNextDayTime(t time.Time, job JobData) (*time.Time, error) { // 今天的目标时间 todayTime := time.Date(t.Year(), t.Month(), t.Day(), job.Hour, job.Minute, job.Second, 0, t.Location()) if todayTime.After(t) { return &todayTime, nil } // 明天的时间 nextDayTime := time.Date(t.Year(), t.Month(), t.Day()+1, job.Hour, job.Minute, job.Second, 0, t.Location()) return &nextDayTime, nil } func calculateNextHourTime(t time.Time, job JobData) (*time.Time, error) { // 计算当前小时的目标时间 currentHourTime := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), job.Minute, job.Second, 0, t.Location()) if currentHourTime.After(t) { return ¤tHourTime, nil } // 下一个小时的时间 nextHourTime := time.Date(t.Year(), t.Month(), t.Day(), t.Hour()+1, job.Minute, job.Second, 0, t.Location()) return &nextHourTime, nil } func calculateNextMinuteTime(t time.Time, job JobData) (*time.Time, error) { // 计算当前分钟的目标时间 currentMinuteTime := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), job.Second, 0, t.Location()) if currentMinuteTime.After(t) { return ¤tMinuteTime, nil } // 下一分钟的时间 nextMinuteTime := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()+1, job.Second, 0, t.Location()) return &nextMinuteTime, nil } // 计算cron任务下下次执行时间 func calculateNextCronTime(t time.Time, job JobData) (*time.Time, error) { if job.CronExpression == "" { return nil, ErrCronExpression } if job.CronSchedule == nil { return nil, ErrCronParser } s := *job.CronSchedule next := s.Next(t) return &next, nil } func GetCronSche(CronExpression string, cronParser *cron.Parser) (*cron.Schedule, error) { if CronExpression == "" { return nil, ErrCronExpression } if cronParser == nil { return nil, ErrCronParser } sche, err := cronParser.Parse(CronExpression) if err != nil { return nil, err } return &sche, nil } // 检查是否本周期可以运行 // 检查是否本周期可以运行(已弃用,使用新的时间比较逻辑) // 保留此函数用于向后兼容,但建议使用新的时间计算逻辑 func canRun(t time.Time, job JobData) bool { targetTime := time.Date(t.Year(), t.Month(), t.Day(), job.Hour, job.Minute, job.Second, 0, t.Location()) switch job.JobType { case JobTypeEveryMonth: // 对于月任务,需要比较日期 targetTime = time.Date(t.Year(), t.Month(), job.Day, job.Hour, job.Minute, job.Second, 0, t.Location()) return !targetTime.Before(t) case JobTypeEveryWeek: // 对于周任务,需要比较星期 currentWeekday := t.Weekday() if currentWeekday < job.Weekday { return true } if currentWeekday == job.Weekday { return targetTime.After(t) || targetTime.Equal(t) } return false case JobTypeEveryDay: return targetTime.After(t) || targetTime.Equal(t) case JobTypeEveryHour: hourTarget := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), job.Minute, job.Second, 0, t.Location()) return hourTarget.After(t) || hourTarget.Equal(t) case JobTypeEveryMinute: minuteTarget := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), job.Second, 0, t.Location()) return minuteTarget.After(t) || minuteTarget.Equal(t) default: return false } }