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:
2026-08-07 06:48:05 -06:00
co-authored by Claude Opus 5
parent cb6d42363b
commit 836d97847e
12 changed files with 787 additions and 233 deletions
@@ -46,9 +46,9 @@
</div>
<div class="stat-item">
<n-text>正确率</n-text>
<n-gradient-text type="warning" font-size="28">{{
count.rate
}}</n-gradient-text>
<n-gradient-text type="warning" font-size="28"
>{{ count.rate }}%</n-gradient-text
>
</div>
<template v-if="person.count > 0">
<div class="stat-item">
@@ -140,9 +140,9 @@
style="font-size: 20px"
@close="hideStudent(item.username)"
>
{{ item.real_name }}
{{ item.realName }}
</n-tag>
<span v-else style="font-size: 24px">{{ item.real_name }}</span>
<span v-else style="font-size: 24px">{{ item.realName }}</span>
</template>
</n-flex>
</n-tab-pane>
@@ -158,7 +158,7 @@ import { Doughnut } from "vue-chartjs"
import { Chart as ChartJS, ArcElement, Title, Tooltip, Legend } from "chart.js"
import { NButton, NFlex, NText, type DataTableRowKey } from "naive-ui"
import { JUDGE_STATUS } from "utils/constants"
import type { SUBMISSION_RESULT } from "utils/types"
import type { SubmissionStatisticsUser, UnacceptedStudent } from "@oj2/contract"
// 注册 Chart.js 组件
ChartJS.register(ArcElement, Title, Tooltip, Legend)
@@ -182,12 +182,12 @@ function openSubmission(id: string) {
window.open(`/submission/${id}`, "_blank", "noopener")
}
const columns: DataTableColumn<UserStatistic>[] = [
const columns: DataTableColumn<SubmissionStatisticsUser>[] = [
{
type: "expand",
renderExpand: (row) => {
return h(NFlex, { size: "small", wrap: true }, () =>
row.submission_items.map((item) =>
row.submissionItems.map((item) =>
h(
NButton,
{
@@ -207,9 +207,14 @@ const columns: DataTableColumn<UserStatistic>[] = [
},
},
{ title: "用户", key: "username" },
{ title: "提交数", key: "submission_count" },
{ title: "已解决", key: "accepted_count" },
{ title: "正确率", key: "correct_rate" },
{ title: "提交数", key: "submissionCount" },
{ title: "已解决", key: "acceptedCount" },
// 新后端返回的是数值,百分号在这里补 —— 旧后端直接返回 "85.5%" 字符串
{
title: "正确率",
key: "correctRate",
render: (row) => `${row.correctRate}%`,
},
]
const query = reactive({
@@ -230,24 +235,8 @@ const person = reactive({
const route = useRoute()
const router = useRouter()
interface UserStatistic {
username: string
submission_count: number
accepted_count: number
correct_rate: string
submission_items: Array<{
id: string
result: SUBMISSION_RESULT
}>
}
interface UnacceptedItem {
username: string
real_name: string
}
const list = ref<UserStatistic[]>([])
const listUnaccepted = ref<UnacceptedItem[]>([])
const list = ref<SubmissionStatisticsUser[]>([])
const listUnaccepted = ref<UnacceptedStudent[]>([])
const expandedRowKeys = ref<DataTableRowKey[]>([])
const HIDE_DURATION = 2 * 60 * 60 * 1000
@@ -432,16 +421,16 @@ async function handleStatistics() {
query.problem,
query.username,
)
count.total = res.data.submission_count
count.accepted = res.data.accepted_count
count.rate = res.data.correct_rate
count.total = res.data.submissionCount
count.accepted = res.data.acceptedCount
count.rate = res.data.correctRate
list.value = res.data.data
listUnaccepted.value = res.data.data_unaccepted
person.count = res.data.person_count
person.rate = res.data.person_rate
listUnaccepted.value = res.data.dataUnaccepted
person.count = res.data.personCount
person.rate = res.data.personRate
}
function rowKey(row: UserStatistic): DataTableRowKey {
function rowKey(row: SubmissionStatisticsUser): DataTableRowKey {
return row.username
}
@@ -449,7 +438,7 @@ function updateExpandedRowKeys(keys: DataTableRowKey[]) {
expandedRowKeys.value = keys.slice(-1)
}
function rowProps(row: UserStatistic) {
function rowProps(row: SubmissionStatisticsUser) {
return {
style: "cursor: pointer;",
onClick: () => {