Files
NextEdu/src/modules/lesson-preparation/data-access-attachments.ts
SpecialX 524ecade19 feat(lesson-preparation): major update with data-access splits and new components
- Update ai-feedback-dialog, attachment-picker, blocks/text-study-block

- Update detail-panel (detail-panel, detail-props)

- Update inline-question-editor, lesson-plan-card, lesson-plan-editor,

  lesson-plan-mobile-view, schedule-dialog, version-history-drawer

- Update paper-editor (inline-qa-dialog, paper-context-menu)

- Update structure-tree (structure-tree, tree-node-row)

- Update config/block-registry, hooks (editor-slice, use-lesson-plan-persistence,

  use-node-ai-assist), lib (ai-node-assist, consistency-check, export, type-guards)

- Update publish-service, services/default-question-service

- Update data-access files (ai-evaluation, analytics, attachments, calendar,

  comments, formative, knowledge, review, schedules, templates, versions, main)
2026-07-07 16:20:34 +08:00

176 lines
5.0 KiB
TypeScript

/**
* M6 资源附件库 - 数据访问层
*/
import "server-only";
import { cacheFn } from "@/shared/lib/cache";
import { db } from "@/shared/db";
import { lessonPlanAttachments } from "@/shared/db/schema";
import { and, eq, desc } from "drizzle-orm";
import type { AttachmentType } from "./lib/type-guards";
import { isAttachmentType } from "./lib/type-guards";
/** 附件记录 */
export interface LessonPlanAttachment {
id: string;
planId: string;
blockId?: string;
fileId: string;
displayName: string;
attachmentType: AttachmentType;
uploadedBy: string;
createdAt: Date;
}
/**
* 查询课案的所有附件
*/
export async function getAttachmentsByPlanIdRaw(
planId: string,
): Promise<LessonPlanAttachment[]> {
const rows = await db
.select({
id: lessonPlanAttachments.id,
planId: lessonPlanAttachments.planId,
blockId: lessonPlanAttachments.blockId,
fileId: lessonPlanAttachments.fileId,
displayName: lessonPlanAttachments.displayName,
attachmentType: lessonPlanAttachments.attachmentType,
uploadedBy: lessonPlanAttachments.uploadedBy,
createdAt: lessonPlanAttachments.createdAt,
})
.from(lessonPlanAttachments)
.where(eq(lessonPlanAttachments.planId, planId))
.orderBy(desc(lessonPlanAttachments.createdAt));
return rows.map(mapRowToAttachment);
}
export const getAttachmentsByPlanId = cacheFn(getAttachmentsByPlanIdRaw, { tags: ["lesson-preparation"], ttl: 300, keyParts: ["lp", "attachments", "by-plan"] });
/**
* 查询特定 Block 的附件
*/
export async function getAttachmentsByBlockIdRaw(
planId: string,
blockId: string,
): Promise<LessonPlanAttachment[]> {
const rows = await db
.select({
id: lessonPlanAttachments.id,
planId: lessonPlanAttachments.planId,
blockId: lessonPlanAttachments.blockId,
fileId: lessonPlanAttachments.fileId,
displayName: lessonPlanAttachments.displayName,
attachmentType: lessonPlanAttachments.attachmentType,
uploadedBy: lessonPlanAttachments.uploadedBy,
createdAt: lessonPlanAttachments.createdAt,
})
.from(lessonPlanAttachments)
.where(
and(
eq(lessonPlanAttachments.planId, planId),
eq(lessonPlanAttachments.blockId, blockId),
),
)
.orderBy(desc(lessonPlanAttachments.createdAt));
return rows.map(mapRowToAttachment);
}
export const getAttachmentsByBlockId = cacheFn(getAttachmentsByBlockIdRaw, { tags: ["lesson-preparation"], ttl: 300, keyParts: ["lp", "attachments", "by-block"] });
/**
* 添加附件
*/
export async function createAttachment(
input: {
planId: string;
blockId?: string;
fileId: string;
displayName: string;
attachmentType?: AttachmentType;
},
uploadedBy: string,
): Promise<LessonPlanAttachment> {
const [row] = await db.insert(lessonPlanAttachments).values({
planId: input.planId,
blockId: input.blockId,
fileId: input.fileId,
displayName: input.displayName,
attachmentType: input.attachmentType ?? "reference",
uploadedBy,
});
const insertedId = row.insertId;
const all = await getAttachmentsByPlanId(input.planId);
const created = all.find((a) => a.id === String(insertedId));
if (!created) throw new Error("ATTACHMENT_CREATE_FAILED");
return created;
}
/**
* 删除附件
*/
export async function deleteAttachment(
attachmentId: string,
): Promise<void> {
await db
.delete(lessonPlanAttachments)
.where(eq(lessonPlanAttachments.id, attachmentId));
}
/**
* 更新附件类型
*/
export async function updateAttachmentType(
attachmentId: string,
attachmentType: AttachmentType,
): Promise<void> {
await db
.update(lessonPlanAttachments)
.set({ attachmentType })
.where(eq(lessonPlanAttachments.id, attachmentId));
}
/**
* 按 ID 获取附件
*/
export async function getAttachmentByIdRaw(
id: string,
): Promise<LessonPlanAttachment | null> {
const rows = await db
.select({
id: lessonPlanAttachments.id,
planId: lessonPlanAttachments.planId,
blockId: lessonPlanAttachments.blockId,
fileId: lessonPlanAttachments.fileId,
displayName: lessonPlanAttachments.displayName,
attachmentType: lessonPlanAttachments.attachmentType,
uploadedBy: lessonPlanAttachments.uploadedBy,
createdAt: lessonPlanAttachments.createdAt,
})
.from(lessonPlanAttachments)
.where(eq(lessonPlanAttachments.id, id))
.limit(1);
return rows.length === 0 ? null : mapRowToAttachment(rows[0]!);
}
export const getAttachmentById = cacheFn(getAttachmentByIdRaw, { tags: ["lesson-preparation"], ttl: 600, keyParts: ["lp", "attachments", "by-id"] });
function mapRowToAttachment(
row: typeof lessonPlanAttachments.$inferSelect,
): LessonPlanAttachment {
const attachmentType = isAttachmentType(row.attachmentType)
? row.attachmentType
: "reference";
return {
id: row.id,
planId: row.planId,
blockId: row.blockId ?? undefined,
fileId: row.fileId,
displayName: row.displayName,
attachmentType,
uploadedBy: row.uploadedBy,
createdAt: row.createdAt,
};
}