- 流程图评到 A 或 S,记进所有已加入、且包含这道题的题单(和判题那一路同一个口径,不看从哪个 入口交的),徽章和题单类成就照样推送。原来题单进度只在判代码时记,流程图作业在题单里永远是 「没做」。流程图提交的 id 进不了 problemset_submission(外键指向代码提交表),所以只写 progress_detail —— 解锁旧提交的那道闸看的也是它 - 课堂条(/me/class-activity)的 myStatus:流程图 A / S 也算 accepted,评完的非 A/S 算 tried - 题单防抄闸门补到流程图上:加入题单之前画的图,对本人藏起来(列表 showLink、详情 404、 current / history 不数它),规则和代码提交同一份(problemSetJoinTimes 挪进 services/problemset)。 原来加入之前画到 A 的那张图点一下就能载回画布、交上去就是 A - 新增 GET /problems/:id/flowchart/scores:一次给出自己评完的每一次的分数和等级,外加被藏起来 的次数(原来 history 一次只给一条,看分数走势得一页一页翻) - 「A/S 算过关」和按分数定等级收进 flowchart/grade.ts;判题那边的记账 + 通知抽成 recordSolvedAndNotify,两条路共用 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
948e9a734d
commit
3fe3caa944
8 files changed
+280
-145
No files matched your search
@@ -10,7 +10,7 @@ import {
|
||||
type SubmissionListItem,
|
||||
type SubmissionTrace,
|
||||
} from "@oj2/contract"
|
||||
import { and, count, desc, eq, gt, inArray, isNull, or, sql, type SQL } from "drizzle-orm"
|
||||
import { and, count, desc, eq, inArray, isNull, sql, type SQL } from "drizzle-orm"
|
||||
import { Hono } from "hono"
|
||||
|
||||
import { optionalAuth, requireAuth, requireSuperAdmin } from "../auth/middleware"
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "../services/contest"
|
||||
import { CodeFormatError, formatCode } from "../services/format-code"
|
||||
import { getBooleanOption } from "../services/options"
|
||||
import { problemSetJoinTimes } from "../services/problemset"
|
||||
import { consumeToken } from "../services/throttling"
|
||||
import { dayStart } from "../time"
|
||||
import { asFilterValue, asRecord, isAdminRole, queryInteger } from "./helpers"
|
||||
@@ -248,53 +249,6 @@ submissionRoutes.post("/code/format", requireAuth, async (c) => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 题单防作弊闸门:查出这些题目里,哪些题的旧提交要对该用户藏起来,返回 problemId → 加入时间。
|
||||
*
|
||||
* 对齐旧后端 `submission/serializers.py:12` 的 `bulk_fetch_problemset_progress`。学生加入含
|
||||
* 某道题的题单后,他在加入之前留下的 AC 代码还摆在提交列表里,复制粘贴就能把题单刷完。
|
||||
* 备份快照里 1734 人次、188 名学生进过这个窗口(占已解题次的 22.5%),不是边角情况。
|
||||
*
|
||||
* 解锁的三条路全写在 where 里,任一成立就查不出来、也就不遮挡:
|
||||
* - 已经在题单里做出这道题(progress_detail 里有这道题的 key)
|
||||
* - 题单过了截止时间(end_time;为空表示不设期限,只能靠做出来解锁)
|
||||
* - 题单被归档(status 不是 active)
|
||||
*
|
||||
* 一道题可能同时落在多个已加入的题单里,取最晚的 join_time——「存在任一题单要求遮挡就遮挡」
|
||||
* 等价于「提交时间早于最晚的那次加入」。旧后端这里用 `.first()` 取任意一条,一题多题单时
|
||||
* 行为不确定,换成聚合顺手定死。
|
||||
*/
|
||||
async function problemSetJoinTimes(userId: number, problemIds: number[]) {
|
||||
const joinTimes = new Map<number, string>()
|
||||
if (problemIds.length === 0) return joinTimes
|
||||
const rows = await db
|
||||
.select({
|
||||
problemId: schema.problemsetProblem.problemId,
|
||||
// 聚合表达式不走列的类型映射,但 OID 还是 1184 —— db/index.ts 给这个 OID 挂了
|
||||
// 「转成 ISO 8601」的 parser,所以这里拿到的和 `mode:"string"` 的列同形状。
|
||||
// 原来那个 `::text` 要撤掉:它的 OID 是 25、绕过那个 parser,反而会变成 PG 文本。
|
||||
joinTime: sql<string>`max(${schema.problemsetProgress.joinTime})`,
|
||||
})
|
||||
.from(schema.problemsetProgress)
|
||||
.innerJoin(schema.problemset, eq(schema.problemset.id, schema.problemsetProgress.problemsetId))
|
||||
.innerJoin(
|
||||
schema.problemsetProblem,
|
||||
eq(schema.problemsetProblem.problemsetId, schema.problemset.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.problemsetProgress.userId, userId),
|
||||
inArray(schema.problemsetProblem.problemId, problemIds),
|
||||
eq(schema.problemset.status, "active"),
|
||||
or(isNull(schema.problemset.endTime), gt(schema.problemset.endTime, sql`now()`)),
|
||||
sql`not jsonb_exists(${schema.problemsetProgress.progressDetail}, ${schema.problemsetProblem.problemId}::text)`,
|
||||
),
|
||||
)
|
||||
.groupBy(schema.problemsetProblem.problemId)
|
||||
for (const row of rows) joinTimes.set(row.problemId, row.joinTime)
|
||||
return joinTimes
|
||||
}
|
||||
|
||||
// 参数按「实际用到的字段」声明,而不是整行 $inferSelect:列表接口只 select 需要的列,
|
||||
// 传不进完整行。完整行在结构上满足这两个窄类型,详情接口照旧调用不受影响。
|
||||
function canViewSubmission(
|
||||
|
||||
Reference in new issue
Block a user