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

@@ -1,6 +1,7 @@
import "server-only"
import { and, count, desc, eq, inArray, like, or, sql } from "drizzle-orm"
import { cache } from "react"
import { db } from "@/shared/db"
import { fileAttachments } from "@/shared/db/schema"
@@ -50,7 +51,8 @@ export async function createFileAttachment(
const created = await getFileAttachment(data.id)
return created
} catch {
} catch (error) {
console.error("createFileAttachment failed:", error)
return null
}
}
@@ -58,80 +60,87 @@ export async function createFileAttachment(
/**
* 按 ID 查询文件附件
*/
export async function getFileAttachment(id: string): Promise<FileAttachment | null> {
try {
const [row] = await db
.select()
.from(fileAttachments)
.where(eq(fileAttachments.id, id))
.limit(1)
export const getFileAttachment = cache(
async (id: string): Promise<FileAttachment | null> => {
try {
const [row] = await db
.select()
.from(fileAttachments)
.where(eq(fileAttachments.id, id))
.limit(1)
return row ? mapRow(row) : null
} catch {
return null
}
}
return row ? mapRow(row) : null
} catch (error) {
console.error("getFileAttachment failed:", error)
return null
}
},
)
/**
* 按关联资源查询文件列表
*/
export async function getFileAttachmentsByTarget(
targetType: string,
targetId: string
): Promise<FileAttachment[]> {
try {
const rows = await db
.select()
.from(fileAttachments)
.where(
and(
eq(fileAttachments.targetType, targetType),
eq(fileAttachments.targetId, targetId)
export const getFileAttachmentsByTarget = cache(
async (targetType: string, targetId: string): Promise<FileAttachment[]> => {
try {
const rows = await db
.select()
.from(fileAttachments)
.where(
and(
eq(fileAttachments.targetType, targetType),
eq(fileAttachments.targetId, targetId)
)
)
)
.orderBy(desc(fileAttachments.createdAt))
.orderBy(desc(fileAttachments.createdAt))
return rows.map(mapRow)
} catch {
return []
}
}
return rows.map(mapRow)
} catch (error) {
console.error("getFileAttachmentsByTarget failed:", error)
return []
}
},
)
/**
* 按上传者查询文件列表
*/
export async function getFileAttachmentsByUploader(
uploaderId: string
): Promise<FileAttachment[]> {
try {
const rows = await db
.select()
.from(fileAttachments)
.where(eq(fileAttachments.uploaderId, uploaderId))
.orderBy(desc(fileAttachments.createdAt))
export const getFileAttachmentsByUploader = cache(
async (uploaderId: string): Promise<FileAttachment[]> => {
try {
const rows = await db
.select()
.from(fileAttachments)
.where(eq(fileAttachments.uploaderId, uploaderId))
.orderBy(desc(fileAttachments.createdAt))
return rows.map(mapRow)
} catch {
return []
}
}
return rows.map(mapRow)
} catch (error) {
console.error("getFileAttachmentsByUploader failed:", error)
return []
}
},
)
/**
* 查询所有文件(用于管理员文件管理页面)
*/
export async function getAllFileAttachments(limit = 100): Promise<FileAttachment[]> {
try {
const rows = await db
.select()
.from(fileAttachments)
.orderBy(desc(fileAttachments.createdAt))
.limit(limit)
export const getAllFileAttachments = cache(
async (limit = 100): Promise<FileAttachment[]> => {
try {
const rows = await db
.select()
.from(fileAttachments)
.orderBy(desc(fileAttachments.createdAt))
.limit(limit)
return rows.map(mapRow)
} catch {
return []
}
}
return rows.map(mapRow)
} catch (error) {
console.error("getAllFileAttachments failed:", error)
return []
}
},
)
/**
* 删除文件附件记录
@@ -140,7 +149,8 @@ export async function deleteFileAttachment(id: string): Promise<boolean> {
try {
await db.delete(fileAttachments).where(eq(fileAttachments.id, id))
return true
} catch {
} catch (error) {
console.error("deleteFileAttachment failed:", error)
return false
}
}
@@ -156,7 +166,8 @@ export async function deleteFileAttachments(ids: string[]): Promise<BatchDeleteR
try {
await db.delete(fileAttachments).where(inArray(fileAttachments.id, ids))
return { success: true, deletedCount: ids.length, failedIds: [] }
} catch {
} catch (error) {
console.error("deleteFileAttachments batch failed:", error)
// 失败时回退到逐条删除,尽量多删
const failedIds: string[] = []
let deletedCount = 0
@@ -164,7 +175,8 @@ export async function deleteFileAttachments(ids: string[]): Promise<BatchDeleteR
try {
await db.delete(fileAttachments).where(eq(fileAttachments.id, id))
deletedCount += 1
} catch {
} catch (err) {
console.error("deleteFileAttachments single failed:", err)
failedIds.push(id)
}
}
@@ -181,87 +193,93 @@ export async function deleteFileAttachments(ids: string[]): Promise<BatchDeleteR
* - mimeType: 精确匹配或前缀匹配(如 "image/"
* - search: 在 originalName / filename 中模糊匹配
*/
export async function getFileAttachmentsWithFilters(
params: FileAttachmentQueryParams
): Promise<FileAttachment[]> {
try {
const { mimeType, search, limit = 100, offset = 0 } = params
export const getFileAttachmentsWithFilters = cache(
async (params: FileAttachmentQueryParams): Promise<FileAttachment[]> => {
try {
const { mimeType, search, limit = 100, offset = 0 } = params
const conditions = []
if (mimeType) {
if (mimeType.endsWith("/")) {
conditions.push(like(fileAttachments.mimeType, `${mimeType}%`))
} else {
conditions.push(eq(fileAttachments.mimeType, mimeType))
const conditions = []
if (mimeType) {
if (mimeType.endsWith("/")) {
conditions.push(like(fileAttachments.mimeType, `${mimeType}%`))
} else {
conditions.push(eq(fileAttachments.mimeType, mimeType))
}
}
}
if (search) {
const kw = `%${search}%`
conditions.push(
or(
if (search) {
const kw = `%${search}%`
const nameCondition = or(
like(fileAttachments.originalName, kw),
like(fileAttachments.filename, kw)
)!
)
)
if (nameCondition) conditions.push(nameCondition)
}
const where = conditions.length > 0 ? and(...conditions) : undefined
const rows = await db
.select()
.from(fileAttachments)
.where(where)
.orderBy(desc(fileAttachments.createdAt))
.limit(limit)
.offset(offset)
return rows.map(mapRow)
} catch (error) {
console.error("getFileAttachmentsWithFilters failed:", error)
return []
}
const where = conditions.length > 0 ? and(...conditions) : undefined
const rows = await db
.select()
.from(fileAttachments)
.where(where)
.orderBy(desc(fileAttachments.createdAt))
.limit(limit)
.offset(offset)
return rows.map(mapRow)
} catch {
return []
}
}
},
)
/**
* 获取文件统计信息(总数、总大小、按类型分组)
*/
export async function getFileStats(): Promise<FileStats> {
try {
const rows = await db
.select({
mimeType: fileAttachments.mimeType,
count: count(),
size: sql<number>`COALESCE(SUM(${fileAttachments.size}), 0)`,
})
.from(fileAttachments)
.groupBy(fileAttachments.mimeType)
export const getFileStats = cache(
async (): Promise<FileStats> => {
try {
const rows = await db
.select({
mimeType: fileAttachments.mimeType,
count: count(),
size: sql<number>`COALESCE(SUM(${fileAttachments.size}), 0)`,
})
.from(fileAttachments)
.groupBy(fileAttachments.mimeType)
const byType = rows.map((r) => ({
mimeType: r.mimeType,
count: Number(r.count),
size: Number(r.size),
}))
const byType = rows.map((r) => ({
mimeType: r.mimeType,
count: Number(r.count),
size: Number(r.size),
}))
const totalCount = byType.reduce((sum, r) => sum + r.count, 0)
const totalSize = byType.reduce((sum, r) => sum + r.size, 0)
const totalCount = byType.reduce((sum, r) => sum + r.count, 0)
const totalSize = byType.reduce((sum, r) => sum + r.size, 0)
return { totalCount, totalSize, byType }
} catch {
return { totalCount: 0, totalSize: 0, byType: [] }
}
}
return { totalCount, totalSize, byType }
} catch (error) {
console.error("getFileStats failed:", error)
return { totalCount: 0, totalSize: 0, byType: [] }
}
},
)
/**
* 按 ID 列表批量查询文件(用于批量删除前获取磁盘路径)
*/
export async function getFileAttachmentsByIds(ids: string[]): Promise<FileAttachment[]> {
if (ids.length === 0) return []
try {
const rows = await db
.select()
.from(fileAttachments)
.where(inArray(fileAttachments.id, ids))
return rows.map(mapRow)
} catch {
return []
}
}
export const getFileAttachmentsByIds = cache(
async (ids: string[]): Promise<FileAttachment[]> => {
if (ids.length === 0) return []
try {
const rows = await db
.select()
.from(fileAttachments)
.where(inArray(fileAttachments.id, ids))
return rows.map(mapRow)
} catch (error) {
console.error("getFileAttachmentsByIds failed:", error)
return []
}
},
)