Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions app/api/ai/recommend/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,47 @@
import { NextResponse } from "next/server"
import { NextRequest, NextResponse } from "next/server"
import { BITES_CATALOG, AccessibilityFilter, BitesRestaurant, filterBitesCatalog } from "@/lib/bitesCatalog"
import { chatSpark, SparkMessage } from "@/lib/spark"

export const runtime = "nodejs"

const RATE_LIMIT_WINDOW_MS = 60_000
const RATE_LIMIT_MAX = 5

const ipHits = new Map<string, number[]>()

function cleanupRateLimitMap() {
const now = Date.now()
for (const [ip, hits] of ipHits) {
const recent = hits.filter((t) => now - t < RATE_LIMIT_WINDOW_MS)
if (recent.length === 0) {
ipHits.delete(ip)
} else {
ipHits.set(ip, recent)
}
}
}

function getClientIP(req: NextRequest): string {
return req.headers.get("x-real-ip")
|| req.headers.get("x-forwarded-for")?.split(",")[0]?.trim()
|| "unknown"
}

function checkRateLimit(ip: string): boolean {
const now = Date.now()
const hits = ipHits.get(ip) || []
const recent = hits.filter((t) => now - t < RATE_LIMIT_WINDOW_MS)
if (recent.length >= RATE_LIMIT_MAX) {
ipHits.set(ip, recent)
return false
}
recent.push(now)
ipHits.set(ip, recent)
// 每次写入时触发一次清理,避免 Map 无限增长
if (Math.random() < 0.1) cleanupRateLimitMap()
return true
}

interface RecommendRequestBody {
location?: string
preferences?: string
Expand Down Expand Up @@ -59,7 +97,7 @@ function safeParseJson(input: string): any {
}
}

export async function POST(req: Request) {
export async function POST(req: NextRequest) {
let location = ""
let preferences = ""
let accessibility: AccessibilityFilter = {}
Expand All @@ -73,6 +111,15 @@ export async function POST(req: Request) {
return NextResponse.json({ recommendations: filterBitesCatalog("", {}).slice(0, 5), source: "fallback_bad_request" })
}

const ip = getClientIP(req)
if (!checkRateLimit(ip)) {
const fallback = filterBitesCatalog(location, accessibility)
return NextResponse.json({
recommendations: fallback.slice(0, 5),
source: "fallback_rate_limited",
}, { status: 429 })
}

const filtersText = `听障友好: ${accessibility?.deafFriendly ? "是" : "否"}; 视障友好: ${accessibility?.blindFriendly ? "是" : "否"}`

const system: SparkMessage = {
Expand Down