归档 artalk-cf 评论后端 + rss-robot 到 blog-admin(含技术选型/模块分布 README)
Deploy to Production / pre-check (push) Successful in 58s
Deploy to Production / build (push) Successful in 4m3s
Deploy to Production / deploy-edgeone (push) Successful in 3m48s
Deploy to Production / finalize (push) Successful in 26s
Deploy to Production / notify-failure (push) Skipped
Deploy to Production / pre-check (push) Successful in 58s
Deploy to Production / build (push) Successful in 4m3s
Deploy to Production / deploy-edgeone (push) Successful in 3m48s
Deploy to Production / finalize (push) Successful in 26s
Deploy to Production / notify-failure (push) Skipped
This commit is contained in:
1 parent
855a001046
commit
a74b3c7127
98 files changed
+15657
No files matched your search
@@ -0,0 +1,140 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user