refactor: 一批名不副实的命名改掉(只动一端,不碰契约)

后端:
- getUserByToken → authenticateToken:它会删会话、续 TTL、记在线,不是纯读
- todayStart → dayStart(instant):本来就接受任意时刻,不只是「今天」
- objectValue → asRecord,删掉 4 份一模一样的私有拷贝和 `as toObject` 别名
- 报错文案 "does not exists" / "Already be a public problem" 改通顺

前端:
- utils/types 里 Contest / User / Problem 三个别名删掉:Contest 原来指后台
  比赛、契约里同名的却是学生侧。现在 Contest = 学生侧、AdminContest = 后台,
  后台用户表单叫 AdminUserForm,题目详情直接用 ProblemDetail
- SubmissionInfo → ContestSubmissionInfo,免得和 submission.info 撞名
- utoa / atou → compressToBase64 / decompressFromBase64(实际是 zlib + base64),
  encode / decode → base64EncodeUtf8 / base64DecodeUtf8
- filterResult → toProblemRow、toProblemListItem → toAdminProblemRow,
  行字段 submission / rate → submissionCount / acRate
- 组件:Modal → AddFromLibraryModal、Form → EditorToolbar、
  ContestEditor → BasicEditor(题单也在用它)、rank 的 Index → RankMedal、
  Name → UserNameCell、Password → PasswordCell、ContestType → ContestAccessTag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 16:08:21 +08:00
1 parent 572a818d49
commit 5ae830a7f9
62 files changed
+284 -320

No files matched your search

+4 -9
View File
@@ -54,12 +54,7 @@ import {
type AttemptRule,
} from "../services/throttling"
import { localTime, weekStart } from "../time"
import {
isTeacherOrAbove,
objectValue,
queryInteger,
sampleUser,
} from "./helpers"
import { isTeacherOrAbove, asRecord, queryInteger, sampleUser } from "./helpers"
export const accountRoutes = new Hono<AppEnv>()
@@ -632,8 +627,8 @@ accountRoutes.post(
.from(schema.userProfile)
.where(eq(schema.userProfile.userId, user.id))
.limit(1)
const status = objectValue(profile?.value)
const problems = objectValue(status.problems)
const status = asRecord(profile?.value)
const problems = asRecord(status.problems)
const ids = Object.keys(problems).map(Number).filter(Number.isInteger)
if (ids.length > 0) {
const rows = await db
@@ -649,7 +644,7 @@ accountRoutes.post(
rows.map((row) => [String(row.id), row.displayId]),
)
for (const [id, value] of Object.entries(problems)) {
const item = objectValue(value)
const item = asRecord(value)
const displayId = displayIds.get(id)
if (displayId) item._id = displayId
problems[id] = item
+2 -2
View File
@@ -11,7 +11,7 @@ import { Hono } from "hono"
import { requireAuth, type AppEnv } from "../auth/middleware"
import { db, schema } from "../db"
import { failure, parseBody, success } from "../http"
import { objectValue } from "./helpers"
import { asRecord } from "./helpers"
export const achievementRoutes = new Hono<AppEnv>()
@@ -73,7 +73,7 @@ achievementRoutes.get("/achievements", requireAuth, async (c) => {
.where(eq(schema.user.isDisabled, false)),
])
const unlocked = new Map(unlockedRows.map((row) => [row.achievementId, row]))
const metrics = objectValue(statRows[0]?.metrics)
const metrics = asRecord(statRows[0]?.metrics)
const active = activeRows[0]?.value ?? 0
const result = achievements.map((achievement) => {
const record = unlocked.get(achievement.id)
+2 -2
View File
@@ -25,7 +25,7 @@ import { publishConfigUpdate } from "../../events"
import { failure, parseBody, success } from "../../http"
import { sniffImageExtension } from "../../services/image"
import { getWebsiteOptions } from "../../services/options"
import { todayStart } from "../../time"
import { dayStart } from "../../time"
import { queryInteger } from "../helpers"
export const adminConfRoutes = new Hono<AppEnv>()
@@ -232,7 +232,7 @@ adminConfRoutes.get("/dashboard", requireSuperAdmin, async (c) => {
db
.select({ value: count() })
.from(schema.submission)
.where(gte(schema.submission.createTime, todayStart())),
.where(gte(schema.submission.createTime, dayStart())),
db
.select({ value: count() })
.from(schema.contest)
+9 -9
View File
@@ -48,7 +48,7 @@ import { config } from "../../config"
import { readFile } from "node:fs/promises"
import { resolve } from "node:path"
import { getTopReactions } from "../../services/reaction"
import { objectValue, queryInteger, sampleUser } from "../helpers"
import { asRecord, queryInteger, sampleUser } from "../helpers"
export const adminProblemRoutes = new Hono<AppEnv>()
@@ -205,7 +205,7 @@ async function serialize(row: ProblemRow) {
source: row.source,
submissionNumber: row.submissionNumber,
acceptedNumber: row.acceptedNumber,
statisticInfo: objectValue(row.statisticInfo),
statisticInfo: asRecord(row.statisticInfo),
contestId: row.contestId,
createdBy: sampleUser(
creator ?? { id: row.createdById, username: "" },
@@ -616,9 +616,9 @@ adminProblemRoutes.delete(
.where(eq(schema.problem.id, id))
.limit(1)
if (!existing)
return failure(c, 404, "problem-not-found", "Problem does not exists")
return failure(c, 404, "problem-not-found", "Problem does not exist")
if (!(await canEdit(c.get("user")!, existing))) {
return failure(c, 404, "problem-not-found", "Problem does not exists")
return failure(c, 404, "problem-not-found", "Problem does not exist")
}
return deleteProblem(c, id)
},
@@ -793,7 +793,7 @@ adminProblemRoutes.post(
return failure(c, 404, "problem-not-found", "Problem does not exist")
}
if (!problem.contestId || problem.isPublic) {
return failure(c, 409, "already-public", "Already be a public problem")
return failure(c, 409, "already-public", "Already a public problem")
}
const [duplicate] = await db
.select({ id: schema.problem.id })
@@ -936,9 +936,9 @@ adminProblemRoutes.get(
)
.limit(1)
if (!problem)
return failure(c, 404, "problem-not-found", "Problem does not exists")
return failure(c, 404, "problem-not-found", "Problem does not exist")
if (!(await canEdit(c.get("user")!, problem))) {
return failure(c, 404, "problem-not-found", "Problem does not exists")
return failure(c, 404, "problem-not-found", "Problem does not exist")
}
try {
const archive = await packTestCaseZip(problem.testCaseId)
@@ -973,9 +973,9 @@ adminProblemRoutes.get(
)
.limit(1)
if (!problem)
return failure(c, 404, "problem-not-found", "Problem does not exists")
return failure(c, 404, "problem-not-found", "Problem does not exist")
if (!(await canEdit(c.get("user")!, problem))) {
return failure(c, 404, "problem-not-found", "Problem does not exists")
return failure(c, 404, "problem-not-found", "Problem does not exist")
}
const info = await readInfo(problem.testCaseId)
if (!info)
+2 -2
View File
@@ -15,7 +15,7 @@ import { requireSuperAdmin, type AppEnv } from "../../auth/middleware"
import { db, schema } from "../../db"
import { failure, parseBody, success } from "../../http"
import { exerciseDataError } from "../../services/exercise"
import { objectValue, queryInteger, sampleUser } from "../helpers"
import { asRecord, queryInteger, sampleUser } from "../helpers"
export const adminTutorialRoutes = new Hono<AppEnv>()
@@ -156,7 +156,7 @@ function serializeExercise(row: typeof schema.exercise.$inferSelect) {
return {
id: row.id,
type: row.type,
data: objectValue(row.data),
data: asRecord(row.data),
order: row.order,
} satisfies AdminExercise
}
+2 -2
View File
@@ -55,7 +55,7 @@ import {
import {
countFailedSubmissions,
isTeacherOrAbove,
objectValue,
asRecord,
queryInteger,
} from "./helpers"
@@ -317,7 +317,7 @@ aiRoutes.get("/ai/pinned", requireAuth, async (c) => {
id: row.analysis.id,
provider: row.analysis.provider,
model: row.analysis.model,
data: objectValue(row.analysis.data),
data: asRecord(row.analysis.data),
analysis: row.analysis.analysis,
createTime: row.analysis.createTime,
isPinned: row.analysis.isPinned,
+3 -3
View File
@@ -24,7 +24,7 @@ import { requireAuth, requireSuperAdmin, type AppEnv } from "../auth/middleware"
import { db, schema } from "../db"
import { failure, parseBody, success } from "../http"
import { JudgeStatus } from "../judge/status"
import { objectValue, queryInteger, sampleUser } from "./helpers"
import { asRecord, queryInteger, sampleUser } from "./helpers"
export const contentRoutes = new Hono<AppEnv>()
@@ -167,7 +167,7 @@ contentRoutes.get("/messages", requireAuth, async (c) => {
// info / ip / contestId 三个字段不在 embeddedSubmissionSchema 里,故不传 ——
// 对齐旧后端 SubmissionSafeModelSerializer 的 exclude,这三个键不出现在响应中
language: submission.language,
statisticInfo: objectValue(submission.statisticInfo),
statisticInfo: asRecord(submission.statisticInfo),
// 展示用题号而非数字主键,站内信页面拿它拼 /problem/<题号>
problem: displayId,
showLink: true,
@@ -590,7 +590,7 @@ contentRoutes.get("/tutorials/:id/exercises", async (c) => {
({
id: row.id,
type: row.type,
data: objectValue(row.data),
data: asRecord(row.data),
order: row.order,
}) satisfies Exercise,
),
+5 -10
View File
@@ -43,12 +43,7 @@ import {
lockoutRemaining,
type AttemptRule,
} from "../services/throttling"
import {
objectValue,
publicTemplates,
queryInteger,
sampleUser,
} from "./helpers"
import { asRecord, publicTemplates, queryInteger, sampleUser } from "./helpers"
export const contestRoutes = new Hono<ContestEnv>()
@@ -223,11 +218,11 @@ async function contestProblemStatuses(userId: number | undefined) {
.from(schema.userProfile)
.where(eq(schema.userProfile.userId, userId))
.limit(1)
return objectValue(objectValue(profile?.status).contest_problems)
return asRecord(asRecord(profile?.status).contest_problems)
}
function myStatusOf(statuses: Record<string, unknown>, problemId: number) {
const status = objectValue(statuses[String(problemId)]).status
const status = asRecord(statuses[String(problemId)]).status
return typeof status === "number" ? status : null
}
@@ -353,7 +348,7 @@ contestRoutes.get(
prompt: row.problem.prompt,
submissionNumber: allowed ? row.problem.submissionNumber : 0,
acceptedNumber: allowed ? row.problem.acceptedNumber : 0,
statisticInfo: allowed ? objectValue(row.problem.statisticInfo) : {},
statisticInfo: allowed ? asRecord(row.problem.statisticInfo) : {},
contestId: contest.id,
tags: tags.get(row.problem.id) ?? [],
createdBy: sampleUser(row.user, row.realName),
@@ -365,7 +360,7 @@ contestRoutes.get(
mermaidCode: row.problem.allowFlowchart ? null : row.problem.mermaidCode,
flowchartData: row.problem.allowFlowchart
? null
: objectValue(row.problem.flowchartData),
: asRecord(row.problem.flowchartData),
flowchartHint: row.problem.flowchartHint,
sqlConfig: row.problem.sqlConfig,
sqlDisplay: row.problem.sqlDisplay,
+7 -9
View File
@@ -31,11 +31,11 @@ import { flowchartQueue } from "../queue"
import { getBooleanOption } from "../services/options"
import { consumeToken } from "../services/throttling"
import { buildWordFrequencies } from "../services/word-frequency"
import { todayStart } from "../time"
import { dayStart } from "../time"
import {
isAdminRole,
matchedUsers,
objectValue,
asRecord,
queryInteger,
rounded,
stripClassPrefix,
@@ -69,14 +69,14 @@ function flowchartData(
username,
problemId: flowchart.problemId,
mermaidCode: flowchart.mermaidCode,
flowchartData: objectValue(flowchart.flowchartData),
flowchartData: asRecord(flowchart.flowchartData),
status: flowchart.status,
createTime: flowchart.createTime,
aiScore: flowchart.aiScore,
aiGrade: flowchart.aiGrade,
aiFeedback: flowchart.aiFeedback,
aiSuggestions: flowchart.aiSuggestions,
aiCriteriaDetails: objectValue(flowchart.aiCriteriaDetails),
aiCriteriaDetails: asRecord(flowchart.aiCriteriaDetails),
aiProvider: flowchart.aiProvider,
aiModel: flowchart.aiModel,
processingTime: flowchart.processingTime,
@@ -269,9 +269,7 @@ flowchartRoutes.get("/flowcharts", requireAuth, async (c) => {
)
if (onlyMyself) filters.push(eq(schema.flowchartSubmission.userId, user.id))
if (c.req.query("today") === "1")
filters.push(
sql`${schema.flowchartSubmission.createTime} >= ${todayStart()}`,
)
filters.push(sql`${schema.flowchartSubmission.createTime} >= ${dayStart()}`)
if (["S", "A", "B", "C"].includes(grade ?? ""))
filters.push(eq(schema.flowchartSubmission.aiGrade, grade!))
const where = and(...filters)
@@ -520,8 +518,8 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => {
if (texts.length < WORDCLOUD_TEXT_LIMIT) texts.push(value)
}
for (const row of textRows) {
for (const [key, value] of Object.entries(objectValue(row.criteria))) {
const detail = objectValue(value)
for (const [key, value] of Object.entries(asRecord(row.criteria))) {
const detail = asRecord(value)
// 和上面那条聚合同一道闸:分数不是数字的项当没配过,满分和评语也都不收
if (typeof detail.score !== "number") continue
if (!criteriaMax.has(key)) {
+2 -2
View File
@@ -57,7 +57,7 @@ export function asFilterValue<T extends string | number>(
return value as T
}
export function objectValue(value: unknown): Record<string, unknown> {
export function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {}
@@ -96,7 +96,7 @@ export function isSuperAdmin(user: AuthUser | null | undefined) {
export function publicTemplates(value: unknown) {
const templates: Record<string, string> = {}
for (const [language, raw] of Object.entries(objectValue(value))) {
for (const [language, raw] of Object.entries(asRecord(value))) {
if (typeof raw !== "string") continue
const match = raw.match(/\/\/TEMPLATE BEGIN\n([\s\S]+?)\/\/TEMPLATE END/)
templates[language] = match?.[1] ?? ""
+11 -17
View File
@@ -29,26 +29,20 @@ import { db, schema } from "../db"
import { astRequirements } from "../judge/ast"
import { failure, success } from "../http"
import { JudgeStatus } from "../judge/status"
import { localTime, shiftMonthsByCalendar, todayStart } from "../time"
import { dayStart, localTime, shiftMonthsByCalendar } from "../time"
import {
asFilterValue,
countFailedSubmissions,
objectValue as toObject,
asRecord,
queryInteger,
sampleUser,
} from "./helpers"
export const problemRoutes = new Hono<AppEnv>()
function objectValue(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {}
}
function publicTemplates(value: unknown) {
const templates: Record<string, string> = {}
for (const [language, raw] of Object.entries(objectValue(value))) {
for (const [language, raw] of Object.entries(asRecord(value))) {
if (typeof raw !== "string") continue
const match = raw.match(/\/\/TEMPLATE BEGIN\n([\s\S]+?)\/\/TEMPLATE END/)
templates[language] = match?.[1] ?? ""
@@ -63,7 +57,7 @@ async function getProblemStatuses(userId: number | undefined) {
.from(schema.userProfile)
.where(eq(schema.userProfile.userId, userId))
.limit(1)
return toObject(toObject(profile?.value).problems)
return asRecord(asRecord(profile?.value).problems)
}
async function getProblemTags(problemIds: number[]) {
@@ -94,7 +88,7 @@ function listItem(
tags: Map<number, string[]>,
statuses: Record<string, unknown>,
) {
const status = toObject(statuses[String(row.problem.id)]).status
const status = asRecord(statuses[String(row.problem.id)]).status
return {
id: row.problem.id,
_id: row.problem.displayId,
@@ -287,7 +281,7 @@ problemRoutes.get("/problems/:id/beat-count", optionalAuth, async (c) => {
)
if (!mine?.value) return success(c, "0")
// 「近两年」按东八区日历算到当天零点
const since = todayStart(shiftMonthsByCalendar(new Date(), -24))
const since = dayStart(shiftMonthsByCalendar(new Date(), -24))
const [active, accepted] = await Promise.all([
db
.select({ value: count() })
@@ -341,7 +335,7 @@ problemRoutes.get("/problems/:displayId/similar", optionalAuth, async (c) => {
// 而这个接口恰好只在**刚 AC** 或**连挂三次**时才被调用,正是候选最容易全中的时候。
const statuses = await getProblemStatuses(c.get("user")?.id)
const solvedIds = Object.entries(statuses)
.filter(([, value]) => toObject(value).status === JudgeStatus.ACCEPTED)
.filter(([, value]) => asRecord(value).status === JudgeStatus.ACCEPTED)
.map(([key]) => Number(key))
.filter((id) => Number.isInteger(id))
// difficulty 是 text(Low / Mid / High),直接 order by 走的是字典序 ——
@@ -478,8 +472,8 @@ problemRoutes.get("/problems/:displayId", optionalAuth, async (c) => {
.from(schema.userProfile)
.where(eq(schema.userProfile.userId, user.id))
.limit(1)
const statuses = objectValue(objectValue(profile?.status).problems)
const problemStatus = objectValue(statuses[String(row.problem.id)]).status
const statuses = asRecord(asRecord(profile?.status).problems)
const problemStatus = asRecord(statuses[String(row.problem.id)]).status
if (typeof problemStatus === "number") myStatus = problemStatus
// 前端拿这个数决定「让 AI 分析我的代码」露不露面,口径必须和 POST /ai/hint
@@ -508,7 +502,7 @@ problemRoutes.get("/problems/:displayId", optionalAuth, async (c) => {
prompt: row.problem.prompt,
submissionNumber: row.problem.submissionNumber,
acceptedNumber: row.problem.acceptedNumber,
statisticInfo: objectValue(row.problem.statisticInfo),
statisticInfo: asRecord(row.problem.statisticInfo),
contestId: row.problem.contestId,
tags: tagRows.map((tag) => tag.name),
createdBy: sampleUser(
@@ -522,7 +516,7 @@ problemRoutes.get("/problems/:displayId", optionalAuth, async (c) => {
mermaidCode: row.problem.allowFlowchart ? null : row.problem.mermaidCode,
flowchartData: row.problem.allowFlowchart
? null
: objectValue(row.problem.flowchartData),
: asRecord(row.problem.flowchartData),
flowchartHint: row.problem.flowchartHint,
sqlConfig: row.problem.sqlConfig,
sqlDisplay: row.problem.sqlDisplay,
+3 -3
View File
@@ -33,7 +33,7 @@ import {
import { db, schema } from "../db"
import { failure, parseBody, success } from "../http"
import { computeProgress } from "../services/problemset"
import { asFilterValue, objectValue, queryInteger, sampleUser } from "./helpers"
import { asFilterValue, asRecord, queryInteger, sampleUser } from "./helpers"
export const problemsetRoutes = new Hono<AppEnv>()
@@ -299,7 +299,7 @@ problemsetRoutes.get("/problem-sets/:id/problems", optionalAuth, async (c) => {
)
.limit(1)
: []
const completed = objectValue(progressRows[0]?.detail)
const completed = asRecord(progressRows[0]?.detail)
return success(
c,
rows.map(
@@ -582,7 +582,7 @@ problemsetRoutes.get(
totalProblemsCount: progress.totalProblemsCount,
totalScore: progress.totalScore,
completedProblems: Object.keys(
objectValue(progress.progressDetail),
asRecord(progress.progressDetail),
).flatMap((key) => problemMap.get(key) ?? []),
}) satisfies ProblemSetProgress,
)
+2 -2
View File
@@ -35,7 +35,7 @@ import {
} from "../judge/status"
import { type ContestEnv } from "../services/contest"
import { getBooleanOption } from "../services/options"
import { localTime, todayStart } from "../time"
import { localTime, dayStart } from "../time"
import { isAdminRole, matchedUsers, rounded, stripClassPrefix } from "./helpers"
export const submissionStatisticsRoutes = new Hono<ContestEnv>()
@@ -68,7 +68,7 @@ submissionStatisticsRoutes.get(
isAdminRole(c.get("user"))
const where = and(
isNull(schema.submission.contestId),
sql`${schema.submission.createTime} >= ${todayStart()}`,
sql`${schema.submission.createTime} >= ${dayStart()}`,
)
const acceptedFilter = sql`count(*) filter (where ${inArray(schema.submission.result, ACCEPTED_RESULTS)})`
const judgingFilter = sql`count(*) filter (where ${inArray(schema.submission.result, UNJUDGED_RESULTS)})`
+10 -16
View File
@@ -44,8 +44,8 @@ import {
import { CodeFormatError, formatCode } from "../services/format-code"
import { getBooleanOption } from "../services/options"
import { consumeToken } from "../services/throttling"
import { todayStart } from "../time"
import { asFilterValue, isAdminRole, queryInteger } from "./helpers"
import { dayStart } from "../time"
import { asFilterValue, asRecord, isAdminRole, queryInteger } from "./helpers"
import {
problemFilter,
submissionStatisticsRoutes,
@@ -54,12 +54,6 @@ import {
export const submissionRoutes = new Hono<ContestEnv>()
function objectValue(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {}
}
/**
* 落编辑过程信号。**失败只记日志、不影响提交** —— 这是附带的统计数据,
* 提交已经进库了,不能因为它回一个 500 让学生以为没交上。
@@ -234,7 +228,7 @@ submissionRoutes.get("/submissions/today-count", async (c) => {
const [row] = await db
.select({ value: count() })
.from(schema.flowchartSubmission)
.where(sql`${schema.flowchartSubmission.createTime} >= ${todayStart()}`)
.where(sql`${schema.flowchartSubmission.createTime} >= ${dayStart()}`)
return success(c, row?.value ?? 0)
}
const [row] = await db
@@ -243,7 +237,7 @@ submissionRoutes.get("/submissions/today-count", async (c) => {
.where(
and(
isNull(schema.submission.contestId),
sql`${schema.submission.createTime} >= ${todayStart()}`,
sql`${schema.submission.createTime} >= ${dayStart()}`,
),
)
return success(c, row?.value ?? 0)
@@ -452,10 +446,10 @@ const submissionListColumns = {
function caseSummary(submission: typeof schema.submission.$inferSelect) {
if (submission.contestId !== null || submission.language === "SQL")
return null
const data = objectValue(submission.info).data
const data = asRecord(submission.info).data
if (!Array.isArray(data) || data.length === 0) return null
const passed = data.filter(
(item) => objectValue(item).result === JudgeStatus.ACCEPTED,
(item) => asRecord(item).result === JudgeStatus.ACCEPTED,
).length
return { passed, total: data.length }
}
@@ -508,7 +502,7 @@ async function submissionDetail(id: string, user: AuthUser) {
result: row.submission.result,
info: full ? row.submission.info : {},
language: row.submission.language,
statisticInfo: objectValue(row.submission.statisticInfo),
statisticInfo: asRecord(row.submission.statisticInfo),
// contest 也在旧后端的排除名单里,同样只给管理员
contestId: full ? row.submission.contestId : null,
problemId: row.submission.problemId,
@@ -652,7 +646,7 @@ submissionRoutes.get("/submissions", optionalAuth, async (c) => {
if (language)
filters.push(eq(schema.submission.language, asFilterValue(language)))
if (c.req.query("today") === "1")
filters.push(sql`${schema.submission.createTime} >= ${todayStart()}`)
filters.push(sql`${schema.submission.createTime} >= ${dayStart()}`)
const where = and(...filters)
// count 不 join problem:无条件 join 会让计划器把 count 退化成 seq scan
// (生产快照实测 7.5ms → 78ms)。题号已经解析成 problem_id,也用不着 join。
@@ -689,7 +683,7 @@ submissionRoutes.get("/submissions", optionalAuth, async (c) => {
username: submission.username,
result: submission.result,
language: submission.language,
statisticInfo: objectValue(submission.statisticInfo),
statisticInfo: asRecord(submission.statisticInfo),
// 题单被删掉之后外键把 problemset_id 置了空,这里自然就没标记了
problemSet:
submission.problemsetId !== null &&
@@ -773,7 +767,7 @@ submissionRoutes.get(
username: submission.username,
result: submission.result,
language: submission.language,
statisticInfo: objectValue(submission.statisticInfo),
statisticInfo: asRecord(submission.statisticInfo),
// 比赛提交没有来源题单:题单只收非比赛题(admin/problemset.ts 加题时卡了
// isNull(problem.contestId)),提交接口那边也只在 contestId 为空时才认这个字段
problemSet: null,