140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
import type { Env } from '../types';
|
||||
|
|
|
|||
|
|
export const CN_OFFSET_MS = 8 * 60 * 60 * 1000;
|
|||
|
|
|
|||
|
|
/** 毫秒时间戳 → "YYYY-MM-DD HH:mm:ss"(Asia/Shanghai,中国无夏令时) */
|
|||
|
|
export function formatDateCN(ms: number): string {
|
|||
|
|
const d = new Date((ms || 0) + CN_OFFSET_MS);
|
|||
|
|
const p = (n: number, len = 2) => String(n).padStart(len, '0');
|
|||
|
|
return (
|
|||
|
|
`${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ` +
|
|||
|
|
`${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}`
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function now(): number {
|
|||
|
|
return Date.now();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------------ 响应封装
|
|||
|
|
|
|||
|
|
export function json(data: unknown, init: ResponseInit = {}): Response {
|
|||
|
|
const headers = new Headers(init.headers);
|
|||
|
|
headers.set('Content-Type', 'application/json; charset=utf-8');
|
|||
|
|
return new Response(JSON.stringify(data ?? {}), { ...init, headers });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Artalk 成功响应:RespData 是「裸数据」,RespSuccess 是 {"msg":"Success"} */
|
|||
|
|
export function ok(data: unknown): Response {
|
|||
|
|
return json(data);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function okMsg(msg = 'Success', extra: Record<string, unknown> = {}): Response {
|
|||
|
|
return json({ msg, ...extra });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/** Artalk 错误响应:{"msg": "..."} + 非 200 状态码 */
|
|||
|
|
export function fail(status: number, msg: string, extra: Record<string, unknown> = {}): Response {
|
|||
|
|
return json({ msg, ...extra }, { status });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------------ 请求解析
|
|||
|
|
|
|||
|
|
export function getClientIP(req: Request): string {
|
|||
|
|
return (
|
|||
|
|
req.headers.get('CF-Connecting-IP') ||
|
|||
|
|
(req.headers.get('X-Forwarded-For') || '').split(',')[0].trim() ||
|
|||
|
|
'127.0.0.1'
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function getUserAgent(req: Request): string {
|
|||
|
|
return req.headers.get('User-Agent') || '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function readBody(req: Request): Promise<Record<string, any>> {
|
|||
|
|
const ct = req.headers.get('Content-Type') || '';
|
|||
|
|
try {
|
|||
|
|
if (ct.includes('application/json')) {
|
|||
|
|
const v = await req.json();
|
|||
|
|
return v && typeof v === 'object' ? (v as Record<string, any>) : {};
|
|||
|
|
}
|
|||
|
|
if (ct.includes('form-urlencoded')) {
|
|||
|
|
const text = await req.text();
|
|||
|
|
const out: Record<string, any> = {};
|
|||
|
|
for (const [k, v] of new URLSearchParams(text)) out[k] = v;
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
const text = await req.text();
|
|||
|
|
if (!text) return {};
|
|||
|
|
try {
|
|||
|
|
const v = JSON.parse(text);
|
|||
|
|
return v && typeof v === 'object' ? v : {};
|
|||
|
|
} catch {
|
|||
|
|
const out: Record<string, any> = {};
|
|||
|
|
for (const [k, v] of new URLSearchParams(text)) out[k] = v;
|
|||
|
|
return out;
|
|||
|
|
}
|
|||
|
|
} catch {
|
|||
|
|
return {};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function qp(url: URL, key: string): string {
|
|||
|
|
return url.searchParams.get(key) ?? '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function qInt(url: URL, key: string, dft = 0): number {
|
|||
|
|
const v = parseInt(url.searchParams.get(key) ?? '', 10);
|
|||
|
|
return Number.isFinite(v) ? v : dft;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function qBool(url: URL, key: string): boolean {
|
|||
|
|
const v = (url.searchParams.get(key) ?? '').toLowerCase();
|
|||
|
|
return v === '1' || v === 'true' || v === 'yes';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------------ 校验
|
|||
|
|
|
|||
|
|
export function isEmail(s: string): boolean {
|
|||
|
|
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test((s || '').trim());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function isUrl(s: string): boolean {
|
|||
|
|
try {
|
|||
|
|
const u = new URL(s);
|
|||
|
|
return u.protocol === 'http:' || u.protocol === 'https:';
|
|||
|
|
} catch {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function trimTo(s: string, max: number): string {
|
|||
|
|
const v = String(s ?? '');
|
|||
|
|
return v.length > max ? v.slice(0, max) : v;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ------------------------------------------------------------------ CORS
|
|||
|
|
|
|||
|
|
export function corsHeaders(req: Request, env: Env): Headers {
|
|||
|
|
const headers = new Headers();
|
|||
|
|
const origin = req.headers.get('Origin') || '';
|
|||
|
|
const allow = (env.ALLOWED_ORIGINS || '')
|
|||
|
|
.split(',')
|
|||
|
|
.map((s) => s.trim())
|
|||
|
|
.filter(Boolean);
|
|||
|
|
|
|||
|
|
if (origin && (allow.includes(origin) || allow.includes('*'))) {
|
|||
|
|
headers.set('Access-Control-Allow-Origin', origin);
|
|||
|
|
} else if (!origin) {
|
|||
|
|
headers.set('Access-Control-Allow-Origin', '*');
|
|||
|
|
}
|
|||
|
|
headers.set('Vary', 'Origin');
|
|||
|
|
headers.set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
|
|||
|
|
headers.set(
|
|||
|
|
'Access-Control-Allow-Headers',
|
|||
|
|
'Content-Type,Authorization,X-Requested-With,X-Api-Version',
|
|||
|
|
);
|
|||
|
|
headers.set('Access-Control-Max-Age', '86400');
|
|||
|
|
return headers;
|
|||
|
|
}
|