Mimingguang ee078b61bc feat: 后台管理系统完整重构 + 前后台数据联通
- 后台全面引入 Naive UI 组件库,统一 UI 规范
- 前台 usePublicApi 切换到 API 调用(GET /api/content/[key])
- 前后台数据源统一(site_content 表)
- 产品线统一管理(tour/camp/course 三套编辑器)
- 产品数据归一化(itinerary/faq/pricing 格式统一)
- 表单提交 API 改写入 submissions 表
- 新增 submissions 标记已读 API
- site-content PUT 支持 upsert
- 修复前台 bug(stories 路由冲突、占位符警告、selector breadcrumb)
- 消除 PageHero 类型警告(useSEO reactive 修复)
- 25 个前台页面全部 HTTP 200,展示效果不变

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:48:02 +08:00

43 行
1.1 KiB
JavaScript

/**
* 简单内存速率限制
* 每个 IP 每小时最多允许 MAX_REQUESTS 次提交
*/
const WINDOW_MS = 60 * 60 * 1000 // 1 小时
const MAX_REQUESTS = 5
// Map<ip, { count: number, resetAt: number }>
const store = new Map()
// 每 2 小时清理一次过期记录,防止内存泄漏
setInterval(() => {
const now = Date.now()
for (const [ip, record] of store.entries()) {
if (now > record.resetAt) {
store.delete(ip)
}
}
}, 2 * 60 * 60 * 1000)
/**
* 检查并记录一次请求
* @param {string} ip
* @returns {{ allowed: boolean, remaining: number, resetAt: number }}
*/
export function checkRateLimit(ip) {
const now = Date.now()
const record = store.get(ip)
if (!record || now > record.resetAt) {
store.set(ip, { count: 1, resetAt: now + WINDOW_MS })
return { allowed: true, remaining: MAX_REQUESTS - 1, resetAt: now + WINDOW_MS }
}
if (record.count >= MAX_REQUESTS) {
return { allowed: false, remaining: 0, resetAt: record.resetAt }
}
record.count++
return { allowed: true, remaining: MAX_REQUESTS - record.count, resetAt: record.resetAt }
}