- 后台全面引入 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>
43 行
1.1 KiB
JavaScript
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 }
|
|
}
|