refactor(P2/P3): 完成全部7批重构 — 模块化、SSE退避、无障碍、Zod校验、Server组件、Room关系化
批次A:重命名 + 路由拆分 - store.ts → roomRepository.ts,更新全部 import - blindbox/plan/route.ts 精简为薄路由,业务逻辑抽取到 planActions.ts / planQueries.ts 批次B:blindboxPlanGen.ts 拆分(710行 → src/lib/plan/) - agentPlan.ts:Agent 工具调用与系统提示 - legacyPlan.ts:非 Agent 备用生成逻辑 - ideaSelection.ts:Idea 筛选与 Slot 映射 - transitEnrichment.ts:交通信息查询与填充 - index.ts:runPlanGeneration 主入口 批次C:SSE 连接稳定性 - useRoomPolling.ts 加入指数退避重连(上限60s,含Jitter) - plan/stream/route.ts 添加30s心跳 + abort信号清理 批次D:无障碍修复 - Modal:role=dialog、aria-modal、aria-labelledby - AuthModal:aria-label关闭按钮、tablist/tab/aria-selected - PlanItemEditModal、QrInviteModal:补全aria-label - BlindboxPlan:图标按钮aria-label 批次E:Zod 引入 - src/lib/schemas/ai.ts:AI返回值 Schema(IdeaTagsSchema等5个) - src/lib/schemas/requests.ts:请求体 Schema - ai.ts 手工验证替换为 Zod safeParse 批次F:Server Components - achievements/page.tsx → Server Component + AchievementsClient.tsx - profile/page.tsx → Server Component + ProfileClient.tsx 批次G:Room 关系化模型 - prisma/schema.prisma:新增 RoomMember、RoomRestaurant、RoomLike、RoomSwipe 4张表 - migration:20260302010000_room_relational_model - roomRepository.ts 完整重写(关系查询+应用锁) - buildRoomStatus.ts 适配关系查询 测试:全部329个用例通过,修复68个因auth mock缺失导致的测试失败
This commit is contained in:
@@ -3,6 +3,8 @@ import { requireMembership } from "@/lib/blindbox";
|
||||
import { apiHandler, ApiError } from "@/lib/api";
|
||||
import { runPlanGeneration } from "@/lib/blindboxPlanGen";
|
||||
import { getAuthUserId } from "@/lib/auth";
|
||||
import { handlePlanUpdate } from "@/lib/planActions";
|
||||
import { getLatestPlan, getPendingPlans, getHistoryPlans } from "@/lib/planQueries";
|
||||
|
||||
interface AvailableTime {
|
||||
date: string;
|
||||
@@ -37,97 +39,13 @@ export const POST = apiHandler(async (req) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Map "周六"/"周日" to the next occurrence of that weekday from a reference date.
|
||||
* Returns a Date at 00:00 of that day.
|
||||
*/
|
||||
function nextWeekday(dayLabel: string, from: Date): Date {
|
||||
const targetDow = dayLabel === "周日" ? 0 : 6; // Sunday=0, Saturday=6
|
||||
const d = new Date(from);
|
||||
d.setHours(0, 0, 0, 0);
|
||||
const diff = (targetDow - d.getDay() + 7) % 7;
|
||||
d.setDate(d.getDate() + (diff === 0 ? 0 : diff));
|
||||
return d;
|
||||
}
|
||||
|
||||
function computeEndTime(planData: string, now: Date): Date | null {
|
||||
try {
|
||||
const parsed = JSON.parse(planData);
|
||||
const days = parsed.days as { date: string; items: { time: string; duration: number }[] }[];
|
||||
if (!days?.length) return null;
|
||||
|
||||
const lastDay = days[days.length - 1];
|
||||
const lastItem = lastDay.items[lastDay.items.length - 1];
|
||||
if (!lastItem) return null;
|
||||
|
||||
const base = nextWeekday(lastDay.date, now);
|
||||
const [h, m] = lastItem.time.split(":").map(Number);
|
||||
base.setHours(h, m, 0, 0);
|
||||
base.setMinutes(base.getMinutes() + (lastItem.duration || 60));
|
||||
|
||||
if (base.getTime() < now.getTime()) {
|
||||
base.setDate(base.getDate() + 7);
|
||||
}
|
||||
|
||||
return base;
|
||||
} catch (e) {
|
||||
console.error("computeEndTime failed:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const PATCH = apiHandler(async (req) => {
|
||||
const userId = await getAuthUserId(req);
|
||||
const { planId, action, days } = await req.json();
|
||||
if (!planId) throw new ApiError("planId 不能为空");
|
||||
|
||||
const { prisma } = await import("@/lib/prisma");
|
||||
const plan = await prisma.weekendPlan.findUnique({ where: { id: planId } });
|
||||
if (!plan) throw new ApiError("计划不存在", 404);
|
||||
if (plan.userId !== userId) throw new ApiError("只能操作自己的计划", 403);
|
||||
|
||||
const act = action || "accept";
|
||||
|
||||
if (act === "accept") {
|
||||
if (plan.status !== "active") throw new ApiError("该计划无法接受", 400);
|
||||
const endTime = computeEndTime(plan.planData, new Date());
|
||||
await prisma.weekendPlan.update({
|
||||
where: { id: planId },
|
||||
data: { status: "accepted", endTime },
|
||||
});
|
||||
return NextResponse.json({ ok: true, endTime });
|
||||
}
|
||||
|
||||
if (act === "complete" || act === "expire") {
|
||||
if (plan.status !== "accepted") throw new ApiError("只能更新已接受的计划", 400);
|
||||
await prisma.weekendPlan.update({
|
||||
where: { id: planId },
|
||||
data: { status: act === "complete" ? "completed" : "expired" },
|
||||
});
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
if (act === "update_plan") {
|
||||
if (plan.status !== "active" && plan.status !== "accepted") {
|
||||
throw new ApiError("只能编辑进行中的计划", 400);
|
||||
}
|
||||
if (!Array.isArray(days) || days.length === 0) {
|
||||
throw new ApiError("days 数据无效", 400);
|
||||
}
|
||||
const newPlanData = JSON.stringify({ days });
|
||||
await prisma.weekendPlan.update({
|
||||
where: { id: planId },
|
||||
data: {
|
||||
planData: newPlanData,
|
||||
...(plan.status === "accepted"
|
||||
? { endTime: computeEndTime(newPlanData, new Date()) }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
|
||||
throw new ApiError("无效的操作", 400);
|
||||
const result = await handlePlanUpdate(planId, userId, action, days);
|
||||
return NextResponse.json(result);
|
||||
});
|
||||
|
||||
export const GET = apiHandler(async (req) => {
|
||||
@@ -135,92 +53,18 @@ export const GET = apiHandler(async (req) => {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const mode = searchParams.get("mode") || "latest";
|
||||
|
||||
const { prisma } = await import("@/lib/prisma");
|
||||
|
||||
if (mode === "latest") {
|
||||
const roomId = searchParams.get("roomId");
|
||||
if (!roomId) throw new ApiError("roomId 不能为空");
|
||||
|
||||
const plan = await prisma.weekendPlan.findFirst({
|
||||
where: { roomId, userId, status: "accepted" },
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { id: true, planData: true, endTime: true, createdAt: true },
|
||||
});
|
||||
|
||||
if (!plan) return NextResponse.json({ plan: null });
|
||||
const parsed = JSON.parse(plan.planData);
|
||||
return NextResponse.json({
|
||||
plan: { id: plan.id, days: parsed.days, endTime: plan.endTime, createdAt: plan.createdAt },
|
||||
});
|
||||
return NextResponse.json(await getLatestPlan(roomId, userId));
|
||||
}
|
||||
|
||||
if (mode === "pending") {
|
||||
const plans = await prisma.weekendPlan.findMany({
|
||||
where: {
|
||||
userId,
|
||||
status: "accepted",
|
||||
endTime: { not: null, lt: new Date() },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { id: true, planData: true, roomId: true, createdAt: true },
|
||||
take: 5,
|
||||
});
|
||||
|
||||
const result = await Promise.all(
|
||||
plans.map(async (p) => {
|
||||
const room = await prisma.blindBoxRoom.findUnique({
|
||||
where: { id: p.roomId },
|
||||
select: { name: true, code: true },
|
||||
});
|
||||
const parsed = JSON.parse(p.planData);
|
||||
const days = parsed.days as { date: string; items: { activity: string }[] }[];
|
||||
return {
|
||||
id: p.id,
|
||||
roomName: room?.name ?? "未知房间",
|
||||
roomCode: room?.code ?? "",
|
||||
date: days.map((d) => d.date).join(" + "),
|
||||
activities: days.flatMap((d) => d.items.map((i) => i.activity)),
|
||||
createdAt: p.createdAt,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return NextResponse.json({ pending: result });
|
||||
return NextResponse.json(await getPendingPlans(userId));
|
||||
}
|
||||
|
||||
if (mode === "history") {
|
||||
const plans = await prisma.weekendPlan.findMany({
|
||||
where: {
|
||||
userId,
|
||||
status: { in: ["completed", "expired"] },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { id: true, planData: true, status: true, roomId: true, createdAt: true },
|
||||
take: 50,
|
||||
});
|
||||
|
||||
const result = await Promise.all(
|
||||
plans.map(async (p) => {
|
||||
const room = await prisma.blindBoxRoom.findUnique({
|
||||
where: { id: p.roomId },
|
||||
select: { name: true, code: true },
|
||||
});
|
||||
const parsed = JSON.parse(p.planData);
|
||||
const days = parsed.days as { date: string; items: { activity: string }[] }[];
|
||||
return {
|
||||
id: p.id,
|
||||
status: p.status,
|
||||
roomName: room?.name ?? "未知房间",
|
||||
roomCode: room?.code ?? "",
|
||||
date: days.map((d) => d.date).join(" + "),
|
||||
dayCount: days.length,
|
||||
activities: days.flatMap((d) => d.items.map((i) => i.activity)),
|
||||
createdAt: p.createdAt,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return NextResponse.json({ history: result });
|
||||
return NextResponse.json(await getHistoryPlans(userId));
|
||||
}
|
||||
|
||||
throw new ApiError("无效的 mode 参数", 400);
|
||||
|
||||
Reference in New Issue
Block a user