fix(会员账期): 非法北京本地时间改用 None 表达
- beijing_local_micros 返回 Option<i64>,不再用 0 兼作非法哨兵,合法 epoch 0 可被区分。 - advance_beijing_months_clamped 对夹取后的合法输入 expect,相关测试助手同步更新。 - 补 beijing_local_micros 区分合法 epoch 与非法输入的用例。
This commit is contained in:
@@ -20,19 +20,24 @@ pub fn beijing_local_date_parts(micros: i64) -> (i32, u8, u8, i64) {
|
||||
(year, month, day, time_of_day_micros)
|
||||
}
|
||||
|
||||
/// 北京时间本地年月日 + 当日已过微秒 → UTC 微秒时间戳;本地日期或当日时刻越界时返回 0。
|
||||
pub fn beijing_local_micros(year: i32, month: u8, day: u8, time_of_day_micros: i64) -> i64 {
|
||||
/// 北京时间本地年月日 + 当日已过微秒 → UTC 微秒时间戳;本地日期或当日时刻越界时返回 `None`。
|
||||
///
|
||||
/// 返回 `Option` 而不是用 `0` 兼作非法哨兵:北京 1970-01-01 08:00 本身就是合法的 `0`,
|
||||
/// 调用方必须能区分「非法日期」与「合法 epoch」。
|
||||
pub fn beijing_local_micros(year: i32, month: u8, day: u8, time_of_day_micros: i64) -> Option<i64> {
|
||||
if !(1..=12).contains(&month)
|
||||
|| day == 0
|
||||
|| day > days_in_month(year, month)
|
||||
|| !(0..PROFILE_RUNTIME_DAY_MICROS).contains(&time_of_day_micros)
|
||||
{
|
||||
return 0;
|
||||
return None;
|
||||
}
|
||||
days_from_civil(year, month, day)
|
||||
.saturating_mul(PROFILE_RUNTIME_DAY_MICROS)
|
||||
.saturating_add(time_of_day_micros)
|
||||
.saturating_sub(PROFILE_TASK_BEIJING_OFFSET_MICROS)
|
||||
Some(
|
||||
days_from_civil(year, month, day)
|
||||
.saturating_mul(PROFILE_RUNTIME_DAY_MICROS)
|
||||
.saturating_add(time_of_day_micros)
|
||||
.saturating_sub(PROFILE_TASK_BEIJING_OFFSET_MICROS),
|
||||
)
|
||||
}
|
||||
|
||||
/// 把北京时间锚点推进 `months` 个自然月,日期夹取到目标月最后一天,时刻保持不变。
|
||||
@@ -45,6 +50,7 @@ pub fn advance_beijing_months_clamped(anchor_micros: i64, months: u32) -> i64 {
|
||||
let target_month = (total_months.rem_euclid(12) + 1) as u8;
|
||||
let target_day = day.min(days_in_month(target_year, target_month));
|
||||
beijing_local_micros(target_year, target_month, target_day, time_of_day_micros)
|
||||
.expect("夹取后的北京本地年月日与当日时刻必定合法")
|
||||
}
|
||||
|
||||
/// 第 `index` 期(1-based)的账期窗口 `(cycle_started_at, cycle_resets_at)`。
|
||||
@@ -85,6 +91,7 @@ mod tests {
|
||||
/// 北京时间某时刻 → UTC 微秒。
|
||||
fn beijing(year: i32, month: u8, day: u8, hour: i64, minute: i64) -> i64 {
|
||||
beijing_local_micros(year, month, day, hour * 3_600_000_000 + minute * 60_000_000)
|
||||
.expect("测试用的北京本地时间应当合法")
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -96,6 +103,18 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beijing_local_micros_distinguishes_epoch_from_invalid_input() {
|
||||
// 中文注释:北京 1970-01-01 08:00 就是合法的 UTC 0,不能再被 0 哨兵吞掉。
|
||||
assert_eq!(beijing(1970, 1, 1, 8, 0), 0);
|
||||
assert_eq!(beijing_local_micros(2026, 13, 1, 0), None);
|
||||
assert_eq!(beijing_local_micros(2026, 2, 30, 0), None);
|
||||
assert_eq!(
|
||||
beijing_local_micros(2026, 1, 1, PROFILE_RUNTIME_DAY_MICROS),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advance_months_clamps_to_month_end() {
|
||||
// 中文注释:1/31 开通 → 2/28(2026 非闰年)→ 3/31,每期都从原始锚点重算。
|
||||
|
||||
@@ -259,6 +259,7 @@ mod tests {
|
||||
|
||||
fn beijing(year: i32, month: u8, day: u8, hour: i64) -> i64 {
|
||||
beijing_local_micros(year, month, day, hour * 3_600_000_000)
|
||||
.expect("测试用的北京本地时间应当合法")
|
||||
}
|
||||
|
||||
fn snapshot(
|
||||
|
||||
@@ -4142,26 +4142,26 @@ mod tests {
|
||||
#[test]
|
||||
fn membership_plan_window_recomputes_each_period_from_the_original_anchor() {
|
||||
// 中文注释:1/31 开通 → 第 1 期到 2/28,第 2 期到 3/31;每期都从原始锚点重算。
|
||||
let anchor = beijing_local_micros(2026, 1, 31, 0);
|
||||
let anchor = beijing_local_micros(2026, 1, 31, 0).expect("测试用的北京本地时间应当合法");
|
||||
assert_eq!(
|
||||
membership_cycle_window(anchor, 1),
|
||||
(
|
||||
beijing_local_micros(2026, 1, 31, 0),
|
||||
beijing_local_micros(2026, 2, 28, 0)
|
||||
beijing_local_micros(2026, 1, 31, 0).expect("测试用的北京本地时间应当合法"),
|
||||
beijing_local_micros(2026, 2, 28, 0).expect("测试用的北京本地时间应当合法")
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
membership_cycle_window(anchor, 2),
|
||||
(
|
||||
beijing_local_micros(2026, 2, 28, 0),
|
||||
beijing_local_micros(2026, 3, 31, 0)
|
||||
beijing_local_micros(2026, 2, 28, 0).expect("测试用的北京本地时间应当合法"),
|
||||
beijing_local_micros(2026, 3, 31, 0).expect("测试用的北京本地时间应当合法")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn membership_cycle_advance_only_writes_when_a_new_period_starts() {
|
||||
let anchor = beijing_local_micros(2026, 1, 31, 0);
|
||||
let anchor = beijing_local_micros(2026, 1, 31, 0).expect("测试用的北京本地时间应当合法");
|
||||
let day = PROFILE_RUNTIME_DAY_MICROS;
|
||||
let row = || ProfileMembership {
|
||||
user_id: "user-1".to_string(),
|
||||
|
||||
Reference in New Issue
Block a user