feat(阶段4): 教程 / 练习 / AI 学情报告的后台接口
GET/POST admin/tutorials GET/PUT/DELETE admin/tutorials/:id PUT admin/tutorials/:id/visibility GET admin/tutorials/:id/exercises POST admin/exercises PUT/DELETE admin/exercises/:id GET admin/ai/reports (?pinnedOnly=true 不分页) GET admin/ai/reports/:id POST admin/ai/reports/:id/pin 几处判断: - 练习改成挂在教程下的嵌套路径,旧后端是 ?tutorial_id= 查询参数。本来就是一对多的 从属关系,嵌套更贴事实,也省掉「忘了传 tutorial_id」这类错误。 - 删教程必须先删练习。Django 的 on_delete=CASCADE 是应用层实现的,库里外键实际是 NO ACTION(核对过 pg_constraint.confdeltype='a'),直接删会撞外键变 500。 已实测:带 2 个练习的教程能正常删掉且练习一并清除。**后台每个 DELETE 都要照此 核一遍子表**,这是本阶段的通用陷阱。 - 改可见性不动 updatedAt —— 上下架不是内容修改,改了会打乱按更新时间排序的直觉。 - AI 报告的 data / systemPrompt / userPrompt 一律不下发,里面是喂给模型的原始 学情数据与提示词。列表只给 120 字摘要,与旧 AIAnalysisListSerializer 一致。 实测:匿名 401 / 学生 403 / 超管 200;教程练习增删改查、非法 type 400、 挂到不存在的教程 404、置顶互斥(同一学生至多一份)、再钉一次取消,全部符合预期。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,3 +36,98 @@ export const createAnnouncementRequestSchema = z.object({
|
||||
export const updateAnnouncementRequestSchema = createAnnouncementRequestSchema
|
||||
|
||||
export type AdminAnnouncement = z.infer<typeof adminAnnouncementSchema>
|
||||
|
||||
// ---------------------------------------------------------------- 教程 / 练习
|
||||
|
||||
export const tutorialTypeSchema = z.enum(["python", "c"])
|
||||
|
||||
export const adminTutorialSchema = z.object({
|
||||
id: z.number().int(),
|
||||
title: z.string(),
|
||||
content: z.string(),
|
||||
code: z.string().nullable(),
|
||||
isPublic: z.boolean(),
|
||||
order: z.number().int(),
|
||||
type: tutorialTypeSchema,
|
||||
createdBy: sampleUserSchema,
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
})
|
||||
|
||||
export const adminTutorialListItemSchema = adminTutorialSchema.omit({
|
||||
// 教程正文是整篇 markdown,列表页只排序和切换可见性,不需要它
|
||||
content: true,
|
||||
code: true,
|
||||
})
|
||||
|
||||
/** 后台教程列表按语言分组返回,对齐旧 TutorialAdminAPI.get 的 `{python, c}` 形状 */
|
||||
export const adminTutorialGroupsSchema = z.object({
|
||||
python: z.array(adminTutorialListItemSchema),
|
||||
c: z.array(adminTutorialListItemSchema),
|
||||
})
|
||||
|
||||
export const createTutorialRequestSchema = z.object({
|
||||
title: z.string().trim().min(1).max(128),
|
||||
content: z.string(),
|
||||
code: z.string().nullable().default(null),
|
||||
isPublic: z.boolean().default(false),
|
||||
order: z.number().int().default(0),
|
||||
type: tutorialTypeSchema,
|
||||
})
|
||||
|
||||
export const updateTutorialRequestSchema = createTutorialRequestSchema
|
||||
export const setTutorialVisibilityRequestSchema = z.object({ isPublic: z.boolean() })
|
||||
|
||||
export const exerciseTypeSchema = z.enum([
|
||||
"mcq",
|
||||
"sort",
|
||||
"fill",
|
||||
"match",
|
||||
"predict",
|
||||
"debug",
|
||||
"group",
|
||||
])
|
||||
|
||||
export const adminExerciseSchema = z.object({
|
||||
id: z.number().int(),
|
||||
type: exerciseTypeSchema,
|
||||
data: z.record(z.string(), z.unknown()),
|
||||
order: z.number().int(),
|
||||
})
|
||||
|
||||
export const createExerciseRequestSchema = z.object({
|
||||
tutorialId: z.number().int().positive(),
|
||||
type: exerciseTypeSchema,
|
||||
data: z.record(z.string(), z.unknown()),
|
||||
order: z.number().int().default(0),
|
||||
})
|
||||
|
||||
export const updateExerciseRequestSchema = createExerciseRequestSchema.omit({
|
||||
// 练习不支持改挂到别的教程下 —— 旧 EditExerciseSerializer 也没有 tutorial_id
|
||||
tutorialId: true,
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- AI 报告
|
||||
|
||||
/**
|
||||
* 列表只给摘要,正文要点开详情才拿 —— 与旧 AIAnalysisListSerializer 一致。
|
||||
* `data`(原始 prompt 与结构化输入)和 `system_prompt` / `user_prompt` 两侧都不下发。
|
||||
*/
|
||||
export const adminAiReportListItemSchema = z.object({
|
||||
id: z.number().int(),
|
||||
username: z.string(),
|
||||
createTime: z.string(),
|
||||
analysisExcerpt: z.string(),
|
||||
isPinned: z.boolean(),
|
||||
})
|
||||
|
||||
export const adminAiReportSchema = z.object({
|
||||
id: z.number().int(),
|
||||
username: z.string(),
|
||||
className: z.string().nullable(),
|
||||
createTime: z.string(),
|
||||
analysis: z.string(),
|
||||
})
|
||||
|
||||
export const adminAiReportListSchema = paginatedSchema(adminAiReportListItemSchema)
|
||||
export const toggleAiReportPinResponseSchema = z.object({ isPinned: z.boolean() })
|
||||
|
||||
Reference in New Issue
Block a user