import type { LedgerRecord, RecurringRule } from '../types'; import { addRecord, getRecurringRules, setRecurringLastGenerated, getSetting } from '../database/ledgerDB'; import { SETTING_KEYS } from '../constants'; import { formatDate } from './dateUtils'; // 计算某规则在 date 这天是否应该生成 export function isDueOn(rule: RecurringRule, date: Date): boolean { const y = date.getFullYear(); const m = date.getMonth() + 1; const d = date.getDate(); switch (rule.frequency) { case 'daily': return true; case 'weekly': return date.getDay() === rule.dayOfWeek; case 'monthly': { // 处理 31 号等不存在的日期:当月最后一天兜底 const lastDay = new Date(y, m, 0).getDate(); const target = Math.min(rule.dayOfMonth, lastDay); return d === target; } case 'yearly': { if (rule.monthOfYear !== m) return false; const lastDay = new Date(y, m, 0).getDate(); const target = Math.min(rule.dayOfMonth, lastDay); return d === target; } default: return false; } } // 规则的上一个应生成日期(用于判断是否已生成) // v0.11 修复:notBefore=规则创建日,不回溯生成创建之前的到期记录(避免新建规则当天"补记") function previousDueDate(rule: RecurringRule, today: Date, notBefore: Date): Date | null { // 从今天往前找最近一个到期日 for (let i = 0; i < 370; i++) { const d = new Date(today.getFullYear(), today.getMonth(), today.getDate() - i); if (d < notBefore) return null; if (isDueOn(rule, d)) return d; } return null; } // 检查并生成到期的周期记账(App 启动 / 首页挂载时调用) // 返回本次生成的记录数 export async function runRecurringCheck(): Promise { const rules = await getRecurringRules(); const today = new Date(); let generated = 0; // 周期记录归属当前登录用户(未登录时为 0,家庭账本中能正确显示记账人) const uidStr = await getSetting(SETTING_KEYS.SYNC_USER_ID); const syncUserId = Number(uidStr ?? '0') || 0; for (const rule of rules) { if (!rule.enabled) continue; const due = previousDueDate(rule, today, new Date(rule.createdAt)); if (!due) continue; const dueStr = formatDate(due); // 已生成过(>= lastGenerated)则跳过 if (rule.lastGenerated && dueStr <= rule.lastGenerated) continue; // 生成记录(仅生成最近一个到期日,不补历史,避免刷屏) // uuid 由「规则 + 到期日」决定:同一台规则在两台设备上各自到期生成时, // 服务端按 uuid 做 LWW upsert 只会留一条(此前 last_generated 是设备本地状态, // 两台设备都在同步前生成 → 房租这类订阅会被记两次)。 const deterministic = rule.uuid.length <= 52 ? `${rule.uuid}@${dueStr}` : ''; await addRecord(rule.amount, rule.category, rule.type, dueStr, rule.note || rule.name, false, { userId: syncUserId, ...(deterministic ? { uuid: deterministic } : {}), }); await setRecurringLastGenerated(rule.id, dueStr); generated++; } return generated; } // 供其他模块使用的类型引用 export type { LedgerRecord };