Ctrl K
ComunidadGuías…

src/utils/recurring.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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 };