src/utils/recurring.ts
Rama main · réplica de lectura del repositorio original.
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<number> {
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 };