fix: 服务端验证强化 — 房间ID/坐标/swipe/盲盒竞态/空格

- #15: 房间 ID 扩展为 6 位字母数字,createRoom 用 P2002 重试替代 find-then-create
- #16: 盲盒编辑/删除改用 updateMany/deleteMany 原子操作,防止 TOCTOU
- #17: lat/lng 用 Number.isFinite + 范围校验 (-90~90, -180~180)
- #18: swipe action 必须为 'like' 或 'pass'
- #19: user PUT 的 JSON.parse(preferences) 加 try/catch
- #26: requireString 拒绝纯空格字符串
This commit is contained in:
2026-02-26 20:19:56 +08:00
parent 93f20747e4
commit dfb3cfa136
6 changed files with 41 additions and 31 deletions
+19 -14
View File
@@ -1,4 +1,5 @@
import { prisma } from "./prisma";
import { Prisma } from "@prisma/client";
import { Restaurant, SceneType } from "@/types";
const ROOM_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
@@ -15,8 +16,14 @@ export interface RoomData {
scene: SceneType;
}
const ROOM_ID_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
function generateRoomId(): string {
return String(Math.floor(1000 + Math.random() * 9000));
let id = "";
for (let i = 0; i < 6; i++) {
id += ROOM_ID_CHARS[Math.floor(Math.random() * ROOM_ID_CHARS.length)];
}
return id;
}
function normalize(raw: Partial<RoomData>): RoomData {
@@ -69,26 +76,24 @@ export async function createRoom(restaurants: Restaurant[], creatorId: string, s
};
const expiresAt = new Date(Date.now() + ROOM_TTL_MS);
let roomId: string;
let attempts = 0;
const payload = JSON.stringify(data);
while (attempts < 20) {
roomId = generateRoomId();
const existing = await prisma.room.findUnique({ where: { id: roomId } });
if (!existing) {
for (let attempts = 0; attempts < 20; attempts++) {
const roomId = generateRoomId();
try {
await prisma.room.create({
data: { id: roomId, data: JSON.stringify(data), expiresAt },
data: { id: roomId, data: payload, expiresAt },
});
return roomId;
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") {
continue;
}
throw e;
}
attempts++;
}
roomId = generateRoomId() + String(Date.now()).slice(-2);
await prisma.room.create({
data: { id: roomId, data: JSON.stringify(data), expiresAt },
});
return roomId;
throw new Error("无法生成唯一房间号,请稍后重试");
}
export async function getRoomData(
+1 -1
View File
@@ -43,7 +43,7 @@ export function validateRoomName(raw: unknown, fallback = "我们的周末"): st
}
export function requireString(value: unknown, fieldName: string): string {
if (!value || typeof value !== "string") {
if (!value || typeof value !== "string" || !value.trim()) {
throw new ApiError(`${fieldName}不能为空`);
}
return value;