diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 8d72ef7..c802679 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,27 +1,22 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import bcrypt from "bcryptjs"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function POST(req: NextRequest) { +export const POST = apiHandler(async (req) => { const { username, password } = await req.json(); - if (!username || !password) { - return NextResponse.json({ error: "请输入用户名和密码" }, { status: 400 }); - } + if (!username || !password) throw new ApiError("请输入用户名和密码"); const user = await prisma.user.findUnique({ where: { username: username.trim() } }); - if (!user) { - return NextResponse.json({ error: "用户名或密码错误" }, { status: 401 }); - } + if (!user) throw new ApiError("用户名或密码错误", 401); const valid = await bcrypt.compare(password, user.passwordHash); - if (!valid) { - return NextResponse.json({ error: "用户名或密码错误" }, { status: 401 }); - } + if (!valid) throw new ApiError("用户名或密码错误", 401); return NextResponse.json({ id: user.id, username: user.username, avatar: user.avatar, }); -} +}); diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index 3f5cc28..37a3c73 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -1,27 +1,21 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import bcrypt from "bcryptjs"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function POST(req: NextRequest) { +export const POST = apiHandler(async (req) => { const { username, password, avatar } = await req.json(); - if (!username || !password) { - return NextResponse.json({ error: "用户名和密码为必填项" }, { status: 400 }); - } + if (!username || !password) throw new ApiError("用户名和密码为必填项"); const trimmedUsername = username.trim(); if (trimmedUsername.length < 2 || trimmedUsername.length > 16) { - return NextResponse.json({ error: "用户名需要 2-16 个字符" }, { status: 400 }); - } - - if (password.length < 6) { - return NextResponse.json({ error: "密码至少 6 个字符" }, { status: 400 }); + throw new ApiError("用户名需要 2-16 个字符"); } + if (password.length < 6) throw new ApiError("密码至少 6 个字符"); const existing = await prisma.user.findUnique({ where: { username: trimmedUsername } }); - if (existing) { - return NextResponse.json({ error: "用户名已被注册" }, { status: 409 }); - } + if (existing) throw new ApiError("用户名已被注册", 409); const passwordHash = await bcrypt.hash(password, 10); @@ -38,4 +32,4 @@ export async function POST(req: NextRequest) { username: user.username, avatar: user.avatar, }); -} +}); diff --git a/src/app/api/blindbox/draw/route.ts b/src/app/api/blindbox/draw/route.ts index bc14902..cab04e2 100644 --- a/src/app/api/blindbox/draw/route.ts +++ b/src/app/api/blindbox/draw/route.ts @@ -1,51 +1,42 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { errorResponse, validateMembership } from "@/lib/blindbox"; +import { validateMembership } from "@/lib/blindbox"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function POST(req: NextRequest) { - try { - const { roomId, userId } = await req.json(); +export const POST = apiHandler(async (req) => { + const { roomId, userId } = await req.json(); - if (!userId || typeof userId !== "string") { - return errorResponse("请先登录", 401); - } - if (!roomId || typeof roomId !== "string") { - return errorResponse("roomId 不能为空", 400); - } + if (!userId || typeof userId !== "string") throw new ApiError("请先登录", 401); + if (!roomId || typeof roomId !== "string") throw new ApiError("roomId 不能为空"); - const isMember = await validateMembership(roomId, userId); - if (!isMember) { - return errorResponse("你不是这个房间的成员", 403); - } + const isMember = await validateMembership(roomId, userId); + if (!isMember) throw new ApiError("你不是这个房间的成员", 403); - const pool = await prisma.blindBoxIdea.findMany({ - where: { roomId, status: "in_pool" }, - select: { id: true }, - }); + const pool = await prisma.blindBoxIdea.findMany({ + where: { roomId, status: "in_pool" }, + select: { id: true }, + }); - if (pool.length === 0) { - return errorResponse("盒子是空的,先往里面塞点想法吧!", 404); - } - - const picked = pool[Math.floor(Math.random() * pool.length)]; - - const idea = await prisma.blindBoxIdea.update({ - where: { id: picked.id }, - data: { status: "drawn", drawnById: userId }, - include: { - user: { select: { id: true, username: true, avatar: true } }, - drawnBy: { select: { id: true, username: true, avatar: true } }, - }, - }); - - return NextResponse.json({ - id: idea.id, - content: idea.content, - createdAt: idea.createdAt, - submitter: idea.user, - drawnBy: idea.drawnBy, - }); - } catch { - return errorResponse("抽取失败", 500); + if (pool.length === 0) { + throw new ApiError("盒子是空的,先往里面塞点想法吧!", 404); } -} + + const picked = pool[Math.floor(Math.random() * pool.length)]; + + const idea = await prisma.blindBoxIdea.update({ + where: { id: picked.id }, + data: { status: "drawn", drawnById: userId }, + include: { + user: { select: { id: true, username: true, avatar: true } }, + drawnBy: { select: { id: true, username: true, avatar: true } }, + }, + }); + + return NextResponse.json({ + id: idea.id, + content: idea.content, + createdAt: idea.createdAt, + submitter: idea.user, + drawnBy: idea.drawnBy, + }); +}); diff --git a/src/app/api/blindbox/room/[code]/route.ts b/src/app/api/blindbox/room/[code]/route.ts index 498b199..5ba4ab9 100644 --- a/src/app/api/blindbox/room/[code]/route.ts +++ b/src/app/api/blindbox/room/[code]/route.ts @@ -1,63 +1,48 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { errorResponse, getRoomByCode } from "@/lib/blindbox"; +import { getRoomByCode } from "@/lib/blindbox"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function GET( - _req: NextRequest, - { params }: { params: Promise<{ code: string }> }, -) { - try { - const { code } = await params; - const room = await getRoomByCode(code.toUpperCase()); +export const GET = apiHandler(async (_req, { params }) => { + const { code } = await params; + const room = await getRoomByCode(code.toUpperCase()); - if (!room) { - return errorResponse("房间不存在", 404); - } + if (!room) throw new ApiError("房间不存在", 404); - return NextResponse.json({ - id: room.id, - code: room.code, - name: room.name, - creatorId: room.creatorId, - poolCount: room._count.ideas, - members: room.members.map((m) => ({ - ...m.user, - joinedAt: m.joinedAt, - })), - }); - } catch { - return errorResponse("获取房间信息失败", 500); + return NextResponse.json({ + id: room.id, + code: room.code, + name: room.name, + creatorId: room.creatorId, + poolCount: room._count.ideas, + members: room.members.map((m) => ({ + ...m.user, + joinedAt: m.joinedAt, + })), + }); +}); + +export const DELETE = apiHandler(async (req, { params }) => { + const { code } = await params; + const { userId } = await req.json(); + + if (!userId) throw new ApiError("缺少用户 ID"); + + const room = await prisma.blindBoxRoom.findUnique({ + where: { code: code.toUpperCase() }, + }); + if (!room) throw new ApiError("房间不存在", 404); + + if (room.creatorId === userId) { + await prisma.blindBoxRoom.delete({ where: { id: room.id } }); + return NextResponse.json({ action: "deleted" }); } -} -export async function DELETE( - req: NextRequest, - { params }: { params: Promise<{ code: string }> }, -) { - try { - const { code } = await params; - const { userId } = await req.json(); + const membership = await prisma.blindBoxMember.findUnique({ + where: { roomId_userId: { roomId: room.id, userId } }, + }); + if (!membership) throw new ApiError("你不是该房间成员", 403); - if (!userId) return errorResponse("缺少用户 ID", 400); - - const room = await prisma.blindBoxRoom.findUnique({ - where: { code: code.toUpperCase() }, - }); - if (!room) return errorResponse("房间不存在", 404); - - if (room.creatorId === userId) { - await prisma.blindBoxRoom.delete({ where: { id: room.id } }); - return NextResponse.json({ action: "deleted" }); - } - - const membership = await prisma.blindBoxMember.findUnique({ - where: { roomId_userId: { roomId: room.id, userId } }, - }); - if (!membership) return errorResponse("你不是该房间成员", 403); - - await prisma.blindBoxMember.delete({ where: { id: membership.id } }); - return NextResponse.json({ action: "left" }); - } catch { - return errorResponse("操作失败", 500); - } -} + await prisma.blindBoxMember.delete({ where: { id: membership.id } }); + return NextResponse.json({ action: "left" }); +}); diff --git a/src/app/api/blindbox/room/join/route.ts b/src/app/api/blindbox/room/join/route.ts index ab3fe0b..8ac094c 100644 --- a/src/app/api/blindbox/room/join/route.ts +++ b/src/app/api/blindbox/room/join/route.ts @@ -1,38 +1,36 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { errorResponse } from "@/lib/blindbox"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function POST(req: NextRequest) { - try { - const { userId, code } = await req.json(); +export const POST = apiHandler(async (req) => { + const { userId, code } = await req.json(); - if (!userId || typeof userId !== "string") { - return errorResponse("请先登录", 401); - } - if (!code || typeof code !== "string") { - return errorResponse("请输入房间号", 400); - } + if (!userId || typeof userId !== "string") throw new ApiError("请先登录", 401); + if (!code || typeof code !== "string") throw new ApiError("请输入房间号"); - const room = await prisma.blindBoxRoom.findUnique({ - where: { code: code.trim().toUpperCase() }, + const room = await prisma.blindBoxRoom.findUnique({ + where: { code: code.trim().toUpperCase() }, + }); + if (!room) throw new ApiError("房间不存在,请检查房间号", 404); + + const existing = await prisma.blindBoxMember.findUnique({ + where: { roomId_userId: { roomId: room.id, userId } }, + }); + if (existing) { + return NextResponse.json({ + id: room.id, + code: room.code, + name: room.name, + alreadyMember: true, }); - if (!room) { - return errorResponse("房间不存在,请检查房间号", 404); - } - - const existing = await prisma.blindBoxMember.findUnique({ - where: { roomId_userId: { roomId: room.id, userId } }, - }); - if (existing) { - return NextResponse.json({ id: room.id, code: room.code, name: room.name, alreadyMember: true }); - } - - await prisma.blindBoxMember.create({ - data: { roomId: room.id, userId }, - }); - - return NextResponse.json({ id: room.id, code: room.code, name: room.name }, { status: 201 }); - } catch { - return errorResponse("加入房间失败", 500); } -} + + await prisma.blindBoxMember.create({ + data: { roomId: room.id, userId }, + }); + + return NextResponse.json( + { id: room.id, code: room.code, name: room.name }, + { status: 201 }, + ); +}); diff --git a/src/app/api/blindbox/room/route.ts b/src/app/api/blindbox/room/route.ts index 46f603c..c5942e8 100644 --- a/src/app/api/blindbox/room/route.ts +++ b/src/app/api/blindbox/room/route.ts @@ -1,40 +1,32 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { errorResponse, generateUniqueRoomCode } from "@/lib/blindbox"; +import { generateUniqueRoomCode } from "@/lib/blindbox"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function POST(req: NextRequest) { - try { - const { userId, name } = await req.json(); +export const POST = apiHandler(async (req) => { + const { userId, name } = await req.json(); - if (!userId || typeof userId !== "string") { - return errorResponse("请先登录", 401); - } + if (!userId || typeof userId !== "string") throw new ApiError("请先登录", 401); - const roomName = (name || "").trim() || "我们的周末"; - if (roomName.length > 30) { - return errorResponse("房间名不能超过 30 个字", 400); - } + const roomName = (name || "").trim() || "我们的周末"; + if (roomName.length > 30) throw new ApiError("房间名不能超过 30 个字"); - const user = await prisma.user.findUnique({ where: { id: userId } }); - if (!user) { - return errorResponse("用户不存在", 404); - } + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) throw new ApiError("用户不存在", 404); - const code = await generateUniqueRoomCode(); + const code = await generateUniqueRoomCode(); - const room = await prisma.blindBoxRoom.create({ - data: { - code, - name: roomName, - creatorId: userId, - members: { - create: { userId }, - }, - }, - }); + const room = await prisma.blindBoxRoom.create({ + data: { + code, + name: roomName, + creatorId: userId, + members: { create: { userId } }, + }, + }); - return NextResponse.json({ id: room.id, code: room.code, name: room.name }, { status: 201 }); - } catch { - return errorResponse("创建房间失败", 500); - } -} + return NextResponse.json( + { id: room.id, code: room.code, name: room.name }, + { status: 201 }, + ); +}); diff --git a/src/app/api/blindbox/rooms/route.ts b/src/app/api/blindbox/rooms/route.ts index 616dad0..6983317 100644 --- a/src/app/api/blindbox/rooms/route.ts +++ b/src/app/api/blindbox/rooms/route.ts @@ -1,69 +1,59 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { errorResponse } from "@/lib/blindbox"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function GET(req: NextRequest) { +export const GET = apiHandler(async (req) => { const userId = req.nextUrl.searchParams.get("userId"); + if (!userId) throw new ApiError("请先登录", 401); - if (!userId) { - return errorResponse("请先登录", 401); - } - - try { - const memberships = await prisma.blindBoxMember.findMany({ - where: { userId }, - include: { - room: { - include: { - members: { - include: { user: { select: { id: true, username: true, avatar: true } } }, - orderBy: { joinedAt: "asc" }, - take: 5, - }, - _count: { - select: { - ideas: true, - members: true, - }, - }, - ideas: { - where: { status: "drawn" }, - orderBy: { createdAt: "desc" }, - take: 1, - select: { content: true, createdAt: true }, - }, + const memberships = await prisma.blindBoxMember.findMany({ + where: { userId }, + include: { + room: { + include: { + members: { + include: { user: { select: { id: true, username: true, avatar: true } } }, + orderBy: { joinedAt: "asc" }, + take: 5, + }, + _count: { + select: { ideas: true, members: true }, + }, + ideas: { + where: { status: "drawn" }, + orderBy: { createdAt: "desc" }, + take: 1, + select: { content: true, createdAt: true }, }, }, }, - orderBy: { joinedAt: "desc" }, - }); + }, + orderBy: { joinedAt: "desc" }, + }); - const rooms = memberships.map((m) => ({ - id: m.room.id, - code: m.room.code, - name: m.room.name, - memberCount: m.room._count.members, - ideaCount: m.room._count.ideas, - poolCount: 0, - members: m.room.members.map((mb) => mb.user), - lastDrawn: m.room.ideas[0] ?? null, - joinedAt: m.joinedAt, - })); + const rooms = memberships.map((m) => ({ + id: m.room.id, + code: m.room.code, + name: m.room.name, + memberCount: m.room._count.members, + ideaCount: m.room._count.ideas, + poolCount: 0, + members: m.room.members.map((mb) => mb.user), + lastDrawn: m.room.ideas[0] ?? null, + joinedAt: m.joinedAt, + })); - const roomIds = rooms.map((r) => r.id); - const poolCounts = await prisma.blindBoxIdea.groupBy({ - by: ["roomId"], - where: { roomId: { in: roomIds }, status: "in_pool" }, - _count: true, - }); - const poolMap = new Map(poolCounts.map((p) => [p.roomId, p._count])); + const roomIds = rooms.map((r) => r.id); + const poolCounts = await prisma.blindBoxIdea.groupBy({ + by: ["roomId"], + where: { roomId: { in: roomIds }, status: "in_pool" }, + _count: true, + }); + const poolMap = new Map(poolCounts.map((p) => [p.roomId, p._count])); - for (const room of rooms) { - room.poolCount = poolMap.get(room.id) ?? 0; - } - - return NextResponse.json({ rooms }); - } catch { - return errorResponse("获取房间列表失败", 500); + for (const room of rooms) { + room.poolCount = poolMap.get(room.id) ?? 0; } -} + + return NextResponse.json({ rooms }); +}); diff --git a/src/app/api/blindbox/route.ts b/src/app/api/blindbox/route.ts index 31acc9e..64237bf 100644 --- a/src/app/api/blindbox/route.ts +++ b/src/app/api/blindbox/route.ts @@ -1,130 +1,95 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { errorResponse, validateMembership } from "@/lib/blindbox"; +import { validateMembership } from "@/lib/blindbox"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function POST(req: NextRequest) { - try { - const { roomId, userId, content } = await req.json(); +export const POST = apiHandler(async (req) => { + const { roomId, userId, content } = await req.json(); - if (!userId || typeof userId !== "string") { - return errorResponse("请先登录", 401); - } - if (!roomId || typeof roomId !== "string") { - return errorResponse("roomId 不能为空", 400); - } - if (!content || typeof content !== "string" || content.trim().length === 0) { - return errorResponse("内容不能为空", 400); - } - if (content.trim().length > 200) { - return errorResponse("内容不能超过 200 字", 400); - } - - const isMember = await validateMembership(roomId, userId); - if (!isMember) { - return errorResponse("你不是这个房间的成员", 403); - } - - const idea = await prisma.blindBoxIdea.create({ - data: { - roomId, - userId, - content: content.trim(), - }, - }); - - return NextResponse.json({ id: idea.id }, { status: 201 }); - } catch { - return errorResponse("提交失败", 500); + if (!userId || typeof userId !== "string") throw new ApiError("请先登录", 401); + if (!roomId || typeof roomId !== "string") throw new ApiError("roomId 不能为空"); + if (!content || typeof content !== "string" || content.trim().length === 0) { + throw new ApiError("内容不能为空"); } -} + if (content.trim().length > 200) throw new ApiError("内容不能超过 200 字"); -export async function GET(req: NextRequest) { + const isMember = await validateMembership(roomId, userId); + if (!isMember) throw new ApiError("你不是这个房间的成员", 403); + + const idea = await prisma.blindBoxIdea.create({ + data: { roomId, userId, content: content.trim() }, + }); + + return NextResponse.json({ id: idea.id }, { status: 201 }); +}); + +export const GET = apiHandler(async (req) => { const roomId = req.nextUrl.searchParams.get("roomId"); const userId = req.nextUrl.searchParams.get("userId"); - if (!roomId) { - return errorResponse("缺少 roomId", 400); - } - if (!userId) { - return errorResponse("请先登录", 401); - } + if (!roomId) throw new ApiError("缺少 roomId"); + if (!userId) throw new ApiError("请先登录", 401); const isMember = await validateMembership(roomId, userId); - if (!isMember) { - return errorResponse("你不是这个房间的成员", 403); + if (!isMember) throw new ApiError("你不是这个房间的成员", 403); + + const [poolCount, myIdeas, drawn] = await Promise.all([ + prisma.blindBoxIdea.count({ + where: { roomId, status: "in_pool" }, + }), + prisma.blindBoxIdea.findMany({ + where: { roomId, userId, status: "in_pool" }, + orderBy: { createdAt: "desc" }, + select: { id: true, content: true, createdAt: true }, + }), + prisma.blindBoxIdea.findMany({ + where: { roomId, status: "drawn" }, + orderBy: { createdAt: "desc" }, + include: { + user: { select: { id: true, username: true, avatar: true } }, + drawnBy: { select: { id: true, username: true, avatar: true } }, + }, + }), + ]); + + return NextResponse.json({ poolCount, myIdeas, drawn }); +}); + +export const PUT = apiHandler(async (req) => { + const { ideaId, userId, content } = await req.json(); + + if (!userId) throw new ApiError("请先登录", 401); + if (!ideaId) throw new ApiError("缺少 ideaId"); + if (!content || typeof content !== "string" || content.trim().length === 0) { + throw new ApiError("内容不能为空"); } + if (content.trim().length > 200) throw new ApiError("内容不能超过 200 字"); - try { - const [poolCount, myIdeas, drawn] = await Promise.all([ - prisma.blindBoxIdea.count({ - where: { roomId, status: "in_pool" }, - }), - prisma.blindBoxIdea.findMany({ - where: { roomId, userId, status: "in_pool" }, - orderBy: { createdAt: "desc" }, - select: { id: true, content: true, createdAt: true }, - }), - prisma.blindBoxIdea.findMany({ - where: { roomId, status: "drawn" }, - orderBy: { createdAt: "desc" }, - include: { - user: { select: { id: true, username: true, avatar: true } }, - drawnBy: { select: { id: true, username: true, avatar: true } }, - }, - }), - ]); + const idea = await prisma.blindBoxIdea.findUnique({ where: { id: ideaId } }); + if (!idea) throw new ApiError("想法不存在", 404); + if (idea.userId !== userId) throw new ApiError("只能编辑自己的想法", 403); + if (idea.status !== "in_pool") throw new ApiError("已抽中的想法不能编辑"); - return NextResponse.json({ poolCount, myIdeas, drawn }); - } catch { - return errorResponse("查询失败", 500); - } -} + const updated = await prisma.blindBoxIdea.update({ + where: { id: ideaId }, + data: { content: content.trim() }, + }); -export async function PUT(req: NextRequest) { - try { - const { ideaId, userId, content } = await req.json(); + return NextResponse.json({ id: updated.id, content: updated.content }); +}); - if (!userId) return errorResponse("请先登录", 401); - if (!ideaId) return errorResponse("缺少 ideaId", 400); - if (!content || typeof content !== "string" || content.trim().length === 0) { - return errorResponse("内容不能为空", 400); - } - if (content.trim().length > 200) { - return errorResponse("内容不能超过 200 字", 400); - } +export const DELETE = apiHandler(async (req) => { + const { ideaId, userId } = await req.json(); - const idea = await prisma.blindBoxIdea.findUnique({ where: { id: ideaId } }); - if (!idea) return errorResponse("想法不存在", 404); - if (idea.userId !== userId) return errorResponse("只能编辑自己的想法", 403); - if (idea.status !== "in_pool") return errorResponse("已抽中的想法不能编辑", 400); + if (!userId) throw new ApiError("请先登录", 401); + if (!ideaId) throw new ApiError("缺少 ideaId"); - const updated = await prisma.blindBoxIdea.update({ - where: { id: ideaId }, - data: { content: content.trim() }, - }); + const idea = await prisma.blindBoxIdea.findUnique({ where: { id: ideaId } }); + if (!idea) throw new ApiError("想法不存在", 404); + if (idea.userId !== userId) throw new ApiError("只能删除自己的想法", 403); + if (idea.status !== "in_pool") throw new ApiError("已抽中的想法不能删除"); - return NextResponse.json({ id: updated.id, content: updated.content }); - } catch { - return errorResponse("编辑失败", 500); - } -} + await prisma.blindBoxIdea.delete({ where: { id: ideaId } }); -export async function DELETE(req: NextRequest) { - try { - const { ideaId, userId } = await req.json(); - - if (!userId) return errorResponse("请先登录", 401); - if (!ideaId) return errorResponse("缺少 ideaId", 400); - - const idea = await prisma.blindBoxIdea.findUnique({ where: { id: ideaId } }); - if (!idea) return errorResponse("想法不存在", 404); - if (idea.userId !== userId) return errorResponse("只能删除自己的想法", 403); - if (idea.status !== "in_pool") return errorResponse("已抽中的想法不能删除", 400); - - await prisma.blindBoxIdea.delete({ where: { id: ideaId } }); - - return NextResponse.json({ deleted: true }); - } catch { - return errorResponse("删除失败", 500); - } -} + return NextResponse.json({ deleted: true }); +}); diff --git a/src/app/api/location/regeo/route.ts b/src/app/api/location/regeo/route.ts index 8225c5a..ec3b6cc 100644 --- a/src/app/api/location/regeo/route.ts +++ b/src/app/api/location/regeo/route.ts @@ -1,54 +1,39 @@ import { NextResponse } from "next/server"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function GET(req: Request) { - const { searchParams } = new URL(req.url); - const lat = searchParams.get("lat"); - const lng = searchParams.get("lng"); +export const GET = apiHandler(async (req) => { + const lat = req.nextUrl.searchParams.get("lat"); + const lng = req.nextUrl.searchParams.get("lng"); - if (!lat || !lng) { - return NextResponse.json( - { error: "lat and lng are required" }, - { status: 400 }, - ); - } + if (!lat || !lng) throw new ApiError("lat and lng are required"); const apiKey = process.env.AMAP_API_KEY; - if (!apiKey) { - return NextResponse.json( - { error: "AMAP_API_KEY not configured" }, - { status: 500 }, - ); - } + if (!apiKey) throw new ApiError("AMAP_API_KEY not configured", 500); - try { - const url = new URL("https://restapi.amap.com/v3/geocode/regeo"); - url.searchParams.set("key", apiKey); - url.searchParams.set("location", `${lng},${lat}`); - url.searchParams.set("extensions", "base"); + const url = new URL("https://restapi.amap.com/v3/geocode/regeo"); + url.searchParams.set("key", apiKey); + url.searchParams.set("location", `${lng},${lat}`); + url.searchParams.set("extensions", "base"); - const res = await fetch(url.toString()); - const data = await res.json(); + const res = await fetch(url.toString()); + const data = await res.json(); - if (data.status !== "1" || !data.regeocode) { - return NextResponse.json({ name: null }); - } - - const comp = data.regeocode.addressComponent; - const district = comp?.district || comp?.city || ""; - const township = comp?.township || ""; - const neighborhood = comp?.neighborhood?.name || ""; - - const name = [district, township, neighborhood] - .filter(Boolean) - .join(" ") - .trim(); - - return NextResponse.json({ - name: name || data.regeocode.formatted_address || null, - formatted: data.regeocode.formatted_address || null, - }); - } catch (e) { - console.error("Regeo error:", e); + if (data.status !== "1" || !data.regeocode) { return NextResponse.json({ name: null }); } -} + + const comp = data.regeocode.addressComponent; + const district = comp?.district || comp?.city || ""; + const township = comp?.township || ""; + const neighborhood = comp?.neighborhood?.name || ""; + + const name = [district, township, neighborhood] + .filter(Boolean) + .join(" ") + .trim(); + + return NextResponse.json({ + name: name || data.regeocode.formatted_address || null, + formatted: data.regeocode.formatted_address || null, + }); +}); diff --git a/src/app/api/location/suggest/route.ts b/src/app/api/location/suggest/route.ts index 2771b72..01c1caf 100644 --- a/src/app/api/location/suggest/route.ts +++ b/src/app/api/location/suggest/route.ts @@ -1,52 +1,37 @@ import { NextResponse } from "next/server"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function GET(req: Request) { - const { searchParams } = new URL(req.url); - const keywords = searchParams.get("keywords")?.trim(); - - if (!keywords) { - return NextResponse.json([]); - } +export const GET = apiHandler(async (req) => { + const keywords = req.nextUrl.searchParams.get("keywords")?.trim(); + if (!keywords) return NextResponse.json([]); const apiKey = process.env.AMAP_API_KEY; - if (!apiKey) { - return NextResponse.json( - { error: "AMAP_API_KEY not configured" }, - { status: 500 }, - ); - } + if (!apiKey) throw new ApiError("AMAP_API_KEY not configured", 500); - try { - const url = new URL("https://restapi.amap.com/v3/assistant/inputtips"); - url.searchParams.set("key", apiKey); - url.searchParams.set("keywords", keywords); - url.searchParams.set("datatype", "poi"); + const url = new URL("https://restapi.amap.com/v3/assistant/inputtips"); + url.searchParams.set("key", apiKey); + url.searchParams.set("keywords", keywords); + url.searchParams.set("datatype", "poi"); - const res = await fetch(url.toString()); - const data = await res.json(); + const res = await fetch(url.toString()); + const data = await res.json(); - if (data.status !== "1" || !data.tips) { - return NextResponse.json([]); - } + if (data.status !== "1" || !data.tips) return NextResponse.json([]); - const suggestions = data.tips - .filter((t: { location?: string }) => t.location && t.location !== "") - .slice(0, 8) - .map((t: { id: string; name: string; district?: string; address?: string; location: string }) => { - const [lng, lat] = t.location.split(",").map(Number); - return { - id: t.id, - name: t.name, - district: t.district || "", - address: t.address || "", - lat, - lng, - }; - }); + const suggestions = data.tips + .filter((t: { location?: string }) => t.location && t.location !== "") + .slice(0, 8) + .map((t: { id: string; name: string; district?: string; address?: string; location: string }) => { + const [lng, lat] = t.location.split(",").map(Number); + return { + id: t.id, + name: t.name, + district: t.district || "", + address: t.address || "", + lat, + lng, + }; + }); - return NextResponse.json(suggestions); - } catch (e) { - console.error("Location suggest error:", e); - return NextResponse.json([]); - } -} + return NextResponse.json(suggestions); +}); diff --git a/src/app/api/room/[id]/join/route.ts b/src/app/api/room/[id]/join/route.ts index ea49960..fb901ae 100644 --- a/src/app/api/room/[id]/join/route.ts +++ b/src/app/api/room/[id]/join/route.ts @@ -1,64 +1,38 @@ import { NextResponse } from "next/server"; import { atomicUpdateRoom } from "@/lib/store"; import { notify } from "@/lib/roomEvents"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function POST( - req: Request, - { params }: { params: Promise<{ id: string }> }, -) { +export const POST = apiHandler(async (req, { params }) => { const { id } = await params; + const { userId } = await req.json(); - try { - const { userId } = await req.json(); - if (!userId) { - return NextResponse.json({ error: "userId required" }, { status: 400 }); + if (!userId) throw new ApiError("userId required"); + + const updated = await atomicUpdateRoom(id, (data) => { + if (data.kickedUsers.includes(userId)) { + throw new ApiError("你已被移出该房间", 403); } - - const updated = await atomicUpdateRoom(id, (data) => { - if (data.kickedUsers.includes(userId)) { - throw new Error("KICKED"); - } - if (data.locked && !data.users.includes(userId)) { - throw new Error("LOCKED"); - } - if (!data.users.includes(userId)) { - data.users.push(userId); - } - return data; - }); - - if (!updated) { - return NextResponse.json( - { error: "房间不存在或已过期" }, - { status: 404 }, - ); + if (data.locked && !data.users.includes(userId)) { + throw new ApiError("房间已锁定,无法加入", 403); } - - notify(id); - - return NextResponse.json({ - roomId: id, - userCount: updated.users.length, - }); - } catch (e) { - if (e instanceof Error) { - if (e.message === "LOCKED") { - return NextResponse.json( - { error: "房间已锁定,无法加入" }, - { status: 403 }, - ); - } - if (e.message === "KICKED") { - return NextResponse.json( - { error: "你已被移出该房间" }, - { status: 403 }, - ); - } + if (!data.users.includes(userId)) { + data.users.push(userId); } - console.error("Failed to join room:", e); + return data; + }); + + if (!updated) { return NextResponse.json( - { error: "加入房间失败" }, - { status: 500 }, + { error: "房间不存在或已过期" }, + { status: 404 }, ); } -} + + notify(id); + + return NextResponse.json({ + roomId: id, + userCount: updated.users.length, + }); +}); diff --git a/src/app/api/room/[id]/manage/route.ts b/src/app/api/room/[id]/manage/route.ts index 1219256..bf900b3 100644 --- a/src/app/api/room/[id]/manage/route.ts +++ b/src/app/api/room/[id]/manage/route.ts @@ -1,106 +1,73 @@ import { NextResponse } from "next/server"; import { atomicUpdateRoom } from "@/lib/store"; import { notify } from "@/lib/roomEvents"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function POST( - req: Request, - { params }: { params: Promise<{ id: string }> }, -) { +export const POST = apiHandler(async (req, { params }) => { const { id } = await params; + const { userId, action, targetUserId } = await req.json(); - try { - const { userId, action, targetUserId } = await req.json(); - if (!userId || !action) { - return NextResponse.json( - { error: "userId and action required" }, - { status: 400 }, - ); + if (!userId || !action) { + throw new ApiError("userId and action required"); + } + + const updated = await atomicUpdateRoom(id, (data) => { + if (data.creatorId !== userId) { + throw new ApiError("只有房主可以执行此操作", 403); } - const updated = await atomicUpdateRoom(id, (data) => { - if (data.creatorId !== userId) { - throw new Error("FORBIDDEN"); - } + switch (action) { + case "lock": + data.locked = true; + break; - switch (action) { - case "lock": - data.locked = true; - break; + case "unlock": + data.locked = false; + break; - case "unlock": - data.locked = false; - break; + case "kick": + if (!targetUserId || targetUserId === userId) { + throw new ApiError("无效的操作对象"); + } + data.users = data.users.filter((u) => u !== targetUserId); + if (!data.kickedUsers.includes(targetUserId)) { + data.kickedUsers.push(targetUserId); + } + delete data.swipeCounts[targetUserId]; + for (const rid of Object.keys(data.likes)) { + data.likes[rid] = data.likes[rid].filter( + (u) => u !== targetUserId, + ); + } + if ( + data.match && + data.likes[data.match]?.length !== data.users.length + ) { + data.match = null; + } + break; - case "kick": - if (!targetUserId || targetUserId === userId) { - throw new Error("INVALID_TARGET"); - } - data.users = data.users.filter((u) => u !== targetUserId); - if (!data.kickedUsers.includes(targetUserId)) { - data.kickedUsers.push(targetUserId); - } - delete data.swipeCounts[targetUserId]; - for (const rid of Object.keys(data.likes)) { - data.likes[rid] = data.likes[rid].filter( - (u) => u !== targetUserId, - ); - } - if ( - data.match && - data.likes[data.match]?.length !== data.users.length - ) { - data.match = null; - } - break; + case "end_voting": + for (const u of data.users) { + data.swipeCounts[u] = data.restaurants.length; + } + break; - case "end_voting": - for (const u of data.users) { - data.swipeCounts[u] = data.restaurants.length; - } - break; - - default: - throw new Error("UNKNOWN_ACTION"); - } - - return data; - }); - - if (!updated) { - return NextResponse.json( - { error: "房间不存在或已过期" }, - { status: 404 }, - ); + default: + throw new ApiError("未知操作"); } - notify(id); + return data; + }); - return NextResponse.json({ ok: true }); - } catch (e) { - if (e instanceof Error) { - if (e.message === "FORBIDDEN") { - return NextResponse.json( - { error: "只有房主可以执行此操作" }, - { status: 403 }, - ); - } - if (e.message === "INVALID_TARGET") { - return NextResponse.json( - { error: "无效的操作对象" }, - { status: 400 }, - ); - } - if (e.message === "UNKNOWN_ACTION") { - return NextResponse.json( - { error: "未知操作" }, - { status: 400 }, - ); - } - } - console.error("Failed to manage room:", e); + if (!updated) { return NextResponse.json( - { error: "操作失败" }, - { status: 500 }, + { error: "房间不存在或已过期" }, + { status: 404 }, ); } -} + + notify(id); + + return NextResponse.json({ ok: true }); +}); diff --git a/src/app/api/room/[id]/reset/route.ts b/src/app/api/room/[id]/reset/route.ts index a5272fd..e02a9d2 100644 --- a/src/app/api/room/[id]/reset/route.ts +++ b/src/app/api/room/[id]/reset/route.ts @@ -1,11 +1,9 @@ import { NextResponse } from "next/server"; import { atomicUpdateRoom } from "@/lib/store"; import { notify } from "@/lib/roomEvents"; +import { apiHandler } from "@/lib/api"; -export async function POST( - req: Request, - { params }: { params: Promise<{ id: string }> }, -) { +export const POST = apiHandler(async (req, { params }) => { const { id } = await params; let restaurantIds: string[] | undefined; @@ -18,33 +16,25 @@ export async function POST( // No body or invalid JSON — plain reset } - try { - const updated = await atomicUpdateRoom(id, (data) => { - if (restaurantIds && restaurantIds.length > 0) { - const idSet = new Set(restaurantIds); - data.restaurants = data.restaurants.filter((r) => idSet.has(r.id)); - } - data.likes = {}; - data.swipeCounts = {}; - data.match = null; - return data; - }); - - if (!updated) { - return NextResponse.json( - { error: "房间不存在或已过期" }, - { status: 404 }, - ); + const updated = await atomicUpdateRoom(id, (data) => { + if (restaurantIds && restaurantIds.length > 0) { + const idSet = new Set(restaurantIds); + data.restaurants = data.restaurants.filter((r) => idSet.has(r.id)); } + data.likes = {}; + data.swipeCounts = {}; + data.match = null; + return data; + }); - notify(id); - - return NextResponse.json({ ok: true }); - } catch (e) { - console.error("Failed to reset room:", e); + if (!updated) { return NextResponse.json( - { error: "重置失败" }, - { status: 500 }, + { error: "房间不存在或已过期" }, + { status: 404 }, ); } -} + + notify(id); + + return NextResponse.json({ ok: true }); +}); diff --git a/src/app/api/room/[id]/route.ts b/src/app/api/room/[id]/route.ts index 7c2a1f5..c7a309d 100644 --- a/src/app/api/room/[id]/route.ts +++ b/src/app/api/room/[id]/route.ts @@ -1,28 +1,18 @@ import { NextResponse } from "next/server"; import { buildRoomStatus } from "@/lib/buildRoomStatus"; +import { apiHandler } from "@/lib/api"; -export async function GET( - _req: Request, - { params }: { params: Promise<{ id: string }> }, -) { +export const GET = apiHandler(async (_req, { params }) => { const { id } = await params; - try { - const status = await buildRoomStatus(id); + const status = await buildRoomStatus(id); - if (!status) { - return NextResponse.json( - { error: "房间不存在或已过期" }, - { status: 404 }, - ); - } - - return NextResponse.json(status); - } catch (e) { - console.error("Failed to get room:", e); + if (!status) { return NextResponse.json( - { error: "获取房间信息失败" }, - { status: 500 }, + { error: "房间不存在或已过期" }, + { status: 404 }, ); } -} + + return NextResponse.json(status); +}); diff --git a/src/app/api/room/[id]/swipe/route.ts b/src/app/api/room/[id]/swipe/route.ts index ffdedbd..c448003 100644 --- a/src/app/api/room/[id]/swipe/route.ts +++ b/src/app/api/room/[id]/swipe/route.ts @@ -1,69 +1,55 @@ import { NextResponse } from "next/server"; import { atomicUpdateRoom } from "@/lib/store"; import { notify } from "@/lib/roomEvents"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function POST( - req: Request, - { params }: { params: Promise<{ id: string }> }, -) { +export const POST = apiHandler(async (req, { params }) => { const { id } = await params; + const { userId, restaurantId, action } = await req.json(); - try { - const { userId, restaurantId, action } = await req.json(); + if (!userId || restaurantId == null || !action) { + throw new ApiError("userId, restaurantId, and action are required"); + } - if (!userId || restaurantId == null || !action) { - return NextResponse.json( - { error: "userId, restaurantId, and action are required" }, - { status: 400 }, - ); - } + const rid = String(restaurantId); - const rid = String(restaurantId); + const updated = await atomicUpdateRoom(id, (data) => { + const restaurantIndex = data.restaurants.findIndex((r) => r.id === rid); + const alreadySwiped = + restaurantIndex >= 0 && + restaurantIndex < (data.swipeCounts[userId] ?? 0); - const updated = await atomicUpdateRoom(id, (data) => { - const restaurantIndex = data.restaurants.findIndex((r) => r.id === rid); - const alreadySwiped = - restaurantIndex >= 0 && - restaurantIndex < (data.swipeCounts[userId] ?? 0); + if (alreadySwiped) return data; - if (alreadySwiped) return data; - - if (action === "like") { - if (!data.likes[rid]) { - data.likes[rid] = []; - } - if (!data.likes[rid].includes(userId)) { - data.likes[rid].push(userId); - } - - if (data.likes[rid].length === data.users.length) { - data.match = rid; - } + if (action === "like") { + if (!data.likes[rid]) { + data.likes[rid] = []; + } + if (!data.likes[rid].includes(userId)) { + data.likes[rid].push(userId); } - data.swipeCounts[userId] = (data.swipeCounts[userId] ?? 0) + 1; - - return data; - }); - - if (!updated) { - return NextResponse.json( - { error: "房间不存在或已过期" }, - { status: 404 }, - ); + if (data.likes[rid].length === data.users.length) { + data.match = rid; + } } - notify(id); + data.swipeCounts[userId] = (data.swipeCounts[userId] ?? 0) + 1; - return NextResponse.json({ - match: updated.match, - likeCount: updated.likes[rid]?.length ?? 0, - }); - } catch (e) { - console.error("Failed to process swipe:", e); + return data; + }); + + if (!updated) { return NextResponse.json( - { error: "操作失败" }, - { status: 500 }, + { error: "房间不存在或已过期" }, + { status: 404 }, ); } -} + + notify(id); + + return NextResponse.json({ + match: updated.match, + likeCount: updated.likes[rid]?.length ?? 0, + }); +}); diff --git a/src/app/api/room/[id]/undo/route.ts b/src/app/api/room/[id]/undo/route.ts index 16ca7c3..651f398 100644 --- a/src/app/api/room/[id]/undo/route.ts +++ b/src/app/api/room/[id]/undo/route.ts @@ -1,60 +1,46 @@ import { NextResponse } from "next/server"; import { atomicUpdateRoom } from "@/lib/store"; import { notify } from "@/lib/roomEvents"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function POST( - req: Request, - { params }: { params: Promise<{ id: string }> }, -) { +export const POST = apiHandler(async (req, { params }) => { const { id } = await params; + const { userId, restaurantId } = await req.json(); - try { - const { userId, restaurantId } = await req.json(); + if (!userId || restaurantId == null) { + throw new ApiError("userId and restaurantId are required"); + } - if (!userId || restaurantId == null) { - return NextResponse.json( - { error: "userId and restaurantId are required" }, - { status: 400 }, - ); + const rid = String(restaurantId); + + const updated = await atomicUpdateRoom(id, (data) => { + if (data.likes[rid]) { + data.likes[rid] = data.likes[rid].filter((u) => u !== userId); + if (data.likes[rid].length === 0) { + delete data.likes[rid]; + } } - const rid = String(restaurantId); - - const updated = await atomicUpdateRoom(id, (data) => { - if (data.likes[rid]) { - data.likes[rid] = data.likes[rid].filter((u) => u !== userId); - if (data.likes[rid].length === 0) { - delete data.likes[rid]; - } - } - - if (data.match === rid) { - data.match = null; - } - - const count = data.swipeCounts[userId] ?? 0; - if (count > 0) { - data.swipeCounts[userId] = count - 1; - } - - return data; - }); - - if (!updated) { - return NextResponse.json( - { error: "房间不存在或已过期" }, - { status: 404 }, - ); + if (data.match === rid) { + data.match = null; } - notify(id); + const count = data.swipeCounts[userId] ?? 0; + if (count > 0) { + data.swipeCounts[userId] = count - 1; + } - return NextResponse.json({ ok: true }); - } catch (e) { - console.error("Failed to undo swipe:", e); + return data; + }); + + if (!updated) { return NextResponse.json( - { error: "撤回失败" }, - { status: 500 }, + { error: "房间不存在或已过期" }, + { status: 404 }, ); } -} + + notify(id); + + return NextResponse.json({ ok: true }); +}); diff --git a/src/app/api/room/create/route.ts b/src/app/api/room/create/route.ts index d4c0921..950dd95 100644 --- a/src/app/api/room/create/route.ts +++ b/src/app/api/room/create/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import { createRoom } from "@/lib/store"; import { Restaurant, SceneType } from "@/types"; import { getSceneConfig } from "@/lib/sceneConfig"; +import { apiHandler, ApiError } from "@/lib/api"; interface AmapPoiV5 { id: string; @@ -101,78 +102,63 @@ function filterByPrice( }); } -export async function POST(req: Request) { - try { - const body = await req.json(); - const { - lat, - lng, - radius = 3000, - priceRange = "any", - cuisine = "不限", - userId = "", - scene = "eat" as SceneType, - } = body; +export const POST = apiHandler(async (req) => { + const body = await req.json(); + const { + lat, + lng, + radius = 3000, + priceRange = "any", + cuisine = "不限", + userId = "", + scene = "eat" as SceneType, + } = body; - const sceneConfig = getSceneConfig(scene === "drink" ? "drink" : "eat"); + const sceneConfig = getSceneConfig(scene === "drink" ? "drink" : "eat"); - if (!lat || !lng) { - return NextResponse.json( - { error: "无法获取位置信息,请允许定位权限后重试" }, - { status: 400 }, - ); - } + if (!lat || !lng) { + throw new ApiError("无法获取位置信息,请允许定位权限后重试"); + } - const apiKey = process.env.AMAP_API_KEY; - if (!apiKey) { - console.error("AMAP_API_KEY not configured"); - return NextResponse.json( - { error: "服务配置异常,请稍后重试" }, - { status: 500 }, - ); - } + const apiKey = process.env.AMAP_API_KEY; + if (!apiKey) { + throw new ApiError("服务配置异常,请稍后重试", 500); + } - const url = new URL("https://restapi.amap.com/v5/place/around"); - url.searchParams.set("key", apiKey); - url.searchParams.set("location", `${lng},${lat}`); - url.searchParams.set("radius", String(radius)); - url.searchParams.set("types", sceneConfig.poiTypes); - url.searchParams.set("show_fields", "business,photos"); - url.searchParams.set("sortrule", "weight"); + const url = new URL("https://restapi.amap.com/v5/place/around"); + url.searchParams.set("key", apiKey); + url.searchParams.set("location", `${lng},${lat}`); + url.searchParams.set("radius", String(radius)); + url.searchParams.set("types", sceneConfig.poiTypes); + url.searchParams.set("show_fields", "business,photos"); + url.searchParams.set("sortrule", "weight"); - const needsPriceFilter = priceRange !== "any"; - url.searchParams.set("page_size", needsPriceFilter ? "25" : "15"); + const needsPriceFilter = priceRange !== "any"; + url.searchParams.set("page_size", needsPriceFilter ? "25" : "15"); - if (cuisine && cuisine !== "不限") { - url.searchParams.set("keywords", cuisine); - } + if (cuisine && cuisine !== "不限") { + url.searchParams.set("keywords", cuisine); + } - const amapRes = await fetch(url.toString()); - const amapData = await amapRes.json(); + const amapRes = await fetch(url.toString()); + const amapData = await amapRes.json(); - let restaurants: Restaurant[] = []; - if (amapData.status === "1" && amapData.pois?.length > 0) { - let results: Restaurant[] = amapData.pois.map( - (poi: AmapPoiV5) => mapPoiToRestaurant(poi, sceneConfig.defaultImage), - ); - results = filterByPrice(results, priceRange); - restaurants = results.slice(0, 15); - } + let restaurants: Restaurant[] = []; + if (amapData.status === "1" && amapData.pois?.length > 0) { + let results: Restaurant[] = amapData.pois.map( + (poi: AmapPoiV5) => mapPoiToRestaurant(poi, sceneConfig.defaultImage), + ); + results = filterByPrice(results, priceRange); + restaurants = results.slice(0, 15); + } - if (restaurants.length === 0) { - return NextResponse.json( - { error: sceneConfig.emptyError }, - { status: 404 }, - ); - } - - const roomId = await createRoom(restaurants, userId, sceneConfig.key); - return NextResponse.json({ roomId, restaurants }); - } catch (e) { - console.error("Failed to create room:", e); + if (restaurants.length === 0) { return NextResponse.json( - { error: "搜索失败,请检查网络后重试" }, - { status: 500 }, + { error: sceneConfig.emptyError }, + { status: 404 }, ); } -} + + const roomId = await createRoom(restaurants, userId, sceneConfig.key); + return NextResponse.json({ roomId, restaurants }); +}); diff --git a/src/app/api/user/favorite/route.ts b/src/app/api/user/favorite/route.ts index e17a8de..6fecf41 100644 --- a/src/app/api/user/favorite/route.ts +++ b/src/app/api/user/favorite/route.ts @@ -1,11 +1,10 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function GET(req: NextRequest) { +export const GET = apiHandler(async (req) => { const userId = req.nextUrl.searchParams.get("userId"); - if (!userId) { - return NextResponse.json([]); - } + if (!userId) return NextResponse.json([]); const favorites = await prisma.favorite.findMany({ where: { userId }, @@ -20,19 +19,15 @@ export async function GET(req: NextRequest) { createdAt: f.createdAt.toISOString(), })), ); -} +}); -export async function POST(req: NextRequest) { +export const POST = apiHandler(async (req) => { const { userId, restaurant } = await req.json(); - if (!userId || !restaurant) { - return NextResponse.json({ error: "缺少必要字段" }, { status: 400 }); - } + if (!userId || !restaurant) throw new ApiError("缺少必要字段"); const user = await prisma.user.findUnique({ where: { id: userId } }); - if (!user) { - return NextResponse.json({ error: "请先设置个人资料" }, { status: 404 }); - } + if (!user) throw new ApiError("请先设置个人资料", 404); const existing = await prisma.favorite.findFirst({ where: { @@ -53,20 +48,16 @@ export async function POST(req: NextRequest) { }); return NextResponse.json({ id: fav.id }); -} +}); -export async function DELETE(req: NextRequest) { +export const DELETE = apiHandler(async (req) => { const { userId, favoriteId } = await req.json(); - if (!userId || !favoriteId) { - return NextResponse.json({ error: "缺少必要字段" }, { status: 400 }); - } + if (!userId || !favoriteId) throw new ApiError("缺少必要字段"); const fav = await prisma.favorite.findUnique({ where: { id: favoriteId } }); - if (!fav || fav.userId !== userId) { - return NextResponse.json({ error: "收藏不存在" }, { status: 404 }); - } + if (!fav || fav.userId !== userId) throw new ApiError("收藏不存在", 404); await prisma.favorite.delete({ where: { id: favoriteId } }); return NextResponse.json({ ok: true }); -} +}); diff --git a/src/app/api/user/history/route.ts b/src/app/api/user/history/route.ts index b5503df..6c40f01 100644 --- a/src/app/api/user/history/route.ts +++ b/src/app/api/user/history/route.ts @@ -1,13 +1,12 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; +import { apiHandler, ApiError } from "@/lib/api"; const MAX_HISTORY = 50; -export async function GET(req: NextRequest) { +export const GET = apiHandler(async (req) => { const userId = req.nextUrl.searchParams.get("userId"); - if (!userId) { - return NextResponse.json([]); - } + if (!userId) return NextResponse.json([]); const decisions = await prisma.decision.findMany({ where: { userId }, @@ -26,20 +25,18 @@ export async function GET(req: NextRequest) { createdAt: d.createdAt.toISOString(), })), ); -} +}); -export async function POST(req: NextRequest) { +export const POST = apiHandler(async (req) => { const { userId, roomId, restaurant, matchType, participants } = await req.json(); if (!userId || !roomId || !restaurant || !matchType) { - return NextResponse.json({ error: "缺少必要字段" }, { status: 400 }); + throw new ApiError("缺少必要字段"); } const user = await prisma.user.findUnique({ where: { id: userId } }); - if (!user) { - return NextResponse.json({ error: "用户未注册" }, { status: 404 }); - } + if (!user) throw new ApiError("用户未注册", 404); const existing = await prisma.decision.findFirst({ where: { userId, roomId }, @@ -73,4 +70,4 @@ export async function POST(req: NextRequest) { } return NextResponse.json({ id: decision.id }); -} +}); diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 2719750..03c8bc0 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -1,17 +1,14 @@ -import { NextRequest, NextResponse } from "next/server"; +import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import bcrypt from "bcryptjs"; +import { apiHandler, ApiError } from "@/lib/api"; -export async function GET(req: NextRequest) { +export const GET = apiHandler(async (req) => { const userId = req.nextUrl.searchParams.get("id"); - if (!userId) { - return NextResponse.json(null); - } + if (!userId) return NextResponse.json(null); const user = await prisma.user.findUnique({ where: { id: userId } }); - if (!user) { - return NextResponse.json(null); - } + if (!user) return NextResponse.json(null); const decisionCount = await prisma.decision.count({ where: { userId } }); @@ -24,48 +21,36 @@ export async function GET(req: NextRequest) { createdAt: user.createdAt.toISOString(), decisionCount, }); -} +}); -export async function PUT(req: NextRequest) { +export const PUT = apiHandler(async (req) => { const body = await req.json(); const { userId } = body; - if (!userId) { - return NextResponse.json({ error: "缺少用户 ID" }, { status: 400 }); - } + if (!userId) throw new ApiError("缺少用户 ID"); const existing = await prisma.user.findUnique({ where: { id: userId } }); - if (!existing) { - return NextResponse.json({ error: "用户不存在" }, { status: 404 }); - } + if (!existing) throw new ApiError("用户不存在", 404); const updateData: Record = {}; if (body.username !== undefined) { const trimmed = body.username.trim(); if (trimmed.length < 2 || trimmed.length > 16) { - return NextResponse.json({ error: "用户名需要 2-16 个字符" }, { status: 400 }); + throw new ApiError("用户名需要 2-16 个字符"); } if (trimmed !== existing.username) { const taken = await prisma.user.findUnique({ where: { username: trimmed } }); - if (taken) { - return NextResponse.json({ error: "用户名已被占用" }, { status: 409 }); - } + if (taken) throw new ApiError("用户名已被占用", 409); } updateData.username = trimmed; } if (body.newPassword !== undefined) { - if (!body.currentPassword) { - return NextResponse.json({ error: "请输入当前密码" }, { status: 400 }); - } + if (!body.currentPassword) throw new ApiError("请输入当前密码"); const valid = await bcrypt.compare(body.currentPassword, existing.passwordHash); - if (!valid) { - return NextResponse.json({ error: "当前密码错误" }, { status: 403 }); - } - if (body.newPassword.length < 6) { - return NextResponse.json({ error: "新密码至少 6 个字符" }, { status: 400 }); - } + if (!valid) throw new ApiError("当前密码错误", 403); + if (body.newPassword.length < 6) throw new ApiError("新密码至少 6 个字符"); updateData.passwordHash = await bcrypt.hash(body.newPassword, 10); } @@ -75,7 +60,7 @@ export async function PUT(req: NextRequest) { if (body.email !== undefined) { if (body.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) { - return NextResponse.json({ error: "邮箱格式不正确" }, { status: 400 }); + throw new ApiError("邮箱格式不正确"); } updateData.email = body.email || null; } @@ -96,4 +81,4 @@ export async function PUT(req: NextRequest) { email: user.email, preferences: JSON.parse(user.preferences), }); -} +}); diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..d2d7da1 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from "next/server"; + +export class ApiError extends Error { + constructor( + message: string, + public status: number = 400, + ) { + super(message); + } +} + +type RouteContext = { params: Promise> }; + +type RouteHandler = ( + req: NextRequest, + ctx: RouteContext, +) => Promise; + +/** + * Wraps a Next.js route handler with unified error handling. + * - ApiError instances are converted to JSON responses with matching status codes + * - Unknown errors are logged and returned as 500 + */ +export function apiHandler(handler: RouteHandler): RouteHandler { + return async (req, ctx) => { + try { + return await handler(req, ctx); + } catch (e) { + if (e instanceof ApiError) { + return NextResponse.json({ error: e.message }, { status: e.status }); + } + console.error(`[API ${req.method} ${req.nextUrl.pathname}]`, e); + return NextResponse.json({ error: "操作失败" }, { status: 500 }); + } + }; +} diff --git a/src/lib/blindbox.ts b/src/lib/blindbox.ts index 3105e85..7376c5a 100644 --- a/src/lib/blindbox.ts +++ b/src/lib/blindbox.ts @@ -1,9 +1,4 @@ import { prisma } from "@/lib/prisma"; -import { NextResponse } from "next/server"; - -export function errorResponse(message: string, status: number) { - return NextResponse.json({ error: message }, { status }); -} export function generateRoomCode(): string { const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";