feat: 契约生命周期 + 到期通知 + 成就墙
- 扩展 WeekendPlan schema: 新增 endTime 字段与 userId 索引 - PATCH /api/blindbox/plan 支持 accept/complete/expire 操作, 接受时自动计算契约结束时间 - GET /api/blindbox/plan 支持 mode 参数 (latest/pending/history) - 房间页接受契约后自动返回想法池,顶部显示"契约进行中"指示器 - 契约到期时触发浏览器通知 + 页面加载时弹出完成确认弹窗 - 新增 /achievements 成就墙页面:统计数据 + 决策记录/契约记录双标签 - 首页和个人中心新增成就墙入口
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { apiHandler, requireUserId } from "@/lib/api";
|
||||
|
||||
export const GET = apiHandler(async (req) => {
|
||||
const userId = req.nextUrl.searchParams.get("userId");
|
||||
requireUserId(userId);
|
||||
|
||||
const [decisions, contracts] = await Promise.all([
|
||||
prisma.decision.findMany({
|
||||
where: { userId: userId! },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 50,
|
||||
}),
|
||||
prisma.weekendPlan.findMany({
|
||||
where: {
|
||||
userId: userId!,
|
||||
status: { in: ["completed", "expired"] },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 50,
|
||||
select: {
|
||||
id: true,
|
||||
planData: true,
|
||||
status: true,
|
||||
roomId: true,
|
||||
createdAt: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const roomIds = [...new Set(contracts.map((c) => c.roomId))];
|
||||
const rooms = await prisma.blindBoxRoom.findMany({
|
||||
where: { id: { in: roomIds } },
|
||||
select: { id: true, name: true, code: true },
|
||||
});
|
||||
const roomMap = new Map(rooms.map((r) => [r.id, r]));
|
||||
|
||||
const completedCount = contracts.filter((c) => c.status === "completed").length;
|
||||
|
||||
const contractRecords = contracts.map((c) => {
|
||||
const parsed = JSON.parse(c.planData);
|
||||
const days = parsed.days as { date: string; items: { activity: string }[] }[];
|
||||
const room = roomMap.get(c.roomId);
|
||||
return {
|
||||
id: c.id,
|
||||
status: c.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: c.createdAt.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
const decisionRecords = decisions.map((d) => ({
|
||||
id: d.id,
|
||||
roomId: d.roomId,
|
||||
restaurantName: d.restaurantName,
|
||||
restaurantData: JSON.parse(d.restaurantData),
|
||||
matchType: d.matchType,
|
||||
participants: d.participants,
|
||||
createdAt: d.createdAt.toISOString(),
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
stats: {
|
||||
totalDecisions: decisions.length,
|
||||
totalContracts: contracts.length,
|
||||
completedContracts: completedCount,
|
||||
completionRate: contracts.length > 0 ? Math.round((completedCount / contracts.length) * 100) : 0,
|
||||
},
|
||||
decisions: decisionRecords,
|
||||
contracts: contractRecords,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user