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" import type { BatchDeleteResult, CreateFileAttachmentInput, FileAttachment, FileAttachmentQueryParams, FileStats, } from "./types" const toIso = (d: Date): string => d.toISOString() const mapRow = (row: typeof fileAttachments.$inferSelect): FileAttachment => ({ id: row.id, filename: row.filename, originalName: row.originalName, mimeType: row.mimeType, size: row.size, storagePath: row.storagePath, url: row.url, uploaderId: row.uploaderId, targetType: row.targetType, targetId: row.targetId, createdAt: toIso(row.createdAt), }) /** * 插入文件附件记录 */ export async function createFileAttachment( data: CreateFileAttachmentInput ): Promise { try { await db.insert(fileAttachments).values({ id: data.id, filename: data.filename, originalName: data.originalName, mimeType: data.mimeType, size: data.size, storagePath: data.storagePath, url: data.url, uploaderId: data.uploaderId, targetType: data.targetType ?? null, targetId: data.targetId ?? null, }) const created = await getFileAttachment(data.id) return created } catch (error) { console.error("createFileAttachment failed:", error) return null } } /** * 按 ID 查询文件附件 */ export const getFileAttachment = cache( async (id: string): Promise => { try { const [row] = await db .select() .from(fileAttachments) .where(eq(fileAttachments.id, id)) .limit(1) return row ? mapRow(row) : null } catch (error) { console.error("getFileAttachment failed:", error) return null } }, ) /** * 按关联资源查询文件列表 */ export const getFileAttachmentsByTarget = cache( async (targetType: string, targetId: string): Promise => { try { const rows = await db .select() .from(fileAttachments) .where( and( eq(fileAttachments.targetType, targetType), eq(fileAttachments.targetId, targetId) ) ) .orderBy(desc(fileAttachments.createdAt)) return rows.map(mapRow) } catch (error) { console.error("getFileAttachmentsByTarget failed:", error) return [] } }, ) /** * 按上传者查询文件列表 */ export const getFileAttachmentsByUploader = cache( async (uploaderId: string): Promise => { try { const rows = await db .select() .from(fileAttachments) .where(eq(fileAttachments.uploaderId, uploaderId)) .orderBy(desc(fileAttachments.createdAt)) return rows.map(mapRow) } catch (error) { console.error("getFileAttachmentsByUploader failed:", error) return [] } }, ) /** * 查询所有文件(用于管理员文件管理页面) */ export const getAllFileAttachments = cache( async (limit = 100): Promise => { try { const rows = await db .select() .from(fileAttachments) .orderBy(desc(fileAttachments.createdAt)) .limit(limit) return rows.map(mapRow) } catch (error) { console.error("getAllFileAttachments failed:", error) return [] } }, ) /** * 删除文件附件记录 */ export async function deleteFileAttachment(id: string): Promise { try { await db.delete(fileAttachments).where(eq(fileAttachments.id, id)) return true } catch (error) { console.error("deleteFileAttachment failed:", error) return false } } /** * 批量删除文件附件记录 * 仅删除数据库记录,磁盘文件由调用方处理 */ export async function deleteFileAttachments(ids: string[]): Promise { if (ids.length === 0) { return { success: true, deletedCount: 0, failedIds: [] } } try { await db.delete(fileAttachments).where(inArray(fileAttachments.id, ids)) return { success: true, deletedCount: ids.length, failedIds: [] } } catch (error) { console.error("deleteFileAttachments batch failed:", error) // 失败时回退到逐条删除,尽量多删 const failedIds: string[] = [] let deletedCount = 0 for (const id of ids) { try { await db.delete(fileAttachments).where(eq(fileAttachments.id, id)) deletedCount += 1 } catch (err) { console.error("deleteFileAttachments single failed:", err) failedIds.push(id) } } return { success: failedIds.length === 0, deletedCount, failedIds, } } } /** * 按条件筛选文件列表(管理员页面) * - mimeType: 精确匹配或前缀匹配(如 "image/") * - search: 在 originalName / filename 中模糊匹配 */ export const getFileAttachmentsWithFilters = cache( async (params: FileAttachmentQueryParams): Promise => { 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)) } } 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 [] } }, ) /** * 获取文件统计信息(总数、总大小、按类型分组) */ export const getFileStats = cache( async (): Promise => { try { const rows = await db .select({ mimeType: fileAttachments.mimeType, count: count(), size: sql`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 totalCount = byType.reduce((sum, r) => sum + r.count, 0) const totalSize = byType.reduce((sum, r) => sum + r.size, 0) return { totalCount, totalSize, byType } } catch (error) { console.error("getFileStats failed:", error) return { totalCount: 0, totalSize: 0, byType: [] } } }, ) /** * 按 ID 列表批量查询文件(用于批量删除前获取磁盘路径) */ export const getFileAttachmentsByIds = cache( async (ids: string[]): Promise => { 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 [] } }, )