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

+42
View File
@@ -8,6 +8,7 @@ import {
type SubmissionDetail,
type SubmissionList,
type SubmissionListItem,
type SubmissionTrace,
} from "@oj2/contract"
import {
and,
@@ -59,6 +60,38 @@ function objectValue(value: unknown): Record<string, unknown> {
: {}
}
/**
* 落编辑过程信号。**失败只记日志、不影响提交** —— 这是附带的统计数据,
* 提交已经进库了,不能因为它回一个 500 让学生以为没交上。
*
* `since_prev_ms` 在同一条 INSERT 里用子查询算,排掉刚插进去的这条自己;
* 两次提交并发到达时也各自取到的是对方之外的最近一条。这道题第一次提交时
* `max()` 为 null,列就是 null。
*/
async function saveTrace(
submissionId: string,
userId: number,
problemId: number,
createTime: string,
trace: SubmissionTrace,
) {
try {
await db.insert(schema.submissionTrace).values({
submissionId,
...trace,
sincePrevMs: sql`(
select (extract(epoch from ${createTime}::timestamptz - max(${schema.submission.createTime})) * 1000)::bigint
from ${schema.submission}
where ${schema.submission.userId} = ${userId}
and ${schema.submission.problemId} = ${problemId}
and ${schema.submission.id} <> ${submissionId}
)`,
})
} catch (error) {
console.error("Failed to record submission trace", error)
}
}
submissionRoutes.post("/submissions", requireAuth, async (c) => {
const parsed = createSubmissionRequestSchema.safeParse(
await c.req.json().catch(() => null),
@@ -168,6 +201,15 @@ submissionRoutes.post("/submissions", requireAuth, async (c) => {
contestId,
})
if (parsed.data.trace)
await saveTrace(
submissionId,
user.id,
problem.id,
createTime,
parsed.data.trace,
)
try {
await judgeQueue.add(
"judge",