feat(阶段4): 站点配置 / 判题机 / 孤儿用例 / 概览 / 图片上传
GET/POST admin/website GET admin/judge-servers PUT admin/judge-servers/:id DELETE admin/judge-servers/:hostname GET/DELETE admin/orphan-test-cases GET admin/dashboard GET admin/random-usernames POST admin/upload-image 顺带补上配置广播:旧后端改配置会经 WebSocket 推给所有开着页面的人,改完立刻生效。 新后端只服务 /ws/submissions,前端的 ConfigWebSocket 还连着旧 Django Channels。 现在加了 /ws/config 通道(同一个 Bun.serve 只能挂一个 handler,用 socket data 上的 kind 区分),前端 ConfigWebSocket 改走 /ws2/config。 几处判断: - **判活不能比字符串**。库里 timestamptz 形如 `2026-08-07 13:42:50+00`(空格分隔), toISOString() 是 `...T13:42:44.000Z`(T 分隔),字典序空格 < 'T',同一天的心跳永远 小于阈值 —— 所有判题机都会显示离线。实测确实复现(dashboard 说 1 台在线、列表却 两台全 abnormal),已改为 Date.parse 后比较。 - 删指定的孤儿用例时**先确认它确实是孤儿**。旧后端不校验,一个手抖的 id 就能删掉在用 题目的测试数据,而测试数据没有别处备份。 - 图片上传的文件名完全由服务端生成,不带用户提供的任何一段;另加 10MB 上限 —— 旧后端靠 nginx 兜,但机房那台机器盘写满之后判题也会一起挂。 - 停用判题机后不再 process_pending_task():任务在 BullMQ 里排着,worker 恢复自己接着 消费,不存在旧自研分发器那种「没有新提交就一直 waiting」的问题。 - dashboard 不再下发 env.FORCE_HTTPS / STATIC_CDN_HOST,前端从未读过。 实测:学生 403;配置读写回读 + oj 侧 /site 同步生效 + 还原;判题机列表带 token、 状态判定正确(一台 normal 一台 abnormal,与 dashboard 计数一致);删不存在 404; 删非孤儿用例 404;随机点名缺班级号 400。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,11 +21,13 @@ import type {
|
||||
} from "utils/types"
|
||||
|
||||
export function getBaseInfo() {
|
||||
return http.get("admin/dashboard_info")
|
||||
return legacyResponse(api2.get("admin/dashboard"))
|
||||
}
|
||||
|
||||
export function randomUser10(classroom: string) {
|
||||
return http.get("admin/random_user", { params: { classroom } })
|
||||
return legacyResponse(
|
||||
api2.get("admin/random-usernames", { params: { classroom } }),
|
||||
)
|
||||
}
|
||||
|
||||
export async function getProblemList(
|
||||
@@ -185,11 +187,14 @@ export function getContestList(offset = 0, limit = 10, keyword: string) {
|
||||
export async function uploadImage(file: File): Promise<string> {
|
||||
const form = new window.FormData()
|
||||
form.append("image", file)
|
||||
// 该端点不走 { error, data } 信封,直接返回上传结果
|
||||
const res = (await http.post("admin/upload_image", form, {
|
||||
const res = await api2.post<{
|
||||
success: boolean
|
||||
filePath: string
|
||||
msg: string
|
||||
}>("admin/upload-image", form, {
|
||||
headers: { "content-type": "multipart/form-data" },
|
||||
})) as unknown as { success: boolean; file_path: string; msg: "Success" }
|
||||
return res.success ? res.file_path : ""
|
||||
})
|
||||
return res.data.success ? res.data.filePath : ""
|
||||
}
|
||||
|
||||
// 上传测试用例;SQL 题的压缩包是 1.sql..N.sql(每个文件一个测试点的建表+数据脚本)
|
||||
@@ -268,27 +273,38 @@ export function addProblemForContest(
|
||||
}
|
||||
|
||||
export function getWebsite() {
|
||||
return http.get<WebsiteConfig>("admin/website")
|
||||
return legacyResponse<WebsiteConfig>(api2.get("admin/website"))
|
||||
}
|
||||
|
||||
export function editWebsite(data: WebsiteConfig) {
|
||||
return http.post("admin/website", data)
|
||||
return api2.post("admin/website", {
|
||||
websiteBaseUrl: data.website_base_url,
|
||||
websiteName: data.website_name,
|
||||
websiteNameShortcut: data.website_name_shortcut,
|
||||
websiteFooter: data.website_footer,
|
||||
allowRegister: data.allow_register,
|
||||
submissionListShowAll: data.submission_list_show_all,
|
||||
classList: data.class_list,
|
||||
enableMaxkb: data.enable_maxkb,
|
||||
})
|
||||
}
|
||||
|
||||
export function listInvalidTestcases() {
|
||||
return http.get("admin/prune_test_case")
|
||||
return legacyResponse(api2.get("admin/orphan-test-cases"))
|
||||
}
|
||||
|
||||
export function pruneInvalidTestcases(id?: string) {
|
||||
return http.delete("admin/prune_test_case", { params: { id } })
|
||||
return api2.delete("admin/orphan-test-cases", { params: { id } })
|
||||
}
|
||||
|
||||
export function getJudgeServer() {
|
||||
return http.get<{ token: string; servers: Server[] }>("admin/judge_server")
|
||||
return legacyResponse<{ token: string; servers: Server[] }>(
|
||||
api2.get("admin/judge-servers"),
|
||||
)
|
||||
}
|
||||
|
||||
export function deleteJudgeServer(hostname: string) {
|
||||
return http.delete("admin/judge_server", { params: { hostname } })
|
||||
return api2.delete(`admin/judge-servers/${encodeURIComponent(hostname)}`)
|
||||
}
|
||||
|
||||
export function getAnnouncementList(offset = 0, limit = 10) {
|
||||
|
||||
@@ -517,8 +517,10 @@ export interface ConfigUpdate extends WebSocketMessage {
|
||||
*/
|
||||
class ConfigWebSocket extends BaseWebSocket<ConfigUpdate> {
|
||||
constructor() {
|
||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
||||
super({
|
||||
path: "config",
|
||||
url: `${protocol}//${window.location.host}/ws2/config`,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user