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:
67
apps/api/src/services/word-frequency.ts
Normal file
67
apps/api/src/services/word-frequency.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Jieba } from "@node-rs/jieba"
|
||||
import { dict } from "@node-rs/jieba/dict"
|
||||
|
||||
/**
|
||||
* 流程图评语词云的分词。对齐旧后端 `flowchart/views/admin.py` 的
|
||||
* STOPWORDS / CUSTOM_WORDS / _build_word_frequencies 三段。
|
||||
*
|
||||
* 停用词表逐词照搬,改一个词就会让词云和旧后端对不上 —— 教师是拿它横向比不同班级的,
|
||||
* 词表变了历史截图就没法比。
|
||||
*/
|
||||
const STOPWORDS = new Set(
|
||||
(
|
||||
"的 了 是 在 和 有 就 不 也 都 要 会 这 那 到 说 上 为 与 及 等 " +
|
||||
"把 被 从 而 所 但 如 又 或 很 更 还 让 对 已 向 只 能 以 中 可以 " +
|
||||
"可能 需要 没有 使用 进行 注意 建议 应该 考虑 整体 基本 部分 " +
|
||||
"一个 一些 一下 一定 一种 这个 所有 其他 比较 存在 明确 " +
|
||||
"正确 良好 清晰 合理 较好 不错 符合 标准 "
|
||||
)
|
||||
.split(" ")
|
||||
.filter(Boolean),
|
||||
)
|
||||
|
||||
const CUSTOM_WORDS = [
|
||||
"循环结构", "条件判断", "判断条件", "结束条件", "循环条件",
|
||||
"异常处理", "边界条件", "输入输出", "输入验证", "开始结束",
|
||||
"结束节点", "开始节点", "判断节点", "流程走向", "逻辑错误",
|
||||
"逻辑缺陷", "逻辑不清", "缺少分支", "缺少步骤", "缺少判断",
|
||||
"缺少循环", "死循环", "无限循环", "循环出口", "循环体",
|
||||
"条件分支", "分支结构", "分支不全", "分支缺失", "符号使用",
|
||||
"符号不规范", "连线混乱", "变量初始化", "赋值操作", "累加操作",
|
||||
"终止条件", "退出条件", "返回值",
|
||||
]
|
||||
|
||||
/**
|
||||
* 词典加载有一次性开销(约 100ms),放在模块级会拖慢 API 冷启动,
|
||||
* 而词云只有教师偶尔点一次。改成首次调用时才建。
|
||||
*/
|
||||
let instance: Jieba | null = null
|
||||
|
||||
function jieba() {
|
||||
if (instance) return instance
|
||||
const built = Jieba.withDict(dict)
|
||||
// 对应旧后端的 jieba.add_word(w, freq=9999)。
|
||||
// @node-rs/jieba@2 没有导出 insertWord/addWord,改用用户词典缓冲区,格式为「词 词频」。
|
||||
built.loadDict(
|
||||
Buffer.from(CUSTOM_WORDS.map((word) => `${word} 9999`).join("\n") + "\n"),
|
||||
)
|
||||
instance = built
|
||||
return built
|
||||
}
|
||||
|
||||
export function buildWordFrequencies(texts: string[], topN = 80) {
|
||||
const counter = new Map<string, number>()
|
||||
const cutter = jieba()
|
||||
for (const raw of texts) {
|
||||
const text = raw.replaceAll("【重点】", "")
|
||||
for (const token of cutter.cut(text)) {
|
||||
const word = token.trim()
|
||||
if (word.length < 2 || STOPWORDS.has(word)) continue
|
||||
counter.set(word, (counter.get(word) ?? 0) + 1)
|
||||
}
|
||||
}
|
||||
return [...counter]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, topN)
|
||||
.map(([word, count]) => ({ word, count }))
|
||||
}
|
||||
Reference in New Issue
Block a user