import { addShanghaiDays, previousShanghaiMonth, previousShanghaiWeek, shanghaiMonthRange, shanghaiWeekRange, shanghaiWeekday, shanghaiYearMonth, shanghaiYmd, startOfShanghaiDay, } from './shanghai-date'; export const WECOM_REPORT_KINDS = ['daily', 'weekly', 'monthly'] as const; export type WecomReportKind = (typeof WECOM_REPORT_KINDS)[number]; export function isWecomReportKind(v: string): v is WecomReportKind { return (WECOM_REPORT_KINDS as readonly string[]).includes(v); } export type WecomReportStats = { usersTotal: number; usersIncrement: number; partnersTotal: number; partnersIncrement: number; storesTotal: number; storesIncrement: number; ordersTotal: number; ordersIncrement: number; orderAmountTotal: number; orderAmountIncrement: number; redeemsTotal: number; redeemsIncrement: number; redeemAmountTotal: number; redeemAmountIncrement: number; }; export type WecomReportPeriod = { kind: WecomReportKind; start: Date; endExclusive: Date; periodKey: string; title: string; rangeLabel: string; incrementLabel: string; }; export type WecomReportSchedule = { enabled: boolean; sendHour: number; sendMinute: number; /** 1=周一 … 7=周日,仅周报 */ sendWeekday: number; /** 1–31,仅月报 */ sendMonthDay: number; lastSentPeriod: string | null; }; function pad2(n: number): string { return String(n).padStart(2, '0'); } function shanghaiDateTime(ymd: string, hour: number, minute: number): Date { return new Date(`${ymd}T${pad2(hour)}:${pad2(minute)}:00+08:00`); } function lastDayOfShanghaiMonth(year: number, month: number): number { return Number(shanghaiYmd(new Date(shanghaiMonthRange(year, month).endExclusive.getTime() - 1)).slice(8, 10)); } /** 日报=当天;周报=上一自然周;月报=上一自然月(北京日历) */ export function wecomReportPeriod(kind: WecomReportKind, now = new Date()): WecomReportPeriod { if (kind === 'daily') { const start = startOfShanghaiDay(now); const ymd = shanghaiYmd(start); return { kind, start, endExclusive: addShanghaiDays(start, 1), periodKey: ymd, title: `日报(${ymd})`, rangeLabel: ymd, incrementLabel: '当日新增', }; } if (kind === 'weekly') { const { start, endExclusive } = previousShanghaiWeek(now); const from = shanghaiYmd(start); const to = shanghaiYmd(new Date(endExclusive.getTime() - 1)); return { kind, start, endExclusive, periodKey: from, title: `周报(${from} ~ ${to})`, rangeLabel: `${from} ~ ${to}`, incrementLabel: '本期新增', }; } const { year, month } = previousShanghaiMonth(now); const { start, endExclusive } = shanghaiMonthRange(year, month); return { kind, start, endExclusive, periodKey: `${year}-${pad2(month)}`, title: `月报(${year}年${month}月)`, rangeLabel: `${shanghaiYmd(start)} ~ ${shanghaiYmd(new Date(endExclusive.getTime() - 1))}`, incrementLabel: '本期新增', }; } /** 进行中的周期截到 now,已结束的周期用期末 */ export function wecomReportCutoff(period: WecomReportPeriod, now = new Date()): Date { return now.getTime() < period.endExclusive.getTime() ? now : period.endExclusive; } export function wecomReportDueAt(kind: WecomReportKind, schedule: WecomReportSchedule, now = new Date()): Date { const hour = Math.min(23, Math.max(0, Math.floor(schedule.sendHour))); const minute = Math.min(59, Math.max(0, Math.floor(schedule.sendMinute))); if (kind === 'daily') { return shanghaiDateTime(shanghaiYmd(now), hour, minute); } if (kind === 'weekly') { const { start } = shanghaiWeekRange(now); const iso = Math.min(7, Math.max(1, Math.floor(schedule.sendWeekday) || 1)); const day = addShanghaiDays(start, iso - 1); return shanghaiDateTime(shanghaiYmd(day), hour, minute); } const { year, month } = shanghaiYearMonth(now); const last = lastDayOfShanghaiMonth(year, month); const day = Math.min(last, Math.max(1, Math.floor(schedule.sendMonthDay) || 1)); return shanghaiDateTime(`${year}-${pad2(month)}-${pad2(day)}`, hour, minute); } export function wecomReportShouldFire( kind: WecomReportKind, schedule: WecomReportSchedule, now = new Date(), ): boolean { if (!schedule.enabled) return false; const period = wecomReportPeriod(kind, now); if (schedule.lastSentPeriod === period.periodKey) return false; return now.getTime() >= wecomReportDueAt(kind, schedule, now).getTime(); } function fmtCount(n: number): string { return Math.round(n).toLocaleString('zh-CN'); } function fmtMoney(n: number): string { return Number(n || 0).toFixed(2); } function line(label: string, total: number, inc: number, incLabel: string, money = false): string { const fmt = money ? fmtMoney : fmtCount; return `${label}:${fmt(total)}(${incLabel} ${fmt(inc)})`; } export function formatWecomReportMarkdown(period: WecomReportPeriod, stats: WecomReportStats): string { const inc = period.incrementLabel; return [ `**杜康好客 · ${period.title}**`, `统计区间:${period.rangeLabel}(北京时间)`, '', line('用户数量', stats.usersTotal, stats.usersIncrement, inc), line('合伙人数量', stats.partnersTotal, stats.partnersIncrement, inc), line('门店数量', stats.storesTotal, stats.storesIncrement, inc), line('订单数量', stats.ordersTotal, stats.ordersIncrement, inc), line('订单金额', stats.orderAmountTotal, stats.orderAmountIncrement, inc, true), line('核销单数量', stats.redeemsTotal, stats.redeemsIncrement, inc), line('核销单金额', stats.redeemAmountTotal, stats.redeemAmountIncrement, inc, true), ].join('\n'); } /** 仅用于单测:避免把 shanghaiWeekday 的周日=0 与 ISO 周一=1 搞混 */ export function wecomReportIsoWeekday(d: Date): number { const wd = shanghaiWeekday(d); return wd === 0 ? 7 : wd; }