feat(提交): 采集编辑过程信号,落进 submission_trace
Some checks failed
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:
2026-09-19 05:46:55 -06:00
parent 20a6ddc79c
commit a0ef204bd2
12 changed files with 4152 additions and 0 deletions

View File

@@ -953,6 +953,44 @@ export const submission = pgTable(
],
)
/**
* 提交时附带的编辑过程信号,和 submission 一对一。字段含义见契约的
* `submissionTraceSchema`,这里只记表本身的取舍:
*
* - **没有行 ≠ 可疑。** 2026-09 之前的全部历史提交、刷新过页面的、老版本前端交的
* 都没有 trace用它的地方一律把「缺失」当「无数据」。
* - 类型化的列而不是一个 jsonb「可信 AC」和学情热力图要在 SQL 里按这些值筛、聚合。
* - `since_prev_ms` 是唯一由**服务端**算的一列(距同一用户同一道题上一次提交),
* 客户端伪造不了;这道题的第一次提交为 null。
* - CASCADE 挂在 submission 上、不挂 user它是提交的附属提交没了它没有意义
* 人是谁顺着 submission 就能查到。同表的 message / problemset_submission 也是这一档。
*/
export const submissionTrace = pgTable(
"submission_trace",
{
submissionId: text("submission_id").primaryKey().notNull(),
activeMs: integer("active_ms").notNull(),
sinceOpenMs: integer("since_open_ms").notNull(),
typedChars: integer("typed_chars").notNull(),
pastedChars: integer("pasted_chars").notNull(),
pasteCount: integer("paste_count").notNull(),
maxPaste: integer("max_paste").notNull(),
deletedChars: integer("deleted_chars").notNull(),
blurCount: integer("blur_count").notNull(),
initialLen: integer("initial_len").notNull(),
collab: boolean().notNull(),
// bigintint4 的毫秒数只够 24.8 天,隔一个假期回来重交就溢出了
sincePrevMs: bigint("since_prev_ms", { mode: "number" }),
},
(table) => [
foreignKey({
columns: [table.submissionId],
foreignColumns: [submission.id],
name: "submission_trace_submission_id_fk_submission_id",
}).onDelete("cascade"),
],
)
export const tutorial = pgTable(
"tutorial",
{