我正在使用 Next.js App Router API 路由(app/api/notify/route.ts)来检查当前时间(巴西时区)并在达到预定时间时触发通知。
这在本地可以正常工作,但部署到 Vercel 后,我注意到了意外的行为:
❌ 问题:在第一个请求之后,所有后续请求都返回相同的时间戳(从第一次执行开始的冻结时间),即使我在处理程序中使用了 new Date()。
看起来该函数正在被缓存或者环境正在重用相同的上下文。
✅ 预期行为:每次访问路线时,new Date() 都应返回当前时间,以反映请求的实际时刻。
✅ 可重现的步骤:将这个简单的处理程序部署到 Vercel。
向端点发送多个 GET 请求,延迟几秒/几分钟。
您会注意到第一次调用后时间戳和 isoTime 从未改变。
import connectMongo from "@/libs/mongoose";
import Routine from "@/models/Routine";
import User from "@/models/User";
import PushSubscription from "@/models/PushSubscription";
const webPush = require("web-push");
const vapidPublicKey = process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY;
const vapidPrivateKey = process.env.WEB_PUSH_PRIVATE_KEY;
if (!vapidPublicKey || !vapidPrivateKey) {
throw new Error("VAPID keys not configured");
}
console.log("VAPID keys configured correctly");
webPush.setVapidDetails("mailto:[email protected]", vapidPublicKey, vapidPrivateKey);
const getBrazilDateTime = () => {
const now = new Date();
const brazilTime = new Date(now.getTime() - 3 * 60 * 60 * 1000); // adjust for Brazil timezone
return brazilTime;
};
const getDayNameInPortuguese = (date) => {
const days = ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"];
return days[date.getDay()];
};
const getTaskStartTime = (task, currentDay) => {
if (task.dailySchedule instanceof Map && task.dailySchedule.has(currentDay)) {
const daySchedule = task.dailySchedule.get(currentDay);
if (daySchedule && daySchedule.startTime) return daySchedule.startTime;
} else if (task.dailySchedule && typeof task.dailySchedule === "object" && task.dailySchedule[currentDay]?.startTime) {
return task.dailySchedule[currentDay].startTime;
}
if (task.startTime) return task.startTime;
return null;
};
const deduplicateSubscriptions = (subscriptions) => {
const uniqueEndpoints = new Set();
return subscriptions.filter((sub) => {
if (uniqueEndpoints.has(sub.endpoint)) return false;
uniqueEndpoints.add(sub.endpoint);
return true;
});
};
const notifiedTasksCache = new Map();
const isTaskAlreadyNotified = (taskId, userId) => {
const key = `${taskId}-${userId}`;
const lastNotified = notifiedTasksCache.get(key);
if (!lastNotified) return false;
const tenMinutesAgo = Date.now() - 10 * 60 * 1000;
return lastNotified > tenMinutesAgo;
};
const markTaskAsNotified = (taskId, userId) => {
const key = `${taskId}-${userId}`;
notifiedTasksCache.set(key, Date.now());
};
export async function GET(request) {
const headers = new Headers({
"Cache-Control": "no-store, max-age=0, must-revalidate",
"Content-Type": "application/json",
});
const logs = [];
const addLog = (message) => {
console.log(message);
logs.push(`[${new Date().toISOString()}] ${message}`);
};
addLog("🔔 Starting notification check...");
addLog(`🕒 Start timestamp: ${Date.now()}`);
try {
addLog("Connecting to MongoDB...");
await connectMongo();
addLog("MongoDB connection established");
// Update the time on each cycle to ensure the time is current.
const spDate = getBrazilDateTime();
const currentDay = getDayNameInPortuguese(spDate);
const currentTime = spDate.toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
addLog(`📅 Brazil date and time: ${spDate.toLocaleString("pt-BR")}`);
addLog(`📅 Day of the week: ${currentDay}`);
addLog(`⏰ Current time: ${currentTime}`);
const users = await User.find({ activeRoutine: { $exists: true, $ne: null } });
addLog(`👥 Found ${users.length} users with active routines`);
if (!users.length) return NextResponse.json({ message: "No users with active routines found." });
const routineIds = users.map((user) => user.activeRoutine).filter(Boolean);
const routines = await Routine.find({ _id: { $in: routineIds } });
const routineMap = new Map();
routines.forEach((routine) => routineMap.set(routine._id.toString(), routine));
let notificationsSent = 0;
let usersNotified = 0;
let duplicatesSkipped = 0;
await Promise.all(
users.map(async (user) => {
const routineId = user.activeRoutine?.toString();
if (!routineId) return;
const routine = routineMap.get(routineId);
if (!routine) return;
const matchingTasks = routine.tasks.filter((task) => {
const taskDays = task.days || [];
const includesDay = taskDays.includes(currentDay);
const taskStartTime = getTaskStartTime(task, currentDay);
return includesDay && taskStartTime === currentTime;
});
if (!matchingTasks.length) return;
for (const matchingTask of matchingTasks) {
if (isTaskAlreadyNotified(matchingTask._id.toString(), user._id.toString())) {
duplicatesSkipped++;
continue;
}
let subscriptions = await PushSubscription.find({ userId: user._id });
if (!subscriptions.length) continue;
subscriptions = deduplicateSubscriptions(subscriptions);
addLog(` 📱 User ${user.email} has ${subscriptions.length} unique devices`);
const payload = JSON.stringify({
title: `🔔 ${matchingTask.name} - Time to start!`,
body: `⏰ ${currentTime} - ${matchingTask.details || "Stay focused on your routine!"}`,
icon: "/icon512_rounded.png",
badge: "/icon192_rounded.png",
tag: `task-${matchingTask._id}`,
data: {
url: `/dashboard/r/${routine._id}`,
taskId: matchingTask._id.toString(),
type: "task-reminder",
timestamp: new Date().toISOString(),
},
actions: [
{ action: "open", title: "📋 View Details" },
{ action: "dismiss", title: "✔️ Got it" },
],
vibrate: [200, 100, 200],
requireInteraction: true,
});
await Promise.all(
subscriptions.map(async (subscription) => {
try {
await webPush.sendNotification(
{
endpoint: subscription.endpoint,
keys: subscription.keys,
},
payload
);
notificationsSent++;
markTaskAsNotified(matchingTask._id.toString(), user._id.toString());
} catch (error) {
if (error.statusCode === 410) {
await PushSubscription.deleteOne({ _id: subscription._id });
}
}
})
);
usersNotified++;
}
})
);
return NextResponse.json(
{
message: `Notifications sent successfully!`,
notificationsSent,
usersNotified,
duplicatesSkipped,
logs,
},
{ headers }
);
} catch (error) {
console.error("Error sending notifications:", error);
return NextResponse.json({ error: "Error processing notifications.", logs }, { status: 500, headers });
}
}
🔍 我尝试过的方法:删除所有外部逻辑 — 上面的最小处理程序仍然会重现该问题。
Vercel 缓存控制标头:似乎不相关。
检查全局或任何静态变量是否保留状态 - 并非如此。
❓ 我的问题:这是 Vercel 函数缓存或 Next.js App Router 中冷启动行为的已知问题吗?
我如何确保对每个请求都评估新的 Date(),而不仅仅是在第一个请求时?