完整性更新
现在已经实现了大部分基础功能
This commit is contained in:
@@ -1,226 +1,396 @@
|
||||
import { Textbook, Chapter, CreateTextbookInput, CreateChapterInput, UpdateChapterContentInput, KnowledgePoint, CreateKnowledgePointInput, UpdateTextbookInput } from "./types";
|
||||
import "server-only"
|
||||
|
||||
// Mock Data (Moved from data/mock-data.ts and enhanced)
|
||||
let MOCK_TEXTBOOKS: Textbook[] = [
|
||||
// ... (previous textbooks remain same, keeping for brevity)
|
||||
{
|
||||
id: "tb_01",
|
||||
title: "Advanced Mathematics Grade 10",
|
||||
subject: "Mathematics",
|
||||
grade: "Grade 10",
|
||||
publisher: "Next Education Press",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
_count: { chapters: 12 },
|
||||
},
|
||||
// ... (other textbooks)
|
||||
];
|
||||
import { cache } from "react"
|
||||
import { and, asc, eq, inArray, like, or, sql, type SQL } from "drizzle-orm"
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
|
||||
let MOCK_CHAPTERS: Chapter[] = [
|
||||
// ... (previous chapters)
|
||||
{
|
||||
id: "ch_01",
|
||||
textbookId: "tb_01",
|
||||
title: "Chapter 1: Real Numbers",
|
||||
order: 1,
|
||||
parentId: null,
|
||||
content: "# Chapter 1: Real Numbers\n\nIn this chapter, we will explore the properties of real numbers...",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
children: [
|
||||
{
|
||||
id: "ch_01_01",
|
||||
textbookId: "tb_01",
|
||||
title: "1.1 Introduction to Real Numbers",
|
||||
order: 1,
|
||||
parentId: "ch_01",
|
||||
content: "## 1.1 Introduction\n\nReal numbers include rational and irrational numbers.",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
import { db } from "@/shared/db"
|
||||
import { chapters, knowledgePoints, textbooks } from "@/shared/db/schema"
|
||||
import type {
|
||||
Chapter,
|
||||
CreateChapterInput,
|
||||
CreateKnowledgePointInput,
|
||||
CreateTextbookInput,
|
||||
KnowledgePoint,
|
||||
Textbook,
|
||||
UpdateChapterContentInput,
|
||||
UpdateTextbookInput,
|
||||
} from "./types"
|
||||
|
||||
let MOCK_KNOWLEDGE_POINTS: KnowledgePoint[] = [
|
||||
{
|
||||
id: "kp_01",
|
||||
name: "Real Numbers",
|
||||
description: "Definition and properties of real numbers",
|
||||
level: 1,
|
||||
order: 1,
|
||||
chapterId: "ch_01",
|
||||
},
|
||||
{
|
||||
id: "kp_02",
|
||||
name: "Rational Numbers",
|
||||
description: "Numbers that can be expressed as a fraction",
|
||||
level: 2,
|
||||
order: 1,
|
||||
chapterId: "ch_01_01",
|
||||
const normalizeOptional = (v: string | null | undefined) => {
|
||||
const trimmed = v?.trim()
|
||||
if (!trimmed) return null
|
||||
return trimmed
|
||||
}
|
||||
|
||||
const sortChapters = (a: Chapter, b: Chapter) => {
|
||||
const ao = a.order ?? 0
|
||||
const bo = b.order ?? 0
|
||||
if (ao !== bo) return ao - bo
|
||||
return a.title.localeCompare(b.title)
|
||||
}
|
||||
|
||||
const buildChapterTree = (rows: Chapter[]): Chapter[] => {
|
||||
const byId = new Map<string, Chapter & { children: Chapter[] }>()
|
||||
for (const ch of rows) {
|
||||
byId.set(ch.id, { ...ch, children: [] })
|
||||
}
|
||||
];
|
||||
|
||||
// ... (existing imports and mock data)
|
||||
const roots: Array<Chapter & { children: Chapter[] }> = []
|
||||
for (const ch of byId.values()) {
|
||||
const pid = ch.parentId
|
||||
if (pid && byId.has(pid)) {
|
||||
byId.get(pid)!.children.push(ch)
|
||||
} else {
|
||||
roots.push(ch)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTextbooks(query?: string, subject?: string, grade?: string): Promise<Textbook[]> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const results = [...MOCK_TEXTBOOKS];
|
||||
// ... (filtering logic)
|
||||
return results;
|
||||
const sortRecursive = (nodes: Array<Chapter & { children: Chapter[] }>) => {
|
||||
nodes.sort(sortChapters)
|
||||
for (const n of nodes) {
|
||||
sortRecursive(n.children as Array<Chapter & { children: Chapter[] }>)
|
||||
}
|
||||
}
|
||||
|
||||
sortRecursive(roots)
|
||||
return roots
|
||||
}
|
||||
|
||||
export async function getTextbookById(id: string): Promise<Textbook | undefined> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
return MOCK_TEXTBOOKS.find((t) => t.id === id);
|
||||
}
|
||||
export const getTextbooks = cache(async (query?: string, subject?: string, grade?: string): Promise<Textbook[]> => {
|
||||
const conditions: SQL[] = []
|
||||
|
||||
export async function getChaptersByTextbookId(textbookId: string): Promise<Chapter[]> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
return MOCK_CHAPTERS.filter((c) => c.textbookId === textbookId);
|
||||
}
|
||||
const q = query?.trim()
|
||||
if (q) {
|
||||
const needle = `%${q}%`
|
||||
conditions.push(
|
||||
or(
|
||||
like(textbooks.title, needle),
|
||||
like(textbooks.subject, needle),
|
||||
like(textbooks.grade, needle),
|
||||
like(textbooks.publisher, needle)
|
||||
)!
|
||||
)
|
||||
}
|
||||
|
||||
const s = subject?.trim()
|
||||
if (s && s !== "all") conditions.push(eq(textbooks.subject, s))
|
||||
|
||||
const g = grade?.trim()
|
||||
if (g && g !== "all") conditions.push(eq(textbooks.grade, g))
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: textbooks.id,
|
||||
title: textbooks.title,
|
||||
subject: textbooks.subject,
|
||||
grade: textbooks.grade,
|
||||
publisher: textbooks.publisher,
|
||||
createdAt: textbooks.createdAt,
|
||||
updatedAt: textbooks.updatedAt,
|
||||
chaptersCount: sql<number>`COUNT(${chapters.id})`,
|
||||
})
|
||||
.from(textbooks)
|
||||
.leftJoin(chapters, eq(chapters.textbookId, textbooks.id))
|
||||
.where(conditions.length ? and(...conditions) : undefined)
|
||||
.groupBy(textbooks.id, textbooks.title, textbooks.subject, textbooks.grade, textbooks.publisher, textbooks.createdAt, textbooks.updatedAt)
|
||||
.orderBy(asc(textbooks.title), asc(textbooks.subject), asc(textbooks.grade))
|
||||
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
title: r.title,
|
||||
subject: r.subject,
|
||||
grade: r.grade,
|
||||
publisher: r.publisher,
|
||||
createdAt: r.createdAt,
|
||||
updatedAt: r.updatedAt,
|
||||
_count: { chapters: Number(r.chaptersCount ?? 0) },
|
||||
}))
|
||||
})
|
||||
|
||||
export const getTextbookById = cache(async (id: string): Promise<Textbook | undefined> => {
|
||||
const [row] = await db
|
||||
.select({
|
||||
id: textbooks.id,
|
||||
title: textbooks.title,
|
||||
subject: textbooks.subject,
|
||||
grade: textbooks.grade,
|
||||
publisher: textbooks.publisher,
|
||||
createdAt: textbooks.createdAt,
|
||||
updatedAt: textbooks.updatedAt,
|
||||
chaptersCount: sql<number>`COUNT(${chapters.id})`,
|
||||
})
|
||||
.from(textbooks)
|
||||
.leftJoin(chapters, eq(chapters.textbookId, textbooks.id))
|
||||
.where(eq(textbooks.id, id))
|
||||
.groupBy(textbooks.id, textbooks.title, textbooks.subject, textbooks.grade, textbooks.publisher, textbooks.createdAt, textbooks.updatedAt)
|
||||
.limit(1)
|
||||
|
||||
if (!row) return undefined
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
subject: row.subject,
|
||||
grade: row.grade,
|
||||
publisher: row.publisher,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
_count: { chapters: Number(row.chaptersCount ?? 0) },
|
||||
}
|
||||
})
|
||||
|
||||
export const getChaptersByTextbookId = cache(async (textbookId: string): Promise<Chapter[]> => {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: chapters.id,
|
||||
textbookId: chapters.textbookId,
|
||||
title: chapters.title,
|
||||
order: chapters.order,
|
||||
parentId: chapters.parentId,
|
||||
content: chapters.content,
|
||||
createdAt: chapters.createdAt,
|
||||
updatedAt: chapters.updatedAt,
|
||||
})
|
||||
.from(chapters)
|
||||
.where(eq(chapters.textbookId, textbookId))
|
||||
.orderBy(asc(chapters.order), asc(chapters.title))
|
||||
|
||||
return buildChapterTree(
|
||||
rows.map((r) => ({
|
||||
id: r.id,
|
||||
textbookId: r.textbookId,
|
||||
title: r.title,
|
||||
order: r.order,
|
||||
parentId: r.parentId,
|
||||
content: r.content ?? null,
|
||||
createdAt: r.createdAt,
|
||||
updatedAt: r.updatedAt,
|
||||
}))
|
||||
)
|
||||
})
|
||||
|
||||
export async function createTextbook(data: CreateTextbookInput): Promise<Textbook> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
const newTextbook: Textbook = {
|
||||
id: `tb_${Math.random().toString(36).substr(2, 9)}`,
|
||||
...data,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
const id = createId()
|
||||
const now = new Date()
|
||||
|
||||
const row = {
|
||||
id,
|
||||
title: data.title.trim(),
|
||||
subject: data.subject.trim(),
|
||||
grade: normalizeOptional(data.grade),
|
||||
publisher: normalizeOptional(data.publisher),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
await db.insert(textbooks).values(row)
|
||||
|
||||
return {
|
||||
...row,
|
||||
_count: { chapters: 0 },
|
||||
};
|
||||
MOCK_TEXTBOOKS = [newTextbook, ...MOCK_TEXTBOOKS];
|
||||
return newTextbook;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateTextbook(data: UpdateTextbookInput): Promise<Textbook> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
|
||||
const index = MOCK_TEXTBOOKS.findIndex((t) => t.id === data.id);
|
||||
if (index === -1) throw new Error("Textbook not found");
|
||||
|
||||
const updatedTextbook = {
|
||||
...MOCK_TEXTBOOKS[index],
|
||||
...data,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
MOCK_TEXTBOOKS[index] = updatedTextbook;
|
||||
return updatedTextbook;
|
||||
await db
|
||||
.update(textbooks)
|
||||
.set({
|
||||
title: data.title.trim(),
|
||||
subject: data.subject.trim(),
|
||||
grade: normalizeOptional(data.grade),
|
||||
publisher: normalizeOptional(data.publisher),
|
||||
})
|
||||
.where(eq(textbooks.id, data.id))
|
||||
|
||||
const updated = await getTextbookById(data.id)
|
||||
if (!updated) throw new Error("Textbook not found")
|
||||
return updated
|
||||
}
|
||||
|
||||
export async function deleteTextbook(id: string): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
MOCK_TEXTBOOKS = MOCK_TEXTBOOKS.filter((t) => t.id !== id);
|
||||
await db.delete(textbooks).where(eq(textbooks.id, id))
|
||||
}
|
||||
|
||||
// ... (rest of the file)
|
||||
|
||||
export async function createChapter(data: CreateChapterInput): Promise<Chapter> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const newChapter: Chapter = {
|
||||
id: `ch_${Math.random().toString(36).substr(2, 9)}`,
|
||||
textbookId: data.textbookId,
|
||||
title: data.title,
|
||||
order: data.order || 0,
|
||||
parentId: data.parentId || null,
|
||||
content: "",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
children: []
|
||||
};
|
||||
const id = createId()
|
||||
const now = new Date()
|
||||
|
||||
// Logic to add to nested structure (simplified for mock: add to root or find parent)
|
||||
// For deep nesting in mock, we'd need recursive search.
|
||||
// Here we just push to root or try to find parent in top level for simplicity of demo.
|
||||
|
||||
if (data.parentId) {
|
||||
const parent = MOCK_CHAPTERS.find(c => c.id === data.parentId);
|
||||
if (parent) {
|
||||
if (!parent.children) parent.children = [];
|
||||
parent.children.push(newChapter);
|
||||
} else {
|
||||
// Try searching one level deep
|
||||
for (const ch of MOCK_CHAPTERS) {
|
||||
if (ch.children) {
|
||||
const subParent = ch.children.find(c => c.id === data.parentId);
|
||||
if (subParent) {
|
||||
if (!subParent.children) subParent.children = [];
|
||||
subParent.children.push(newChapter);
|
||||
return newChapter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
MOCK_CHAPTERS.push(newChapter);
|
||||
const row = {
|
||||
id,
|
||||
textbookId: data.textbookId,
|
||||
title: data.title.trim(),
|
||||
order: data.order ?? 0,
|
||||
parentId: data.parentId ?? null,
|
||||
content: "",
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}
|
||||
|
||||
await db.insert(chapters).values(row)
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
textbookId: row.textbookId,
|
||||
title: row.title,
|
||||
order: row.order,
|
||||
parentId: row.parentId,
|
||||
content: row.content,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
children: [],
|
||||
}
|
||||
|
||||
return newChapter;
|
||||
}
|
||||
|
||||
export async function updateChapterContent(data: UpdateChapterContentInput): Promise<Chapter> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Recursive find and update
|
||||
const updateContentRecursive = (chapters: Chapter[]): Chapter | null => {
|
||||
for (const ch of chapters) {
|
||||
if (ch.id === data.chapterId) {
|
||||
ch.content = data.content;
|
||||
ch.updatedAt = new Date();
|
||||
return ch;
|
||||
}
|
||||
if (ch.children) {
|
||||
const found = updateContentRecursive(ch.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
await db.update(chapters).set({ content: data.content }).where(eq(chapters.id, data.chapterId))
|
||||
|
||||
const updated = updateContentRecursive(MOCK_CHAPTERS);
|
||||
if (!updated) throw new Error("Chapter not found");
|
||||
|
||||
return updated;
|
||||
const [row] = await db
|
||||
.select({
|
||||
id: chapters.id,
|
||||
textbookId: chapters.textbookId,
|
||||
title: chapters.title,
|
||||
order: chapters.order,
|
||||
parentId: chapters.parentId,
|
||||
content: chapters.content,
|
||||
createdAt: chapters.createdAt,
|
||||
updatedAt: chapters.updatedAt,
|
||||
})
|
||||
.from(chapters)
|
||||
.where(eq(chapters.id, data.chapterId))
|
||||
.limit(1)
|
||||
|
||||
if (!row) throw new Error("Chapter not found")
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
textbookId: row.textbookId,
|
||||
title: row.title,
|
||||
order: row.order,
|
||||
parentId: row.parentId,
|
||||
content: row.content ?? null,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
children: [],
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteChapter(id: string): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Recursive delete
|
||||
MOCK_CHAPTERS = MOCK_CHAPTERS.filter(c => c.id !== id);
|
||||
MOCK_CHAPTERS.forEach(c => {
|
||||
if (c.children) {
|
||||
c.children = c.children.filter(child => child.id !== id);
|
||||
}
|
||||
});
|
||||
const [target] = await db
|
||||
.select({ id: chapters.id, textbookId: chapters.textbookId })
|
||||
.from(chapters)
|
||||
.where(eq(chapters.id, id))
|
||||
.limit(1)
|
||||
|
||||
if (!target) return
|
||||
|
||||
const all = await db
|
||||
.select({ id: chapters.id, parentId: chapters.parentId })
|
||||
.from(chapters)
|
||||
.where(eq(chapters.textbookId, target.textbookId))
|
||||
|
||||
const childrenByParent = new Map<string, string[]>()
|
||||
for (const ch of all) {
|
||||
if (!ch.parentId) continue
|
||||
const arr = childrenByParent.get(ch.parentId) ?? []
|
||||
arr.push(ch.id)
|
||||
childrenByParent.set(ch.parentId, arr)
|
||||
}
|
||||
|
||||
const idsToDelete: string[] = []
|
||||
const stack = [id]
|
||||
const seen = new Set<string>()
|
||||
while (stack.length) {
|
||||
const cur = stack.pop()!
|
||||
if (seen.has(cur)) continue
|
||||
seen.add(cur)
|
||||
idsToDelete.push(cur)
|
||||
const kids = childrenByParent.get(cur)
|
||||
if (kids) stack.push(...kids)
|
||||
}
|
||||
|
||||
await db.delete(knowledgePoints).where(inArray(knowledgePoints.chapterId, idsToDelete))
|
||||
await db.delete(chapters).where(inArray(chapters.id, idsToDelete))
|
||||
}
|
||||
|
||||
// Knowledge Points
|
||||
export const getKnowledgePointsByChapterId = cache(async (chapterId: string): Promise<KnowledgePoint[]> => {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: knowledgePoints.id,
|
||||
name: knowledgePoints.name,
|
||||
description: knowledgePoints.description,
|
||||
parentId: knowledgePoints.parentId,
|
||||
chapterId: knowledgePoints.chapterId,
|
||||
level: knowledgePoints.level,
|
||||
order: knowledgePoints.order,
|
||||
})
|
||||
.from(knowledgePoints)
|
||||
.where(eq(knowledgePoints.chapterId, chapterId))
|
||||
.orderBy(asc(knowledgePoints.order), asc(knowledgePoints.name))
|
||||
|
||||
export async function getKnowledgePointsByChapterId(chapterId: string): Promise<KnowledgePoint[]> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
return MOCK_KNOWLEDGE_POINTS.filter(kp => kp.chapterId === chapterId);
|
||||
}
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
description: r.description ?? null,
|
||||
parentId: r.parentId ?? null,
|
||||
chapterId: r.chapterId ?? undefined,
|
||||
level: r.level ?? 0,
|
||||
order: r.order ?? 0,
|
||||
}))
|
||||
})
|
||||
|
||||
export const getKnowledgePointsByTextbookId = cache(async (textbookId: string): Promise<KnowledgePoint[]> => {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: knowledgePoints.id,
|
||||
name: knowledgePoints.name,
|
||||
description: knowledgePoints.description,
|
||||
parentId: knowledgePoints.parentId,
|
||||
chapterId: knowledgePoints.chapterId,
|
||||
level: knowledgePoints.level,
|
||||
order: knowledgePoints.order,
|
||||
})
|
||||
.from(knowledgePoints)
|
||||
.innerJoin(chapters, eq(chapters.id, knowledgePoints.chapterId))
|
||||
.where(eq(chapters.textbookId, textbookId))
|
||||
.orderBy(asc(chapters.order), asc(knowledgePoints.order), asc(knowledgePoints.name))
|
||||
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
description: r.description ?? null,
|
||||
parentId: r.parentId ?? null,
|
||||
chapterId: r.chapterId ?? undefined,
|
||||
level: r.level ?? 0,
|
||||
order: r.order ?? 0,
|
||||
}))
|
||||
})
|
||||
|
||||
export async function createKnowledgePoint(data: CreateKnowledgePointInput): Promise<KnowledgePoint> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
const newKP: KnowledgePoint = {
|
||||
id: `kp_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
const id = createId()
|
||||
|
||||
const row = {
|
||||
id,
|
||||
name: data.name.trim(),
|
||||
description: normalizeOptional(data.description ?? null),
|
||||
chapterId: data.chapterId,
|
||||
level: 1, // simplified
|
||||
order: 0
|
||||
};
|
||||
|
||||
MOCK_KNOWLEDGE_POINTS.push(newKP);
|
||||
return newKP;
|
||||
level: 1,
|
||||
order: 0,
|
||||
}
|
||||
|
||||
await db.insert(knowledgePoints).values(row)
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
parentId: null,
|
||||
chapterId: row.chapterId,
|
||||
level: row.level,
|
||||
order: row.order,
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteKnowledgePoint(id: string): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
MOCK_KNOWLEDGE_POINTS = MOCK_KNOWLEDGE_POINTS.filter(kp => kp.id !== id);
|
||||
await db.delete(knowledgePoints).where(eq(knowledgePoints.id, id))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user