feat: 完善题目列表查询接口,支持搜索、多维筛选和排序
- 新增关键词搜索(同时匹配题干 stem 和选项 distractors) - 新增按难度(difficulty)、来源(source)筛选 - 新增动态排序:支持 createdAt/updatedAt/difficulty,可选 asc/desc - 路由层增加 sortBy/sortOrder 白名单校验
This commit is contained in:
parent
aeebcba77c
commit
db2f3af8a3
@ -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
|
||||
|
||||
@ -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<void> {
|
||||
app.get('/', async (request) => {
|
||||
const { page = '1', limit = '20', status, categoryId } = request.query as Record<string, string>;
|
||||
const query = request.query as Record<string, string>;
|
||||
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 };
|
||||
});
|
||||
|
||||
@ -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<SortField, string> = {
|
||||
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);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user