fix: SSE 认证 + 收藏去重 + 数据库索引和级联删除

- #7: SSE events 接口校验 userId 房间成员身份,start() 加 try/catch
- #9: Favorite 新增 restaurantId 字段做精确去重,不再用 JSON contains
- #10: 补齐 Decision/Favorite/Room/BlindBoxIdea 缺失索引
- #11: Decision/Favorite/BlindBoxMember/BlindBoxIdea 加 onDelete Cascade
This commit is contained in:
2026-02-26 20:15:45 +08:00
parent 9c7f18e0fa
commit 508903b67d
3 changed files with 67 additions and 34 deletions
+24 -5
View File
@@ -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 () => {
+26 -24
View File
@@ -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) => {