146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
import { maskContactPhone } from './phone';
|
||
import { addShanghaiDays, shanghaiYmd, startOfShanghaiDay } from './shanghai-date';
|
||
import {
|
||
wecomReportPeriod,
|
||
type WecomReportKind,
|
||
type WecomReportPeriod,
|
||
type WecomReportStats,
|
||
} from './wecom-report';
|
||
|
||
export const WECOM_PLUGIN_PAGE_SIZE_DEFAULT = 5;
|
||
export const WECOM_PLUGIN_PAGE_SIZE_MAX = 10;
|
||
|
||
export const WECOM_PLUGIN_METRICS_KINDS = ['today', 'daily', 'weekly', 'monthly'] as const;
|
||
export type WecomPluginMetricsKind = (typeof WECOM_PLUGIN_METRICS_KINDS)[number];
|
||
|
||
export type WecomPluginMetricsPeriod = Omit<WecomReportPeriod, 'kind'> & {
|
||
kind: WecomPluginMetricsKind;
|
||
};
|
||
|
||
export function isWecomPluginMetricsKind(v: string): v is WecomPluginMetricsKind {
|
||
return (WECOM_PLUGIN_METRICS_KINDS as readonly string[]).includes(v);
|
||
}
|
||
|
||
export function parseWecomPluginMetricsKind(raw?: string | null): WecomPluginMetricsKind | null {
|
||
const v = String(raw ?? '').trim().toLowerCase();
|
||
if (!v) return 'today';
|
||
return isWecomPluginMetricsKind(v) ? v : null;
|
||
}
|
||
|
||
export function clampWecomPluginPageSize(raw?: string | number | null): number {
|
||
const n = typeof raw === 'number' ? raw : Number(raw);
|
||
if (!Number.isFinite(n) || n <= 0) return WECOM_PLUGIN_PAGE_SIZE_DEFAULT;
|
||
return Math.min(WECOM_PLUGIN_PAGE_SIZE_MAX, Math.max(1, Math.floor(n)));
|
||
}
|
||
|
||
export function clampWecomPluginPage(raw?: string | number | null): number {
|
||
const n = typeof raw === 'number' ? raw : Number(raw);
|
||
if (!Number.isFinite(n) || n <= 0) return 1;
|
||
return Math.min(100, Math.floor(n));
|
||
}
|
||
|
||
/** 长度必须相同后再逐字符 XOR,避免短密码走快速失败路径时的明显差异(仍非密码学级) */
|
||
export function verifyWecomPluginApiKey(provided: string, expected?: string | null): boolean {
|
||
const exp = String(expected ?? '');
|
||
const got = String(provided ?? '');
|
||
if (!exp || !got || exp.length !== got.length) return false;
|
||
let diff = 0;
|
||
for (let i = 0; i < exp.length; i++) {
|
||
diff |= exp.charCodeAt(i) ^ got.charCodeAt(i);
|
||
}
|
||
return diff === 0;
|
||
}
|
||
|
||
export function isWecomPluginEnabled(env: {
|
||
WECOM_PLUGIN_ENABLED?: string;
|
||
WECOM_PLUGIN_API_KEY?: string;
|
||
}): boolean {
|
||
return env.WECOM_PLUGIN_ENABLED === 'true' && Boolean(String(env.WECOM_PLUGIN_API_KEY ?? '').trim());
|
||
}
|
||
|
||
/** 在少量启用实例中做常量时间匹配(长度不等仍会先失败) */
|
||
export function matchWecomPluginByApiKey<T extends { apiKey: string }>(
|
||
provided: string,
|
||
candidates: T[],
|
||
): T | null {
|
||
const got = String(provided ?? '').trim();
|
||
if (!got) return null;
|
||
let hit: T | null = null;
|
||
for (const row of candidates) {
|
||
if (verifyWecomPluginApiKey(got, row.apiKey)) hit = row;
|
||
}
|
||
return hit;
|
||
}
|
||
|
||
export function wecomPluginMetricsPeriod(
|
||
kind: WecomPluginMetricsKind,
|
||
now = new Date(),
|
||
): WecomPluginMetricsPeriod {
|
||
if (kind === 'today') {
|
||
const start = startOfShanghaiDay(now);
|
||
const ymd = shanghaiYmd(start);
|
||
return {
|
||
kind,
|
||
start,
|
||
endExclusive: now,
|
||
periodKey: ymd,
|
||
title: `今日(${ymd})`,
|
||
rangeLabel: ymd,
|
||
incrementLabel: '今日新增',
|
||
};
|
||
}
|
||
const period = wecomReportPeriod(kind as WecomReportKind, now);
|
||
return { ...period, kind };
|
||
}
|
||
|
||
export type WecomPluginDateRange = { gte?: Date; lt?: Date };
|
||
|
||
/** 插件时间筛选:from/to 支持 YYYY-MM-DD 或 ISO;日期含当天全天 */
|
||
export function parseWecomPluginDateRange(
|
||
fromRaw?: string | null,
|
||
toRaw?: string | null,
|
||
): WecomPluginDateRange | undefined {
|
||
const from = parseWecomPluginDate(fromRaw);
|
||
const to = parseWecomPluginDate(toRaw);
|
||
if (!from && !to) return undefined;
|
||
const range: WecomPluginDateRange = {};
|
||
if (from) range.gte = from;
|
||
if (to) {
|
||
range.lt = isWecomPluginDateOnly(toRaw) ? addShanghaiDays(startOfShanghaiDay(to), 1) : to;
|
||
}
|
||
return range;
|
||
}
|
||
|
||
function isWecomPluginDateOnly(raw?: string | null): boolean {
|
||
return /^\d{4}-\d{2}-\d{2}$/.test(String(raw ?? '').trim());
|
||
}
|
||
|
||
function parseWecomPluginDate(raw?: string | null): Date | null {
|
||
const v = String(raw ?? '').trim();
|
||
if (!v) return null;
|
||
if (/^\d{4}-\d{2}-\d{2}$/.test(v)) {
|
||
return startOfShanghaiDay(new Date(`${v}T00:00:00+08:00`));
|
||
}
|
||
const d = new Date(v);
|
||
return Number.isFinite(d.getTime()) ? d : null;
|
||
}
|
||
|
||
export function toWecomPluginMetricsView(stats: WecomReportStats) {
|
||
return {
|
||
...stats,
|
||
newStores: stats.storesIncrement,
|
||
};
|
||
}
|
||
|
||
export function toWecomPluginUserView(user: {
|
||
userNo: string;
|
||
nickname?: string | null;
|
||
phone?: string | null;
|
||
}): { userNo: string; nickname: string; phone: string } {
|
||
return {
|
||
userNo: user.userNo,
|
||
nickname: user.nickname?.trim() || '—',
|
||
phone: maskContactPhone(user.phone),
|
||
};
|
||
}
|