From ac3f2b0451f71d34484c966f6475101eff31c02c Mon Sep 17 00:00:00 2001 From: jacky Date: Thu, 11 Jun 2026 19:03:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Vercel=20=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E6=94=AF=E6=8C=81=EF=BC=88Serverless=20API=20+=20KV?= =?UTF-8?q?=20=E5=AD=98=E5=82=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/[campus]/[...path].js | 141 +++++++++++++++++++ api/_db.js | 44 ++++++ package.json | 3 +- public/{beijiao => beijiaonct}/admin.html | 0 public/{beijiao => beijiaonct}/css/style.css | 0 public/{beijiao => beijiaonct}/index.html | 0 public/{beijiao => beijiaonct}/js/app.js | 0 public/{chencun => chencunnct}/admin.html | 0 public/{chencun => chencunnct}/css/style.css | 0 public/{chencun => chencunnct}/index.html | 0 public/{chencun => chencunnct}/js/app.js | 0 public/index.html | 54 +++++++ vercel.json | 6 + 13 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 api/[campus]/[...path].js create mode 100644 api/_db.js rename public/{beijiao => beijiaonct}/admin.html (100%) rename public/{beijiao => beijiaonct}/css/style.css (100%) rename public/{beijiao => beijiaonct}/index.html (100%) rename public/{beijiao => beijiaonct}/js/app.js (100%) rename public/{chencun => chencunnct}/admin.html (100%) rename public/{chencun => chencunnct}/css/style.css (100%) rename public/{chencun => chencunnct}/index.html (100%) rename public/{chencun => chencunnct}/js/app.js (100%) create mode 100644 public/index.html create mode 100644 vercel.json diff --git a/api/[campus]/[...path].js b/api/[campus]/[...path].js new file mode 100644 index 0000000..807bb72 --- /dev/null +++ b/api/[campus]/[...path].js @@ -0,0 +1,141 @@ +const db = require('../_db'); + +const ALLOWED_CAMPUSES = ['chencunnct', 'beijiaonct']; +const CAMPUS_NAMES = { chencunnct: '陈村校区', beijiaonct: '北滘校区' }; + +module.exports = async function handler(req, res) { + // 设置 CORS + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + return res.status(200).end(); + } + + const campus = req.query.campus; + if (!ALLOWED_CAMPUSES.includes(campus)) { + return res.status(404).json({ ok: false, msg: '校区不存在' }); + } + + const pathParts = (req.query.path || []); + const route = pathParts[0] || ''; + + // 路由分发 + if (route === 'register' && req.method === 'POST') { + return handleRegister(req, res, campus); + } + if (route === 'registrations' && req.method === 'GET') { + return handleGetRegistrations(req, res, campus); + } + if (route === 'stats' && req.method === 'GET') { + return handleGetStats(req, res, campus); + } + if (route === 'registrations' && req.method === 'DELETE') { + return handleDeleteRegistration(req, res, campus); + } + + return res.status(404).json({ ok: false, msg: '接口不存在' }); +}; + +// 报名 +async function handleRegister(req, res, campus) { + const { name, period, teacher } = req.body || {}; + + if (!name || !period || !teacher) { + return res.json({ ok: false, msg: '请填写完整信息' }); + } + + try { + const registrations = await db.getRegistrations(campus); + const id = await db.getNextId(campus); + const now = new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false }) + .replace(/\//g, '-'); + + registrations.unshift({ + id, + name: name.trim(), + period, + teacher, + createdAt: now + }); + + await db.saveRegistrations(campus, registrations); + console.log('[报名·' + CAMPUS_NAMES[campus] + '] ' + name + ' - ' + period + ' - ' + teacher); + return res.json({ ok: true, msg: '报名成功' }); + } catch (err) { + console.error('[报名错误]', err); + return res.json({ ok: false, msg: '服务器错误,请稍后再试' }); + } +} + +// 获取报名列表 +async function handleGetRegistrations(req, res, campus) { + const password = req.query.password || req.headers['x-admin-password']; + if (!db.verifyPassword(password)) { + return res.status(401).json({ ok: false, msg: '密码错误' }); + } + + try { + const registrations = await db.getRegistrations(campus); + return res.json({ ok: true, data: registrations }); + } catch (err) { + console.error('[查询错误]', err); + return res.status(500).json({ ok: false, msg: '服务器错误' }); + } +} + +// 获取统计 +async function handleGetStats(req, res, campus) { + const password = req.query.password || req.headers['x-admin-password']; + if (!db.verifyPassword(password)) { + return res.status(401).json({ ok: false, msg: '密码错误' }); + } + + try { + const registrations = await db.getRegistrations(campus); + const periodStats = {}; + const teacherStats = {}; + + registrations.forEach(function(r) { + periodStats[r.period] = (periodStats[r.period] || 0) + 1; + teacherStats[r.teacher] = (teacherStats[r.teacher] || 0) + 1; + }); + + return res.json({ + ok: true, + data: { + total: registrations.length, + byPeriod: periodStats, + byTeacher: teacherStats + } + }); + } catch (err) { + console.error('[统计错误]', err); + return res.status(500).json({ ok: false, msg: '服务器错误' }); + } +} + +// 删除报名 +async function handleDeleteRegistration(req, res, campus) { + const password = req.query.password || req.headers['x-admin-password']; + if (!db.verifyPassword(password)) { + return res.status(401).json({ ok: false, msg: '密码错误' }); + } + + const idStr = req.query.path ? req.query.path[1] : null; + if (!idStr) { + return res.status(400).json({ ok: false, msg: '缺少 ID' }); + } + + try { + const registrations = await db.getRegistrations(campus); + const id = parseInt(idStr); + const filtered = registrations.filter(function(r) { return r.id !== id; }); + await db.saveRegistrations(campus, filtered); + return res.json({ ok: true, msg: '删除成功' }); + } catch (err) { + console.error('[删除错误]', err); + return res.status(500).json({ ok: false, msg: '服务器错误' }); + } +} diff --git a/api/_db.js b/api/_db.js new file mode 100644 index 0000000..de90391 --- /dev/null +++ b/api/_db.js @@ -0,0 +1,44 @@ +// Vercel KV 存储,带内存回退(首次部署未配置 KV 时也能运行) +let kv = null; +try { + const mod = require('@vercel/kv'); + kv = mod.kv; +} catch (e) { + // KV 未安装,使用内存回退 +} + +const ADMIN_PASSWORD = 'admin123'; + +// 内存存储(KV 不可用时的回退方案) +const memoryStore = {}; + +async function getRegistrations(campus) { + if (kv) { + return await kv.get(campus + ':registrations') || []; + } + return memoryStore[campus] || []; +} + +async function saveRegistrations(campus, data) { + if (kv) { + await kv.set(campus + ':registrations', data); + } else { + memoryStore[campus] = data; + } +} + +async function getNextId(campus) { + if (kv) { + const id = await kv.get(campus + ':nextId') || 1; + await kv.set(campus + ':nextId', id + 1); + return id; + } + if (!memoryStore[campus + ':nextId']) memoryStore[campus + ':nextId'] = 1; + return memoryStore[campus + ':nextId']++; +} + +function verifyPassword(password) { + return password === ADMIN_PASSWORD; +} + +module.exports = { getRegistrations, saveRegistrations, getNextId, verifyPassword }; diff --git a/package.json b/package.json index 2c4e7b4..c0f0ab5 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "type": "commonjs", "dependencies": { "express": "^4.22.2", - "sql.js": "^1.14.1" + "sql.js": "^1.14.1", + "@vercel/kv": "^3.0.1" } } diff --git a/public/beijiao/admin.html b/public/beijiaonct/admin.html similarity index 100% rename from public/beijiao/admin.html rename to public/beijiaonct/admin.html diff --git a/public/beijiao/css/style.css b/public/beijiaonct/css/style.css similarity index 100% rename from public/beijiao/css/style.css rename to public/beijiaonct/css/style.css diff --git a/public/beijiao/index.html b/public/beijiaonct/index.html similarity index 100% rename from public/beijiao/index.html rename to public/beijiaonct/index.html diff --git a/public/beijiao/js/app.js b/public/beijiaonct/js/app.js similarity index 100% rename from public/beijiao/js/app.js rename to public/beijiaonct/js/app.js diff --git a/public/chencun/admin.html b/public/chencunnct/admin.html similarity index 100% rename from public/chencun/admin.html rename to public/chencunnct/admin.html diff --git a/public/chencun/css/style.css b/public/chencunnct/css/style.css similarity index 100% rename from public/chencun/css/style.css rename to public/chencunnct/css/style.css diff --git a/public/chencun/index.html b/public/chencunnct/index.html similarity index 100% rename from public/chencun/index.html rename to public/chencunnct/index.html diff --git a/public/chencun/js/app.js b/public/chencunnct/js/app.js similarity index 100% rename from public/chencun/js/app.js rename to public/chencunnct/js/app.js diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..47e9ead --- /dev/null +++ b/public/index.html @@ -0,0 +1,54 @@ + + + + + + NCT 集训报名 · 选择校区 + + + +
+

🏫 选择校区

+

请选择您所在的校区

+ +
+ + diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..b982ca6 --- /dev/null +++ b/vercel.json @@ -0,0 +1,6 @@ +{ + "rewrites": [ + { "source": "/chencunnct/api/:path*", "destination": "/api/chencunnct/:path*" }, + { "source": "/beijiaonct/api/:path*", "destination": "/api/beijiaonct/:path*" } + ] +}