Bug fixes (from bugs/ directory): - Fix cross-module DB queries in 9 modules (homework, grades, parent, diagnostic, elective, proctoring, notifications, scheduling, classes) by routing through data-access functions - Fix shared/lib <-> auth circular dependency via new session.ts module - Fix divide-by-zero guard in grades data-access - Fix audit export data truncation (paginated fetch for full datasets) - Fix missing transactions in homework grading and elective lottery - Fix missing revalidatePath in course-plans actions - Fix frontend permission checks using requirePermission instead of requireAuth - Fix dashboard role routing using session.user.roles - Fix student auth pattern (migrate getDemoStudentUser to users module) - Fix ActionState return type handling in components Code quality fixes: - Remove 60+ as type assertions (replace with type guards) - Remove non-null assertions (use optional chaining or explicit checks) - Convert dynamic imports to static imports (grades, diagnostic) - Add React.cache() wrapping for read functions - Parallelize independent queries with Promise.all - Add explicit return types to 30+ arrow functions - Replace any with unknown + type guards - Fix import type for type-only imports - Add Zod validation schemas for classes and diagnostic modules - Extract duplicate code (normalizeRoleName, normalizeBcryptHash, logger IP extraction) - Add console.error to silent catch blocks - Fix permission naming consistency (exam:proctor_read -> exam:proctor:read) Architecture doc sync: - Update 004_architecture_impact_map.md and 005_architecture_data.json - Update management-modules-audit.md for P0-7 cross-module fix Moved deleted proctoring event route to deletes/ folder.
180 lines
5.8 KiB
TypeScript
180 lines
5.8 KiB
TypeScript
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<NotificationPreferences> => {
|
||
// 先查询
|
||
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<NotificationPreferences | null> {
|
||
// 先查询是否存在
|
||
const [existing] = await db
|
||
.select()
|
||
.from(notificationPreferences)
|
||
.where(eq(notificationPreferences.userId, userId))
|
||
.limit(1)
|
||
|
||
if (existing) {
|
||
// 更新
|
||
const updateData: Partial<typeof notificationPreferences.$inferInsert> = {}
|
||
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
|
||
}
|
||
}
|