first commit

This commit is contained in:
Michael Dong
2026-02-05 11:24:40 +08:00
commit a98e12f286
144 changed files with 26459 additions and 0 deletions

View File

@@ -0,0 +1,207 @@
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<FixedOffset>,
) -> Option<DateTime<FixedOffset>> {
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<i32>,
by_monthday: Option<i32>,
) -> 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<FixedOffset> {
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);
}
}