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:
@@ -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),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user