import dayjs from "dayjs"; import { FinanceAutoRepeatType } from "@/types/finance"; /** repeat_date 以 ! 开头表示"排除模式":指定的日期/星期几不执行 */ function isExcludeMode(repeatDate: string): boolean { return repeatDate.startsWith("!"); } /** 去掉排除前缀,返回干净的日期字符串 */ function cleanRepeatDate(repeatDate: string): string { return isExcludeMode(repeatDate) ? repeatDate.slice(1) : repeatDate; } /** * 粗粒度判断:从上次执行至今,是否"可能"有遗漏的触发日期 * 只做时间周期级别的判断,不做具体日期匹配(日期匹配交给 shouldRunOnDate / getMissedDates) */ export function shouldRun(repeatType: number, repeatDate: string, lastRun: string | null | undefined): boolean { const today = dayjs(); if (!lastRun) return true; // 从未执行过,需要尝试 const last = dayjs(lastRun); // 排除模式下每天都是潜在触发日,简单判断 lastRun 不是今天即可 if (isExcludeMode(repeatDate)) { return !last.isSame(today, "day"); } switch (repeatType) { case FinanceAutoRepeatType.Daily: return !last.isSame(today, "day"); case FinanceAutoRepeatType.Weekly: { const days = (repeatDate || "").split(",").map(Number).filter((d) => !isNaN(d)); if (days.length === 0) return false; let check = today; for (let i = 0; i < 7; i++) { if (days.includes(check.day())) { return check.isAfter(last, "day"); } check = check.subtract(1, "day"); } return false; } case FinanceAutoRepeatType.Monthly: return !last.isSame(today, "month"); case FinanceAutoRepeatType.Quarterly: return last.quarter() !== today.quarter() || last.year() !== today.year(); case FinanceAutoRepeatType.Yearly: // 支持同一年内多个触发日(如 3/15 和 6/1),不再仅按年份判断 return last.isBefore(today, "day"); default: return false; } } /** * 获取应该补记账的日期列表(从上次执行到今天) * 逐日遍历,通过 shouldRunOnDate 精确匹配触发日 */ export function getMissedDates( repeatType: number, repeatDate: string, lastRun: string | null | undefined, ): string[] { const today = dayjs(); // 首次执行:只在今天恰好是触发日时才创建账单 if (!lastRun) { if (shouldRunOnDate(repeatType, repeatDate, today)) { return [today.format("YYYY-MM-DD")]; } return []; } const last = dayjs(lastRun); const dates: string[] = []; let cursor = last.add(1, "day"); while (cursor.isBefore(today, "day") || cursor.isSame(today, "day")) { const d = cursor.format("YYYY-MM-DD"); if (shouldRunOnDate(repeatType, repeatDate, cursor)) { dates.push(d); } cursor = cursor.add(1, "day"); } return dates; } function shouldRunOnDate(repeatType: number, repeatDate: string, date: dayjs.Dayjs): boolean { const exclude = isExcludeMode(repeatDate); const raw = cleanRepeatDate(repeatDate); switch (repeatType) { case FinanceAutoRepeatType.Daily: { if (!exclude || !raw) return true; // 每天 排除星期几(如 "!0" → 排除周日) const excludedDays = raw.split(",").map(Number).filter((d) => !isNaN(d)); return !excludedDays.includes(date.day()); } case FinanceAutoRepeatType.Weekly: { const days = raw.split(",").map(Number).filter((d) => !isNaN(d)); if (days.length === 0) return false; const match = days.includes(date.day()); return exclude ? !match : match; } case FinanceAutoRepeatType.Monthly: case FinanceAutoRepeatType.Quarterly: { const dates = raw.split(",").map(Number).filter((d) => !isNaN(d)); if (dates.length === 0) return false; const match = dates.includes(date.date()); return exclude ? !match : match; } case FinanceAutoRepeatType.Yearly: { // 月,日 成对存储,如 "3,15,6,1" → 3月15日 + 6月1日 const parts = raw.split(",").map(Number).filter((n) => !isNaN(n)); for (let i = 0; i < parts.length - 1; i += 2) { if (parts[i] === date.month() + 1 && parts[i + 1] === date.date()) { return exclude ? false : true; } } return exclude; // 排除模式未命中 → 执行;正常模式未命中 → 不执行 } default: return false; } } export const REPEAT_LABELS: Record = { [FinanceAutoRepeatType.Daily]: "每天", [FinanceAutoRepeatType.Weekly]: "每周", [FinanceAutoRepeatType.Monthly]: "每月", }; export const WEEK_DAYS = [ { value: 1, label: "一" }, { value: 2, label: "二" }, { value: 3, label: "三" }, { value: 4, label: "四" }, { value: 5, label: "五" }, { value: 6, label: "六" }, { value: 0, label: "日" }, ];