feat: 添加题目批量发布、归档和删除接口
- 新增 batchUpdateStatus 通用方法,带状态流转校验和 BatchResult 报告 - 改造 batchPublish 使用新方法,返回成功/失败详情 - 新增 batchArchive 和 batch-delete 端点(软删除) - 使用 inArray 批量查询和更新,优化数据库往返 - 更新 API 文档,补充三个批量接口说明
This commit is contained in:
parent
6a5490dea4
commit
1b142f2866
@ -1264,7 +1264,7 @@
|
|||||||
|
|
||||||
#### POST /admin/questions/batch-publish
|
#### POST /admin/questions/batch-publish
|
||||||
|
|
||||||
批量发布题目。
|
批量发布题目(带状态流转校验,仅 reviewing 状态可发布)。
|
||||||
|
|
||||||
**认证**: Admin Token
|
**认证**: Admin Token
|
||||||
|
|
||||||
@ -1275,15 +1275,77 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**参数说明**:
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| ids | string[] | 是 | 题目 ID 数组,1-200 个,每个为合法 UUID |
|
||||||
|
|
||||||
**响应**:
|
**响应**:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": null,
|
"data": {
|
||||||
|
"total": 3,
|
||||||
|
"succeeded": 2,
|
||||||
|
"failed": [
|
||||||
|
{ "id": "uuid3", "reason": "不允许从 draft 变更为 published" }
|
||||||
|
]
|
||||||
|
},
|
||||||
"error": null
|
"error": null
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**data 字段说明**:
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| total | number | 提交的 ID 总数 |
|
||||||
|
| succeeded | number | 成功更新的数量 |
|
||||||
|
| failed | array | 失败记录列表(包含 id 和 reason) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### POST /admin/questions/batch-archive
|
||||||
|
|
||||||
|
批量归档题目(带状态流转校验,draft/reviewing/published 状态可归档)。
|
||||||
|
|
||||||
|
**认证**: Admin Token
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ids": ["uuid1", "uuid2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明**:
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| ids | string[] | 是 | 题目 ID 数组,1-200 个,每个为合法 UUID |
|
||||||
|
|
||||||
|
**响应**: 与 batch-publish 相同的 `BatchResult` 格式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### POST /admin/questions/batch-delete
|
||||||
|
|
||||||
|
批量删除题目(软删除,等同于批量归档)。
|
||||||
|
|
||||||
|
**认证**: Admin Token
|
||||||
|
|
||||||
|
**请求体**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ids": ["uuid1", "uuid2"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**参数说明**:
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| ids | string[] | 是 | 题目 ID 数组,1-200 个,每个为合法 UUID |
|
||||||
|
|
||||||
|
**响应**: 与 batch-publish 相同的 `BatchResult` 格式。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 分类管理
|
### 分类管理
|
||||||
|
|||||||
@ -26,7 +26,7 @@ const updateQuestionSchema = z.object({
|
|||||||
status: z.enum(['draft', 'reviewing', 'published', 'archived']).optional(),
|
status: z.enum(['draft', 'reviewing', 'published', 'archived']).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const batchPublishSchema = z.object({ ids: z.array(z.string().uuid()) });
|
const batchIdsSchema = z.object({ ids: z.array(z.string().uuid()).min(1).max(200) });
|
||||||
|
|
||||||
export async function adminQuestionsRoutes(app: FastifyInstance): Promise<void> {
|
export async function adminQuestionsRoutes(app: FastifyInstance): Promise<void> {
|
||||||
app.get('/', async (request) => {
|
app.get('/', async (request) => {
|
||||||
@ -96,11 +96,29 @@ export async function adminQuestionsRoutes(app: FastifyInstance): Promise<void>
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/batch-publish', async (request) => {
|
app.post('/batch-publish', async (request) => {
|
||||||
const parsed = batchPublishSchema.safeParse(request.body);
|
const parsed = batchIdsSchema.safeParse(request.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return { success: false, data: null, error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message } };
|
return { success: false, data: null, error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message } };
|
||||||
}
|
}
|
||||||
await questionService.batchPublish(parsed.data.ids);
|
const data = await questionService.batchPublish(parsed.data.ids);
|
||||||
return { success: true, data: null, error: null };
|
return { success: true, data, error: null };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/batch-archive', async (request) => {
|
||||||
|
const parsed = batchIdsSchema.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.batchArchive(parsed.data.ids);
|
||||||
|
return { success: true, data, error: null };
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/batch-delete', async (request) => {
|
||||||
|
const parsed = batchIdsSchema.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.batchArchive(parsed.data.ids);
|
||||||
|
return { success: true, data, error: null };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { db } from '../../db/client.js';
|
import { db } from '../../db/client.js';
|
||||||
import { questions, knowledgeCards } from '../../db/schema.js';
|
import { questions, knowledgeCards } from '../../db/schema.js';
|
||||||
import { eq, and, sql } from 'drizzle-orm';
|
import { eq, and, sql, inArray } from 'drizzle-orm';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
interface ListOptions {
|
interface ListOptions {
|
||||||
@ -125,8 +125,49 @@ export async function updateQuestionStatus(id: string, newStatus: QuestionStatus
|
|||||||
return getQuestionById(id);
|
return getQuestionById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function batchPublish(ids: string[]) {
|
export interface BatchResult {
|
||||||
for (const id of ids) {
|
total: number;
|
||||||
await db.update(questions).set({ status: 'published' }).where(eq(questions.id, id));
|
succeeded: number;
|
||||||
}
|
failed: Array<{ id: string; reason: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function batchUpdateStatus(ids: string[], targetStatus: QuestionStatus): Promise<BatchResult> {
|
||||||
|
// 去重,避免重复计数
|
||||||
|
const uniqueIds = [...new Set(ids)];
|
||||||
|
const result: BatchResult = { total: uniqueIds.length, succeeded: 0, failed: [] };
|
||||||
|
|
||||||
|
const existing = await db.select({ id: questions.id, status: questions.status })
|
||||||
|
.from(questions)
|
||||||
|
.where(inArray(questions.id, uniqueIds));
|
||||||
|
const existingMap = new Map<string, QuestionStatus>();
|
||||||
|
for (const row of existing) existingMap.set(row.id, row.status as QuestionStatus);
|
||||||
|
|
||||||
|
// 分类:合法 ID 和失败 ID
|
||||||
|
const validIds: string[] = [];
|
||||||
|
for (const id of uniqueIds) {
|
||||||
|
const currentStatus = existingMap.get(id);
|
||||||
|
if (!currentStatus) {
|
||||||
|
result.failed.push({ id, reason: '题目不存在' });
|
||||||
|
} else if (!ALLOWED_TRANSITIONS[currentStatus]?.includes(targetStatus)) {
|
||||||
|
result.failed.push({ id, reason: `不允许从 ${currentStatus} 变更为 ${targetStatus}` });
|
||||||
|
} else {
|
||||||
|
validIds.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单次批量 UPDATE
|
||||||
|
if (validIds.length > 0) {
|
||||||
|
await db.update(questions).set({ status: targetStatus }).where(inArray(questions.id, validIds));
|
||||||
|
result.succeeded = validIds.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function batchPublish(ids: string[]): Promise<BatchResult> {
|
||||||
|
return batchUpdateStatus(ids, 'published');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function batchArchive(ids: string[]): Promise<BatchResult> {
|
||||||
|
return batchUpdateStatus(ids, 'archived');
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user