export function attachPortalNotificationRoutes({ app, userAuthReady = Promise.resolve(), getUserAuth = () => null, getScheduleService = () => null, userToken, logger = console, setIntervalFn = setInterval, clearIntervalFn = clearInterval, now = Date.now, } = {}) { if ( !app || typeof userToken !== 'function' || typeof setIntervalFn !== 'function' || typeof clearIntervalFn !== 'function' || typeof now !== 'function' ) { throw new Error( 'attachPortalNotificationRoutes requires route dependencies', ); } app.get('/auth/notifications', async (req, res) => { await userAuthReady; const userAuth = getUserAuth(); const scheduleService = getScheduleService(); if (!userAuth || !scheduleService) { return res.status(503).json({ message: '通知服务未启用', }); } const me = await userAuth.getMe(userToken(req)); if (!me) { return res.status(401).json({ message: '未登录', }); } const rawStatus = typeof req.query?.status === 'string' ? req.query.status : 'unread'; const status = [ 'all', 'unread', 'read', ].includes(rawStatus) ? rawStatus : 'all'; const limit = Math.min( Math.max(Number(req.query?.limit) || 20, 1), 100, ); try { const notifications = await scheduleService.listUserNotifications({ userId: me.id, status, limit, }); res.json({ notifications }); } catch (error) { logger.warn( 'List user notifications failed:', error instanceof Error ? error.message : error, ); res.status(500).json({ message: '通知列表加载失败', }); } }); app.get( '/auth/notifications/events', async (req, res) => { await userAuthReady; const userAuth = getUserAuth(); const scheduleService = getScheduleService(); if (!userAuth || !scheduleService) { return res.status(503).json({ message: '通知服务未启用', }); } const me = await userAuth.getMe( userToken(req), ); if (!me) { return res.status(401).json({ message: '未登录', }); } res.status(200); res.setHeader( 'Content-Type', 'text/event-stream; charset=utf-8', ); res.setHeader( 'Cache-Control', 'no-cache, no-transform', ); res.setHeader('Connection', 'keep-alive'); res.flushHeaders?.(); let closed = false; let lastNotificationId = null; const sendEvent = (event, data) => { if (closed || res.destroyed) return; res.write(`event: ${event}\n`); res.write( `data: ${JSON.stringify(data)}\n\n`, ); }; const checkUnread = async () => { if (closed) return; try { const notifications = await scheduleService.listUserNotifications({ userId: me.id, status: 'unread', limit: 1, }); const latest = notifications[0] ?? null; const nextId = latest?.id ?? null; if ( nextId && nextId !== lastNotificationId ) { lastNotificationId = nextId; sendEvent('notification', { notification: latest, }); } else if (!nextId) { lastNotificationId = null; } } catch { sendEvent('sync', { reason: 'check_failed', }); } }; sendEvent('ready', { ok: true }); await checkUnread(); const checkTimer = setIntervalFn(() => { void checkUnread(); }, 2500); const keepaliveTimer = setIntervalFn(() => { sendEvent('ping', { at: now() }); }, 25000); req.on('close', () => { closed = true; clearIntervalFn(checkTimer); clearIntervalFn(keepaliveTimer); }); }, ); app.post( '/auth/notifications/:id/read', async (req, res) => { await userAuthReady; const userAuth = getUserAuth(); const scheduleService = getScheduleService(); if (!userAuth || !scheduleService) { return res.status(503).json({ message: '通知服务未启用', }); } const me = await userAuth.getMe( userToken(req), ); if (!me) { return res.status(401).json({ message: '未登录', }); } const ok = await scheduleService.markUserNotificationRead({ userId: me.id, notificationId: req.params.id, }); if (!ok) { return res.status(404).json({ message: '通知不存在或已读', }); } res.json({ ok: true }); }, ); app.post( '/auth/notifications/read-all', async (req, res) => { await userAuthReady; const userAuth = getUserAuth(); const scheduleService = getScheduleService(); if (!userAuth || !scheduleService) { return res.status(503).json({ message: '通知服务未启用', }); } const me = await userAuth.getMe( userToken(req), ); if (!me) { return res.status(401).json({ message: '未登录', }); } const updated = await scheduleService.markAllUserNotificationsRead( { userId: me.id }, ); res.json({ ok: true, updated, }); }, ); app.delete( '/auth/notifications/:id', async (req, res) => { await userAuthReady; const userAuth = getUserAuth(); const scheduleService = getScheduleService(); if (!userAuth || !scheduleService) { return res.status(503).json({ message: '通知服务未启用', }); } const me = await userAuth.getMe( userToken(req), ); if (!me) { return res.status(401).json({ message: '未登录', }); } const ok = await scheduleService.deleteUserNotification({ userId: me.id, notificationId: req.params.id, }); if (!ok) { return res.status(404).json({ message: '通知不存在', }); } res.status(204).end(); }, ); app.delete( '/auth/notifications', async (req, res) => { await userAuthReady; const userAuth = getUserAuth(); const scheduleService = getScheduleService(); if (!userAuth || !scheduleService) { return res.status(503).json({ message: '通知服务未启用', }); } const me = await userAuth.getMe( userToken(req), ); if (!me) { return res.status(401).json({ message: '未登录', }); } const status = typeof req.query?.status === 'string' ? req.query.status : 'all'; const deleted = await scheduleService.clearUserNotifications({ userId: me.id, status, }); res.json({ ok: true, deleted, }); }, ); }