@@ -0,0 +1,179 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user