feat(统计): 统计接口补上每人每道题的每一次提交、流程图每人最好的一次,流程图也能一次查几道题
- 新增 GET /submissions/statistics/grid:范围内每个学生的每次提交(只含普通学生,最多 5000 条), 没选班级时附带各班花名册人数,统计页按「人 × 题」画方块串、按班级汇总 - /flowcharts/statistics 题号可以一次填几道,多返回 people(每人分数最高的那张) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
99c158498c
commit
f09da88ff7
8 files changed
+288
-1492
No files matched your search
@@ -8,6 +8,7 @@
|
||||
|
||||
import {
|
||||
type SubmissionStatistics,
|
||||
type SubmissionStatisticsGrid,
|
||||
type SubmissionStatisticsItems,
|
||||
type TodaySubmissionStatistics,
|
||||
} from "@oj2/contract"
|
||||
@@ -652,3 +653,134 @@ submissionStatisticsRoutes.get("/submissions/statistics/items", requireTeacher,
|
||||
truncated,
|
||||
} satisfies SubmissionStatisticsItems)
|
||||
})
|
||||
|
||||
/** 方块串一次最多给多少条。一个班一个月七八百条,5000 是全年级查一周的量 */
|
||||
const GRID_LIMIT = 5000
|
||||
|
||||
/**
|
||||
* 统计页的方块串:范围内每个学生的每一次提交(口径见契约 submissionStatisticsGridSchema)。
|
||||
* 用户名的匹配和统计接口同一套(ilike,填 `ks253` 圈一个班)。
|
||||
*/
|
||||
submissionStatisticsRoutes.get("/submissions/statistics/grid", requireTeacher, async (c) => {
|
||||
const scope = await statisticsScope(c)
|
||||
if (!scope.ok) return failure(c, scope.status, scope.code, scope.message)
|
||||
const filters = [...scope.filters]
|
||||
|
||||
const username = c.req.query("username")?.trim()
|
||||
if (username) {
|
||||
const ids = (await matchedUsers(username)).map((row) => row.id)
|
||||
filters.push(ids.length ? inArray(schema.submission.userId, ids) : sql`false`)
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: schema.submission.id,
|
||||
userId: schema.submission.userId,
|
||||
problemId: schema.submission.problemId,
|
||||
result: schema.submission.result,
|
||||
createTime: schema.submission.createTime,
|
||||
username: schema.user.username,
|
||||
className: schema.user.className,
|
||||
})
|
||||
.from(schema.submission)
|
||||
// 只要普通学生:老师试题的提交不该出现在「谁做了几次」里
|
||||
.innerJoin(schema.user, eq(schema.user.id, schema.submission.userId))
|
||||
.where(
|
||||
and(...filters, eq(schema.user.adminType, "Regular User"), eq(schema.user.isDisabled, false)),
|
||||
)
|
||||
.orderBy(desc(schema.submission.createTime))
|
||||
.limit(GRID_LIMIT + 1)
|
||||
const truncated = rows.length > GRID_LIMIT
|
||||
const kept = rows.slice(0, GRID_LIMIT).reverse()
|
||||
|
||||
// 题目:传了就按传的顺序,没传就按范围里第一次有人交的时间
|
||||
const requested = parseDisplayIds(c.req.query("problemDisplayId") ?? "")
|
||||
const problemIds = [...new Set(kept.map((row) => row.problemId))]
|
||||
const problems = problemIds.length
|
||||
? await db
|
||||
.select({
|
||||
id: schema.problem.id,
|
||||
displayId: schema.problem.displayId,
|
||||
title: schema.problem.title,
|
||||
})
|
||||
.from(schema.problem)
|
||||
.where(inArray(schema.problem.id, problemIds))
|
||||
: []
|
||||
const problemById = new Map(problems.map((row) => [row.id, row]))
|
||||
const ordered = requested.length
|
||||
? requested.flatMap((id) => {
|
||||
const hit = problems.find((row) => row.displayId.toLowerCase() === id.toLowerCase())
|
||||
return hit ? [hit] : []
|
||||
})
|
||||
: problemIds.map((id) => problemById.get(id)!).filter(Boolean)
|
||||
// 传了题号但这些题在范围里没人交:表头仍然要有它们
|
||||
if (requested.length && ordered.length < requested.length) {
|
||||
const missing = requested.filter(
|
||||
(id) => !ordered.some((row) => row.displayId.toLowerCase() === id.toLowerCase()),
|
||||
)
|
||||
const { ids } = await findPublicProblemsByDisplayIds(missing)
|
||||
if (ids.length) {
|
||||
const extra = await db
|
||||
.select({
|
||||
id: schema.problem.id,
|
||||
displayId: schema.problem.displayId,
|
||||
title: schema.problem.title,
|
||||
})
|
||||
.from(schema.problem)
|
||||
.where(inArray(schema.problem.id, ids))
|
||||
ordered.push(...extra)
|
||||
const rank = new Map(requested.map((id, i) => [id.toLowerCase(), i]))
|
||||
ordered.sort(
|
||||
(a, b) =>
|
||||
(rank.get(a.displayId.toLowerCase()) ?? 0) - (rank.get(b.displayId.toLowerCase()) ?? 0),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const byUser = new Map<number, SubmissionStatisticsGrid["rows"][number]>()
|
||||
for (const row of kept) {
|
||||
let entry = byUser.get(row.userId)
|
||||
if (!entry) {
|
||||
entry = {
|
||||
username: row.username,
|
||||
realName: stripClassPrefix(row.username, row.className),
|
||||
className: row.className,
|
||||
submissions: [],
|
||||
}
|
||||
byUser.set(row.userId, entry)
|
||||
}
|
||||
entry.submissions.push({
|
||||
id: row.id,
|
||||
problemDisplayId: problemById.get(row.problemId)?.displayId ?? "",
|
||||
result: row.result,
|
||||
createTime: row.createTime,
|
||||
})
|
||||
}
|
||||
|
||||
// 按班级汇总时要知道每个班多少人,「一道没交」才算得出来
|
||||
const classSizes: Record<string, number> = {}
|
||||
if (!username) {
|
||||
const names = [...new Set(kept.map((row) => row.className).filter((name) => name !== null))]
|
||||
if (names.length) {
|
||||
const sizes = await db
|
||||
.select({ className: schema.user.className, value: count() })
|
||||
.from(schema.user)
|
||||
.where(
|
||||
and(
|
||||
inArray(schema.user.className, names as string[]),
|
||||
eq(schema.user.adminType, "Regular User"),
|
||||
eq(schema.user.isDisabled, false),
|
||||
),
|
||||
)
|
||||
.groupBy(schema.user.className)
|
||||
for (const row of sizes) if (row.className) classSizes[row.className] = row.value
|
||||
}
|
||||
}
|
||||
|
||||
return success(c, {
|
||||
problems: ordered.map((row) => ({ problemDisplayId: row.displayId, title: row.title })),
|
||||
rows: [...byUser.values()],
|
||||
classSizes,
|
||||
truncated,
|
||||
} satisfies SubmissionStatisticsGrid)
|
||||
})
|
||||
Reference in new issue
Block a user