feat(teacher): 题库模块(QuestionBank)
工作内容 - 新增 /teacher/questions 页面,支持 Suspense/空状态/加载态 - 题库 CRUD Server Actions:创建/更新/递归删除子题,变更后 revalidatePath - getQuestions 支持 q/type/difficulty/knowledgePointId 筛选与分页返回 meta - UI:表格列/筛选器/创建编辑弹窗,content JSON 兼容组卷 - 更新中文设计文档:docs/design/004_question_bank_implementation.md
This commit is contained in:
@@ -2,41 +2,52 @@ import 'server-only';
|
||||
|
||||
import { db } from "@/shared/db";
|
||||
import { questions, questionsToKnowledgePoints } from "@/shared/db/schema";
|
||||
import { and, eq, inArray, count, desc, sql } from "drizzle-orm";
|
||||
import { and, count, desc, eq, inArray, sql, type SQL } from "drizzle-orm";
|
||||
import { cache } from "react";
|
||||
import type { Question, QuestionType } from "./types";
|
||||
|
||||
// Types for filters
|
||||
export type GetQuestionsParams = {
|
||||
q?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
ids?: string[];
|
||||
knowledgePointId?: string;
|
||||
type?: QuestionType;
|
||||
difficulty?: number;
|
||||
};
|
||||
|
||||
// Cached Data Access Function
|
||||
// Using React's cache() to deduplicate requests if called multiple times in one render pass
|
||||
export const getQuestions = cache(async ({
|
||||
q,
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
pageSize = 50,
|
||||
ids,
|
||||
knowledgePointId,
|
||||
type,
|
||||
difficulty,
|
||||
}: GetQuestionsParams = {}) => {
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// Build Where Conditions
|
||||
const conditions = [];
|
||||
const conditions: SQL[] = [];
|
||||
|
||||
if (ids && ids.length > 0) {
|
||||
conditions.push(inArray(questions.id, ids));
|
||||
}
|
||||
|
||||
if (q && q.trim().length > 0) {
|
||||
const needle = `%${q.trim().toLowerCase()}%`;
|
||||
conditions.push(
|
||||
sql`LOWER(CAST(${questions.content} AS CHAR)) LIKE ${needle}`
|
||||
);
|
||||
}
|
||||
|
||||
if (type) {
|
||||
conditions.push(eq(questions.type, type));
|
||||
}
|
||||
|
||||
if (difficulty) {
|
||||
conditions.push(eq(questions.difficulty, difficulty));
|
||||
}
|
||||
|
||||
// Filter by Knowledge Point (using subquery pattern for Many-to-Many)
|
||||
if (knowledgePointId) {
|
||||
const subQuery = db
|
||||
.select({ questionId: questionsToKnowledgePoints.questionId })
|
||||
@@ -52,29 +63,24 @@ export const getQuestions = cache(async ({
|
||||
|
||||
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
// 1. Get Total Count (for Pagination)
|
||||
// Optimization: separate count query is often faster than fetching all data
|
||||
const [totalResult] = await db
|
||||
.select({ count: count() })
|
||||
.from(questions)
|
||||
.where(whereClause);
|
||||
|
||||
const total = totalResult?.count ?? 0;
|
||||
const total = Number(totalResult?.count ?? 0);
|
||||
|
||||
// 2. Get Data with Relations
|
||||
const data = await db.query.questions.findMany({
|
||||
const rows = await db.query.questions.findMany({
|
||||
where: whereClause,
|
||||
limit: pageSize,
|
||||
offset: offset,
|
||||
orderBy: [desc(questions.createdAt)],
|
||||
with: {
|
||||
// Preload Knowledge Points
|
||||
questionsToKnowledgePoints: {
|
||||
with: {
|
||||
knowledgePoint: true,
|
||||
},
|
||||
},
|
||||
// Preload Author
|
||||
author: {
|
||||
columns: {
|
||||
id: true,
|
||||
@@ -82,13 +88,37 @@ export const getQuestions = cache(async ({
|
||||
image: true,
|
||||
},
|
||||
},
|
||||
// Preload Child Questions (first level)
|
||||
children: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data,
|
||||
data: rows.map((row) => {
|
||||
const knowledgePoints =
|
||||
row.questionsToKnowledgePoints?.map((rel) => rel.knowledgePoint) ?? [];
|
||||
|
||||
const author = row.author
|
||||
? {
|
||||
id: row.author.id,
|
||||
name: row.author.name,
|
||||
image: row.author.image,
|
||||
}
|
||||
: null;
|
||||
|
||||
const mapped: Question = {
|
||||
id: row.id,
|
||||
content: row.content,
|
||||
type: row.type,
|
||||
difficulty: row.difficulty ?? 1,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
author,
|
||||
knowledgePoints: knowledgePoints.map((kp) => ({ id: kp.id, name: kp.name })),
|
||||
childrenCount: row.children?.length ?? 0,
|
||||
};
|
||||
|
||||
return mapped;
|
||||
}),
|
||||
meta: {
|
||||
page,
|
||||
pageSize,
|
||||
|
||||
Reference in New Issue
Block a user