完整性更新
Some checks failed
CI / build-and-test (push) Failing after 3m50s
CI / deploy (push) Has been skipped

现在已经实现了大部分基础功能
This commit is contained in:
SpecialX
2026-01-08 11:14:03 +08:00
parent 0da2eac0b4
commit 57807def37
155 changed files with 26421 additions and 1036 deletions

View File

@@ -3,8 +3,16 @@ import { db } from "../src/shared/db";
import {
users, roles, usersToRoles,
questions, knowledgePoints, questionsToKnowledgePoints,
exams, examQuestions, examSubmissions, submissionAnswers,
textbooks, chapters
exams, examQuestions,
homeworkAssignments,
homeworkAssignmentQuestions,
homeworkAssignmentTargets,
homeworkSubmissions,
homeworkAnswers,
textbooks, chapters,
schools,
grades,
classes, classEnrollments, classSchedule
} from "../src/shared/db/schema";
import { createId } from "@paralleldrive/cuid2";
import { faker } from "@faker-js/faker";
@@ -30,9 +38,12 @@ async function seed() {
try {
await db.execute(sql`SET FOREIGN_KEY_CHECKS = 0;`);
const tables = [
"class_schedule", "class_enrollments", "classes",
"homework_answers", "homework_submissions", "homework_assignment_targets", "homework_assignment_questions", "homework_assignments",
"submission_answers", "exam_submissions", "exam_questions", "exams",
"questions_to_knowledge_points", "questions", "knowledge_points",
"chapters", "textbooks",
"grades", "schools",
"users_to_roles", "roles", "users", "accounts", "sessions"
];
for (const table of tables) {
@@ -52,14 +63,16 @@ async function seed() {
admin: "role_admin",
teacher: "role_teacher",
student: "role_student",
grade_head: "role_grade_head"
grade_head: "role_grade_head",
teaching_head: "role_teaching_head"
};
await db.insert(roles).values([
{ id: roleMap.admin, name: "admin", description: "System Administrator" },
{ id: roleMap.teacher, name: "teacher", description: "Academic Instructor" },
{ id: roleMap.student, name: "student", description: "Learner" },
{ id: roleMap.grade_head, name: "grade_head", description: "Head of Grade Year" }
{ id: roleMap.grade_head, name: "grade_head", description: "Head of Grade Year" },
{ id: roleMap.teaching_head, name: "teaching_head", description: "Teaching Research Lead" }
]);
// Users
@@ -98,6 +111,107 @@ async function seed() {
{ userId: "user_student_1", roleId: roleMap.student },
]);
const extraStudentIds: string[] = [];
for (let i = 0; i < 12; i++) {
const studentId = createId();
extraStudentIds.push(studentId);
await db.insert(users).values({
id: studentId,
name: faker.person.fullName(),
email: faker.internet.email().toLowerCase(),
role: "student",
image: `https://api.dicebear.com/7.x/avataaars/svg?seed=${studentId}`,
});
await db.insert(usersToRoles).values({ userId: studentId, roleId: roleMap.student });
}
const schoolId = "school_nextedu"
const grade10Id = "grade_10"
await db.insert(schools).values([
{ id: schoolId, name: "Next_Edu School", code: "NEXTEDU" },
{ id: "school_demo_2", name: "Demo School No.2", code: "DEMO2" },
])
await db.insert(grades).values([
{
id: grade10Id,
schoolId,
name: "Grade 10",
order: 10,
gradeHeadId: "user_teacher_math",
teachingHeadId: "user_teacher_math",
},
])
await db.insert(classes).values([
{
id: "class_10_3",
schoolName: "Next_Edu School",
schoolId,
name: "Grade 10 · Class 3",
grade: "Grade 10",
gradeId: grade10Id,
homeroom: "10-3",
room: "Room 304",
invitationCode: "100003",
teacherId: "user_teacher_math",
},
{
id: "class_10_7",
schoolName: "Next_Edu School",
schoolId,
name: "Grade 10 · Class 7",
grade: "Grade 10",
gradeId: grade10Id,
homeroom: "10-7",
room: "Room 201",
invitationCode: "100007",
teacherId: "user_teacher_math",
},
]);
await db.insert(classEnrollments).values([
{ classId: "class_10_3", studentId: "user_student_1", status: "active" },
...extraStudentIds.slice(0, 8).map((studentId) => ({ classId: "class_10_3", studentId, status: "active" as const })),
...extraStudentIds.slice(8, 12).map((studentId) => ({ classId: "class_10_7", studentId, status: "active" as const })),
]);
await db.insert(classSchedule).values([
{ id: "cs_001", classId: "class_10_3", weekday: 1, startTime: "09:00", endTime: "09:45", course: "Mathematics", location: "Room 304" },
{ id: "cs_002", classId: "class_10_3", weekday: 3, startTime: "14:00", endTime: "14:45", course: "Physics", location: "Lab A" },
{ id: "cs_003", classId: "class_10_7", weekday: 2, startTime: "11:00", endTime: "11:45", course: "Mathematics", location: "Room 201" },
]);
await db.insert(textbooks).values([
{
id: "tb_01",
title: "Advanced Mathematics Grade 10",
subject: "Mathematics",
grade: "Grade 10",
publisher: "Next Education Press",
},
])
await db.insert(chapters).values([
{
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...",
},
{
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.",
},
])
// --- 2. Knowledge Graph (Tree) ---
console.log("🧠 Seeding Knowledge Graph...");
@@ -111,103 +225,273 @@ async function seed() {
{ id: kpLinearId, name: "Linear Equations", parentId: kpAlgebraId, level: 2 },
]);
await db.insert(knowledgePoints).values([
{
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",
},
])
// --- 3. Question Bank (Rich Content) ---
console.log("📚 Seeding Question Bank...");
// 3.1 Simple Single Choice
const qSimpleId = createId();
await db.insert(questions).values({
id: qSimpleId,
authorId: "user_teacher_math",
type: "single_choice",
difficulty: 1,
content: {
text: "What is 2 + 2?",
options: [
{ id: "A", text: "3", isCorrect: false },
{ id: "B", text: "4", isCorrect: true },
{ id: "C", text: "5", isCorrect: false }
]
}
});
// Link to KP
await db.insert(questionsToKnowledgePoints).values({
questionId: qSimpleId,
knowledgePointId: kpLinearId // Just for demo
});
// 3.2 Composite Question (Reading Comprehension)
const qParentId = createId();
const qChild1Id = createId();
const qChild2Id = createId();
// Parent (Passage)
await db.insert(questions).values({
id: qParentId,
authorId: "user_teacher_math",
type: "composite",
difficulty: 3,
content: {
text: "Read the following passage about Algebra...\n(Long text here)...",
assets: []
}
});
// Children
await db.insert(questions).values([
const mathExamQuestions: Array<{
id: string;
type: "single_choice" | "text" | "judgment";
difficulty: number;
content: unknown;
score: number;
}> = [
{
id: qChild1Id,
authorId: "user_teacher_math",
parentId: qParentId, // <--- Key: Nested
id: createId(),
type: "single_choice",
difficulty: 2,
difficulty: 1,
score: 4,
content: {
text: "What is the main topic?",
text: "1) What is 2 + 2?",
options: [
{ id: "A", text: "Geometry", isCorrect: false },
{ id: "B", text: "Algebra", isCorrect: true }
]
}
{ id: "A", text: "3", isCorrect: false },
{ id: "B", text: "4", isCorrect: true },
{ id: "C", text: "5", isCorrect: false },
{ id: "D", text: "6", isCorrect: false },
],
},
},
{
id: qChild2Id,
authorId: "user_teacher_math",
parentId: qParentId,
type: "text",
difficulty: 4,
id: createId(),
type: "single_choice",
difficulty: 2,
score: 4,
content: {
text: "Explain the concept of variables.",
}
}
]);
text: "2) If f(x) = 2x + 1, then f(3) = ?",
options: [
{ id: "A", text: "5", isCorrect: false },
{ id: "B", text: "7", isCorrect: true },
{ id: "C", text: "8", isCorrect: false },
{ id: "D", text: "10", isCorrect: false },
],
},
},
{
id: createId(),
type: "single_choice",
difficulty: 2,
score: 4,
content: {
text: "3) Solve 3x - 5 = 7. What is x?",
options: [
{ id: "A", text: "3", isCorrect: false },
{ id: "B", text: "4", isCorrect: true },
{ id: "C", text: "5", isCorrect: false },
{ id: "D", text: "6", isCorrect: false },
],
},
},
{
id: createId(),
type: "single_choice",
difficulty: 3,
score: 4,
content: {
text: "4) Which is a factor of x^2 - 9?",
options: [
{ id: "A", text: "(x - 3)", isCorrect: true },
{ id: "B", text: "(x + 9)", isCorrect: false },
{ id: "C", text: "(x - 9)", isCorrect: false },
{ id: "D", text: "(x^2 + 9)", isCorrect: false },
],
},
},
{
id: createId(),
type: "single_choice",
difficulty: 2,
score: 4,
content: {
text: "5) If a^2 = 49 and a > 0, then a = ?",
options: [
{ id: "A", text: "-7", isCorrect: false },
{ id: "B", text: "0", isCorrect: false },
{ id: "C", text: "7", isCorrect: true },
{ id: "D", text: "49", isCorrect: false },
],
},
},
{
id: createId(),
type: "single_choice",
difficulty: 3,
score: 4,
content: {
text: "6) Simplify (x^2 y)(x y^3).",
options: [
{ id: "A", text: "x^2 y^3", isCorrect: false },
{ id: "B", text: "x^3 y^4", isCorrect: true },
{ id: "C", text: "x^3 y^3", isCorrect: false },
{ id: "D", text: "x^4 y^4", isCorrect: false },
],
},
},
{
id: createId(),
type: "single_choice",
difficulty: 2,
score: 4,
content: {
text: "7) The slope of the line y = -3x + 2 is:",
options: [
{ id: "A", text: "2", isCorrect: false },
{ id: "B", text: "-3", isCorrect: true },
{ id: "C", text: "3", isCorrect: false },
{ id: "D", text: "-2", isCorrect: false },
],
},
},
{
id: createId(),
type: "single_choice",
difficulty: 1,
score: 4,
content: {
text: "8) The probability of getting heads in one fair coin toss is:",
options: [
{ id: "A", text: "0", isCorrect: false },
{ id: "B", text: "1/4", isCorrect: false },
{ id: "C", text: "1/2", isCorrect: true },
{ id: "D", text: "1", isCorrect: false },
],
},
},
{
id: createId(),
type: "single_choice",
difficulty: 2,
score: 4,
content: {
text: "9) In an arithmetic sequence with a1 = 2 and d = 3, a5 = ?",
options: [
{ id: "A", text: "11", isCorrect: false },
{ id: "B", text: "12", isCorrect: false },
{ id: "C", text: "14", isCorrect: true },
{ id: "D", text: "17", isCorrect: false },
],
},
},
{
id: createId(),
type: "single_choice",
difficulty: 2,
score: 4,
content: {
text: "10) The solution set of x^2 = 0 is:",
options: [
{ id: "A", text: "{0}", isCorrect: true },
{ id: "B", text: "{1}", isCorrect: false },
{ id: "C", text: "{-1, 1}", isCorrect: false },
{ id: "D", text: "Empty set", isCorrect: false },
],
},
},
{ id: createId(), type: "text", difficulty: 1, score: 4, content: { text: "11) Fill in the blank: √81 = ____.", correctAnswer: "9" } },
{ id: createId(), type: "text", difficulty: 2, score: 4, content: { text: "12) Fill in the blank: (a - b)^2 = ____.", correctAnswer: ["a^2 - 2ab + b^2", "a² - 2ab + b²"] } },
{ id: createId(), type: "text", difficulty: 1, score: 4, content: { text: "13) Fill in the blank: 2^5 = ____.", correctAnswer: "32" } },
{ id: createId(), type: "text", difficulty: 2, score: 4, content: { text: "14) Fill in the blank: The area of a circle with radius r is ____.", correctAnswer: ["πr^2", "pi r^2", "πr²"] } },
{ id: createId(), type: "text", difficulty: 1, score: 4, content: { text: "15) Fill in the blank: If x = -2, then x^3 = ____.", correctAnswer: "-8" } },
{ id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "16) If x > y, then x + 1 > y + 1.", correctAnswer: true } },
{ id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "17) The graph of y = 2x is a parabola.", correctAnswer: false } },
{ id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "18) The sum of interior angles of a triangle is 180°.", correctAnswer: true } },
{ id: createId(), type: "judgment", difficulty: 2, score: 2, content: { text: "19) (x + y)^2 = x^2 + y^2.", correctAnswer: false } },
{ id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "20) 0 is a positive number.", correctAnswer: false } },
{ id: createId(), type: "text", difficulty: 3, score: 10, content: { text: "21) Solve the system: x + y = 5, x - y = 1." } },
{ id: createId(), type: "text", difficulty: 3, score: 10, content: { text: "22) Expand and simplify: (2x - 3)(x + 4)." } },
{ id: createId(), type: "text", difficulty: 3, score: 10, content: { text: "23) In a right triangle with legs 6 and 8, find the hypotenuse and the area." } },
];
await db.insert(questions).values(
mathExamQuestions.map((q) => ({
id: q.id,
type: q.type,
difficulty: q.difficulty,
content: q.content,
authorId: "user_teacher_math",
}))
);
await db.insert(questionsToKnowledgePoints).values({
questionId: mathExamQuestions[0].id,
knowledgePointId: kpLinearId
});
// --- 4. Exams (New Structure) ---
console.log("📝 Seeding Exams...");
const examId = createId();
const makeGroup = (title: string, children: unknown[]) => ({
id: createId(),
type: "group",
title,
children,
});
const makeQuestionNode = (questionId: string, score: number) => ({
id: createId(),
type: "question",
questionId,
score,
});
const choiceIds = mathExamQuestions.slice(0, 10).map((q) => q.id);
const fillIds = mathExamQuestions.slice(10, 15).map((q) => q.id);
const judgmentIds = mathExamQuestions.slice(15, 20).map((q) => q.id);
const shortAnswerIds = mathExamQuestions.slice(20, 23).map((q) => q.id);
const examStructure = [
{
type: "group",
title: "Part 1: Basics",
children: [
{ type: "question", questionId: qSimpleId, score: 10 }
]
},
{
type: "group",
title: "Part 2: Reading",
children: [
// For composite questions, we usually add the parent, and the system fetches children
{ type: "question", questionId: qParentId, score: 20 }
]
}
makeGroup(
"第一部分单项选择题共10题每题4分共40分",
choiceIds.map((id) => makeQuestionNode(id, 4))
),
makeGroup(
"第二部分填空题共5题每题4分共20分",
fillIds.map((id) => makeQuestionNode(id, 4))
),
makeGroup(
"第三部分判断题共5题每题2分共10分",
judgmentIds.map((id) => makeQuestionNode(id, 2))
),
makeGroup(
"第四部分解答题共3题每题10分共30分",
shortAnswerIds.map((id) => makeQuestionNode(id, 10))
),
];
await db.insert(exams).values({
id: examId,
title: "Algebra Mid-Term 2025",
description: "Comprehensive assessment",
title: "Grade 10 Mathematics Final Exam (Seed)",
description: JSON.stringify({
subject: "Mathematics",
grade: "Grade 10",
difficulty: 3,
totalScore: 100,
durationMin: 120,
questionCount: 23,
tags: ["seed", "math", "grade10", "final"],
}),
creatorId: "user_teacher_math",
status: "published",
startTime: new Date(),
@@ -215,15 +499,118 @@ async function seed() {
});
// Link questions physically (Source of Truth)
await db.insert(examQuestions).values([
{ examId, questionId: qSimpleId, score: 10, order: 0 },
{ examId, questionId: qParentId, score: 20, order: 1 },
// Note: Child questions are often implicitly included or explicitly added depending on logic.
// For this seed, we assume linking Parent is enough for the relation,
// but let's link children too for completeness if the query strategy requires it.
{ examId, questionId: qChild1Id, score: 0, order: 2 },
{ examId, questionId: qChild2Id, score: 0, order: 3 },
]);
const orderedQuestionIds = [...choiceIds, ...fillIds, ...judgmentIds, ...shortAnswerIds];
const scoreById = new Map(mathExamQuestions.map((q) => [q.id, q.score] as const));
await db.insert(examQuestions).values(
orderedQuestionIds.map((questionId, order) => ({
examId,
questionId,
score: scoreById.get(questionId) ?? 0,
order,
}))
);
console.log("📌 Seeding Homework Assignments...");
const assignmentId = createId();
const now = new Date();
const dueAt = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const lateDueAt = new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000);
await db.insert(homeworkAssignments).values({
id: assignmentId,
sourceExamId: examId,
title: "Grade 10 Mathematics Final - Homework (Seed)",
description: "Auto-generated homework assignment from seeded math exam.",
structure: examStructure as unknown,
status: "published",
creatorId: "user_teacher_math",
availableAt: now,
dueAt,
allowLate: true,
lateDueAt,
maxAttempts: 2,
});
await db.insert(homeworkAssignmentQuestions).values(
orderedQuestionIds.map((questionId, order) => ({
assignmentId,
questionId,
score: scoreById.get(questionId) ?? 0,
order,
}))
);
const targetStudentIds = ["user_student_1", ...extraStudentIds.slice(0, 4)];
await db.insert(homeworkAssignmentTargets).values(
targetStudentIds.map((studentId) => ({
assignmentId,
studentId,
}))
);
const scoreForQuestion = (questionId: string) => scoreById.get(questionId) ?? 0;
const buildAnswer = (questionId: string, type: string) => {
if (type === "single_choice") return { answer: "B" };
if (type === "judgment") return { answer: true };
return { answer: "Seed answer" };
};
const questionTypeById = new Map(mathExamQuestions.map((q) => [q.id, q.type] as const));
const submissionIds: string[] = [];
for (let i = 0; i < 3; i++) {
const studentId = targetStudentIds[i];
const submissionId = createId();
submissionIds.push(submissionId);
const submittedAt = new Date(now.getTime() - (i + 1) * 24 * 60 * 60 * 1000);
const status = i === 0 ? "graded" : i === 1 ? "graded" : "submitted";
const perQuestionScores = orderedQuestionIds.map((qid, idx) => {
const max = scoreForQuestion(qid);
if (status !== "graded") return null;
if (max <= 0) return 0;
if (idx % 7 === 0) return Math.max(0, max - 1);
return max;
});
const totalScore =
status === "graded"
? perQuestionScores.reduce<number>((sum, s) => sum + Number(s ?? 0), 0)
: null;
await db.insert(homeworkSubmissions).values({
id: submissionId,
assignmentId,
studentId,
attemptNo: 1,
score: totalScore,
status,
startedAt: submittedAt,
submittedAt,
isLate: false,
createdAt: submittedAt,
updatedAt: submittedAt,
});
await db.insert(homeworkAnswers).values(
orderedQuestionIds.map((questionId, idx) => {
const questionType = questionTypeById.get(questionId) ?? "text";
const score = status === "graded" ? (perQuestionScores[idx] ?? 0) : null;
return {
id: createId(),
submissionId,
questionId,
answerContent: buildAnswer(questionId, questionType),
score,
feedback: status === "graded" ? (score === scoreForQuestion(questionId) ? "Good" : "Check calculation") : null,
createdAt: submittedAt,
updatedAt: submittedAt,
};
})
);
}
const end = performance.now();
console.log(`✅ Seed completed in ${(end - start).toFixed(2)}ms`);