From 508903b67d4ed4011f7dadeff702e4cd67ea4d83 Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 26 Feb 2026 20:15:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20SSE=20=E8=AE=A4=E8=AF=81=20+=20=E6=94=B6?= =?UTF-8?q?=E8=97=8F=E5=8E=BB=E9=87=8D=20+=20=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E7=B4=A2=E5=BC=95=E5=92=8C=E7=BA=A7=E8=81=94=E5=88=A0=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #7: SSE events 接口校验 userId 房间成员身份,start() 加 try/catch - #9: Favorite 新增 restaurantId 字段做精确去重,不再用 JSON contains - #10: 补齐 Decision/Favorite/Room/BlindBoxIdea 缺失索引 - #11: Decision/Favorite/BlindBoxMember/BlindBoxIdea 加 onDelete Cascade --- prisma/schema.prisma | 22 +++++++++--- src/app/api/room/[id]/events/route.ts | 29 +++++++++++++--- src/app/api/user/favorite/route.ts | 50 ++++++++++++++------------- 3 files changed, 67 insertions(+), 34 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6459a79..58a6b1c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,6 +12,8 @@ model Room { data String createdAt DateTime @default(now()) expiresAt DateTime + + @@index([expiresAt]) } model User { @@ -40,15 +42,22 @@ model Decision { matchType String participants Int createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([roomId]) } model Favorite { id String @id @default(cuid()) userId String + restaurantId String @default("") restaurantData String createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([userId, restaurantId]) + @@index([userId]) } model BlindBoxRoom { @@ -70,7 +79,7 @@ model BlindBoxMember { joinedAt DateTime @default(now()) room BlindBoxRoom @relation(fields: [roomId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id]) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([roomId, userId]) } @@ -85,6 +94,9 @@ model BlindBoxIdea { createdAt DateTime @default(now()) room BlindBoxRoom @relation(fields: [roomId], references: [id], onDelete: Cascade) - user User @relation("IdeaSubmitter", fields: [userId], references: [id]) - drawnBy User? @relation("IdeaDrawer", fields: [drawnById], references: [id]) + user User @relation("IdeaSubmitter", fields: [userId], references: [id], onDelete: Cascade) + drawnBy User? @relation("IdeaDrawer", fields: [drawnById], references: [id], onDelete: SetNull) + + @@index([roomId, status]) + @@index([userId]) } diff --git a/src/app/api/room/[id]/events/route.ts b/src/app/api/room/[id]/events/route.ts index 60a4348..a5fe318 100644 --- a/src/app/api/room/[id]/events/route.ts +++ b/src/app/api/room/[id]/events/route.ts @@ -1,4 +1,5 @@ import { buildRoomStatus } from "@/lib/buildRoomStatus"; +import { getRoomData } from "@/lib/store"; import { subscribe } from "@/lib/roomEvents"; export const dynamic = "force-dynamic"; @@ -9,6 +10,19 @@ export async function GET( ) { const { id } = await params; + const url = new URL(req.url); + const userId = url.searchParams.get("userId"); + + if (userId) { + const data = await getRoomData(id); + if (data && !data.users.includes(userId)) { + return new Response(JSON.stringify({ error: "not_a_member" }), { + status: 403, + headers: { "Content-Type": "application/json" }, + }); + } + } + const encoder = new TextEncoder(); const stream = new ReadableStream({ @@ -26,13 +40,18 @@ export async function GET( let alive = true; (async () => { - const status = await buildRoomStatus(id); - if (!status) { - send({ error: "room_not_found" }); + try { + const status = await buildRoomStatus(id); + if (!status) { + send({ error: "room_not_found" }); + controller.close(); + return; + } + if (alive) send(status); + } catch { + send({ error: "load_failed" }); controller.close(); - return; } - if (alive) send(status); })(); const unsubscribe = subscribe(id, async () => { diff --git a/src/app/api/user/favorite/route.ts b/src/app/api/user/favorite/route.ts index dd97664..0095884 100644 --- a/src/app/api/user/favorite/route.ts +++ b/src/app/api/user/favorite/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; +import { Prisma } from "@prisma/client"; import { apiHandler, ApiError, requireUserId, requireUser } from "@/lib/api"; export const GET = apiHandler(async (req) => { @@ -13,11 +14,11 @@ export const GET = apiHandler(async (req) => { }); return NextResponse.json( - favorites.map((f) => ({ - id: f.id, - restaurantData: JSON.parse(f.restaurantData), - createdAt: f.createdAt.toISOString(), - })), + favorites.map((f) => { + let restaurantData = {}; + try { restaurantData = JSON.parse(f.restaurantData); } catch { /* ignore */ } + return { id: f.id, restaurantData, createdAt: f.createdAt.toISOString() }; + }), ); }); @@ -25,29 +26,30 @@ export const POST = apiHandler(async (req) => { const { userId, restaurant } = await req.json(); requireUserId(userId); - if (!restaurant) throw new ApiError("缺少必要字段"); + if (!restaurant?.id || typeof restaurant.id !== "string") { + throw new ApiError("缺少必要字段"); + } await requireUser(userId); - const existing = await prisma.favorite.findFirst({ - where: { - userId, - restaurantData: { contains: `"id":"${restaurant.id}"` }, - }, - }); - - if (existing) { - return NextResponse.json({ id: existing.id, alreadyExists: true }); + try { + const fav = await prisma.favorite.create({ + data: { + userId, + restaurantId: restaurant.id, + restaurantData: JSON.stringify(restaurant), + }, + }); + return NextResponse.json({ id: fav.id }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") { + const existing = await prisma.favorite.findFirst({ + where: { userId, restaurantId: restaurant.id }, + }); + return NextResponse.json({ id: existing?.id ?? "", alreadyExists: true }); + } + throw e; } - - const fav = await prisma.favorite.create({ - data: { - userId, - restaurantData: JSON.stringify(restaurant), - }, - }); - - return NextResponse.json({ id: fav.id }); }); export const DELETE = apiHandler(async (req) => {