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:
@@ -1,5 +1,20 @@
|
||||
import "server-only"
|
||||
|
||||
/**
|
||||
* 私信数据访问层
|
||||
*
|
||||
* 职责:
|
||||
* - getMessages / getMessageById / getMessageThread: 私信查询
|
||||
* - createMessage / markMessageAsRead / deleteMessage: 私信 CRUD
|
||||
* - getUnreadMessageCount: 未读私信计数
|
||||
* - getRecipients: 获取收件人列表(按 DataScope 过滤)
|
||||
*
|
||||
* 注意: 通知相关函数(createNotification / getNotifications /
|
||||
* markNotificationAsRead / markAllNotificationsAsRead / getUnreadNotificationCount)
|
||||
* 已迁移到 notifications/data-access.ts(P0-4 / P1-5 修复)。
|
||||
* 本文件通过 re-export 保持向后兼容,现有调用方无需修改 import 路径。
|
||||
*/
|
||||
|
||||
import { cache } from "react"
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
import { and, count, desc, eq, inArray, or } from "drizzle-orm"
|
||||
@@ -7,7 +22,6 @@ import { and, count, desc, eq, inArray, or } from "drizzle-orm"
|
||||
import { db } from "@/shared/db"
|
||||
import {
|
||||
messages,
|
||||
messageNotifications,
|
||||
users,
|
||||
classEnrollments,
|
||||
classes,
|
||||
@@ -15,18 +29,16 @@ import {
|
||||
import type { DataScope } from "@/shared/types/permissions"
|
||||
import type {
|
||||
Message,
|
||||
Notification,
|
||||
NotificationType,
|
||||
GetMessagesParams,
|
||||
GetNotificationsParams,
|
||||
CreateMessageInput,
|
||||
CreateNotificationInput,
|
||||
PaginatedResult,
|
||||
RecipientOption,
|
||||
} from "./types"
|
||||
|
||||
const toIso = (d: Date | null | undefined): string | null => (d ? d.toISOString() : null)
|
||||
|
||||
const toIsoRequired = (d: Date): string => d.toISOString()
|
||||
|
||||
interface MessageRow {
|
||||
id: string
|
||||
senderId: string
|
||||
@@ -39,17 +51,6 @@ interface MessageRow {
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
interface NotificationRow {
|
||||
id: string
|
||||
userId: string
|
||||
type: string
|
||||
title: string
|
||||
content: string | null
|
||||
link: string | null
|
||||
isRead: boolean
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
async function resolveUserNames(userIds: string[]): Promise<Map<string, string>> {
|
||||
const uniqueIds = [...new Set(userIds)].filter(Boolean)
|
||||
if (uniqueIds.length === 0) return new Map()
|
||||
@@ -71,18 +72,7 @@ const mapMessage = (r: MessageRow, nameMap: Map<string, string>): Message => ({
|
||||
isRead: r.isRead,
|
||||
readAt: toIso(r.readAt),
|
||||
parentMessageId: r.parentMessageId,
|
||||
createdAt: toIso(r.createdAt) as string,
|
||||
})
|
||||
|
||||
const mapNotification = (r: NotificationRow): Notification => ({
|
||||
id: r.id,
|
||||
userId: r.userId,
|
||||
type: r.type as NotificationType,
|
||||
title: r.title,
|
||||
content: r.content,
|
||||
link: r.link,
|
||||
isRead: r.isRead,
|
||||
createdAt: toIso(r.createdAt) as string,
|
||||
createdAt: toIsoRequired(r.createdAt),
|
||||
})
|
||||
|
||||
export const getMessages = cache(
|
||||
@@ -94,7 +84,10 @@ export const getMessages = cache(
|
||||
const conds = []
|
||||
if (params.type === "inbox") conds.push(eq(messages.receiverId, params.userId))
|
||||
else if (params.type === "sent") conds.push(eq(messages.senderId, params.userId))
|
||||
else conds.push(or(eq(messages.receiverId, params.userId), eq(messages.senderId, params.userId))!)
|
||||
else {
|
||||
const cond = or(eq(messages.receiverId, params.userId), eq(messages.senderId, params.userId))
|
||||
if (cond) conds.push(cond)
|
||||
}
|
||||
|
||||
const where = and(...conds)
|
||||
const [rows, [totalRow]] = await Promise.all([
|
||||
@@ -114,7 +107,7 @@ export const getMessageById = cache(
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(messages)
|
||||
.where(and(eq(messages.id, id), or(eq(messages.senderId, userId), eq(messages.receiverId, userId))!))
|
||||
.where(and(eq(messages.id, id), or(eq(messages.senderId, userId), eq(messages.receiverId, userId))))
|
||||
.limit(1)
|
||||
if (!row) return null
|
||||
const nameMap = await resolveUserNames([row.senderId, row.receiverId])
|
||||
@@ -160,7 +153,7 @@ export async function markMessageAsRead(id: string, userId: string): Promise<voi
|
||||
export async function deleteMessage(id: string, userId: string): Promise<void> {
|
||||
await db
|
||||
.delete(messages)
|
||||
.where(and(eq(messages.id, id), or(eq(messages.senderId, userId), eq(messages.receiverId, userId))!))
|
||||
.where(and(eq(messages.id, id), or(eq(messages.senderId, userId), eq(messages.receiverId, userId))))
|
||||
}
|
||||
|
||||
export const getUnreadMessageCount = cache(async (userId: string): Promise<number> => {
|
||||
@@ -171,59 +164,6 @@ export const getUnreadMessageCount = cache(async (userId: string): Promise<numbe
|
||||
return Number(row?.value ?? 0)
|
||||
})
|
||||
|
||||
export const getNotifications = cache(
|
||||
async (userId: string, params?: GetNotificationsParams): Promise<PaginatedResult<Notification>> => {
|
||||
const page = Math.max(1, params?.page ?? 1)
|
||||
const pageSize = Math.max(1, params?.pageSize ?? 20)
|
||||
const offset = (page - 1) * pageSize
|
||||
const conds = [eq(messageNotifications.userId, userId)]
|
||||
if (params?.unreadOnly) conds.push(eq(messageNotifications.isRead, false))
|
||||
const where = and(...conds)
|
||||
|
||||
const [rows, [totalRow]] = await Promise.all([
|
||||
db.select().from(messageNotifications).where(where).orderBy(desc(messageNotifications.createdAt)).limit(pageSize).offset(offset),
|
||||
db.select({ value: count() }).from(messageNotifications).where(where),
|
||||
])
|
||||
const total = Number(totalRow?.value ?? 0)
|
||||
return { items: rows.map(mapNotification), total, page, pageSize, totalPages: Math.ceil(total / pageSize) }
|
||||
}
|
||||
)
|
||||
|
||||
export async function createNotification(data: CreateNotificationInput): Promise<string> {
|
||||
const id = createId()
|
||||
await db.insert(messageNotifications).values({
|
||||
id,
|
||||
userId: data.userId,
|
||||
type: data.type,
|
||||
title: data.title,
|
||||
content: data.content ?? null,
|
||||
link: data.link ?? null,
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
export async function markNotificationAsRead(id: string, userId: string): Promise<void> {
|
||||
await db
|
||||
.update(messageNotifications)
|
||||
.set({ isRead: true })
|
||||
.where(and(eq(messageNotifications.id, id), eq(messageNotifications.userId, userId)))
|
||||
}
|
||||
|
||||
export async function markAllNotificationsAsRead(userId: string): Promise<void> {
|
||||
await db
|
||||
.update(messageNotifications)
|
||||
.set({ isRead: true })
|
||||
.where(and(eq(messageNotifications.userId, userId), eq(messageNotifications.isRead, false)))
|
||||
}
|
||||
|
||||
export const getUnreadNotificationCount = cache(async (userId: string): Promise<number> => {
|
||||
const [row] = await db
|
||||
.select({ value: count() })
|
||||
.from(messageNotifications)
|
||||
.where(and(eq(messageNotifications.userId, userId), eq(messageNotifications.isRead, false)))
|
||||
return Number(row?.value ?? 0)
|
||||
})
|
||||
|
||||
export const getRecipients = cache(
|
||||
async (userId: string, scope: DataScope): Promise<RecipientOption[]> => {
|
||||
if (scope.type === "all") {
|
||||
@@ -250,3 +190,15 @@ export const getRecipients = cache(
|
||||
return []
|
||||
}
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 向后兼容 re-export:通知 CRUD 已迁移到 notifications/data-access.ts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
createNotification,
|
||||
getNotifications,
|
||||
markNotificationAsRead,
|
||||
markAllNotificationsAsRead,
|
||||
getUnreadNotificationCount,
|
||||
} from "@/modules/notifications/data-access"
|
||||
|
||||
Reference in New Issue
Block a user