use chrono::{DateTime, Datelike, Duration, FixedOffset, NaiveDateTime, TimeZone, Weekday}; use crate::entity::recurrence_rule; use crate::entity::sea_orm_active_enums::RecurrenceType; /// 根据循环规则计算下一次触发时间 pub fn calculate_next_due( rule: &recurrence_rule::Model, current_due: DateTime, ) -> Option> { let interval = rule.interval.max(1) as i64; let offset = current_due.offset().clone(); match rule.r#type { RecurrenceType::Hourly => { // 每 N 小时 Some(current_due + Duration::hours(interval)) } RecurrenceType::Daily => { // 每 N 天 Some(current_due + Duration::days(interval)) } RecurrenceType::Weekly => { // 每 N 周,可选指定星期几 if let Some(weekday) = rule.by_weekday { // 找到下一个指定的星期几 let target_weekday = num_to_weekday(weekday); let next = current_due + Duration::weeks(interval); // 调整到目标星期几 let current_weekday = next.weekday(); let days_ahead = (target_weekday.num_days_from_monday() as i64 - current_weekday.num_days_from_monday() as i64 + 7) % 7; if days_ahead == 0 { // 同一天,保持当前时间 Some(next) } else { Some(next + Duration::days(days_ahead)) } } else { Some(current_due + Duration::weeks(interval)) } } RecurrenceType::Monthly => { // 每 N 月,可选指定几号 let target_day = rule.by_monthday.unwrap_or(current_due.day() as i32) as u32; let mut year = current_due.year(); let mut month = current_due.month() as i32 + interval as i32; // 处理年份进位 while month > 12 { month -= 12; year += 1; } // 处理月份天数不足的情况(如 2 月没有 31 号) let day = target_day.min(days_in_month(year, month as u32)); let naive = NaiveDateTime::new( chrono::NaiveDate::from_ymd_opt(year, month as u32, day)?, current_due.time(), ); Some(offset.from_local_datetime(&naive).single()?) } RecurrenceType::Yearly => { // 每 N 年 let year = current_due.year() + interval as i32; let month = current_due.month(); let day = current_due.day().min(days_in_month(year, month)); let naive = NaiveDateTime::new( chrono::NaiveDate::from_ymd_opt(year, month, day)?, current_due.time(), ); Some(offset.from_local_datetime(&naive).single()?) } } } fn num_to_weekday(num: i32) -> Weekday { match num % 7 { 0 => Weekday::Sun, 1 => Weekday::Mon, 2 => Weekday::Tue, 3 => Weekday::Wed, 4 => Weekday::Thu, 5 => Weekday::Fri, 6 => Weekday::Sat, _ => Weekday::Mon, } } fn days_in_month(year: i32, month: u32) -> u32 { match month { 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, 4 | 6 | 9 | 11 => 30, 2 => { if is_leap_year(year) { 29 } else { 28 } } _ => 30, } } fn is_leap_year(year: i32) -> bool { (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) } #[cfg(test)] mod tests { use super::*; use chrono::{NaiveDate, Timelike}; use uuid::Uuid; fn make_rule( rule_type: RecurrenceType, interval: i32, by_weekday: Option, by_monthday: Option, ) -> recurrence_rule::Model { recurrence_rule::Model { id: Uuid::new_v4(), r#type: rule_type, interval, by_weekday, by_monthday, timezone: "Asia/Shanghai".to_string(), created_at: chrono::Utc::now().fixed_offset(), updated_at: chrono::Utc::now().fixed_offset(), } } fn make_datetime(year: i32, month: u32, day: u32, hour: u32, min: u32) -> DateTime { let naive = NaiveDate::from_ymd_opt(year, month, day) .unwrap() .and_hms_opt(hour, min, 0) .unwrap(); FixedOffset::east_opt(0).unwrap().from_local_datetime(&naive).unwrap() } #[test] fn test_hourly() { let rule = make_rule(RecurrenceType::Hourly, 2, None, None); let current = make_datetime(2024, 1, 15, 10, 30); let next = calculate_next_due(&rule, current).unwrap(); assert_eq!(next.hour(), 12); assert_eq!(next.minute(), 30); } #[test] fn test_daily() { let rule = make_rule(RecurrenceType::Daily, 3, None, None); let current = make_datetime(2024, 1, 15, 10, 30); let next = calculate_next_due(&rule, current).unwrap(); assert_eq!(next.day(), 18); } #[test] fn test_weekly() { let rule = make_rule(RecurrenceType::Weekly, 1, None, None); let current = make_datetime(2024, 1, 15, 10, 30); let next = calculate_next_due(&rule, current).unwrap(); assert_eq!(next.day(), 22); } #[test] fn test_monthly() { let rule = make_rule(RecurrenceType::Monthly, 1, None, Some(15)); let current = make_datetime(2024, 1, 15, 10, 30); let next = calculate_next_due(&rule, current).unwrap(); assert_eq!(next.month(), 2); assert_eq!(next.day(), 15); } #[test] fn test_monthly_overflow() { // 1月31号 -> 2月没有31号,应该是28号(或29号) let rule = make_rule(RecurrenceType::Monthly, 1, None, Some(31)); let current = make_datetime(2024, 1, 31, 10, 30); let next = calculate_next_due(&rule, current).unwrap(); assert_eq!(next.month(), 2); assert_eq!(next.day(), 29); // 2024 是闰年 } #[test] fn test_yearly() { let rule = make_rule(RecurrenceType::Yearly, 1, None, None); let current = make_datetime(2024, 6, 15, 10, 30); let next = calculate_next_due(&rule, current).unwrap(); assert_eq!(next.year(), 2025); assert_eq!(next.month(), 6); assert_eq!(next.day(), 15); } }