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:
14
drizzle/0010_grade_record_answers.sql
Normal file
14
drizzle/0010_grade_record_answers.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE `grade_record_answers` (
|
||||||
|
`id` varchar(128) PRIMARY KEY NOT NULL,
|
||||||
|
`grade_record_id` varchar(128) NOT NULL,
|
||||||
|
`question_id` varchar(128) NOT NULL,
|
||||||
|
`score` decimal(6, 2) NOT NULL,
|
||||||
|
`full_score` decimal(6, 2) NOT NULL,
|
||||||
|
`feedback` text,
|
||||||
|
`created_at` timestamp DEFAULT (now()) NOT NULL,
|
||||||
|
`updated_at` timestamp DEFAULT (now()) NOT NULL,
|
||||||
|
CONSTRAINT `gra_gr_fk` FOREIGN KEY (`grade_record_id`) REFERENCES `grade_records`(`id`) ON DELETE cascade ON UPDATE no action,
|
||||||
|
CONSTRAINT `gra_q_fk` FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE cascade ON UPDATE no action
|
||||||
|
);
|
||||||
|
CREATE INDEX `grade_record_answers_record_idx` ON `grade_record_answers`(`grade_record_id`);
|
||||||
|
CREATE INDEX `grade_record_answers_question_idx` ON `grade_record_answers`(`question_id`);
|
||||||
@@ -159,7 +159,7 @@ export const knowledgePointPrerequisites = mysqlTable("knowledge_point_prerequis
|
|||||||
prerequisiteKpId: varchar("prerequisite_kp_id", { length: 128 }).notNull(),
|
prerequisiteKpId: varchar("prerequisite_kp_id", { length: 128 }).notNull(),
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
}, (table) => ({
|
}, (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),
|
kpIdx: index("kp_prereq_kp_idx").on(table.knowledgePointId),
|
||||||
prereqIdx: index("kp_prereq_prereq_idx").on(table.prerequisiteKpId),
|
prereqIdx: index("kp_prereq_prereq_idx").on(table.prerequisiteKpId),
|
||||||
kpFk: foreignKey({
|
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", {
|
export const aiProviders = mysqlTable("ai_providers", {
|
||||||
id: id("id").primaryKey(),
|
id: id("id").primaryKey(),
|
||||||
provider: mysqlEnum("provider", ["zhipu", "openai", "gemini", "custom"]).notNull(),
|
provider: mysqlEnum("provider", ["zhipu", "openai", "gemini", "custom"]).notNull(),
|
||||||
@@ -709,6 +711,8 @@ export const aiProviders = mysqlTable("ai_providers", {
|
|||||||
apiKeyEncrypted: text("api_key_encrypted").notNull(),
|
apiKeyEncrypted: text("api_key_encrypted").notNull(),
|
||||||
apiKeyLast4: varchar("api_key_last4", { length: 4 }),
|
apiKeyLast4: varchar("api_key_last4", { length: 4 }),
|
||||||
isDefault: boolean("is_default").default(false).notNull(),
|
isDefault: boolean("is_default").default(false).notNull(),
|
||||||
|
// V3: 可见性 — public 由管理员发布,全员可用;private 仅创建者可见
|
||||||
|
visibility: aiProviderVisibilityEnum.default("private").notNull(),
|
||||||
createdBy: varchar("created_by", { length: 128 }),
|
createdBy: varchar("created_by", { length: 128 }),
|
||||||
updatedBy: varchar("updated_by", { length: 128 }),
|
updatedBy: varchar("updated_by", { length: 128 }),
|
||||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
@@ -716,6 +720,8 @@ export const aiProviders = mysqlTable("ai_providers", {
|
|||||||
}, (table) => ({
|
}, (table) => ({
|
||||||
providerIdx: index("ai_provider_idx").on(table.provider),
|
providerIdx: index("ai_provider_idx").on(table.provider),
|
||||||
defaultIdx: index("ai_provider_default_idx").on(table.isDefault),
|
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 ---
|
// --- 8. Announcements ---
|
||||||
@@ -885,6 +891,32 @@ export const gradeRecords = mysqlTable("grade_records", {
|
|||||||
}).onDelete("cascade"),
|
}).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 (文件附件) ---
|
// --- 12. File Attachments (文件附件) ---
|
||||||
|
|
||||||
export const fileAttachments = mysqlTable("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),
|
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