feat: 添加题目状态变更接口(带流转校验)
新增 PATCH /admin/questions/:id/status 接口,支持题目状态流转并校验合法性: - draft → reviewing, archived - reviewing → published, draft, archived - published → archived - archived → draft
This commit is contained in:
parent
f260fd6bfb
commit
6a5490dea4
@ -1193,6 +1193,75 @@
|
||||
|
||||
---
|
||||
|
||||
#### PATCH /admin/questions/:id/status
|
||||
|
||||
变更题目状态(带流转校验)。
|
||||
|
||||
**认证**: Admin Token
|
||||
|
||||
**路径参数**:
|
||||
- `id`: 题目 ID
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"status": "draft | reviewing | published | archived (必填)"
|
||||
}
|
||||
```
|
||||
|
||||
**允许的状态流转**:
|
||||
| 当前状态 | 可变更到 |
|
||||
|----------|----------|
|
||||
| draft | reviewing, archived |
|
||||
| reviewing | published, draft, archived |
|
||||
| published | archived |
|
||||
| archived | draft |
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "uuid",
|
||||
"stem": { "text": "题目内容" },
|
||||
"contentType": "text",
|
||||
"correctAnswer": "B",
|
||||
"distractors": ["A", "C", "D"],
|
||||
"categoryId": "uuid",
|
||||
"difficulty": 3,
|
||||
"status": "published",
|
||||
"knowledgeCard": { ... }
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
**错误 (404)**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": "题目不存在"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误 (400)**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "INVALID_STATUS_TRANSITION",
|
||||
"message": "不允许从 published 变更为 reviewing"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### POST /admin/questions/batch-publish
|
||||
|
||||
批量发布题目。
|
||||
@ -1527,6 +1596,7 @@
|
||||
| UNAUTHORIZED | 未认证或认证失败 |
|
||||
| FORBIDDEN | 权限不足(需要 super_admin) |
|
||||
| NOT_FOUND | 资源不存在 |
|
||||
| INVALID_STATUS_TRANSITION | 题目状态流转不合法 |
|
||||
| INVALID_RECEIPT | 支付收据验证失败 |
|
||||
| NOT_IMPLEMENTED | 功能未实现 |
|
||||
| INTERNAL_ERROR | 服务器内部错误 |
|
||||
|
||||
@ -65,6 +65,30 @@ export async function adminQuestionsRoutes(app: FastifyInstance): Promise<void>
|
||||
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);
|
||||
|
||||
@ -101,6 +101,30 @@ export async function archiveQuestion(id: string) {
|
||||
await db.update(questions).set({ status: 'archived' }).where(eq(questions.id, id));
|
||||
}
|
||||
|
||||
type QuestionStatus = 'draft' | 'reviewing' | 'published' | 'archived';
|
||||
|
||||
const ALLOWED_TRANSITIONS: Record<QuestionStatus, QuestionStatus[]> = {
|
||||
draft: ['reviewing', 'archived'],
|
||||
reviewing: ['published', 'draft', 'archived'],
|
||||
published: ['archived'],
|
||||
archived: ['draft'],
|
||||
};
|
||||
|
||||
export async function updateQuestionStatus(id: string, newStatus: QuestionStatus) {
|
||||
const [question] = await db.select().from(questions).where(eq(questions.id, id)).limit(1);
|
||||
if (!question) {
|
||||
throw new Error('QUESTION_NOT_FOUND');
|
||||
}
|
||||
|
||||
const currentStatus = question.status as QuestionStatus;
|
||||
if (!ALLOWED_TRANSITIONS[currentStatus]?.includes(newStatus)) {
|
||||
throw new Error(`INVALID_STATUS_TRANSITION:${currentStatus}:${newStatus}`);
|
||||
}
|
||||
|
||||
await db.update(questions).set({ status: newStatus }).where(eq(questions.id, id));
|
||||
return getQuestionById(id);
|
||||
}
|
||||
|
||||
export async function batchPublish(ids: string[]) {
|
||||
for (const id of ids) {
|
||||
await db.update(questions).set({ status: 'published' }).where(eq(questions.id, id));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user