feat: initial class history template

This commit is contained in:
hanyixuanten committed 2026-10-25 11:23:17 +00:00
commit 92fa0f8f37
33 files changed
+10714

No files matched your search

+169
View File
@@ -0,0 +1,169 @@
/**
* Cloudflare Pages Functions
* GET /api/messages → 返回最新留言列表
* POST /api/messages → 新增一条留言
* 依赖绑定:环境变量 DB(D1 数据库)
*/
const MAX_NAME = 12;
const MAX_TEXT = 120;
const MAX_LIST = 200; // 列表最多返回多少条
const COOLDOWN = 20; // 同一 IP 两条留言的最小间隔(秒)
/* ---------- 工具 ---------- */
function json(data, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Cache-Control': 'no-store',
// 如果前端和接口不同域,可加上 CORS 头;同域可删掉
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
}
});
}
function today() {
const d = new Date();
const p = n => String(n).padStart(2, '0');
return `${d.getFullYear()}.${p(d.getMonth() + 1)}.${p(d.getDate())}`;
}
/* 清理文本:去掉控制字符、折叠多余空白 */
function clean(str, max) {
return String(str ?? '')
.replace(/[\u0000-\u001F\u007F]/g, '')
.replace(/\s+/g, ' ')
.trim()
.slice(0, max);
}
/* ---------- OPTIONS(预检) ---------- */
export async function onRequestOptions() {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Max-Age': '86400'
}
});
}
/* ---------- GET:拉取列表 ---------- */
export async function onRequestGet({ env }) {
if (!env.DB) return json({ ok: false, error: '数据库未绑定' }, 500);
try {
const { results } = await env.DB
.prepare(
`SELECT id, name, text, time
FROM messages
ORDER BY created_at DESC, id DESC
LIMIT ?`
)
.bind(MAX_LIST)
.all();
return json({ ok: true, data: results ?? [] });
} catch (err) {
console.error('[GET /api/messages]', err);
return json({ ok: false, error: '读取失败,请稍后再试' }, 500);
}
}
/* ---------- POST:新增留言 ---------- */
export async function onRequestPost({ request, env }) {
if (!env.DB) return json({ ok: false, error: '数据库未绑定' }, 500);
// 1. 解析请求体
let body;
try {
body = await request.json();
} catch {
return json({ ok: false, error: '请求格式错误' }, 400);
}
// 2. 校验内容
const name = clean(body.name, MAX_NAME) || '匿名同学';
const text = clean(body.text, MAX_TEXT);
if (!text) {
return json({ ok: false, error: '内容不能为空' }, 400);
}
if (String(body.text ?? '').length > 500) {
return json({ ok: false, error: '内容过长' }, 400);
}
// 3. 简单限流:同一 IP 冷却时间内只能发一条
const ip =
request.headers.get('CF-Connecting-IP') ||
request.headers.get('X-Forwarded-For') ||
'';
const now = Math.floor(Date.now() / 1000);
try {
if (ip) {
const last = await env.DB
.prepare(
`SELECT created_at FROM messages
WHERE ip = ?
ORDER BY created_at DESC
LIMIT 1`
)
.bind(ip)
.first();
if (last && now - last.created_at < COOLDOWN) {
const wait = COOLDOWN - (now - last.created_at);
return json(
{ ok: false, error: `发得太快啦,请等 ${wait} 秒` },
429
);
}
}
// 4. 写入数据库
const time = today();
const res = await env.DB
.prepare(
`INSERT INTO messages (name, text, time, ip, created_at)
VALUES (?, ?, ?, ?, ?)`
)
.bind(name, text, time, ip, now)
.run();
// 5. 顺手裁剪:只保留最近 500 条,避免无限增长
// 如果不需要可以删掉这一段
try {
await env.DB
.prepare(
`DELETE FROM messages
WHERE id NOT IN (
SELECT id FROM messages
ORDER BY created_at DESC, id DESC
LIMIT 500
)`
)
.run();
} catch (e) {
console.warn('[cleanup]', e);
}
return json({
ok: true,
data: {
id: res.meta?.last_row_id ?? null,
name,
text,
time
}
});
} catch (err) {
console.error('[POST /api/messages]', err);
return json({ ok: false, error: '保存失败,请稍后再试' }, 500);
}
}