feat: 已接受契约持久化 — 保存/加载/自动展示
- PATCH /api/blindbox/plan: 接受契约时更新状态为 accepted - GET /api/blindbox/plan: 查询房间内最近一次已接受的计划 - 进入房间时自动加载已接受计划并展示行程视图 - 修复整个周末想法不足时重复活动的问题(不足则只生成一天)
This commit is contained in:
@@ -190,16 +190,18 @@ export const POST = apiHandler(async (req) => {
|
|||||||
]
|
]
|
||||||
: [at];
|
: [at];
|
||||||
|
|
||||||
// Select ideas per day, avoiding duplicates across days when possible
|
// Select ideas per day — skip extra days when ideas run out
|
||||||
const dayIdeas: TaggedIdea[][] = [];
|
const dayIdeas: TaggedIdea[][] = [];
|
||||||
const usedIds = new Set<string>();
|
const usedIds = new Set<string>();
|
||||||
for (const dayConfig of dayConfigs) {
|
for (const dayConfig of dayConfigs) {
|
||||||
const remaining = taggedIdeas.filter((i) => !usedIds.has(i.id));
|
const remaining = taggedIdeas.filter((i) => !usedIds.has(i.id));
|
||||||
const pool = remaining.length >= 2 ? remaining : taggedIdeas;
|
if (remaining.length < 2) break;
|
||||||
const selected = selectIdeasForSlots(pool, dayConfig.endHour - dayConfig.startHour);
|
const selected = selectIdeasForSlots(remaining, dayConfig.endHour - dayConfig.startHour);
|
||||||
for (const idea of selected) usedIds.add(idea.id);
|
for (const idea of selected) usedIds.add(idea.id);
|
||||||
dayIdeas.push(selected);
|
dayIdeas.push(selected);
|
||||||
}
|
}
|
||||||
|
// Trim to actual days generated (may be fewer than requested for "整个周末")
|
||||||
|
const actualDayConfigs = dayConfigs.slice(0, dayIdeas.length);
|
||||||
|
|
||||||
const allSelected = dayIdeas.flat();
|
const allSelected = dayIdeas.flat();
|
||||||
if (allSelected.length === 0) {
|
if (allSelected.length === 0) {
|
||||||
@@ -260,7 +262,7 @@ export const POST = apiHandler(async (req) => {
|
|||||||
|
|
||||||
// Generate schedule for each day (parallel AI calls)
|
// Generate schedule for each day (parallel AI calls)
|
||||||
const schedules = await Promise.all(
|
const schedules = await Promise.all(
|
||||||
dayConfigs.map((dayConfig, idx) => {
|
actualDayConfigs.map((dayConfig, idx) => {
|
||||||
const ideas = dayIdeas[idx];
|
const ideas = dayIdeas[idx];
|
||||||
const ctx: ScheduleContext = {
|
const ctx: ScheduleContext = {
|
||||||
ideas: ideas.map((i) => ({
|
ideas: ideas.map((i) => ({
|
||||||
@@ -282,7 +284,7 @@ export const POST = apiHandler(async (req) => {
|
|||||||
const days = schedules
|
const days = schedules
|
||||||
.map((schedule, idx) =>
|
.map((schedule, idx) =>
|
||||||
schedule
|
schedule
|
||||||
? { date: dayConfigs[idx].date, items: schedule.items, summary: schedule.summary }
|
? { date: actualDayConfigs[idx].date, items: schedule.items, summary: schedule.summary }
|
||||||
: null,
|
: null,
|
||||||
)
|
)
|
||||||
.filter((d) => d !== null);
|
.filter((d) => d !== null);
|
||||||
@@ -308,3 +310,47 @@ export const POST = apiHandler(async (req) => {
|
|||||||
createdAt: plan.createdAt,
|
createdAt: plan.createdAt,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const PATCH = apiHandler(async (req) => {
|
||||||
|
const { planId, userId } = await req.json();
|
||||||
|
requireUserId(userId);
|
||||||
|
if (!planId) throw new ApiError("planId 不能为空");
|
||||||
|
|
||||||
|
const plan = await prisma.weekendPlan.findUnique({ where: { id: planId } });
|
||||||
|
if (!plan) throw new ApiError("计划不存在", 404);
|
||||||
|
if (plan.userId !== userId) throw new ApiError("只能接受自己的计划", 403);
|
||||||
|
|
||||||
|
await prisma.weekendPlan.update({
|
||||||
|
where: { id: planId },
|
||||||
|
data: { status: "accepted" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GET = apiHandler(async (req) => {
|
||||||
|
const { searchParams } = new URL(req.url);
|
||||||
|
const roomId = searchParams.get("roomId");
|
||||||
|
const userId = searchParams.get("userId");
|
||||||
|
|
||||||
|
requireUserId(userId);
|
||||||
|
if (!roomId) throw new ApiError("roomId 不能为空");
|
||||||
|
|
||||||
|
const plan = await prisma.weekendPlan.findFirst({
|
||||||
|
where: { roomId, userId: userId!, status: "accepted" },
|
||||||
|
orderBy: { createdAt: "desc" },
|
||||||
|
select: { id: true, planData: 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,
|
||||||
|
createdAt: plan.createdAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export default function BlindboxRoomPage() {
|
|||||||
const [confirmLeave, setConfirmLeave] = useState(false);
|
const [confirmLeave, setConfirmLeave] = useState(false);
|
||||||
const [leaving, setLeaving] = useState(false);
|
const [leaving, setLeaving] = useState(false);
|
||||||
const [locating, setLocating] = useState(false);
|
const [locating, setLocating] = useState(false);
|
||||||
|
const [planId, setPlanId] = useState<string | null>(null);
|
||||||
const [planDays, setPlanDays] = useState<WeekendPlanData[]>([]);
|
const [planDays, setPlanDays] = useState<WeekendPlanData[]>([]);
|
||||||
const [planAccepted, setPlanAccepted] = useState(false);
|
const [planAccepted, setPlanAccepted] = useState(false);
|
||||||
const [generating, setGenerating] = useState(false);
|
const [generating, setGenerating] = useState(false);
|
||||||
@@ -136,9 +137,28 @@ export default function BlindboxRoomPage() {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}, [room]);
|
}, [room]);
|
||||||
|
|
||||||
|
const fetchAcceptedPlan = useCallback(async () => {
|
||||||
|
const p = getCachedProfile();
|
||||||
|
if (!room || !p) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/blindbox/plan?roomId=${room.id}&userId=${p.id}`);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.plan) {
|
||||||
|
setPlanId(data.plan.id);
|
||||||
|
setPlanDays(data.plan.days);
|
||||||
|
setPlanAccepted(true);
|
||||||
|
setPhase("plan_reveal");
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}, [room]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMember && room) fetchIdeas();
|
if (isMember && room) {
|
||||||
}, [isMember, room, fetchIdeas]);
|
fetchIdeas();
|
||||||
|
fetchAcceptedPlan();
|
||||||
|
}
|
||||||
|
}, [isMember, room, fetchIdeas, fetchAcceptedPlan]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMember && inputRef.current) {
|
if (isMember && inputRef.current) {
|
||||||
@@ -212,6 +232,7 @@ export default function BlindboxRoomPage() {
|
|||||||
throw new Error(data.error || "生成失败");
|
throw new Error(data.error || "生成失败");
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
setPlanId(data.id);
|
||||||
setPlanDays(data.days);
|
setPlanDays(data.days);
|
||||||
setPlanAccepted(false);
|
setPlanAccepted(false);
|
||||||
setPhase("plan_reveal");
|
setPhase("plan_reveal");
|
||||||
@@ -782,9 +803,18 @@ export default function BlindboxRoomPage() {
|
|||||||
days={planDays}
|
days={planDays}
|
||||||
accepted={planAccepted}
|
accepted={planAccepted}
|
||||||
regenerating={generating}
|
regenerating={generating}
|
||||||
onAccept={() => {
|
onAccept={async () => {
|
||||||
setPlanAccepted(true);
|
setPlanAccepted(true);
|
||||||
fireConfetti();
|
fireConfetti();
|
||||||
|
if (planId && profile) {
|
||||||
|
try {
|
||||||
|
await fetch("/api/blindbox/plan", {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ planId, userId: profile.id }),
|
||||||
|
});
|
||||||
|
} catch { /* best-effort */ }
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onRegenerate={() => {
|
onRegenerate={() => {
|
||||||
setPhase("time_select");
|
setPhase("time_select");
|
||||||
@@ -792,6 +822,7 @@ export default function BlindboxRoomPage() {
|
|||||||
onShare={() => setShowPlanShareCard(true)}
|
onShare={() => setShowPlanShareCard(true)}
|
||||||
onBack={() => {
|
onBack={() => {
|
||||||
setPhase("pool");
|
setPhase("pool");
|
||||||
|
setPlanId(null);
|
||||||
setPlanDays([]);
|
setPlanDays([]);
|
||||||
setPlanAccepted(false);
|
setPlanAccepted(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user