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:
1 parent
572a818d49
commit
5ae830a7f9
62 files changed
+284
-320
No files matched your search
@@ -161,7 +161,7 @@ export type SessionResult =
|
||||
| { user: AuthUser; reason?: undefined }
|
||||
| { user: null; reason: "anonymous" | "disabled" }
|
||||
|
||||
async function getUserByToken(
|
||||
async function authenticateToken(
|
||||
token: string | undefined,
|
||||
): Promise<SessionResult> {
|
||||
if (!token) return { user: null, reason: "anonymous" }
|
||||
@@ -228,7 +228,7 @@ async function getUserByToken(
|
||||
|
||||
/** 要区分「未登录」和「已被禁用」的用这个 —— 目前只有鉴权中间件需要 */
|
||||
export function resolveSession(c: Context) {
|
||||
return getUserByToken(getCookie(c, config.sessionCookie))
|
||||
return authenticateToken(getCookie(c, config.sessionCookie))
|
||||
}
|
||||
|
||||
/** 只关心「是谁」的调用方用这个 */
|
||||
@@ -237,7 +237,8 @@ export async function getSessionUser(c: Context) {
|
||||
}
|
||||
|
||||
export async function getRequestSessionUser(request: Request) {
|
||||
return (await getUserByToken(readCookie(request, config.sessionCookie))).user
|
||||
return (await authenticateToken(readCookie(request, config.sessionCookie)))
|
||||
.user
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,11 +255,11 @@ export function readRequestSessionToken(request: Request) {
|
||||
* 用 EXPIRE 同时完成「判断存在」和「续期」,比 GET + EXPIRE 少一趟往返;两条 EXPIRE
|
||||
* 走一次 pipeline,仍然只有一趟。
|
||||
*
|
||||
* 续期这件事本身是要的:HTTP 请求会走 getUserByToken 里的 redis.expire 续期,
|
||||
* 续期这件事本身是要的:HTTP 请求会走 authenticateToken 里的 redis.expire 续期,
|
||||
* 而只开着页面挂 WebSocket 的人一次请求都不发,不该因此被算成不活跃踢下线。
|
||||
*
|
||||
* **反向索引必须跟着一起续。** 走到这里的正是那种一次 HTTP 请求都不发的连接,
|
||||
* 它碰不到 getUserByToken 里那两条并排的 expire。只续会话不续索引的话,索引先到期、
|
||||
* 它碰不到 authenticateToken 里那两条并排的 expire。只续会话不续索引的话,索引先到期、
|
||||
* 会话却被巡检一直续着,之后改密码 / 禁用账号走 revokeUserSessions 就 SMEMBERS
|
||||
* 不到这张 token —— WebSocket 那边还有 publishSessionRevoked 按 userId 兜底能断掉,
|
||||
* 但 HTTP 一侧拿着那张 cookie 照用不误,而改密码要的恰恰是让 HTTP 立刻失效。
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { recordSolvedProblem } from "../services/problemset"
|
||||
import { checkAst, type AstRule } from "./ast"
|
||||
import { publishSubmissionUpdate } from "./events"
|
||||
import { asRecord } from "../routes/helpers"
|
||||
import type { JudgeJobData } from "./job"
|
||||
import { judgeConfigFor } from "./languages"
|
||||
import { isAccepted, JudgeStatus, type JudgeStatusValue } from "./status"
|
||||
@@ -37,12 +38,6 @@ interface JudgeResponse {
|
||||
data: JudgeCase[] | unknown
|
||||
}
|
||||
|
||||
function objectValue(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {}
|
||||
}
|
||||
|
||||
function statusValue(value: number): JudgeStatusValue {
|
||||
const statuses = new Set<number>(Object.values(JudgeStatus))
|
||||
return statuses.has(value)
|
||||
@@ -51,7 +46,7 @@ function statusValue(value: number): JudgeStatusValue {
|
||||
}
|
||||
|
||||
function templateForLanguage(value: unknown, language: string) {
|
||||
const template = objectValue(value)[language]
|
||||
const template = asRecord(value)[language]
|
||||
return typeof template === "string" ? template : null
|
||||
}
|
||||
|
||||
@@ -62,7 +57,7 @@ function templateForLanguage(value: unknown, language: string) {
|
||||
* 这里、并且不再骗类型系统。
|
||||
*/
|
||||
function astRulesForLanguage(value: unknown, language: string): AstRule[] {
|
||||
const rules = objectValue(value)[language]
|
||||
const rules = asRecord(value)[language]
|
||||
if (!Array.isArray(rules)) return []
|
||||
return rules.flatMap((rule) => {
|
||||
const parsed = astRuleSchema.safeParse(rule)
|
||||
@@ -163,7 +158,7 @@ async function persistResult(
|
||||
.set({ result, info, statisticInfo })
|
||||
.where(eq(schema.submission.id, submissionId))
|
||||
|
||||
const problemStatistics = objectValue(problem.statisticInfo)
|
||||
const problemStatistics = asRecord(problem.statisticInfo)
|
||||
const resultKey = String(result)
|
||||
const previousResultCount = problemStatistics[resultKey]
|
||||
problemStatistics[resultKey] =
|
||||
@@ -178,10 +173,10 @@ async function persistResult(
|
||||
})
|
||||
.where(eq(schema.problem.id, problemId))
|
||||
|
||||
const acmStatus = objectValue(profile.acmProblemsStatus)
|
||||
const acmStatus = asRecord(profile.acmProblemsStatus)
|
||||
const statusKey = contestId === null ? "problems" : "contest_problems"
|
||||
const problems = objectValue(acmStatus[statusKey])
|
||||
const previous = objectValue(problems[String(problemId)])
|
||||
const problems = asRecord(acmStatus[statusKey])
|
||||
const previous = asRecord(problems[String(problemId)])
|
||||
const previousStatus = previous.status
|
||||
const wasAccepted =
|
||||
typeof previousStatus === "number" && isAccepted(previousStatus)
|
||||
@@ -591,14 +586,14 @@ async function judgeSqlSubmission(
|
||||
problem: typeof schema.problem.$inferSelect,
|
||||
studentSql: string,
|
||||
): Promise<JudgeResponse> {
|
||||
const sqlConfig = objectValue(problem.sqlConfig)
|
||||
const sqlConfig = asRecord(problem.sqlConfig)
|
||||
const mode = sqlConfig.mode
|
||||
if (mode !== "query" && mode !== "modify") {
|
||||
throw new Error("题目缺少 SQL 配置(题型)")
|
||||
}
|
||||
const answers = Array.isArray(problem.answers) ? problem.answers : []
|
||||
const refSql = answers
|
||||
.map((item) => objectValue(item))
|
||||
.map((item) => asRecord(item))
|
||||
.find(
|
||||
(item) =>
|
||||
item.language === "SQL" &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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] ?? ""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)})`
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { eq, sql } from "drizzle-orm"
|
||||
|
||||
import { db, schema } from "../db"
|
||||
import { JudgeStatus, isAccepted } from "../judge/status"
|
||||
import { objectValue } from "../routes/helpers"
|
||||
import { asRecord } from "../routes/helpers"
|
||||
import {
|
||||
metaAchievements,
|
||||
refreshUnlockedCount,
|
||||
@@ -305,7 +305,7 @@ async function computePlan(): Promise<Plan> {
|
||||
})
|
||||
}
|
||||
if (
|
||||
stable(objectValue(problem.statisticInfo)) !== stable(want.statisticInfo)
|
||||
stable(asRecord(problem.statisticInfo)) !== stable(want.statisticInfo)
|
||||
) {
|
||||
rows.push({
|
||||
label,
|
||||
@@ -328,7 +328,7 @@ async function computePlan(): Promise<Plan> {
|
||||
}
|
||||
// acm_problems_status 里除了 problems / contest_problems 之外的键原样保留 ——
|
||||
// persistResult 只写这两个桶,别的键是从哪来的没人说得清,重算不该顺手抹掉。
|
||||
const existing = objectValue(profile.acmProblemsStatus)
|
||||
const existing = asRecord(profile.acmProblemsStatus)
|
||||
const merged: Record<string, unknown> = { ...existing }
|
||||
delete merged.problems
|
||||
delete merged.contest_problems
|
||||
@@ -355,13 +355,13 @@ async function computePlan(): Promise<Plan> {
|
||||
}
|
||||
if (stable(existing) !== stable(merged)) {
|
||||
const keys = new Set([
|
||||
...Object.keys(objectValue(existing.problems)),
|
||||
...Object.keys(asRecord(existing.problems)),
|
||||
...Object.keys(want.status.problems ?? {}),
|
||||
])
|
||||
rows.push({
|
||||
label,
|
||||
field: "acm_problems_status",
|
||||
before: `${Object.keys(objectValue(existing.problems)).length} 题`,
|
||||
before: `${Object.keys(asRecord(existing.problems)).length} 题`,
|
||||
after: `${keys.size} 题(含比赛桶重建)`,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { publishAchievementNotification } from "../events"
|
||||
import { calendarDay, dayNumber, localHour } from "../time"
|
||||
import { findMetric } from "./achievement-metrics"
|
||||
import { isAccepted, JudgeStatus } from "../judge/status"
|
||||
import { objectValue } from "../routes/helpers"
|
||||
import { asRecord } from "../routes/helpers"
|
||||
|
||||
function numberMetric(metrics: Record<string, unknown>, key: string) {
|
||||
const value = metrics[key]
|
||||
@@ -129,7 +129,7 @@ export async function updateAchievementsForSubmission(submissionId: string) {
|
||||
.where(eq(schema.userStat.userId, row.submission.userId))
|
||||
.for("update")
|
||||
if (!stat) throw new Error("User achievement stat could not be created")
|
||||
const value = objectValue(stat.metrics)
|
||||
const value = asRecord(stat.metrics)
|
||||
value.submission_count = numberMetric(value, "submission_count") + 1
|
||||
if (firstAc) {
|
||||
value.accepted_count = numberMetric(value, "accepted_count") + 1
|
||||
@@ -143,7 +143,7 @@ export async function updateAchievementsForSubmission(submissionId: string) {
|
||||
numberMetric(value, "max_wa_before_ac"),
|
||||
priorRows.length,
|
||||
)
|
||||
const perDay = objectValue(value._ac_per_day)
|
||||
const perDay = asRecord(value._ac_per_day)
|
||||
perDay[date] = (typeof perDay[date] === "number" ? perDay[date] : 0) + 1
|
||||
value._ac_per_day = perDay
|
||||
value.max_ac_in_one_day = Math.max(
|
||||
@@ -264,7 +264,7 @@ export async function updateAchievementsForProblemSet(userId: number) {
|
||||
.for("update")
|
||||
.limit(1)
|
||||
if (!stat) throw new Error("User achievement stat could not be created")
|
||||
const value = objectValue(stat.metrics)
|
||||
const value = asRecord(stat.metrics)
|
||||
value.badge_count = badgeRow?.value ?? 0
|
||||
value.problemset_completed = completedRow?.value ?? 0
|
||||
await tx
|
||||
@@ -345,7 +345,7 @@ export async function rescanAchievement(achievementId: number) {
|
||||
.from(schema.userStat)
|
||||
const eligible = stats.filter((stat) => {
|
||||
if (already.has(stat.userId)) return false
|
||||
const value = objectValue(stat.metrics)[achievement.metric]
|
||||
const value = asRecord(stat.metrics)[achievement.metric]
|
||||
if (typeof value !== "number") return false
|
||||
return achievement.operator === "gte"
|
||||
? value >= achievement.threshold
|
||||
|
||||
@@ -13,7 +13,7 @@ import { and, desc, eq, isNotNull } from "drizzle-orm"
|
||||
import { config } from "../config"
|
||||
import { db, schema } from "../db"
|
||||
import { JudgeStatus, judgeStatusName } from "../judge/status"
|
||||
import { objectValue } from "../routes/helpers"
|
||||
import { asRecord } from "../routes/helpers"
|
||||
import { completeChat } from "./ai"
|
||||
import { KEY_LINE_LEVEL } from "./hint-filter"
|
||||
import { readInfo } from "./test-case"
|
||||
@@ -80,7 +80,7 @@ const SINGLE_SYSTEM =
|
||||
"你是编程助教。指出学生代码最关键的一个问题,循序渐进地提示,绝不直接给出核心算法或完整解法。输入读取错误可以直接给出正确片段。使用 Markdown,不超过6句话。"
|
||||
|
||||
function errInfo(row: HintRow) {
|
||||
return String(objectValue(row.submission.statisticInfo).err_info ?? "无")
|
||||
return String(asRecord(row.submission.statisticInfo).err_info ?? "无")
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +122,7 @@ function problemBrief(row: HintRow) {
|
||||
const samples = (
|
||||
Array.isArray(row.problem.samples) ? row.problem.samples : []
|
||||
)
|
||||
.map((item) => objectValue(item))
|
||||
.map((item) => asRecord(item))
|
||||
.filter(
|
||||
(item): item is { input: string; output: string } =>
|
||||
typeof item.input === "string" && typeof item.output === "string",
|
||||
@@ -152,7 +152,7 @@ function numbered(code: string) {
|
||||
/** 同语言的标准答案优先;没有就拿别的语言的(思路一样,照样能帮诊断);再没有就 null */
|
||||
export function referenceAnswer(row: HintRow) {
|
||||
const answers = Array.isArray(row.problem.answers)
|
||||
? row.problem.answers.map((item) => objectValue(item))
|
||||
? row.problem.answers.map((item) => asRecord(item))
|
||||
: []
|
||||
const usable = answers.filter(
|
||||
(item): item is { language: string; code: string } =>
|
||||
@@ -174,10 +174,10 @@ export function referenceAnswer(row: HintRow) {
|
||||
*/
|
||||
async function firstFailedCase(row: HintRow) {
|
||||
if (row.submission.language === "SQL") return null
|
||||
const data = objectValue(row.submission.info).data
|
||||
const data = asRecord(row.submission.info).data
|
||||
if (!Array.isArray(data)) return null
|
||||
const failed = data
|
||||
.map((item) => objectValue(item))
|
||||
.map((item) => asRecord(item))
|
||||
.find((item) => typeof item.result === "number" && item.result !== 0)
|
||||
if (!failed || typeof failed.test_case !== "string") return null
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { and, eq, notInArray, sql } from "drizzle-orm"
|
||||
|
||||
import { db, schema } from "../db"
|
||||
import { objectValue } from "../routes/helpers"
|
||||
import { asRecord } from "../routes/helpers"
|
||||
|
||||
type BadgeRow = typeof schema.problemsetBadge.$inferSelect
|
||||
type ProgressRow = typeof schema.problemsetProgress.$inferSelect
|
||||
@@ -40,7 +40,7 @@ export function computeProgress(
|
||||
if (score === undefined) continue
|
||||
totalScore += score
|
||||
// 分值以题单当前的设置为准,detail 里存的是做出那一刻的快照
|
||||
kept[key] = { ...objectValue(value), score }
|
||||
kept[key] = { ...asRecord(value), score }
|
||||
}
|
||||
// 分母只算必做题。「(选做)」这个标签一直只是卡片上的一行字,进度分母和 all_problems
|
||||
// 奖章照样要求做完 —— 快照里 22 个人做完了全部必做题,界面却显示未完成、全通奖章也拿不到
|
||||
@@ -137,7 +137,7 @@ export function eligibleForBadge(badge: BadgeRow, progress: BadgeCheck) {
|
||||
}
|
||||
if (badge.conditionType === "problem_count") {
|
||||
return (
|
||||
Object.keys(objectValue(progress.progressDetail)).length >=
|
||||
Object.keys(asRecord(progress.progressDetail)).length >=
|
||||
badge.conditionValue
|
||||
)
|
||||
}
|
||||
@@ -232,7 +232,7 @@ export async function resyncProgress(problemsetId: number) {
|
||||
const updated = progresses.map((progress) => ({
|
||||
...progress,
|
||||
...computeProgress(
|
||||
objectValue(progress.progressDetail),
|
||||
asRecord(progress.progressDetail),
|
||||
links,
|
||||
progress.completeTime,
|
||||
now,
|
||||
@@ -309,7 +309,7 @@ export async function recordSolvedProblem(
|
||||
.values({ problemsetId, userId, submissionId, problemId })
|
||||
}
|
||||
|
||||
const detail = objectValue(progress.progressDetail)
|
||||
const detail = asRecord(progress.progressDetail)
|
||||
if (String(problemId) in detail) return []
|
||||
const links = await tx
|
||||
.select({
|
||||
|
||||
@@ -224,7 +224,7 @@ export async function packTestCaseZip(testCaseId: string) {
|
||||
try {
|
||||
entries = await readdir(directory)
|
||||
} catch {
|
||||
throw new TestCaseError("Test case does not exists")
|
||||
throw new TestCaseError("Test case does not exist")
|
||||
}
|
||||
const names = new Set(entries)
|
||||
const isSql = await readInfo(testCaseId)
|
||||
|
||||
@@ -64,10 +64,13 @@ export function localWeekday(day: number): number {
|
||||
return new Date(day * DAY_MS).getUTCDay()
|
||||
}
|
||||
|
||||
/** 「东八区今天」的零点,返回 ISO 字符串。提交列表、流程图列表的 `?today=1` 和后台「今日提交数」用它 */
|
||||
export function todayStart(now: Date | number | string = new Date()): string {
|
||||
/**
|
||||
* `instant` 所在东八区日历日的零点,返回 ISO 字符串,缺省是今天。
|
||||
* 提交列表、流程图列表的 `?today=1` 和后台「今日提交数」用它
|
||||
*/
|
||||
export function dayStart(instant: Date | number | string = new Date()): string {
|
||||
return new Date(
|
||||
dayNumber(calendarDay(now)) * DAY_MS - OFFSET_MS,
|
||||
dayNumber(calendarDay(instant)) * DAY_MS - OFFSET_MS,
|
||||
).toISOString()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "./judge/events"
|
||||
import { JudgeStatus } from "./judge/status"
|
||||
import { createSubscriberRedis } from "./redis"
|
||||
import { asRecord } from "./routes/helpers"
|
||||
import {
|
||||
configTopic,
|
||||
configUpdateChannel,
|
||||
@@ -204,12 +205,6 @@ export function startSessionSweep() {
|
||||
return timer
|
||||
}
|
||||
|
||||
function objectValue(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {}
|
||||
}
|
||||
|
||||
export function submissionWebSocketHandler(): Bun.WebSocketHandler<SubmissionSocketData> {
|
||||
return {
|
||||
open(ws) {
|
||||
@@ -386,7 +381,7 @@ async function handleMessage(
|
||||
return
|
||||
}
|
||||
|
||||
const statistics = objectValue(submission.statisticInfo)
|
||||
const statistics = asRecord(submission.statisticInfo)
|
||||
const status =
|
||||
submission.result === JudgeStatus.PENDING
|
||||
? "pending"
|
||||
|
||||
Reference in new issue
Block a user