新增 PATCH /admin/questions/:id/status 接口,支持题目状态流转并校验合法性: - draft → reviewing, archived - reviewing → published, draft, archived - published → archived - archived → draft
107 lines
4.2 KiB
TypeScript
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 };
|
|
});
|
|
}
|