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:
2026-08-07 07:10:34 -06:00
parent 9a6c3ba225
commit af9cb796bd
5 changed files with 441 additions and 19 deletions

View File

@@ -293,38 +293,58 @@ export function createAnnouncement(announcement: AnnouncementEdit) {
return legacyResponse(api2.post("admin/announcements", body))
}
/** 组件里的 Tutorial 仍是 snake_case出站时转成新后端要的 camelCase */
function toTutorialBody(data: Partial<Tutorial>) {
return {
title: data.title,
content: data.content,
code: data.code ?? null,
isPublic: data.is_public ?? false,
order: data.order ?? 0,
type: data.type,
}
}
export async function getTutorialList() {
const res = await http.get<Tutorial[]>("admin/tutorial")
const res = await legacyResponse<{ [key: string]: Tutorial[] }>(
api2.get("admin/tutorials"),
)
return res.data
}
export async function getTutorial(id: number) {
const res = await http.get<Tutorial>("admin/tutorial", { params: { id } })
const res = await legacyResponse<Tutorial>(api2.get(`admin/tutorials/${id}`))
return res.data
}
export async function createTutorial(data: Partial<Tutorial>) {
const res = await http.post<Tutorial>("admin/tutorial", data)
const res = await legacyResponse<Tutorial>(
api2.post("admin/tutorials", toTutorialBody(data)),
)
return res.data
}
export async function updateTutorial(data: Partial<Tutorial>) {
const res = await http.put("admin/tutorial", data)
const res = await legacyResponse<Tutorial>(
api2.put(`admin/tutorials/${data.id}`, toTutorialBody(data)),
)
return res.data
}
export function deleteTutorial(id: number) {
return http.delete("admin/tutorial", { params: { id } })
return api2.delete(`admin/tutorials/${id}`)
}
export function setTutorialVisibility(id: number, is_public: boolean) {
return http.put("admin/tutorial/visibility", { id, is_public })
return legacyResponse(
api2.put(`admin/tutorials/${id}/visibility`, { isPublic: is_public }),
)
}
export async function getAdminExercises(tutorialId: number) {
const res = await http.get<Exercise[]>("admin/exercise", {
params: { tutorial_id: tutorialId },
})
const res = await legacyResponse<Exercise[]>(
api2.get(`admin/tutorials/${tutorialId}/exercises`),
)
return res.data
}
@@ -334,7 +354,14 @@ export async function createExercise(data: {
data: object
order: number
}) {
const res = await http.post<Exercise>("admin/exercise", data)
const res = await legacyResponse<Exercise>(
api2.post("admin/exercises", {
tutorialId: data.tutorial_id,
type: data.type,
data: data.data,
order: data.order,
}),
)
return res.data
}
@@ -344,12 +371,18 @@ export async function updateExercise(data: {
data: object
order: number
}) {
const res = await http.put("admin/exercise", data)
return res.data as Exercise
const res = await legacyResponse<Exercise>(
api2.put(`admin/exercises/${data.id}`, {
type: data.type,
data: data.data,
order: data.order,
}),
)
return res.data
}
export function deleteExercise(id: number) {
return http.delete("admin/exercise", { params: { id } })
return api2.delete(`admin/exercises/${id}`)
}
// 将竞赛题目转为公开题目
@@ -543,21 +576,25 @@ export function getTopACTrend(params: {
// AI 学习分析报告
export function getAIReportList(offset = 0, limit = 10, username = "") {
return http.get("admin/ai/reports", {
params: { paging: true, offset, limit, username: username || undefined },
})
return legacyResponse(
api2.get("admin/ai/reports", {
params: { offset, limit, username: username || undefined },
}),
)
}
export function getAIReportDetail(id: number) {
return http.get("admin/ai/reports", { params: { id } })
return legacyResponse(api2.get(`admin/ai/reports/${id}`))
}
export function pinAIReport(id: number) {
return http.post("admin/ai/reports", { id })
return legacyResponse(api2.post(`admin/ai/reports/${id}/pin`))
}
export function getPinnedAIReports() {
return http.get("admin/ai/reports", { params: { pinned_only: "true" } })
return legacyResponse(
api2.get("admin/ai/reports", { params: { pinnedOnly: "true" } }),
)
}
// ==================== 成就 ====================