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
|
#### POST /admin/questions/batch-publish
|
||||||
|
|
||||||
批量发布题目。
|
批量发布题目。
|
||||||
@ -1527,6 +1596,7 @@
|
|||||||
| UNAUTHORIZED | 未认证或认证失败 |
|
| UNAUTHORIZED | 未认证或认证失败 |
|
||||||
| FORBIDDEN | 权限不足(需要 super_admin) |
|
| FORBIDDEN | 权限不足(需要 super_admin) |
|
||||||
| NOT_FOUND | 资源不存在 |
|
| NOT_FOUND | 资源不存在 |
|
||||||
|
| INVALID_STATUS_TRANSITION | 题目状态流转不合法 |
|
||||||
| INVALID_RECEIPT | 支付收据验证失败 |
|
| INVALID_RECEIPT | 支付收据验证失败 |
|
||||||
| NOT_IMPLEMENTED | 功能未实现 |
|
| NOT_IMPLEMENTED | 功能未实现 |
|
||||||
| INTERNAL_ERROR | 服务器内部错误 |
|
| INTERNAL_ERROR | 服务器内部错误 |
|
||||||
|
|||||||
@ -65,6 +65,30 @@ export async function adminQuestionsRoutes(app: FastifyInstance): Promise<void>
|
|||||||
return { success: true, data, error: null };
|
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) => {
|
app.delete('/:id', async (request) => {
|
||||||
const { id } = request.params as { id: string };
|
const { id } = request.params as { id: string };
|
||||||
await questionService.archiveQuestion(id);
|
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));
|
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[]) {
|
export async function batchPublish(ids: string[]) {
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
await db.update(questions).set({ status: 'published' }).where(eq(questions.id, id));
|
await db.update(questions).set({ status: 'published' }).where(eq(questions.id, id));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user