feat(阶段3): 补齐用户侧依赖的三个 admin 端点
重判、提交统计、流程图统计这三条挂在旧后端的 admin 路由下,权限也确实是 teacher/super admin,但入口在用户侧页面里(提交列表页的重判按钮、两个统计面板)。 不做完,阶段 3 的出口标准「用户侧全部功能跑在新后端上」就不成立 —— 按 URL 前缀切阶段会漏掉它们。 - POST submissions/:id/rejudge ← GET admin/submission/rejudge - GET submissions/statistics ← GET admin/submission/statistics - GET flowcharts/statistics ← GET admin/flowchart/statistics 几处对齐旧后端的细节: - AST_CHECK_FAILED(10) 与 ACCEPTED(0) 同算通过 - 完成度先用原始花名册人数算、再修正 person_count,顺序照搬,兜住「学生已删号 但提交记录还在」 - 有提交但零通过的学生,两个名单里都不出现(旧后端同样口径) - 词云用 @node-rs/jieba,STOPWORDS 与 38 个自定义词逐词照搬;jieba@2 没有 insertWord,改用 loadDict 加载用户词典 - avgScore 分母是有分数的条数,对齐 Django Avg() 跳过 NULL rejudge 的 jobId 带时间戳。队列保留最近 100 个已完成任务,沿用 submissionId 做 jobId 的话 BullMQ 会认为任务已存在,重判会静默变成空操作。 correctRate 改成数值不带 %,展示格式化交给前端。stripClassPrefix 用 startsWith+slice 而不是 replace,前缀对不上时不会从中间截出乱码;site.ts 原有的 replace 写法一并改掉。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,9 +2,10 @@ import {
|
||||
createSubmissionResponseSchema,
|
||||
problemDetailSchema,
|
||||
submissionDetailSchema,
|
||||
type FlowchartStatistics,
|
||||
type SubmissionStatistics,
|
||||
} from "@oj2/contract"
|
||||
import api2 from "utils/api2"
|
||||
import http from "utils/http"
|
||||
import type { ApiResponse } from "utils/http"
|
||||
import { filterResult } from "oj/transforms"
|
||||
import type {
|
||||
@@ -30,7 +31,9 @@ function toLegacy<T>(value: unknown): T {
|
||||
) as T
|
||||
}
|
||||
|
||||
async function legacyResponse<T>(request: Promise<ApiResponse<unknown>>): Promise<ApiResponse<T>> {
|
||||
async function legacyResponse<T>(
|
||||
request: Promise<ApiResponse<unknown>>,
|
||||
): Promise<ApiResponse<T>> {
|
||||
const response = await request
|
||||
return { error: response.error, data: toLegacy<T>(response.data) }
|
||||
}
|
||||
@@ -117,11 +120,13 @@ export async function getProblemList(
|
||||
}
|
||||
|
||||
export function getAuthors(all = false) {
|
||||
return legacyResponse(api2.get("problem-authors", {
|
||||
params: {
|
||||
all: all ? "1" : "0",
|
||||
},
|
||||
}))
|
||||
return legacyResponse(
|
||||
api2.get("problem-authors", {
|
||||
params: {
|
||||
all: all ? "1" : "0",
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getRandomProblemID() {
|
||||
@@ -132,9 +137,7 @@ export async function getProblem(problemID: string, contestID: string) {
|
||||
const endpoint = contestID
|
||||
? `contests/${encodeURIComponent(contestID)}/problems/${encodeURIComponent(problemID)}`
|
||||
: `problems/${encodeURIComponent(problemID)}`
|
||||
const response = await api2.get<unknown>(
|
||||
endpoint,
|
||||
)
|
||||
const response = await api2.get<unknown>(endpoint)
|
||||
return { error: null, data: detailProblem(response.data) }
|
||||
}
|
||||
|
||||
@@ -200,17 +203,23 @@ export function getSubmissions(params: Partial<SubmissionListPayload>) {
|
||||
const endpoint = params.contest_id
|
||||
? `contests/${encodeURIComponent(params.contest_id)}/submissions`
|
||||
: "submissions"
|
||||
return legacyResponse(api2.get(endpoint, { params: {
|
||||
...params,
|
||||
problemId: params.problem_id,
|
||||
contest_id: undefined,
|
||||
problem_id: undefined,
|
||||
page: undefined,
|
||||
} }))
|
||||
return legacyResponse(
|
||||
api2.get(endpoint, {
|
||||
params: {
|
||||
...params,
|
||||
problemId: params.problem_id,
|
||||
contest_id: undefined,
|
||||
problem_id: undefined,
|
||||
page: undefined,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getRankOfProblem(problem_id: string) {
|
||||
return legacyResponse(api2.get(`problems/${encodeURIComponent(problem_id)}/rank`))
|
||||
return legacyResponse(
|
||||
api2.get(`problems/${encodeURIComponent(problem_id)}/rank`),
|
||||
)
|
||||
}
|
||||
|
||||
export function getTodaySubmissionCount(language?: string) {
|
||||
@@ -218,9 +227,7 @@ export function getTodaySubmissionCount(language?: string) {
|
||||
}
|
||||
|
||||
export function adminRejudge(id: string) {
|
||||
return http.get("admin/submission/rejudge", {
|
||||
params: { id },
|
||||
})
|
||||
return api2.post(`submissions/${encodeURIComponent(id)}/rejudge`)
|
||||
}
|
||||
|
||||
export function getSubmissionStatistics(
|
||||
@@ -228,12 +235,8 @@ export function getSubmissionStatistics(
|
||||
problemID?: string,
|
||||
username?: string,
|
||||
) {
|
||||
return http.get("admin/submission/statistics", {
|
||||
params: {
|
||||
...duration,
|
||||
problem_id: problemID,
|
||||
username,
|
||||
},
|
||||
return api2.get<SubmissionStatistics>("submissions/statistics", {
|
||||
params: { ...duration, problemId: problemID, username },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -243,9 +246,11 @@ export function getRank(
|
||||
n: number,
|
||||
username?: string,
|
||||
) {
|
||||
return legacyResponse(api2.get("rankings/users", {
|
||||
params: { offset, limit, username, top: n },
|
||||
}))
|
||||
return legacyResponse(
|
||||
api2.get("rankings/users", {
|
||||
params: { offset, limit, username, top: n },
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getActivityRank(start: string) {
|
||||
@@ -255,9 +260,11 @@ export function getActivityRank(start: string) {
|
||||
}
|
||||
|
||||
export function getClassRank(grade?: number | null) {
|
||||
return legacyResponse(api2.get("rankings/classes", {
|
||||
params: { grade },
|
||||
}))
|
||||
return legacyResponse(
|
||||
api2.get("rankings/classes", {
|
||||
params: { grade },
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getUserClassRank(
|
||||
@@ -265,7 +272,9 @@ export function getUserClassRank(
|
||||
offset?: number,
|
||||
limit?: number,
|
||||
) {
|
||||
return legacyResponse(api2.get("me/class-rank", { params: { scope, offset, limit } }))
|
||||
return legacyResponse(
|
||||
api2.get("me/class-rank", { params: { scope, offset, limit } }),
|
||||
)
|
||||
}
|
||||
|
||||
export function getClassPK(
|
||||
@@ -304,11 +313,15 @@ export function getContestAccess(id: string) {
|
||||
}
|
||||
|
||||
export function checkContestPassword(contestID: string, password: string) {
|
||||
return api2.post(`contests/${encodeURIComponent(contestID)}/access`, { password })
|
||||
return api2.post(`contests/${encodeURIComponent(contestID)}/access`, {
|
||||
password,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getContestProblems(contestID: string) {
|
||||
const res = await api2.get<any[]>(`contests/${encodeURIComponent(contestID)}/problems`)
|
||||
const res = await api2.get<any[]>(
|
||||
`contests/${encodeURIComponent(contestID)}/problems`,
|
||||
)
|
||||
return res.data.map(listProblem).map(filterResult)
|
||||
}
|
||||
|
||||
@@ -316,17 +329,20 @@ export function getContestRank(
|
||||
contestID: string,
|
||||
query: { limit: number; offset: number },
|
||||
) {
|
||||
return legacyResponse<any>(api2.get(`contests/${encodeURIComponent(contestID)}/rank`, { params: query }))
|
||||
.then((response) => ({
|
||||
...response,
|
||||
data: {
|
||||
...response.data,
|
||||
results: response.data.results.map((item: any) => ({
|
||||
...item,
|
||||
contest: item.contest_id,
|
||||
})),
|
||||
},
|
||||
}))
|
||||
return legacyResponse<any>(
|
||||
api2.get(`contests/${encodeURIComponent(contestID)}/rank`, {
|
||||
params: query,
|
||||
}),
|
||||
).then((response) => ({
|
||||
...response,
|
||||
data: {
|
||||
...response.data,
|
||||
results: response.data.results.map((item: any) => ({
|
||||
...item,
|
||||
contest: item.contest_id,
|
||||
})),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export function uploadAvatar(file: File) {
|
||||
@@ -338,14 +354,18 @@ export function uploadAvatar(file: File) {
|
||||
}
|
||||
|
||||
export function updateProfile(data: { real_name: string; mood: string }) {
|
||||
return legacyResponse(api2.put("me/profile", {
|
||||
realName: data.real_name,
|
||||
mood: data.mood,
|
||||
}))
|
||||
return legacyResponse(
|
||||
api2.put("me/profile", {
|
||||
realName: data.real_name,
|
||||
mood: data.mood,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAnnouncementList(offset = 0, limit = 10) {
|
||||
return legacyResponse(api2.get("announcements", { params: { limit, offset } }))
|
||||
return legacyResponse(
|
||||
api2.get("announcements", { params: { limit, offset } }),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAnnouncement(id: number) {
|
||||
@@ -394,17 +414,19 @@ export function getTutorials(type: "python" | "c") {
|
||||
}
|
||||
|
||||
export function getAIDetailData(start: string, end: string, username?: string) {
|
||||
return legacyResponse<any>(api2.get("ai/detail", { params: { start, end, username } }))
|
||||
.then((response) => ({
|
||||
...response,
|
||||
data: {
|
||||
...response.data,
|
||||
flowcharts: response.data.flowcharts?.map((item: any) => ({
|
||||
return legacyResponse<any>(
|
||||
api2.get("ai/detail", { params: { start, end, username } }),
|
||||
).then((response) => ({
|
||||
...response,
|
||||
data: {
|
||||
...response.data,
|
||||
flowcharts:
|
||||
response.data.flowcharts?.map((item: any) => ({
|
||||
...item,
|
||||
problem__id: item.problem_id,
|
||||
})) ?? [],
|
||||
},
|
||||
}))
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export function getAIDurationData(
|
||||
@@ -412,7 +434,9 @@ export function getAIDurationData(
|
||||
duration: string,
|
||||
username?: string,
|
||||
) {
|
||||
return legacyResponse(api2.get("ai/duration", { params: { end, duration, username } }))
|
||||
return legacyResponse(
|
||||
api2.get("ai/duration", { params: { end, duration, username } }),
|
||||
)
|
||||
}
|
||||
|
||||
export function getAIHeatmapData(username?: string) {
|
||||
@@ -430,8 +454,12 @@ export function getAIPinnedReport() {
|
||||
// ==================== 相似题目推荐 ====================
|
||||
|
||||
export function getSimilarProblems(problemId: string) {
|
||||
return api2.get<any[]>(`problems/${encodeURIComponent(problemId)}/similar`)
|
||||
.then((response) => ({ ...response, data: response.data.map(listProblem).map(filterResult) }))
|
||||
return api2
|
||||
.get<any[]>(`problems/${encodeURIComponent(problemId)}/similar`)
|
||||
.then((response) => ({
|
||||
...response,
|
||||
data: response.data.map(listProblem).map(filterResult),
|
||||
}))
|
||||
}
|
||||
|
||||
export interface YearlyACData {
|
||||
@@ -442,7 +470,9 @@ export interface YearlyACData {
|
||||
}
|
||||
|
||||
export function getProblemYearlyAC(problemId: string) {
|
||||
return legacyResponse<YearlyACData[]>(api2.get(`problems/${encodeURIComponent(problemId)}/yearly-ac`))
|
||||
return legacyResponse<YearlyACData[]>(
|
||||
api2.get(`problems/${encodeURIComponent(problemId)}/yearly-ac`),
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== 流程图相关API ====================
|
||||
@@ -452,11 +482,13 @@ export function submitFlowchart(data: {
|
||||
mermaid_code: string
|
||||
flowchart_data: any // 这个是压缩之后的,元数据太长了
|
||||
}) {
|
||||
return legacyResponse(api2.post("flowcharts", {
|
||||
problemId: data.problem_id,
|
||||
mermaidCode: data.mermaid_code,
|
||||
flowchartData: data.flowchart_data,
|
||||
}))
|
||||
return legacyResponse(
|
||||
api2.post("flowcharts", {
|
||||
problemId: data.problem_id,
|
||||
mermaidCode: data.mermaid_code,
|
||||
flowchartData: data.flowchart_data,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function legacyFlowchart(value: unknown) {
|
||||
@@ -482,11 +514,15 @@ export function getFlowchartSubmissions(params: {
|
||||
today?: string
|
||||
grade?: string
|
||||
}) {
|
||||
return legacyResponse<any>(api2.get("flowcharts", { params: {
|
||||
...params,
|
||||
problemId: params.problem_id,
|
||||
problem_id: undefined,
|
||||
} }))
|
||||
return legacyResponse<any>(
|
||||
api2.get("flowcharts", {
|
||||
params: {
|
||||
...params,
|
||||
problemId: params.problem_id,
|
||||
problem_id: undefined,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export function getFlowchartStatistics(
|
||||
@@ -494,30 +530,36 @@ export function getFlowchartStatistics(
|
||||
problemID?: string,
|
||||
username?: string,
|
||||
) {
|
||||
return http.get("admin/flowchart/statistics", {
|
||||
params: {
|
||||
...duration,
|
||||
problem_id: problemID,
|
||||
username,
|
||||
},
|
||||
return api2.get<FlowchartStatistics>("flowcharts/statistics", {
|
||||
params: { ...duration, problemId: problemID, username },
|
||||
})
|
||||
}
|
||||
|
||||
export function retryFlowchartSubmission(submissionId: string) {
|
||||
return legacyResponse(api2.post(`flowcharts/${encodeURIComponent(submissionId)}/retry`))
|
||||
return legacyResponse(
|
||||
api2.post(`flowcharts/${encodeURIComponent(submissionId)}/retry`),
|
||||
)
|
||||
}
|
||||
|
||||
export function getCurrentProblemFlowchartSubmission(problemId: number) {
|
||||
return api2.get(`problems/${problemId}/flowchart/current`)
|
||||
}
|
||||
|
||||
export async function getFlowchartSubmissionDetail(problemId: number, page = 0) {
|
||||
const response = await api2.get<any>(`problems/${problemId}/flowchart/history`, { params: { page } })
|
||||
export async function getFlowchartSubmissionDetail(
|
||||
problemId: number,
|
||||
page = 0,
|
||||
) {
|
||||
const response = await api2.get<any>(
|
||||
`problems/${problemId}/flowchart/history`,
|
||||
{ params: { page } },
|
||||
)
|
||||
return {
|
||||
...response,
|
||||
data: {
|
||||
...response.data,
|
||||
submission: response.data.submission ? legacyFlowchart(response.data.submission) : null,
|
||||
submission: response.data.submission
|
||||
? legacyFlowchart(response.data.submission)
|
||||
: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -531,22 +573,26 @@ export function getProblemSetList(
|
||||
difficulty = "",
|
||||
status = "",
|
||||
) {
|
||||
return legacyResponse<any>(api2.get("problem-sets", {
|
||||
params: {
|
||||
offset,
|
||||
limit,
|
||||
keyword,
|
||||
difficulty,
|
||||
status,
|
||||
},
|
||||
})).then(mapProblemSetResponse)
|
||||
return legacyResponse<any>(
|
||||
api2.get("problem-sets", {
|
||||
params: {
|
||||
offset,
|
||||
limit,
|
||||
keyword,
|
||||
difficulty,
|
||||
status,
|
||||
},
|
||||
}),
|
||||
).then(mapProblemSetResponse)
|
||||
}
|
||||
|
||||
export function getProblemSetDetail(id: number) {
|
||||
return legacyResponse<any>(api2.get(`problem-sets/${id}`)).then((response) => ({
|
||||
...response,
|
||||
data: legacyProblemSet(response.data),
|
||||
}))
|
||||
return legacyResponse<any>(api2.get(`problem-sets/${id}`)).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: legacyProblemSet(response.data),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function legacyBadge(value: any) {
|
||||
@@ -571,7 +617,9 @@ function mapProblemSetResponse(response: ApiResponse<any>) {
|
||||
}
|
||||
|
||||
export async function getProblemSetProblems(problemSetId: number) {
|
||||
const response = await legacyResponse<any[]>(api2.get(`problem-sets/${problemSetId}/problems`))
|
||||
const response = await legacyResponse<any[]>(
|
||||
api2.get(`problem-sets/${problemSetId}/problems`),
|
||||
)
|
||||
return {
|
||||
...response,
|
||||
data: response.data.map((item) => ({
|
||||
@@ -594,19 +642,20 @@ export function updateProblemSetProgress(
|
||||
problemId: number,
|
||||
submissionId: string,
|
||||
) {
|
||||
return legacyResponse(api2.put("problem-set-progress", {
|
||||
problemSetId,
|
||||
problemId,
|
||||
submissionId,
|
||||
})
|
||||
return legacyResponse(
|
||||
api2.put("problem-set-progress", {
|
||||
problemSetId,
|
||||
problemId,
|
||||
submissionId,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// 获取用户徽章列表
|
||||
export async function getUserBadges(username?: string) {
|
||||
const response = await legacyResponse<any[]>(api2.get(
|
||||
`users/${encodeURIComponent(username ?? "me")}/badges`,
|
||||
))
|
||||
const response = await legacyResponse<any[]>(
|
||||
api2.get(`users/${encodeURIComponent(username ?? "me")}/badges`),
|
||||
)
|
||||
return {
|
||||
...response,
|
||||
data: response.data.map((item) => ({
|
||||
@@ -619,7 +668,9 @@ export async function getUserBadges(username?: string) {
|
||||
|
||||
// 获取题单徽章列表
|
||||
export async function getProblemSetBadges(problemSetId: number) {
|
||||
const response = await legacyResponse<any[]>(api2.get(`problem-sets/${problemSetId}/badges`))
|
||||
const response = await legacyResponse<any[]>(
|
||||
api2.get(`problem-sets/${problemSetId}/badges`),
|
||||
)
|
||||
return { ...response, data: response.data.map(legacyBadge) }
|
||||
}
|
||||
|
||||
@@ -633,14 +684,16 @@ export function getProblemSetUserProgress(
|
||||
completion_status?: "" | "completed" | "in_progress" | "not_started"
|
||||
},
|
||||
) {
|
||||
return legacyResponse(api2.get(`problem-sets/${problemSetId}/user-progress`, {
|
||||
params: {
|
||||
limit: params?.limit,
|
||||
offset: params?.offset,
|
||||
className: params?.class_name,
|
||||
completionStatus: params?.completion_status,
|
||||
},
|
||||
}))
|
||||
return legacyResponse(
|
||||
api2.get(`problem-sets/${problemSetId}/user-progress`, {
|
||||
params: {
|
||||
limit: params?.limit,
|
||||
offset: params?.offset,
|
||||
className: params?.class_name,
|
||||
completionStatus: params?.completion_status,
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
export async function getExercises(tutorialId: number): Promise<Exercise[]> {
|
||||
|
||||
Reference in New Issue
Block a user