Files
dukang/packages/domain/src/wecom-plugin.ts
T

146 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
};
}