feat(db): add grade_record_answers migration and update schema

- Add migration 0010 for grade_record_answers table

- Update shared DB schema with new table definitions
This commit is contained in:
SpecialX
2026-06-24 12:01:26 +08:00
parent eb28a523cb
commit 9d87388524
2 changed files with 144 additions and 1 deletions

View File

@@ -159,7 +159,7 @@ export const knowledgePointPrerequisites = mysqlTable("knowledge_point_prerequis
prerequisiteKpId: varchar("prerequisite_kp_id", { length: 128 }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
}, (table) => ({
kpPairPk: primaryKey({ columns: [table.knowledgePointId, table.prerequisiteKpId] }),
kpPairPk: primaryKey({ columns: [table.knowledgePointId, table.prerequisiteKpId], name: "kp_prereq_pk" }),
kpIdx: index("kp_prereq_kp_idx").on(table.knowledgePointId),
prereqIdx: index("kp_prereq_prereq_idx").on(table.prerequisiteKpId),
kpFk: foreignKey({
@@ -701,6 +701,8 @@ export const homeworkAnswers = mysqlTable("homework_answers", {
}),
}));
export const aiProviderVisibilityEnum = mysqlEnum("visibility", ["public", "private"]);
export const aiProviders = mysqlTable("ai_providers", {
id: id("id").primaryKey(),
provider: mysqlEnum("provider", ["zhipu", "openai", "gemini", "custom"]).notNull(),
@@ -709,6 +711,8 @@ export const aiProviders = mysqlTable("ai_providers", {
apiKeyEncrypted: text("api_key_encrypted").notNull(),
apiKeyLast4: varchar("api_key_last4", { length: 4 }),
isDefault: boolean("is_default").default(false).notNull(),
// V3: 可见性 — public 由管理员发布全员可用private 仅创建者可见
visibility: aiProviderVisibilityEnum.default("private").notNull(),
createdBy: varchar("created_by", { length: 128 }),
updatedBy: varchar("updated_by", { length: 128 }),
createdAt: timestamp("created_at").defaultNow().notNull(),
@@ -716,6 +720,8 @@ export const aiProviders = mysqlTable("ai_providers", {
}, (table) => ({
providerIdx: index("ai_provider_idx").on(table.provider),
defaultIdx: index("ai_provider_default_idx").on(table.isDefault),
visibilityIdx: index("ai_provider_visibility_idx").on(table.visibility),
createdByIdx: index("ai_provider_created_by_idx").on(table.createdBy),
}));
// --- 8. Announcements ---
@@ -885,6 +891,32 @@ export const gradeRecords = mysqlTable("grade_records", {
}).onDelete("cascade"),
}));
// --- 11.1 Grade Record Answers (成绩每题得分) ---
export const gradeRecordAnswers = mysqlTable("grade_record_answers", {
id: id("id").primaryKey(),
gradeRecordId: varchar("grade_record_id", { length: 128 }).notNull().references(() => gradeRecords.id, { onDelete: "cascade" }),
questionId: varchar("question_id", { length: 128 }).notNull().references(() => questions.id, { onDelete: "cascade" }),
score: decimal("score", { precision: 6, scale: 2 }).notNull(),
fullScore: decimal("full_score", { precision: 6, scale: 2 }).notNull(),
feedback: text("feedback"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
}, (table) => ({
gradeRecordIdx: index("grade_record_answers_record_idx").on(table.gradeRecordId),
questionIdx: index("grade_record_answers_question_idx").on(table.questionId),
gradeRecordFk: foreignKey({
columns: [table.gradeRecordId],
foreignColumns: [gradeRecords.id],
name: "gra_gr_fk",
}).onDelete("cascade"),
questionFk: foreignKey({
columns: [table.questionId],
foreignColumns: [questions.id],
name: "gra_q_fk",
}).onDelete("cascade"),
}));
// --- 12. File Attachments (文件附件) ---
export const fileAttachments = mysqlTable("file_attachments", {
@@ -1540,3 +1572,100 @@ export const gradeDrafts = mysqlTable("grade_drafts", {
),
userUpdatedIdx: index("gd_user_updated_idx").on(table.userId, table.updatedAt),
}));
// --- 28. Adaptive Practice (专项练习闭环) ---
/**
* 专项练习类型。
* - error_variant: 错题变式练习(从错题本发起,生成变式题)
* - knowledge_point: 知识点专项练习(按知识点抽题)
* - weak_chapter: 薄弱章节练习(按掌握度自动推荐)
* - ai_recommended: AI 推荐练习AI 根据学情综合推荐)
*/
export const practiceTypeEnum = mysqlEnum("practice_type", [
"error_variant",
"knowledge_point",
"weak_chapter",
"ai_recommended",
]);
export const practiceStatusEnum = mysqlEnum("practice_status", [
"in_progress",
"completed",
"abandoned",
]);
export const practiceAnswerStatusEnum = mysqlEnum("practice_answer_status", [
"pending",
"answered",
"skipped",
]);
/**
* 专项练习会话 - 学生发起的一次专项练习。
*
* 记录练习类型、来源元数据(知识点/错题/章节)、状态和统计。
* 练习题目存储在 practiceAnswers 表中。
*/
export const practiceSessions = mysqlTable("practice_sessions", {
id: id("id").primaryKey(),
studentId: varchar("student_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
subjectId: varchar("subject_id", { length: 128 }),
practiceType: practiceTypeEnum.notNull(),
/** 来源元数据:{ knowledgePointIds?, errorBookItemIds?, chapterId?, difficulty?, sourceQuestionId? } */
sourceMeta: json("source_meta"),
status: practiceStatusEnum.default("in_progress").notNull(),
totalQuestions: int("total_questions").default(0).notNull(),
answeredQuestions: int("answered_questions").default(0).notNull(),
correctCount: int("correct_count").default(0).notNull(),
startedAt: timestamp("started_at", { mode: "date" }).defaultNow().notNull(),
completedAt: timestamp("completed_at", { mode: "date" }),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
}, (table) => ({
studentIdx: index("ps_student_idx").on(table.studentId),
studentStatusIdx: index("ps_student_status_idx").on(table.studentId, table.status),
subjectIdx: index("ps_subject_idx").on(table.subjectId),
}));
/**
* 专项练习答题记录 - 会话中每道题的作答情况。
*
* 支持两种题目来源:
* 1. 题库中的题目questionId 指向 questions 表)
* 2. AI 生成的变式题variantContent 存储 JSON 内容questionId 指向原题)
*/
export const practiceAnswers = mysqlTable("practice_answers", {
id: id("id").primaryKey(),
sessionId: varchar("session_id", { length: 128 }).notNull().references(() => practiceSessions.id, { onDelete: "cascade" }),
studentId: varchar("student_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }),
questionId: varchar("question_id", { length: 128 }).notNull().references(() => questions.id),
/** 变式题内容AI 生成的变式题,未入库时存储在此字段) */
variantContent: json("variant_content"),
/** 是否为变式题 */
isVariant: boolean("is_variant").default(false).notNull(),
orderIndex: int("order_index").notNull(),
status: practiceAnswerStatusEnum.default("pending").notNull(),
studentAnswer: json("student_answer"),
isCorrect: boolean("is_correct"),
score: int("score"),
maxScore: int("max_score").default(1).notNull(),
answeredAt: timestamp("answered_at", { mode: "date" }),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(),
}, (table) => ({
sessionIdx: index("pa_session_idx").on(table.sessionId),
studentIdx: index("pa_student_idx").on(table.studentId),
questionIdx: index("pa_question_idx").on(table.questionId),
sessionOrderIdx: index("pa_session_order_idx").on(table.sessionId, table.orderIndex),
}));