duoqi-api/src/routes/admin/questions.ts
Wang Zhuoxuan 6a5490dea4 feat: 添加题目状态变更接口(带流转校验)
新增 PATCH /admin/questions/:id/status 接口,支持题目状态流转并校验合法性:
- draft → reviewing, archived
- reviewing → published, draft, archived
- published → archived
- archived → draft
2026-04-11 21:17:34 +08:00

107 lines
4.2 KiB
TypeScript

import { FastifyInstance } from 'fastify';
import { z } from 'zod';
import * as questionService from '../../services/admin/question-service.js';
const createQuestionSchema = z.object({
stem: z.record(z.unknown()),
contentType: z.enum(['text', 'image', 'video', 'audio']),
correctAnswer: z.string().min(1),
distractors: z.array(z.string()).min(2),
categoryId: z.string().min(1),
difficulty: z.number().min(1).max(5).optional(),
knowledgeCard: z.object({
summary: z.string().min(1),
deepDive: z.string().optional(),
sourceRef: z.string().optional(),
}).optional(),
});
const updateQuestionSchema = z.object({
stem: z.record(z.unknown()).optional(),
contentType: z.enum(['text', 'image', 'video', 'audio']).optional(),
correctAnswer: z.string().min(1).optional(),
distractors: z.array(z.string()).min(2).optional(),
categoryId: z.string().min(1).optional(),
difficulty: z.number().min(1).max(5).optional(),
status: z.enum(['draft', 'reviewing', 'published', 'archived']).optional(),
});
const batchPublishSchema = z.object({ ids: z.array(z.string().uuid()) });
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 result = await questionService.listQuestions({
page: Number(page),
limit: Number(limit),
status,
categoryId,
});
return { success: true, data: result.items, pagination: result.pagination, error: null };
});
app.get('/:id', async (request) => {
const { id } = request.params as { id: string };
const data = await questionService.getQuestionById(id);
return { success: true, data, error: null };
});
app.post('/', async (request) => {
const parsed = createQuestionSchema.safeParse(request.body);
if (!parsed.success) {
return { success: false, data: null, error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message } };
}
const data = await questionService.createQuestion(parsed.data);
return { success: true, data, error: null };
});
app.put('/:id', async (request) => {
const { id } = request.params as { id: string };
const parsed = updateQuestionSchema.safeParse(request.body);
if (!parsed.success) {
return { success: false, data: null, error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message } };
}
const data = await questionService.updateQuestion(id, parsed.data);
return { success: true, data, error: null };
});
app.patch('/:id/status', async (request, reply) => {
const { id } = request.params as { id: string };
const parsed = z.object({
status: z.enum(['draft', 'reviewing', 'published', 'archived']),
}).safeParse(request.body);
if (!parsed.success) {
return reply.status(400).send({ success: false, data: null, error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message } });
}
try {
const data = await questionService.updateQuestionStatus(id, parsed.data.status);
return { success: true, data, error: null };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
if (msg === 'QUESTION_NOT_FOUND') {
return reply.status(404).send({ success: false, data: null, error: { code: 'NOT_FOUND', message: '题目不存在' } });
}
if (msg.startsWith('INVALID_STATUS_TRANSITION:')) {
const [, from, to] = msg.split(':');
return reply.status(400).send({ success: false, data: null, error: { code: 'INVALID_STATUS_TRANSITION', message: `不允许从 ${from} 变更为 ${to}` } });
}
throw err;
}
});
app.delete('/:id', async (request) => {
const { id } = request.params as { id: string };
await questionService.archiveQuestion(id);
return { success: true, data: null, error: null };
});
app.post('/batch-publish', async (request) => {
const parsed = batchPublishSchema.safeParse(request.body);
if (!parsed.success) {
return { success: false, data: null, error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message } };
}
await questionService.batchPublish(parsed.data.ids);
return { success: true, data: null, error: null };
});
}