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:
SpecialX
2026-06-19 05:13:09 +08:00
parent 063baffe4c
commit 49291fcc31
114 changed files with 12548 additions and 3395 deletions

View 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
}
}