From 4a5ed3b25af155dbb7bec8dbfb024c41d43cc4d0 Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 3 Mar 2026 13:07:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=88=BF=E9=97=B4=E7=82=B9?= =?UTF-8?q?=E8=B5=9E=E5=90=8C=E6=AD=A5=E4=B8=BA=E5=A2=9E=E9=87=8F=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_AUDIT_2026-03-03.md | 12 ++--- src/lib/roomRepository.test.ts | 58 ++++++++++++++++++++++++ src/lib/roomRepository.ts | 81 +++++++++++++++++++++++++++++----- 3 files changed, 134 insertions(+), 17 deletions(-) create mode 100644 src/lib/roomRepository.test.ts diff --git a/PROJECT_AUDIT_2026-03-03.md b/PROJECT_AUDIT_2026-03-03.md index a46af51..b6b2b85 100644 --- a/PROJECT_AUDIT_2026-03-03.md +++ b/PROJECT_AUDIT_2026-03-03.md @@ -211,12 +211,14 @@ - 证据: - `src/lib/planQueries.ts` 已由“逐条房间查询”改为“单次查询带房间关系”。 -### R2 `atomicUpdateRoom` 对 likes 的“全删全建”策略成本较高 +### R2 `atomicUpdateRoom` 对 likes 的“全删全建”策略成本较高【已完成】 +- 修复状态:✅ 已完成(2026-03-03) +- 修复内容: + - 在 `roomRepository` 中新增 `diffRoomLikes`,按差集计算 `toCreate/toDelete`; + - `atomicUpdateRoom` 改为 likes 增量更新(`deleteMany + createMany` 仅处理变更项),替代全量重建; + - 补充 `src/lib/roomRepository.test.ts` 验证增量 diff 行为与去重逻辑。 - 证据: - - `src/lib/roomRepository.ts:185-195` -- 建议: - - 按增量 diff 更新(新增/删除差集)替代全量重建; - - 对高频路径(swipe/undo)优先优化。 + - `src/lib/roomRepository.ts` 已移除 likes 全删全建逻辑,改为差量同步。 ### R3 请求参数契约不统一(前端仍大量发送已废弃 `userId`) - 证据: diff --git a/src/lib/roomRepository.test.ts b/src/lib/roomRepository.test.ts new file mode 100644 index 0000000..1ba0f65 --- /dev/null +++ b/src/lib/roomRepository.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from "vitest"; +import { diffRoomLikes } from "@/lib/roomRepository"; + +describe("diffRoomLikes", () => { + it("returns incremental create/delete changes", () => { + const current = { + r1: ["u1", "u2"], + r2: ["u3"], + }; + const updated = { + r1: ["u1", "u3"], + r3: ["u2"], + }; + + const result = diffRoomLikes(current, updated); + + expect(result.toDelete).toEqual( + expect.arrayContaining([ + { userId: "u2", restaurantId: "r1" }, + { userId: "u3", restaurantId: "r2" }, + ]), + ); + expect(result.toCreate).toEqual( + expect.arrayContaining([ + { userId: "u3", restaurantId: "r1" }, + { userId: "u2", restaurantId: "r3" }, + ]), + ); + }); + + it("deduplicates repeated user likes", () => { + const current = { + r1: ["u1", "u1", "u1"], + }; + const updated = { + r1: ["u1"], + }; + + const result = diffRoomLikes(current, updated); + + expect(result.toCreate).toEqual([]); + expect(result.toDelete).toEqual([]); + }); + + it("returns empty arrays when likes are unchanged", () => { + const current = { + r1: ["u1", "u2"], + }; + const updated = { + r1: ["u1", "u2"], + }; + + const result = diffRoomLikes(current, updated); + + expect(result.toCreate).toEqual([]); + expect(result.toDelete).toEqual([]); + }); +}); diff --git a/src/lib/roomRepository.ts b/src/lib/roomRepository.ts index 5287bbd..64c7591 100644 --- a/src/lib/roomRepository.ts +++ b/src/lib/roomRepository.ts @@ -16,8 +16,56 @@ export interface RoomData { scene: SceneType; } +interface RoomLikeRow { + userId: string; + restaurantId: string; +} + const ROOM_ID_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; +function normalizeLikeRows(likes: Record): RoomLikeRow[] { + const rows: RoomLikeRow[] = []; + const seen = new Set(); + + for (const [restaurantId, userIds] of Object.entries(likes)) { + for (const userId of userIds) { + const key = `${userId}::${restaurantId}`; + if (seen.has(key)) continue; + seen.add(key); + rows.push({ userId, restaurantId }); + } + } + + return rows; +} + +export function diffRoomLikes( + currentLikes: Record, + updatedLikes: Record, +): { toCreate: RoomLikeRow[]; toDelete: RoomLikeRow[] } { + const currentRows = normalizeLikeRows(currentLikes); + const updatedRows = normalizeLikeRows(updatedLikes); + + const currentMap = new Map( + currentRows.map((row) => [`${row.userId}::${row.restaurantId}`, row]), + ); + const updatedMap = new Map( + updatedRows.map((row) => [`${row.userId}::${row.restaurantId}`, row]), + ); + + const toCreate: RoomLikeRow[] = []; + const toDelete: RoomLikeRow[] = []; + + for (const [key, row] of updatedMap) { + if (!currentMap.has(key)) toCreate.push(row); + } + for (const [key, row] of currentMap) { + if (!updatedMap.has(key)) toDelete.push(row); + } + + return { toCreate, toDelete }; +} + function generateRoomId(): string { let id = ""; for (let i = 0; i < 6; i++) { @@ -181,18 +229,27 @@ export async function atomicUpdateRoom( }); } - // Sync likes: rebuild from updated data - if (JSON.stringify(current.likes) !== JSON.stringify(updated.likes)) { - await tx.roomLike.deleteMany({ where: { roomId } }); - const likeRows: { roomId: string; userId: string; restaurantId: string }[] = []; - for (const [restaurantId, userIds] of Object.entries(updated.likes)) { - for (const userId of userIds) { - likeRows.push({ roomId, userId, restaurantId }); - } - } - if (likeRows.length > 0) { - await tx.roomLike.createMany({ data: likeRows }); - } + // Sync likes incrementally to avoid full delete + rebuild on every change. + const { toCreate, toDelete } = diffRoomLikes(current.likes, updated.likes); + if (toDelete.length > 0) { + await tx.roomLike.deleteMany({ + where: { + roomId, + OR: toDelete.map((row) => ({ + userId: row.userId, + restaurantId: row.restaurantId, + })), + }, + }); + } + if (toCreate.length > 0) { + await tx.roomLike.createMany({ + data: toCreate.map((row) => ({ + roomId, + userId: row.userId, + restaurantId: row.restaurantId, + })), + }); } // Sync swipes