diff --git a/docs/api-reference.md b/docs/api-reference.md index f6323f3..8266c70 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1047,8 +1047,13 @@ **查询参数**: - `page`: 页码 (默认: 1) - `limit`: 每页数量 (默认: 20) -- `status`: draft | reviewing | published | archived (可选) +- `status`: draft | reviewing | published | archived (可选,按状态筛选) - `categoryId`: 分类 ID (可选) +- `keyword`: 关键词搜索,匹配题干(stem)和选项(distractors)内容 (可选) +- `difficulty`: 难度值 1-5,精确匹配 (可选) +- `source`: system | ugc,按来源筛选 (可选) +- `sortBy`: 排序字段,createdAt | updatedAt | difficulty (默认: createdAt) +- `sortOrder`: asc | desc (默认: desc) **响应**: ```json diff --git a/src/routes/admin/questions.ts b/src/routes/admin/questions.ts index 35a7d7b..8825caa 100644 --- a/src/routes/admin/questions.ts +++ b/src/routes/admin/questions.ts @@ -45,12 +45,26 @@ const batchIdsSchema = z.object({ ids: z.array(z.string().uuid()).min(1).max(200 export async function adminQuestionsRoutes(app: FastifyInstance): Promise { app.get('/', async (request) => { - const { page = '1', limit = '20', status, categoryId } = request.query as Record; + const query = request.query as Record; + const { page = '1', limit = '20', status, categoryId, keyword, difficulty, source, sortBy, sortOrder } = query; + + if (sortBy && !['createdAt', 'updatedAt', 'difficulty'].includes(sortBy)) { + return { success: false, data: null, error: { code: 'VALIDATION_ERROR', message: 'sortBy 仅支持 createdAt / updatedAt / difficulty' } }; + } + if (sortOrder && !['asc', 'desc'].includes(sortOrder)) { + return { success: false, data: null, error: { code: 'VALIDATION_ERROR', message: 'sortOrder 仅支持 asc / desc' } }; + } + const result = await questionService.listQuestions({ page: Number(page), limit: Number(limit), status, categoryId, + keyword, + difficulty: difficulty !== undefined ? Number(difficulty) : undefined, + source, + sortBy: sortBy as 'createdAt' | 'updatedAt' | 'difficulty' | undefined, + sortOrder: sortOrder as 'asc' | 'desc' | undefined, }); return { success: true, data: result.items, pagination: result.pagination, error: null }; }); diff --git a/src/services/admin/question-service.ts b/src/services/admin/question-service.ts index 81d19bc..56dba8f 100644 --- a/src/services/admin/question-service.ts +++ b/src/services/admin/question-service.ts @@ -1,6 +1,6 @@ import { db } from '../../db/client.js'; import { questions, knowledgeCards, categories } from '../../db/schema.js'; -import { eq, and, sql, inArray } from 'drizzle-orm'; +import { eq, and, or, sql, inArray } from 'drizzle-orm'; import { v4 as uuid } from 'uuid'; /** 单条导入数据的输入结构 */ @@ -124,19 +124,55 @@ function parseCsvRow(line: string): string[] { return fields; } +type SortField = 'createdAt' | 'updatedAt' | 'difficulty'; +type SortOrder = 'asc' | 'desc'; + interface ListOptions { page?: number; limit?: number; status?: string; categoryId?: string; + keyword?: string; + difficulty?: number; + source?: string; + sortBy?: SortField; + sortOrder?: SortOrder; } -export async function listQuestions({ page = 1, limit = 20, status, categoryId }: ListOptions) { +/** 排序字段名映射(Drizzle 列 → SQL 列名) */ +const SORT_COLUMN_MAP: Record = { + createdAt: 'created_at', + updatedAt: 'updated_at', + difficulty: 'difficulty', +}; + +export async function listQuestions({ + page = 1, + limit = 20, + status, + categoryId, + keyword, + difficulty, + source, + sortBy = 'createdAt', + sortOrder = 'desc', +}: ListOptions) { const offset = (page - 1) * limit; const conditions = []; if (status) conditions.push(eq(questions.status, status as 'draft' | 'reviewing' | 'published' | 'archived')); if (categoryId) conditions.push(eq(questions.categoryId, categoryId)); + if (difficulty !== undefined) conditions.push(eq(questions.difficulty, difficulty)); + if (source) conditions.push(eq(questions.source, source as 'system' | 'ugc')); + if (keyword) { + const like = `%${keyword}%`; + conditions.push( + or( + sql`CAST(${questions.stem} AS CHAR) LIKE ${like}`, + sql`CAST(${questions.distractors} AS CHAR) LIKE ${like}`, + )!, + ); + } const where = conditions.length > 0 ? and(...conditions) : undefined; @@ -146,11 +182,14 @@ export async function listQuestions({ page = 1, limit = 20, status, categoryId } .where(where); const total = Number(rows?.count ?? 0); + + const column = SORT_COLUMN_MAP[sortBy] ?? 'created_at'; + const direction = sortOrder === 'asc' ? 'ASC' : 'DESC'; const items = await db .select() .from(questions) .where(where) - .orderBy(sql`created_at DESC`) + .orderBy(sql`${sql.raw(column)} ${sql.raw(direction)}`) .limit(limit) .offset(offset);