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,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 []
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user