import "server-only" /** * 通知偏好数据访问层 * * 职责: * - getNotificationPreferences: 获取用户通知偏好(无记录时自动创建默认记录) * - upsertNotificationPreferences: 更新或创建用户通知偏好 * * 表所有权: notification_preferences(由 notifications 模块统一管理) * * 注意: 本文件从 messaging/notification-preferences.ts 迁移而来, * 消除 notifications -> messaging 的反向依赖(P0-4 / P1-5 修复)。 */ import { cache } from "react" import { createId } from "@paralleldrive/cuid2" import { and, eq } from "drizzle-orm" import { db } from "@/shared/db" import { notificationPreferences } from "@/shared/db/schema" import type { NotificationPreferences, UpdateNotificationPreferencesInput, } from "./types" const toIso = (d: Date): string => d.toISOString() const mapRow = ( row: typeof notificationPreferences.$inferSelect ): NotificationPreferences => ({ id: row.id, userId: row.userId, emailEnabled: row.emailEnabled, smsEnabled: row.smsEnabled, pushEnabled: row.pushEnabled, homeworkNotifications: row.homeworkNotifications, gradeNotifications: row.gradeNotifications, announcementNotifications: row.announcementNotifications, messageNotifications: row.messageNotifications, attendanceNotifications: row.attendanceNotifications, createdAt: toIso(row.createdAt), updatedAt: toIso(row.updatedAt), }) // 默认偏好值(首次创建时使用) const DEFAULTS = { emailEnabled: false, smsEnabled: false, pushEnabled: true, homeworkNotifications: true, gradeNotifications: true, announcementNotifications: true, messageNotifications: true, attendanceNotifications: true, } /** * 获取用户的通知偏好设置 * 如果用户尚无记录,则自动创建一条默认记录并返回 */ export const getNotificationPreferences = cache( async (userId: string): Promise => { // 先查询 const [existing] = await db .select() .from(notificationPreferences) .where(eq(notificationPreferences.userId, userId)) .limit(1) if (existing) { return mapRow(existing) } // 不存在则创建默认记录 const id = createId() try { await db.insert(notificationPreferences).values({ id, userId, ...DEFAULTS, }) const [created] = await db .select() .from(notificationPreferences) .where(eq(notificationPreferences.id, id)) .limit(1) if (created) return mapRow(created) } catch { // 并发情况下可能违反唯一约束,回退到查询 const [fallback] = await db .select() .from(notificationPreferences) .where(eq(notificationPreferences.userId, userId)) .limit(1) if (fallback) return mapRow(fallback) } // 极端情况:返回内存中的默认值(不带 id) return { id: "", userId, ...DEFAULTS, createdAt: toIso(new Date()), updatedAt: toIso(new Date()), } } ) /** * 更新(或创建)用户的通知偏好设置 * 使用 upsert 语义:存在则更新,不存在则插入 */ export async function upsertNotificationPreferences( userId: string, input: UpdateNotificationPreferencesInput ): Promise { // 先查询是否存在 const [existing] = await db .select() .from(notificationPreferences) .where(eq(notificationPreferences.userId, userId)) .limit(1) if (existing) { // 更新 const updateData: Partial = {} if (input.emailEnabled !== undefined) updateData.emailEnabled = input.emailEnabled if (input.smsEnabled !== undefined) updateData.smsEnabled = input.smsEnabled if (input.pushEnabled !== undefined) updateData.pushEnabled = input.pushEnabled if (input.homeworkNotifications !== undefined) updateData.homeworkNotifications = input.homeworkNotifications if (input.gradeNotifications !== undefined) updateData.gradeNotifications = input.gradeNotifications if (input.announcementNotifications !== undefined) updateData.announcementNotifications = input.announcementNotifications if (input.messageNotifications !== undefined) updateData.messageNotifications = input.messageNotifications if (input.attendanceNotifications !== undefined) updateData.attendanceNotifications = input.attendanceNotifications if (Object.keys(updateData).length === 0) { return mapRow(existing) } await db .update(notificationPreferences) .set(updateData) .where(and(eq(notificationPreferences.id, existing.id), eq(notificationPreferences.userId, userId))) const [updated] = await db .select() .from(notificationPreferences) .where(eq(notificationPreferences.id, existing.id)) .limit(1) return updated ? mapRow(updated) : null } // 不存在则插入 const id = createId() try { await db.insert(notificationPreferences).values({ id, userId, emailEnabled: input.emailEnabled ?? DEFAULTS.emailEnabled, smsEnabled: input.smsEnabled ?? DEFAULTS.smsEnabled, pushEnabled: input.pushEnabled ?? DEFAULTS.pushEnabled, homeworkNotifications: input.homeworkNotifications ?? DEFAULTS.homeworkNotifications, gradeNotifications: input.gradeNotifications ?? DEFAULTS.gradeNotifications, announcementNotifications: input.announcementNotifications ?? DEFAULTS.announcementNotifications, messageNotifications: input.messageNotifications ?? DEFAULTS.messageNotifications, attendanceNotifications: input.attendanceNotifications ?? DEFAULTS.attendanceNotifications, }) const [created] = await db .select() .from(notificationPreferences) .where(eq(notificationPreferences.id, id)) .limit(1) return created ? mapRow(created) : null } catch { return null } }