From 1229bb849b2a980bbff1e5d723db6a5f0e8934d2 Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 26 Feb 2026 19:22:17 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8F=90=E5=8F=96=20validation.ts?= =?UTF-8?q?=20=E5=92=8C=20amap.ts=EF=BC=8C=E7=BB=9F=E4=B8=80=20API=20?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E6=A0=A1=E9=AA=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 validation.ts(用户名/密码/邮箱/内容/房间名/必填字段校验) 和 amap.ts(AMAP API key 校验),消除 7 个路由中的重复验证代码。 --- src/app/api/auth/register/route.ts | 8 ++--- src/app/api/blindbox/room/route.ts | 6 ++-- src/app/api/blindbox/route.ts | 25 +++++--------- src/app/api/location/regeo/route.ts | 4 +-- src/app/api/location/suggest/route.ts | 6 ++-- src/app/api/room/create/route.ts | 6 ++-- src/app/api/user/route.ts | 12 +++---- src/lib/amap.ts | 7 ++++ src/lib/validation.ts | 47 +++++++++++++++++++++++++++ 9 files changed, 80 insertions(+), 41 deletions(-) create mode 100644 src/lib/amap.ts create mode 100644 src/lib/validation.ts diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts index 37a3c73..9fbbfd1 100644 --- a/src/app/api/auth/register/route.ts +++ b/src/app/api/auth/register/route.ts @@ -2,17 +2,15 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import bcrypt from "bcryptjs"; import { apiHandler, ApiError } from "@/lib/api"; +import { validateUsername, validatePassword } from "@/lib/validation"; export const POST = apiHandler(async (req) => { const { username, password, avatar } = await req.json(); if (!username || !password) throw new ApiError("用户名和密码为必填项"); - const trimmedUsername = username.trim(); - if (trimmedUsername.length < 2 || trimmedUsername.length > 16) { - throw new ApiError("用户名需要 2-16 个字符"); - } - if (password.length < 6) throw new ApiError("密码至少 6 个字符"); + const trimmedUsername = validateUsername(username); + validatePassword(password); const existing = await prisma.user.findUnique({ where: { username: trimmedUsername } }); if (existing) throw new ApiError("用户名已被注册", 409); diff --git a/src/app/api/blindbox/room/route.ts b/src/app/api/blindbox/room/route.ts index 35b2e19..f7db278 100644 --- a/src/app/api/blindbox/room/route.ts +++ b/src/app/api/blindbox/room/route.ts @@ -1,15 +1,15 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import { generateUniqueRoomCode } from "@/lib/blindbox"; -import { apiHandler, ApiError, requireUserId, requireUser } from "@/lib/api"; +import { apiHandler, requireUserId, requireUser } from "@/lib/api"; +import { validateRoomName } from "@/lib/validation"; export const POST = apiHandler(async (req) => { const { userId, name } = await req.json(); requireUserId(userId); - const roomName = (name || "").trim() || "我们的周末"; - if (roomName.length > 30) throw new ApiError("房间名不能超过 30 个字"); + const roomName = validateRoomName(name); await requireUser(userId); diff --git a/src/app/api/blindbox/route.ts b/src/app/api/blindbox/route.ts index 5dd4665..a5c4de2 100644 --- a/src/app/api/blindbox/route.ts +++ b/src/app/api/blindbox/route.ts @@ -2,31 +2,27 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import { requireMembership } from "@/lib/blindbox"; import { apiHandler, ApiError, requireUserId } from "@/lib/api"; +import { validateIdeaContent, requireString } from "@/lib/validation"; export const POST = apiHandler(async (req) => { const { roomId, userId, content } = await req.json(); requireUserId(userId); - 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 字"); + requireString(roomId, "roomId"); + const trimmedContent = validateIdeaContent(content); await requireMembership(roomId, userId); const idea = await prisma.blindBoxIdea.create({ - data: { roomId, userId, content: content.trim() }, + data: { roomId, userId, content: trimmedContent }, }); return NextResponse.json({ id: idea.id }, { status: 201 }); }); export const GET = apiHandler(async (req) => { - const roomId = req.nextUrl.searchParams.get("roomId"); const userId = requireUserId(req.nextUrl.searchParams.get("userId")); - - if (!roomId) throw new ApiError("缺少 roomId"); + const roomId = requireString(req.nextUrl.searchParams.get("roomId"), "roomId"); await requireMembership(roomId, userId); @@ -56,11 +52,8 @@ export const PUT = apiHandler(async (req) => { const { ideaId, userId, content } = await req.json(); requireUserId(userId); - 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 字"); + requireString(ideaId, "ideaId"); + const trimmedContent = validateIdeaContent(content); const idea = await prisma.blindBoxIdea.findUnique({ where: { id: ideaId } }); if (!idea) throw new ApiError("想法不存在", 404); @@ -69,7 +62,7 @@ export const PUT = apiHandler(async (req) => { const updated = await prisma.blindBoxIdea.update({ where: { id: ideaId }, - data: { content: content.trim() }, + data: { content: trimmedContent }, }); return NextResponse.json({ id: updated.id, content: updated.content }); @@ -79,7 +72,7 @@ export const DELETE = apiHandler(async (req) => { const { ideaId, userId } = await req.json(); requireUserId(userId); - if (!ideaId) throw new ApiError("缺少 ideaId"); + requireString(ideaId, "ideaId"); const idea = await prisma.blindBoxIdea.findUnique({ where: { id: ideaId } }); if (!idea) throw new ApiError("想法不存在", 404); diff --git a/src/app/api/location/regeo/route.ts b/src/app/api/location/regeo/route.ts index ec3b6cc..833cf8c 100644 --- a/src/app/api/location/regeo/route.ts +++ b/src/app/api/location/regeo/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { apiHandler, ApiError } from "@/lib/api"; +import { requireAmapApiKey } from "@/lib/amap"; export const GET = apiHandler(async (req) => { const lat = req.nextUrl.searchParams.get("lat"); @@ -7,8 +8,7 @@ export const GET = apiHandler(async (req) => { if (!lat || !lng) throw new ApiError("lat and lng are required"); - const apiKey = process.env.AMAP_API_KEY; - if (!apiKey) throw new ApiError("AMAP_API_KEY not configured", 500); + const apiKey = requireAmapApiKey(); const url = new URL("https://restapi.amap.com/v3/geocode/regeo"); url.searchParams.set("key", apiKey); diff --git a/src/app/api/location/suggest/route.ts b/src/app/api/location/suggest/route.ts index 01c1caf..69fd5f1 100644 --- a/src/app/api/location/suggest/route.ts +++ b/src/app/api/location/suggest/route.ts @@ -1,12 +1,12 @@ import { NextResponse } from "next/server"; -import { apiHandler, ApiError } from "@/lib/api"; +import { apiHandler } from "@/lib/api"; +import { requireAmapApiKey } from "@/lib/amap"; 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) throw new ApiError("AMAP_API_KEY not configured", 500); + const apiKey = requireAmapApiKey(); const url = new URL("https://restapi.amap.com/v3/assistant/inputtips"); url.searchParams.set("key", apiKey); diff --git a/src/app/api/room/create/route.ts b/src/app/api/room/create/route.ts index 950dd95..37d12f3 100644 --- a/src/app/api/room/create/route.ts +++ b/src/app/api/room/create/route.ts @@ -3,6 +3,7 @@ import { createRoom } from "@/lib/store"; import { Restaurant, SceneType } from "@/types"; import { getSceneConfig } from "@/lib/sceneConfig"; import { apiHandler, ApiError } from "@/lib/api"; +import { requireAmapApiKey } from "@/lib/amap"; interface AmapPoiV5 { id: string; @@ -120,10 +121,7 @@ export const POST = apiHandler(async (req) => { throw new ApiError("无法获取位置信息,请允许定位权限后重试"); } - const apiKey = process.env.AMAP_API_KEY; - if (!apiKey) { - throw new ApiError("服务配置异常,请稍后重试", 500); - } + const apiKey = requireAmapApiKey(); const url = new URL("https://restapi.amap.com/v5/place/around"); url.searchParams.set("key", apiKey); diff --git a/src/app/api/user/route.ts b/src/app/api/user/route.ts index 6c6f8e1..6f07c9e 100644 --- a/src/app/api/user/route.ts +++ b/src/app/api/user/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import bcrypt from "bcryptjs"; import { apiHandler, ApiError, requireUserId, requireUser } from "@/lib/api"; +import { validateUsername, validatePassword, validateEmail } from "@/lib/validation"; export const GET = apiHandler(async (req) => { const userId = req.nextUrl.searchParams.get("id"); @@ -33,10 +34,7 @@ export const PUT = apiHandler(async (req) => { const updateData: Record = {}; if (body.username !== undefined) { - const trimmed = body.username.trim(); - if (trimmed.length < 2 || trimmed.length > 16) { - throw new ApiError("用户名需要 2-16 个字符"); - } + const trimmed = validateUsername(body.username); if (trimmed !== existing.username) { const taken = await prisma.user.findUnique({ where: { username: trimmed } }); if (taken) throw new ApiError("用户名已被占用", 409); @@ -48,7 +46,7 @@ export const PUT = apiHandler(async (req) => { if (!body.currentPassword) throw new ApiError("请输入当前密码"); const valid = await bcrypt.compare(body.currentPassword, existing.passwordHash); if (!valid) throw new ApiError("当前密码错误", 403); - if (body.newPassword.length < 6) throw new ApiError("新密码至少 6 个字符"); + validatePassword(body.newPassword, "新密码"); updateData.passwordHash = await bcrypt.hash(body.newPassword, 10); } @@ -57,9 +55,7 @@ export const PUT = apiHandler(async (req) => { } if (body.email !== undefined) { - if (body.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) { - throw new ApiError("邮箱格式不正确"); - } + if (body.email) validateEmail(body.email); updateData.email = body.email || null; } diff --git a/src/lib/amap.ts b/src/lib/amap.ts new file mode 100644 index 0000000..bb72826 --- /dev/null +++ b/src/lib/amap.ts @@ -0,0 +1,7 @@ +import { ApiError } from "@/lib/api"; + +export function requireAmapApiKey(): string { + const key = process.env.AMAP_API_KEY; + if (!key) throw new ApiError("服务配置异常,请稍后重试", 500); + return key; +} diff --git a/src/lib/validation.ts b/src/lib/validation.ts new file mode 100644 index 0000000..c9f5b5b --- /dev/null +++ b/src/lib/validation.ts @@ -0,0 +1,47 @@ +import { ApiError } from "@/lib/api"; + +export function validateUsername(raw: string): string { + const trimmed = raw.trim(); + if (trimmed.length < 2 || trimmed.length > 16) { + throw new ApiError("用户名需要 2-16 个字符"); + } + return trimmed; +} + +export function validatePassword(password: string, label = "密码"): void { + if (password.length < 6) { + throw new ApiError(`${label}至少 6 个字符`); + } +} + +export function validateEmail(email: string): void { + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + throw new ApiError("邮箱格式不正确"); + } +} + +export function validateIdeaContent(raw: unknown): string { + if (!raw || typeof raw !== "string" || raw.trim().length === 0) { + throw new ApiError("内容不能为空"); + } + const trimmed = raw.trim(); + if (trimmed.length > 200) { + throw new ApiError("内容不能超过 200 字"); + } + return trimmed; +} + +export function validateRoomName(raw: unknown, fallback = "我们的周末"): string { + const trimmed = ((raw as string) || "").trim() || fallback; + if (trimmed.length > 30) { + throw new ApiError("房间名不能超过 30 个字"); + } + return trimmed; +} + +export function requireString(value: unknown, fieldName: string): string { + if (!value || typeof value !== "string") { + throw new ApiError(`${fieldName}不能为空`); + } + return value; +}