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,12 +1,16 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
import { PermissionDeniedError, requireAuth, requirePermission } from "@/shared/lib/auth-guard"
|
||||
import { PermissionDeniedError, requirePermission } from "@/shared/lib/auth-guard"
|
||||
import { Permissions } from "@/shared/types/permissions"
|
||||
import type { ActionState } from "@/shared/types/action-state"
|
||||
import { sendNotification } from "@/modules/notifications/dispatcher"
|
||||
|
||||
import { SendMessageSchema } from "./schema"
|
||||
import {
|
||||
SendMessageSchema,
|
||||
MessageIdSchema,
|
||||
UpdateNotificationPreferencesSchema,
|
||||
} from "./schema"
|
||||
import {
|
||||
getMessages,
|
||||
getMessageById,
|
||||
@@ -87,9 +91,15 @@ export async function sendMessageAction(
|
||||
export async function markMessageAsReadAction(messageId: string): Promise<ActionState<string>> {
|
||||
try {
|
||||
const ctx = await requirePermission(Permissions.MESSAGE_READ)
|
||||
await markMessageAsRead(messageId, ctx.userId)
|
||||
|
||||
const parsed = MessageIdSchema.safeParse({ messageId })
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Invalid message id", errors: parsed.error.flatten().fieldErrors }
|
||||
}
|
||||
|
||||
await markMessageAsRead(parsed.data.messageId, ctx.userId)
|
||||
revalidatePath("/messages")
|
||||
revalidatePath(`/messages/${messageId}`)
|
||||
revalidatePath(`/messages/${parsed.data.messageId}`)
|
||||
return { success: true, message: "Marked as read" }
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
|
||||
@@ -101,9 +111,15 @@ export async function markMessageAsReadAction(messageId: string): Promise<Action
|
||||
export async function deleteMessageAction(messageId: string): Promise<ActionState<string>> {
|
||||
try {
|
||||
const ctx = await requirePermission(Permissions.MESSAGE_DELETE)
|
||||
await deleteMessage(messageId, ctx.userId)
|
||||
|
||||
const parsed = MessageIdSchema.safeParse({ messageId })
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Invalid message id", errors: parsed.error.flatten().fieldErrors }
|
||||
}
|
||||
|
||||
await deleteMessage(parsed.data.messageId, ctx.userId)
|
||||
revalidatePath("/messages")
|
||||
revalidatePath(`/messages/${messageId}`)
|
||||
revalidatePath(`/messages/${parsed.data.messageId}`)
|
||||
return { success: true, message: "Message deleted" }
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) return { success: false, message: e.message }
|
||||
@@ -129,11 +145,18 @@ export async function getMessagesAction(
|
||||
export async function getMessageDetailAction(messageId: string): Promise<ActionState<Message>> {
|
||||
try {
|
||||
const ctx = await requirePermission(Permissions.MESSAGE_READ)
|
||||
const message = await getMessageById(messageId, ctx.userId)
|
||||
|
||||
const parsed = MessageIdSchema.safeParse({ messageId })
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Invalid message id", errors: parsed.error.flatten().fieldErrors }
|
||||
}
|
||||
|
||||
const validMessageId = parsed.data.messageId
|
||||
const message = await getMessageById(validMessageId, ctx.userId)
|
||||
if (!message) return { success: false, message: "Message not found" }
|
||||
// Auto-mark as read when viewed by receiver
|
||||
if (!message.isRead && message.receiverId === ctx.userId) {
|
||||
await markMessageAsRead(messageId, ctx.userId)
|
||||
await markMessageAsRead(validMessageId, ctx.userId)
|
||||
revalidatePath("/messages")
|
||||
}
|
||||
return { success: true, data: message }
|
||||
@@ -160,7 +183,7 @@ export async function getNotificationsAction(
|
||||
params?: { page?: number; pageSize?: number; unreadOnly?: boolean }
|
||||
): Promise<ActionState<{ items: Notification[]; total: number; page: number; pageSize: number; totalPages: number }>> {
|
||||
try {
|
||||
const ctx = await requireAuth()
|
||||
const ctx = await requirePermission(Permissions.MESSAGE_READ)
|
||||
const result = await getNotifications(ctx.userId, params)
|
||||
return { success: true, data: result }
|
||||
} catch (e) {
|
||||
@@ -174,7 +197,7 @@ export async function markNotificationAsReadAction(
|
||||
notificationId: string
|
||||
): Promise<ActionState<string>> {
|
||||
try {
|
||||
const ctx = await requireAuth()
|
||||
const ctx = await requirePermission(Permissions.MESSAGE_READ)
|
||||
await markNotificationAsRead(notificationId, ctx.userId)
|
||||
revalidatePath("/messages")
|
||||
return { success: true, message: "Notification marked as read" }
|
||||
@@ -187,7 +210,7 @@ export async function markNotificationAsReadAction(
|
||||
|
||||
export async function markAllNotificationsAsReadAction(): Promise<ActionState<string>> {
|
||||
try {
|
||||
const ctx = await requireAuth()
|
||||
const ctx = await requirePermission(Permissions.MESSAGE_READ)
|
||||
await markAllNotificationsAsRead(ctx.userId)
|
||||
revalidatePath("/messages")
|
||||
return { success: true, message: "All notifications marked as read" }
|
||||
@@ -200,7 +223,7 @@ export async function markAllNotificationsAsReadAction(): Promise<ActionState<st
|
||||
|
||||
export async function getNotificationPreferencesAction(): Promise<ActionState<NotificationPreferences>> {
|
||||
try {
|
||||
const ctx = await requireAuth()
|
||||
const ctx = await requirePermission(Permissions.MESSAGE_READ)
|
||||
const prefs = await getNotificationPreferences(ctx.userId)
|
||||
return { success: true, data: prefs }
|
||||
} catch (e) {
|
||||
@@ -215,12 +238,12 @@ export async function updateNotificationPreferencesAction(
|
||||
formData: FormData
|
||||
): Promise<ActionState<NotificationPreferences>> {
|
||||
try {
|
||||
const ctx = await requireAuth()
|
||||
const ctx = await requirePermission(Permissions.MESSAGE_READ)
|
||||
|
||||
// 从 FormData 中解析布尔值(checkbox 提交 "on" 或不提交)
|
||||
const parseBool = (key: string): boolean => formData.get(key) === "on"
|
||||
|
||||
const input: UpdateNotificationPreferencesInput = {
|
||||
const parsed = UpdateNotificationPreferencesSchema.safeParse({
|
||||
emailEnabled: parseBool("emailEnabled"),
|
||||
smsEnabled: parseBool("smsEnabled"),
|
||||
pushEnabled: parseBool("pushEnabled"),
|
||||
@@ -229,8 +252,14 @@ export async function updateNotificationPreferencesAction(
|
||||
announcementNotifications: parseBool("announcementNotifications"),
|
||||
messageNotifications: parseBool("messageNotifications"),
|
||||
attendanceNotifications: parseBool("attendanceNotifications"),
|
||||
})
|
||||
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Invalid form data", errors: parsed.error.flatten().fieldErrors }
|
||||
}
|
||||
|
||||
const input: UpdateNotificationPreferencesInput = parsed.data
|
||||
|
||||
const updated = await upsertNotificationPreferences(ctx.userId, input)
|
||||
if (!updated) {
|
||||
return { success: false, message: "Failed to update notification preferences" }
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,166 +1,11 @@
|
||||
import "server-only"
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的通知偏好设置
|
||||
* 如果用户尚无记录,则自动创建一条默认记录并返回
|
||||
* 通知偏好数据访问(向后兼容重导出)
|
||||
*
|
||||
* 注意: 通知偏好函数已迁移到 notifications/preferences.ts(P0-4 / P1-5 修复)。
|
||||
* 本文件通过 re-export 保持向后兼容,现有调用方无需修改 import 路径。
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
export {
|
||||
getNotificationPreferences,
|
||||
upsertNotificationPreferences,
|
||||
} from "@/modules/notifications/preferences"
|
||||
|
||||
@@ -16,3 +16,26 @@ export const SendMessageSchema = z
|
||||
}))
|
||||
|
||||
export type SendMessageInput = z.infer<typeof SendMessageSchema>
|
||||
|
||||
/** 校验单个 messageId / notificationId 路径参数 */
|
||||
export const MessageIdSchema = z.object({
|
||||
messageId: z.string().trim().min(1),
|
||||
})
|
||||
|
||||
export type MessageIdInput = z.infer<typeof MessageIdSchema>
|
||||
|
||||
/** 校验通知偏好更新表单(8 个布尔字段,来自 checkbox FormData) */
|
||||
export const UpdateNotificationPreferencesSchema = z.object({
|
||||
emailEnabled: z.boolean(),
|
||||
smsEnabled: z.boolean(),
|
||||
pushEnabled: z.boolean(),
|
||||
homeworkNotifications: z.boolean(),
|
||||
gradeNotifications: z.boolean(),
|
||||
announcementNotifications: z.boolean(),
|
||||
messageNotifications: z.boolean(),
|
||||
attendanceNotifications: z.boolean(),
|
||||
})
|
||||
|
||||
export type UpdateNotificationPreferencesFormInput = z.infer<
|
||||
typeof UpdateNotificationPreferencesSchema
|
||||
>
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/**
|
||||
* 私信模块类型定义
|
||||
*
|
||||
* 注意: 通知相关类型(NotificationType, Notification, NotificationPreferences,
|
||||
* UpdateNotificationPreferencesInput, CreateNotificationInput, GetNotificationsParams,
|
||||
* PaginatedResult)已迁移到 notifications/types.ts(P0-4 / P1-5 修复)。
|
||||
* 本文件通过 re-export 保持向后兼容,现有调用方无需修改 import 路径。
|
||||
*/
|
||||
|
||||
export type MessageType = "inbox" | "sent" | "all"
|
||||
|
||||
export interface Message {
|
||||
@@ -20,29 +29,6 @@ export interface MessageThread {
|
||||
messages: Message[]
|
||||
}
|
||||
|
||||
export type NotificationType = "message" | "announcement" | "homework" | "grade"
|
||||
|
||||
export interface Notification {
|
||||
id: string
|
||||
userId: string
|
||||
type: NotificationType
|
||||
title: string
|
||||
content: string | null
|
||||
link: string | null
|
||||
isRead: boolean
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export type NotificationListItem = Notification
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
export interface GetMessagesParams {
|
||||
userId: string
|
||||
type: MessageType
|
||||
@@ -50,12 +36,6 @@ export interface GetMessagesParams {
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
export interface GetNotificationsParams {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
unreadOnly?: boolean
|
||||
}
|
||||
|
||||
export interface CreateMessageInput {
|
||||
senderId: string
|
||||
receiverId: string
|
||||
@@ -64,14 +44,6 @@ export interface CreateMessageInput {
|
||||
parentMessageId?: string | null
|
||||
}
|
||||
|
||||
export interface CreateNotificationInput {
|
||||
userId: string
|
||||
type: NotificationType
|
||||
title: string
|
||||
content?: string | null
|
||||
link?: string | null
|
||||
}
|
||||
|
||||
export interface RecipientOption {
|
||||
id: string
|
||||
name: string
|
||||
@@ -79,30 +51,17 @@ export interface RecipientOption {
|
||||
role?: string
|
||||
}
|
||||
|
||||
// 通知偏好设置
|
||||
export interface NotificationPreferences {
|
||||
id: string
|
||||
userId: string
|
||||
emailEnabled: boolean
|
||||
smsEnabled: boolean
|
||||
pushEnabled: boolean
|
||||
homeworkNotifications: boolean
|
||||
gradeNotifications: boolean
|
||||
announcementNotifications: boolean
|
||||
messageNotifications: boolean
|
||||
attendanceNotifications: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// 向后兼容 re-export:通知相关类型已迁移到 notifications/types.ts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// 更新通知偏好的输入(部分字段可选,未提供则保留原值)
|
||||
export interface UpdateNotificationPreferencesInput {
|
||||
emailEnabled?: boolean
|
||||
smsEnabled?: boolean
|
||||
pushEnabled?: boolean
|
||||
homeworkNotifications?: boolean
|
||||
gradeNotifications?: boolean
|
||||
announcementNotifications?: boolean
|
||||
messageNotifications?: boolean
|
||||
attendanceNotifications?: boolean
|
||||
}
|
||||
export type {
|
||||
NotificationType,
|
||||
Notification,
|
||||
NotificationListItem,
|
||||
GetNotificationsParams,
|
||||
CreateNotificationInput,
|
||||
PaginatedResult,
|
||||
NotificationPreferences,
|
||||
UpdateNotificationPreferencesInput,
|
||||
} from "@/modules/notifications/types"
|
||||
|
||||
Reference in New Issue
Block a user