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:
@@ -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" } }),
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== 成就 ====================
|
||||
|
||||
Reference in New Issue
Block a user