feat(提交): 采集编辑过程信号,落进 submission_trace
Deploy / deploy (push) Has been cancelled

AI 时代 OJ 设计的第 1 步:给「可信 AC」和学情分析攒数据,本身不判任何事。
- 契约:提交请求加可选的 trace(活跃时长、键入/粘贴/删除字符数、切后台次数等,
  只有计数、不含按键内容);写成 .optional().catch(undefined),坏了就当没带,
  不让附带数据把提交挡成 400
- 迁移 0016 建 submission_trace,与 submission 一对一、CASCADE;since_prev_ms
  由服务端在同一条 INSERT 里算(排掉自身),bigint —— 实测已有 44 天的间隔,int4 装不下
- 后端写 trace 失败只记日志,不影响提交
- 前端 oj/problem/utils/editTrace.ts 是模块单例(扩展对象不能过 Pinia 的响应式代理),
  只数带 userEvent 的事务:格式化回写 / 载入草稿 / 协作对方的改动天然排除;
  closeBrackets 越过右括号时是原样替换,按 no-op 跳过。比赛编辑器同样挂上
- 实跑:后端四种请求、前端浏览器里键入/粘贴/删除/setCode 回写/切后台/提交后清零,
  计数与预期逐项一致

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5 committed 2026-09-19 05:46:55 -06:00
1 parent 20a6ddc79c
commit a0ef204bd2
12 files changed
+4152

No files matched your search

@@ -7,6 +7,7 @@ import CodeEditor from "shared/components/CodeEditor.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import storage from "utils/storage"
import type { LANGUAGE } from "utils/types"
import { beginEditTrace, editTraceExtensions } from "oj/problem/utils/editTrace"
import Form from "./Form.vue"
const route = useRoute()
@@ -34,6 +35,10 @@ onMounted(() => {
problem.value!.template[codeStore.code.language] ||
SOURCES[codeStore.code.language],
)
beginEditTrace(
`problem_${problem.value!._id}_contest_${contestID}`,
codeStore.code.value.length,
)
})
const changeCode = (v: string) => {
@@ -58,6 +63,7 @@ const changeLanguage = (v: LANGUAGE) => {
v-model:value="codeStore.code.value"
:language="codeStore.code.language"
:height="editorHeight"
:extra-extensions="editTraceExtensions"
@update:model-value="changeCode"
/>
</n-flex>
@@ -8,6 +8,7 @@ import SyncCodeEditor from "shared/components/SyncCodeEditor.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import storage from "utils/storage"
import type { LANGUAGE } from "utils/types"
import { beginEditTrace, editTraceExtensions } from "oj/problem/utils/editTrace"
import Form from "./Form.vue"
const FlowchartEditor = defineAsyncComponent(
@@ -98,6 +99,11 @@ function loadCode() {
problem.value!.template[codeStore.code.language] ||
SOURCES[codeStore.code.language],
)
// 换了题才重新计数,同一道题重复 loadCode(协作结束读回草稿)是接着记
beginEditTrace(
`problem_${problem.value!._id}_contest_${contestID}`,
codeStore.code.value.length,
)
}
onMounted(loadCode)
@@ -151,6 +157,7 @@ provide("flowchartEditorRef", flowchartEditorRef)
:language="codeStore.code.language"
:problem-id="problem!._id"
:height="editorHeight"
:extra-extensions="editTraceExtensions"
@update:model-value="changeCode"
/>
</n-flex>
@@ -12,6 +12,8 @@ import SubmissionResult from "./SubmissionResult.vue"
import { getSubmitButtonState } from "./submitButtonState"
import { useBreakpoints } from "shared/composables/breakpoints"
import { useUserStore } from "shared/store/user"
import { useCollabStore } from "shared/store/collab"
import { restartEditTrace, snapshotEditTrace } from "oj/problem/utils/editTrace"
import {
checkPythonSyntax,
prefetchPythonSyntaxChecker,
@@ -24,6 +26,7 @@ const ProblemReaction = defineAsyncComponent(
// ==================== 基础状态 ====================
const userStore = useUserStore()
const collabStore = useCollabStore()
const codeStore = useCodeStore()
const problemStore = useProblemStore()
const { problem } = storeToRefs(problemStore)
@@ -147,6 +150,11 @@ async function submit() {
problemId: problem.value!.id,
language: codeStore.code.language,
code: codeStore.code.value,
// 编辑过程信号,见 utils/editTrace.ts。协作的判断和 ProblemEditor 的 collabHere 同一个口径
trace: snapshotEditTrace(
collabStore.room !== null &&
collabStore.room.problemId === problem.value!._id,
),
}
if (contestID) {
data.contestId = parseInt(contestID)
@@ -161,6 +169,8 @@ async function submit() {
try {
const res = await submitCode(data)
console.log(`[Submit] 代码已提交: ID=${res.submissionId}`)
// 交上了才清零;被限流 / 网络失败的话这一段接着记,下次提交一起报
restartEditTrace(codeStore.code.value.length)
// 3. 启动冷却 + 监控
startCooldown()