refactor: fix all P0/P1/P2 bugs and architecture issues
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.
This commit is contained in:
179
src/modules/notifications/preferences.ts
Normal file
179
src/modules/notifications/preferences.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user