feat(比赛): 榜单一次给全,带名次变化和每题谁最先做对;期中、期末考试中学生看不到排名;老师能看全班情况
- GET /contests/:id/scoreboard 代替 /rank:整张榜、5 分钟前的名次、每题做对 / 做过人数和学生里最先做对的人(不再用 is_first_ac,老师赛中试做会占掉它) - 期中、期末进行中:学生只拿到自己那一行,人数清零、「全班最先」也不给,考完一起公布 - GET /contests/:id/class-view(老师):真名、每人最后一次交、没进来的人 —— 比赛不绑班,来了一半以上的班才算这场的班 - 比赛列表带题数、参赛人数、我的名次和做对数,能筛「我参加过的」「期中 · 期末」 - PUT /admin/contests/:id/problems/order:比赛题按顺序重编 1、2、3,比赛结束后不让动 - 后台「审核」列表接口去掉,并进前台老师的成绩页;「看过了」的写接口留着 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4ef2794552
commit
b94e028ff4
10 files changed
+528
-740
No files matched your search
@@ -2,7 +2,6 @@ import {
|
||||
createContestRequestSchema,
|
||||
updateAcmHelperRequestSchema,
|
||||
updateContestRequestSchema,
|
||||
type AcmHelperItem,
|
||||
type AdminContest,
|
||||
type AdminContestList,
|
||||
} from "@oj2/contract"
|
||||
@@ -268,60 +267,9 @@ adminContestRoutes.post("/contests/:id/clone", requireTeacher, async (c) => {
|
||||
return success(c, await serialize(row!), 201)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- ACM 赛后核查
|
||||
|
||||
adminContestRoutes.get("/contests/:id/acm-helper", requireTeacher, async (c) => {
|
||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||
// 不卡 visible:赛后核查恰恰常发生在比赛已经收起来之后,而同一场比赛的
|
||||
// PUT acm-helper 从来不卡这一条 —— 卡着就成了「标记还能改、页面打不开」
|
||||
const [contest] = await db.select().from(schema.contest).where(eq(schema.contest.id, id)).limit(1)
|
||||
if (!contest || !ownedBy(c.get("user")!, contest)) {
|
||||
return failure(c, 404, "contest-not-found", "Contest does not exist")
|
||||
}
|
||||
|
||||
const [problems, ranks] = await Promise.all([
|
||||
db
|
||||
.select({ id: schema.problem.id, displayId: schema.problem.displayId })
|
||||
.from(schema.problem)
|
||||
.where(eq(schema.problem.contestId, id)),
|
||||
db
|
||||
.select({
|
||||
id: schema.acmContestRank.id,
|
||||
username: schema.user.username,
|
||||
realName: schema.userProfile.realName,
|
||||
submissionInfo: schema.acmContestRank.submissionInfo,
|
||||
acceptedNumber: schema.acmContestRank.acceptedNumber,
|
||||
})
|
||||
.from(schema.acmContestRank)
|
||||
.innerJoin(schema.user, eq(schema.acmContestRank.userId, schema.user.id))
|
||||
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
|
||||
.where(eq(schema.acmContestRank.contestId, id)),
|
||||
])
|
||||
const displayIds = new Map(problems.map((problem) => [String(problem.id), problem.displayId]))
|
||||
|
||||
const results = []
|
||||
for (const rank of ranks) {
|
||||
if (rank.acceptedNumber <= 0) continue
|
||||
for (const [problemId, info] of Object.entries(rank.submissionInfo)) {
|
||||
if (info.is_ac !== true) continue
|
||||
results.push({
|
||||
id: rank.id,
|
||||
username: rank.username,
|
||||
// 真名在这里是**有意下发**的:核查页就是老师对着名单一个个确认谁抄了。
|
||||
// 接口已由 requireTeacher + ownedBy 双重把关。
|
||||
realName: rank.realName,
|
||||
problemId,
|
||||
problemDisplayId: displayIds.get(problemId) ?? problemId,
|
||||
acInfo: info,
|
||||
checked: info.checked === true,
|
||||
_acTime: typeof info.ac_time === "number" ? info.ac_time : 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
// 按 AC 用时倒序:最后才做出来的排前面,那是最值得看的
|
||||
results.sort((left, right) => right._acTime - left._acTime)
|
||||
return success(c, results.map(({ _acTime, ...item }) => item) satisfies AcmHelperItem[])
|
||||
})
|
||||
// ---------------------------------------------------------------- 赛后核查
|
||||
// 「看过了」的标记。列表原来是后台单独一页(审核),2026-10 并进前台比赛页老师的「成绩」,
|
||||
// 数据从 GET /contests/:id/class-view 来,这里只剩写标记
|
||||
|
||||
adminContestRoutes.put("/contests/:id/acm-helper", requireTeacher, async (c) => {
|
||||
const contestId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
addContestProblemRequestSchema,
|
||||
reorderContestProblemsRequestSchema,
|
||||
astCheckRequestSchema,
|
||||
generateTestInputsRequestSchema,
|
||||
isSqlProblem,
|
||||
@@ -797,6 +798,61 @@ adminProblemRoutes.post(
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* 比赛题按给定顺序重新编号 1、2、3…(新建比赛页拖动排序用)。
|
||||
* 榜单、提交、草稿都按题目 id 记,改编号不影响已经交的东西;比赛结束后就不让动了,
|
||||
* 免得赛后对着「第 3 题」讲评时题号变了。
|
||||
* 唯一约束是 (_id, contest_id),直接改会和还没改的那道撞,所以先统一挪到临时编号再写回来
|
||||
*/
|
||||
adminProblemRoutes.put(
|
||||
"/contests/:contestId/problems/order",
|
||||
requireProblemPermission,
|
||||
async (c) => {
|
||||
const contestId = queryInteger(c.req.param("contestId"), 0, { min: 1 })
|
||||
const parsed = await parseBody(c, reorderContestProblemsRequestSchema)
|
||||
if (!parsed.success) return parsed.response
|
||||
const [contest] = await db
|
||||
.select()
|
||||
.from(schema.contest)
|
||||
.where(eq(schema.contest.id, contestId))
|
||||
.limit(1)
|
||||
const user = c.get("user")!
|
||||
if (!contest || (user.adminType !== "Super Admin" && contest.createdById !== user.id)) {
|
||||
return failure(c, 404, "contest-not-found", "Contest does not exist")
|
||||
}
|
||||
if (contestStatus(contest) === "-1")
|
||||
return failure(c, 409, "contest-ended", "比赛已经结束,不能再改题号")
|
||||
const existing = await db
|
||||
.select({ id: schema.problem.id })
|
||||
.from(schema.problem)
|
||||
.where(eq(schema.problem.contestId, contestId))
|
||||
const ids = parsed.data.problemIds
|
||||
const all = new Set(existing.map((row) => row.id))
|
||||
if (
|
||||
new Set(ids).size !== ids.length ||
|
||||
ids.length !== all.size ||
|
||||
ids.some((id) => !all.has(id))
|
||||
) {
|
||||
return failure(c, 400, "problem-list-mismatch", "题目列表和比赛里的对不上,请刷新后再排")
|
||||
}
|
||||
await db.transaction(async (tx) => {
|
||||
for (const id of ids) {
|
||||
await tx
|
||||
.update(schema.problem)
|
||||
.set({ displayId: `__${id}` })
|
||||
.where(eq(schema.problem.id, id))
|
||||
}
|
||||
for (const [index, id] of ids.entries()) {
|
||||
await tx
|
||||
.update(schema.problem)
|
||||
.set({ displayId: String(index + 1) })
|
||||
.where(eq(schema.problem.id, id))
|
||||
}
|
||||
})
|
||||
return success(c, null)
|
||||
},
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------- 测试用例
|
||||
|
||||
adminProblemRoutes.post("/test-cases", requireProblemPermission, async (c) => {
|
||||
|
||||
Reference in new issue
Block a user