如何基于Node.js、Express、TypeORM实现随机时间推送通知?
实现基于用户时区的随机通知推送(类似BeReal功能)
一、数据库模型设计(TypeORM)
首先在用户表中存储时区、通知状态及上次通知时间,用TypeORM实体定义如下:
import { Entity, Column, PrimaryGeneratedColumn } from "typeorm"; @Entity() export class User { @PrimaryGeneratedColumn() id: number; // IANA标准时区,比如 "Asia/Shanghai"、"America/Los_Angeles" @Column({ type: "varchar", length: 50 }) timezone: string; // 是否开启通知 @Column({ default: true }) isNotificationEnabled: boolean; // 上次发送通知的UTC时间 @Column({ type: "timestamp", nullable: true }) lastNotificationAt: Date; }
二、核心:生成用户时区的随机通知时间
在用户当地时间的指定窗口(比如10:00-22:00)内生成随机时间,再转成UTC时间用于调度。用date-fns-tz处理时区转换:
import { zonedTimeToUtc, utcToZonedTime, getTimezones } from "date-fns-tz"; import { addDays, setHours, setMinutes, randomInt } from "date-fns"; // 生成用户次日的随机通知UTC时间 export function generateRandomNotificationTime(user: User): Date { if (!user.isNotificationEnabled) throw new Error("用户已关闭通知"); if (!getTimezones().includes(user.timezone)) throw new Error("无效时区"); // 目标窗口:用户当地时间10:00 - 21:59 const nextDay = addDays(new Date(), 1); const localMinTime = setHours(setMinutes(nextDay, 0), 10); const localMaxTime = setHours(setMinutes(nextDay, 59), 21); // 转换为用户时区的时间,计算随机分钟数 const zonedMin = utcToZonedTime(localMinTime, user.timezone); const zonedMax = utcToZonedTime(localMaxTime, user.timezone); const randomMinuteOffset = randomInt(0, (zonedMax.getTime() - zonedMin.getTime()) / (1000 * 60)); const randomZonedTime = new Date(zonedMin.getTime() + randomMinuteOffset * 60 * 1000); // 转回UTC时间返回 return zonedTimeToUtc(randomZonedTime, user.timezone); }
三、定时任务调度(node-schedule)
用node-schedule每日批量生成次日的通知任务,或者每分钟扫描待通知用户(大用户量推荐后者):
import schedule from "node-schedule"; import { getRepository } from "typeorm"; import { User } from "./entities/User"; import { sendNotification } from "./notification-service"; // 每日凌晨1点(UTC)为所有用户生成次日通知任务 schedule.scheduleJob("0 0 1 * * *", async () => { const userRepo = getRepository(User); const users = await userRepo.find({ where: { isNotificationEnabled: true } }); for (const user of users) { try { const notifyTime = generateRandomNotificationTime(user); // 为该用户安排通知任务 schedule.scheduleJob(notifyTime, async () => { await sendNotification(user.id, "该发布帖子啦!"); await userRepo.update(user.id, { lastNotificationAt: new Date() }); }); } catch (err) { console.error(`用户${user.id}任务创建失败:`, err); } } });
四、通知推送实现
示例推送函数,实际可对接FCM/APNS等服务:
// notification-service.ts export async function sendNotification(userId: number, message: string): Promise<void> { // 替换为实际推送逻辑,比如调用FCM API console.log(`向用户${userId}推送通知: ${message}`); // 示例FCM调用: // await fetch("https://fcm.googleapis.com/fcm/send", { // method: "POST", // headers: { // "Authorization": "key=YOUR_FCM_SECRET", // "Content-Type": "application/json" // }, // body: JSON.stringify({ // to: userDeviceToken, // notification: { title: "发布提醒", body: message } // }) // }); }
五、Express接口:用户设置时区
提供接口让用户更新时区:
import express from "express"; import { getRepository } from "typeorm"; import { User } from "./entities/User"; import { getTimezones } from "date-fns-tz"; const router = express.Router(); router.post("/api/user/timezone", async (req, res) => { const { userId, timezone } = req.body; const userRepo = getRepository(User); try { if (!getTimezones().includes(timezone)) { return res.status(400).json({ error: "无效时区格式" }); } await userRepo.update(userId, { timezone }); res.json({ success: true, message: "时区更新成功" }); } catch (err) { res.status(500).json({ error: "更新失败" }); } }); export default router;
关键注意事项
- 时区校验:必须用IANA标准时区,避免转换错误。
- 服务重启恢复:重启后需重新生成未执行的任务,可通过
lastNotificationAt判断是否已发送当日通知。 - 大用户量优化:用户数较多时,建议改用每分钟扫描一次UTC时间匹配的用户,避免创建大量定时任务。
内容的提问来源于stack exchange,提问作者jakek05
相关产品推荐
相关产品推荐

