原来只有 `apps/web` 在 Prettier 下(配置在 `apps/web/.prettierrc.toml`、脚本在 web 的 package.json),后端和契约从来没格式化过 —— 手写在 100 列上下,`db/schema.ts` 还是 drizzle-kit pull 留下的 tab 缩进。两套口径分叉久了,跨端改一处就得记着「这边 什么风格」。 - 配置搬到根目录 `.prettierrc.toml`,内容不变(`semi=false`,其余全默认, printWidth 80 —— 和前端已有的格式一致,不另立一套宽度); - 脚本统一成根目录 `bun run fmt`,覆盖 `apps/*/src`、`apps/web/tests` 和两个构建 配置;web 自己那份 `fmt` 和重复的 prettier 依赖删掉; - `.prettierignore` 挡掉两类不该碰的:drizzle-kit 生成的 `src/db/meta/` 结构快照 (它是 db:generate 的比对输入,只该由 drizzle-kit 写)、unplugin 每次 dev 都会 重写的 `auto-imports.d.ts` / `components.d.ts`; - 全量跑了一遍。纯格式,无行为改动:api typecheck / check:routes / check:ast、 前端 type-check 全过,起 api 打了接口确认正常。前端这 39 个文件的小改动是 prettier 版本漂移(类型断言的换行口径变了),不是新配置带来的。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+463
-258
@@ -1,18 +1,25 @@
|
||||
import type { ProblemAuthor, ProblemDetail, ProblemList, ProblemListItem, Tag, YearlyAc } from "@oj2/contract"
|
||||
import type {
|
||||
ProblemAuthor,
|
||||
ProblemDetail,
|
||||
ProblemList,
|
||||
ProblemListItem,
|
||||
Tag,
|
||||
YearlyAc,
|
||||
} from "@oj2/contract"
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
count,
|
||||
countDistinct,
|
||||
desc,
|
||||
eq,
|
||||
gte,
|
||||
ilike,
|
||||
inArray,
|
||||
isNull,
|
||||
notInArray,
|
||||
or,
|
||||
sql,
|
||||
and,
|
||||
asc,
|
||||
count,
|
||||
countDistinct,
|
||||
desc,
|
||||
eq,
|
||||
gte,
|
||||
ilike,
|
||||
inArray,
|
||||
isNull,
|
||||
notInArray,
|
||||
or,
|
||||
sql,
|
||||
} from "drizzle-orm"
|
||||
import { Hono } from "hono"
|
||||
|
||||
@@ -22,296 +29,494 @@ import { astRequirements } from "../judge/ast"
|
||||
import { failure, success } from "../http"
|
||||
import { JudgeStatus } from "../judge/status"
|
||||
import { localTime, shiftMonthsByCalendar, todayStart } from "../time"
|
||||
import { asFilterValue, countFailedSubmissions, objectValue as toObject, queryInteger, sampleUser } from "./helpers"
|
||||
import {
|
||||
asFilterValue,
|
||||
countFailedSubmissions,
|
||||
objectValue as toObject,
|
||||
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>)
|
||||
: {}
|
||||
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))) {
|
||||
if (typeof raw !== "string") continue
|
||||
const match = raw.match(/\/\/TEMPLATE BEGIN\n([\s\S]+?)\/\/TEMPLATE END/)
|
||||
templates[language] = match?.[1] ?? ""
|
||||
}
|
||||
return templates
|
||||
const templates: Record<string, string> = {}
|
||||
for (const [language, raw] of Object.entries(objectValue(value))) {
|
||||
if (typeof raw !== "string") continue
|
||||
const match = raw.match(/\/\/TEMPLATE BEGIN\n([\s\S]+?)\/\/TEMPLATE END/)
|
||||
templates[language] = match?.[1] ?? ""
|
||||
}
|
||||
return templates
|
||||
}
|
||||
|
||||
async function getProblemStatuses(userId: number | undefined) {
|
||||
if (!userId) return {}
|
||||
const [profile] = await db.select({ value: schema.userProfile.acmProblemsStatus })
|
||||
.from(schema.userProfile).where(eq(schema.userProfile.userId, userId)).limit(1)
|
||||
return toObject(toObject(profile?.value).problems)
|
||||
if (!userId) return {}
|
||||
const [profile] = await db
|
||||
.select({ value: schema.userProfile.acmProblemsStatus })
|
||||
.from(schema.userProfile)
|
||||
.where(eq(schema.userProfile.userId, userId))
|
||||
.limit(1)
|
||||
return toObject(toObject(profile?.value).problems)
|
||||
}
|
||||
|
||||
async function getProblemTags(problemIds: number[]) {
|
||||
if (problemIds.length === 0) return new Map<number, string[]>()
|
||||
const rows = await db.select({ problemId: schema.problemTags.problemId, name: schema.problemTag.name })
|
||||
.from(schema.problemTags)
|
||||
.innerJoin(schema.problemTag, eq(schema.problemTags.problemtagId, schema.problemTag.id))
|
||||
.where(inArray(schema.problemTags.problemId, problemIds))
|
||||
const result = new Map<number, string[]>()
|
||||
for (const row of rows) result.set(row.problemId, [...(result.get(row.problemId) ?? []), row.name])
|
||||
return result
|
||||
if (problemIds.length === 0) return new Map<number, string[]>()
|
||||
const rows = await db
|
||||
.select({
|
||||
problemId: schema.problemTags.problemId,
|
||||
name: schema.problemTag.name,
|
||||
})
|
||||
.from(schema.problemTags)
|
||||
.innerJoin(
|
||||
schema.problemTag,
|
||||
eq(schema.problemTags.problemtagId, schema.problemTag.id),
|
||||
)
|
||||
.where(inArray(schema.problemTags.problemId, problemIds))
|
||||
const result = new Map<number, string[]>()
|
||||
for (const row of rows)
|
||||
result.set(row.problemId, [...(result.get(row.problemId) ?? []), row.name])
|
||||
return result
|
||||
}
|
||||
|
||||
function listItem(
|
||||
row: { problem: typeof schema.problem.$inferSelect; user: typeof schema.user.$inferSelect; realName: string | null },
|
||||
tags: Map<number, string[]>,
|
||||
statuses: Record<string, unknown>,
|
||||
row: {
|
||||
problem: typeof schema.problem.$inferSelect
|
||||
user: typeof schema.user.$inferSelect
|
||||
realName: string | null
|
||||
},
|
||||
tags: Map<number, string[]>,
|
||||
statuses: Record<string, unknown>,
|
||||
) {
|
||||
const status = toObject(statuses[String(row.problem.id)]).status
|
||||
return {
|
||||
id: row.problem.id,
|
||||
_id: row.problem.displayId,
|
||||
title: row.problem.title,
|
||||
submissionNumber: row.problem.submissionNumber,
|
||||
acceptedNumber: row.problem.acceptedNumber,
|
||||
difficulty: row.problem.difficulty,
|
||||
createdBy: sampleUser(row.user, row.realName),
|
||||
tags: tags.get(row.problem.id) ?? [],
|
||||
contestId: row.problem.contestId,
|
||||
allowFlowchart: row.problem.allowFlowchart,
|
||||
showFlowchart: row.problem.showFlowchart,
|
||||
hasAstRules: row.problem.astRules !== null,
|
||||
myStatus: typeof status === "number" ? status : null,
|
||||
} satisfies ProblemListItem
|
||||
const status = toObject(statuses[String(row.problem.id)]).status
|
||||
return {
|
||||
id: row.problem.id,
|
||||
_id: row.problem.displayId,
|
||||
title: row.problem.title,
|
||||
submissionNumber: row.problem.submissionNumber,
|
||||
acceptedNumber: row.problem.acceptedNumber,
|
||||
difficulty: row.problem.difficulty,
|
||||
createdBy: sampleUser(row.user, row.realName),
|
||||
tags: tags.get(row.problem.id) ?? [],
|
||||
contestId: row.problem.contestId,
|
||||
allowFlowchart: row.problem.allowFlowchart,
|
||||
showFlowchart: row.problem.showFlowchart,
|
||||
hasAstRules: row.problem.astRules !== null,
|
||||
myStatus: typeof status === "number" ? status : null,
|
||||
} satisfies ProblemListItem
|
||||
}
|
||||
|
||||
problemRoutes.get("/problems", optionalAuth, async (c) => {
|
||||
const limit = queryInteger(c.req.query("limit"), 20, { min: 1, max: 250 })
|
||||
const offset = queryInteger(c.req.query("offset"), 0, { min: 0 })
|
||||
const filters = [eq(schema.problem.visible, true), isNull(schema.problem.contestId)]
|
||||
const author = c.req.query("author")?.trim()
|
||||
const keyword = c.req.query("keyword")?.trim()
|
||||
const difficulty = c.req.query("difficulty")?.trim()
|
||||
const tag = c.req.query("tag")?.trim()
|
||||
if (author) filters.push(eq(schema.user.username, author))
|
||||
if (keyword) filters.push(or(ilike(schema.problem.title, `%${keyword}%`), ilike(schema.problem.displayId, `%${keyword}%`))!)
|
||||
if (difficulty) filters.push(eq(schema.problem.difficulty, asFilterValue(difficulty)))
|
||||
if (tag) {
|
||||
filters.push(inArray(schema.problem.id, db.select({ id: schema.problemTags.problemId }).from(schema.problemTags)
|
||||
.innerJoin(schema.problemTag, eq(schema.problemTags.problemtagId, schema.problemTag.id))
|
||||
.where(eq(schema.problemTag.name, tag))))
|
||||
}
|
||||
const limit = queryInteger(c.req.query("limit"), 20, { min: 1, max: 250 })
|
||||
const offset = queryInteger(c.req.query("offset"), 0, { min: 0 })
|
||||
const filters = [
|
||||
eq(schema.problem.visible, true),
|
||||
isNull(schema.problem.contestId),
|
||||
]
|
||||
const author = c.req.query("author")?.trim()
|
||||
const keyword = c.req.query("keyword")?.trim()
|
||||
const difficulty = c.req.query("difficulty")?.trim()
|
||||
const tag = c.req.query("tag")?.trim()
|
||||
if (author) filters.push(eq(schema.user.username, author))
|
||||
if (keyword)
|
||||
filters.push(
|
||||
or(
|
||||
ilike(schema.problem.title, `%${keyword}%`),
|
||||
ilike(schema.problem.displayId, `%${keyword}%`),
|
||||
)!,
|
||||
)
|
||||
if (difficulty)
|
||||
filters.push(eq(schema.problem.difficulty, asFilterValue(difficulty)))
|
||||
if (tag) {
|
||||
filters.push(
|
||||
inArray(
|
||||
schema.problem.id,
|
||||
db
|
||||
.select({ id: schema.problemTags.problemId })
|
||||
.from(schema.problemTags)
|
||||
.innerJoin(
|
||||
schema.problemTag,
|
||||
eq(schema.problemTags.problemtagId, schema.problemTag.id),
|
||||
)
|
||||
.where(eq(schema.problemTag.name, tag)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const where = and(...filters)
|
||||
const sort = c.req.query("sort")
|
||||
const order = sort === "flowchart"
|
||||
? [desc(schema.problem.allowFlowchart), desc(schema.problem.showFlowchart), desc(schema.problem.createTime)]
|
||||
: sort === "ast"
|
||||
? [desc(sql`(${schema.problem.astRules} is not null)`), desc(schema.problem.createTime)]
|
||||
: sort === "-accepted_number"
|
||||
? [desc(schema.problem.acceptedNumber)]
|
||||
: sort === "accepted_number"
|
||||
? [asc(schema.problem.acceptedNumber)]
|
||||
: sort === "-submission_number"
|
||||
? [desc(schema.problem.submissionNumber)]
|
||||
: sort === "submission_number"
|
||||
? [asc(schema.problem.submissionNumber)]
|
||||
: sort === "difficulty"
|
||||
? [asc(schema.problem.difficulty)]
|
||||
: sort === "create_time"
|
||||
? [asc(schema.problem.createTime)]
|
||||
: [desc(schema.problem.createTime)]
|
||||
const [totalRow] = await db.select({ value: countDistinct(schema.problem.id) }).from(schema.problem)
|
||||
.innerJoin(schema.user, eq(schema.problem.createdById, schema.user.id)).where(where)
|
||||
const rows = await db.select({ problem: schema.problem, user: schema.user, realName: schema.userProfile.realName })
|
||||
.from(schema.problem)
|
||||
.innerJoin(schema.user, eq(schema.problem.createdById, schema.user.id))
|
||||
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
|
||||
.where(where).orderBy(...order).limit(limit).offset(offset)
|
||||
const [tags, statuses] = await Promise.all([
|
||||
getProblemTags(rows.map((row) => row.problem.id)),
|
||||
getProblemStatuses(c.get("user")?.id),
|
||||
])
|
||||
return success(c, {
|
||||
results: rows.map((row) => listItem(row, tags, statuses)),
|
||||
total: totalRow?.value ?? 0,
|
||||
} satisfies ProblemList)
|
||||
const where = and(...filters)
|
||||
const sort = c.req.query("sort")
|
||||
const order =
|
||||
sort === "flowchart"
|
||||
? [
|
||||
desc(schema.problem.allowFlowchart),
|
||||
desc(schema.problem.showFlowchart),
|
||||
desc(schema.problem.createTime),
|
||||
]
|
||||
: sort === "ast"
|
||||
? [
|
||||
desc(sql`(${schema.problem.astRules} is not null)`),
|
||||
desc(schema.problem.createTime),
|
||||
]
|
||||
: sort === "-accepted_number"
|
||||
? [desc(schema.problem.acceptedNumber)]
|
||||
: sort === "accepted_number"
|
||||
? [asc(schema.problem.acceptedNumber)]
|
||||
: sort === "-submission_number"
|
||||
? [desc(schema.problem.submissionNumber)]
|
||||
: sort === "submission_number"
|
||||
? [asc(schema.problem.submissionNumber)]
|
||||
: sort === "difficulty"
|
||||
? [asc(schema.problem.difficulty)]
|
||||
: sort === "create_time"
|
||||
? [asc(schema.problem.createTime)]
|
||||
: [desc(schema.problem.createTime)]
|
||||
const [totalRow] = await db
|
||||
.select({ value: countDistinct(schema.problem.id) })
|
||||
.from(schema.problem)
|
||||
.innerJoin(schema.user, eq(schema.problem.createdById, schema.user.id))
|
||||
.where(where)
|
||||
const rows = await db
|
||||
.select({
|
||||
problem: schema.problem,
|
||||
user: schema.user,
|
||||
realName: schema.userProfile.realName,
|
||||
})
|
||||
.from(schema.problem)
|
||||
.innerJoin(schema.user, eq(schema.problem.createdById, schema.user.id))
|
||||
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
|
||||
.where(where)
|
||||
.orderBy(...order)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
const [tags, statuses] = await Promise.all([
|
||||
getProblemTags(rows.map((row) => row.problem.id)),
|
||||
getProblemStatuses(c.get("user")?.id),
|
||||
])
|
||||
return success(c, {
|
||||
results: rows.map((row) => listItem(row, tags, statuses)),
|
||||
total: totalRow?.value ?? 0,
|
||||
} satisfies ProblemList)
|
||||
})
|
||||
|
||||
problemRoutes.get("/problem-tags", async (c) => {
|
||||
const keyword = c.req.query("keyword")?.trim()
|
||||
// 只数公开题库里可见的题:隐藏的题和比赛题都不算,否则标签会出现在
|
||||
// 首页列表里,点进去却一道题都筛不出来(对齐 /problems 的过滤条件)
|
||||
const rows = await db.select({ id: schema.problemTag.id, name: schema.problemTag.name, problemCount: countDistinct(schema.problemTags.problemId) })
|
||||
.from(schema.problemTag)
|
||||
.innerJoin(schema.problemTags, eq(schema.problemTags.problemtagId, schema.problemTag.id))
|
||||
.innerJoin(schema.problem, and(
|
||||
eq(schema.problem.id, schema.problemTags.problemId),
|
||||
eq(schema.problem.visible, true),
|
||||
isNull(schema.problem.contestId),
|
||||
))
|
||||
.where(keyword ? ilike(schema.problemTag.name, `%${keyword}%`) : undefined)
|
||||
.groupBy(schema.problemTag.id, schema.problemTag.name).having(sql`count(${schema.problemTags.problemId}) > 0`)
|
||||
.orderBy(asc(schema.problemTag.name))
|
||||
return success(c, rows satisfies Tag[])
|
||||
const keyword = c.req.query("keyword")?.trim()
|
||||
// 只数公开题库里可见的题:隐藏的题和比赛题都不算,否则标签会出现在
|
||||
// 首页列表里,点进去却一道题都筛不出来(对齐 /problems 的过滤条件)
|
||||
const rows = await db
|
||||
.select({
|
||||
id: schema.problemTag.id,
|
||||
name: schema.problemTag.name,
|
||||
problemCount: countDistinct(schema.problemTags.problemId),
|
||||
})
|
||||
.from(schema.problemTag)
|
||||
.innerJoin(
|
||||
schema.problemTags,
|
||||
eq(schema.problemTags.problemtagId, schema.problemTag.id),
|
||||
)
|
||||
.innerJoin(
|
||||
schema.problem,
|
||||
and(
|
||||
eq(schema.problem.id, schema.problemTags.problemId),
|
||||
eq(schema.problem.visible, true),
|
||||
isNull(schema.problem.contestId),
|
||||
),
|
||||
)
|
||||
.where(keyword ? ilike(schema.problemTag.name, `%${keyword}%`) : undefined)
|
||||
.groupBy(schema.problemTag.id, schema.problemTag.name)
|
||||
.having(sql`count(${schema.problemTags.problemId}) > 0`)
|
||||
.orderBy(asc(schema.problemTag.name))
|
||||
return success(c, rows satisfies Tag[])
|
||||
})
|
||||
|
||||
problemRoutes.get("/problems/random", async (c) => {
|
||||
const [row] = await db.select({ displayId: schema.problem.displayId }).from(schema.problem)
|
||||
.where(and(eq(schema.problem.visible, true), isNull(schema.problem.contestId))).orderBy(sql`random()`).limit(1)
|
||||
if (!row) return failure(c, 404, "no-problems", "No problem to pick")
|
||||
return success(c, row.displayId)
|
||||
const [row] = await db
|
||||
.select({ displayId: schema.problem.displayId })
|
||||
.from(schema.problem)
|
||||
.where(
|
||||
and(eq(schema.problem.visible, true), isNull(schema.problem.contestId)),
|
||||
)
|
||||
.orderBy(sql`random()`)
|
||||
.limit(1)
|
||||
if (!row) return failure(c, 404, "no-problems", "No problem to pick")
|
||||
return success(c, row.displayId)
|
||||
})
|
||||
|
||||
problemRoutes.get("/problem-authors", async (c) => {
|
||||
const showAll = c.req.query("all") === "1"
|
||||
const rows = await db.select({ username: schema.user.username, problemCount: count(schema.problem.id) })
|
||||
.from(schema.problem).innerJoin(schema.user, eq(schema.problem.createdById, schema.user.id))
|
||||
.where(and(isNull(schema.problem.contestId), eq(schema.user.isDisabled, false), showAll ? undefined : eq(schema.problem.visible, true)))
|
||||
.groupBy(schema.user.username).orderBy(desc(count(schema.problem.id)))
|
||||
return success(c, rows satisfies ProblemAuthor[])
|
||||
const showAll = c.req.query("all") === "1"
|
||||
const rows = await db
|
||||
.select({
|
||||
username: schema.user.username,
|
||||
problemCount: count(schema.problem.id),
|
||||
})
|
||||
.from(schema.problem)
|
||||
.innerJoin(schema.user, eq(schema.problem.createdById, schema.user.id))
|
||||
.where(
|
||||
and(
|
||||
isNull(schema.problem.contestId),
|
||||
eq(schema.user.isDisabled, false),
|
||||
showAll ? undefined : eq(schema.problem.visible, true),
|
||||
),
|
||||
)
|
||||
.groupBy(schema.user.username)
|
||||
.orderBy(desc(count(schema.problem.id)))
|
||||
return success(c, rows satisfies ProblemAuthor[])
|
||||
})
|
||||
|
||||
problemRoutes.get("/problems/:id/beat-count", optionalAuth, async (c) => {
|
||||
const user = c.get("user")
|
||||
if (!user) return success(c, "0")
|
||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||
const [mine] = await db.select({ value: count() }).from(schema.submission).where(and(
|
||||
eq(schema.submission.userId, user.id), eq(schema.submission.problemId, id),
|
||||
inArray(schema.submission.result, [JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]),
|
||||
))
|
||||
if (!mine?.value) return success(c, "0")
|
||||
// 「近两年」按东八区日历算到当天零点
|
||||
const since = todayStart(shiftMonthsByCalendar(new Date(), -24))
|
||||
const [active, accepted] = await Promise.all([
|
||||
db.select({ value: count() }).from(schema.user).where(and(eq(schema.user.isDisabled, false), gte(schema.user.lastLogin, since))),
|
||||
db.select({ value: countDistinct(schema.submission.userId) }).from(schema.submission).where(and(
|
||||
eq(schema.submission.problemId, id), inArray(schema.submission.result, [0, 10]), gte(schema.submission.createTime, since),
|
||||
)),
|
||||
])
|
||||
const total = active[0]?.value ?? 0
|
||||
const solved = accepted[0]?.value ?? 0
|
||||
return success(c, total > 0 && solved < total ? (((total - solved) / total) * 100).toFixed(2) : "0")
|
||||
const user = c.get("user")
|
||||
if (!user) return success(c, "0")
|
||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||
const [mine] = await db
|
||||
.select({ value: count() })
|
||||
.from(schema.submission)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.submission.userId, user.id),
|
||||
eq(schema.submission.problemId, id),
|
||||
inArray(schema.submission.result, [
|
||||
JudgeStatus.ACCEPTED,
|
||||
JudgeStatus.AST_CHECK_FAILED,
|
||||
]),
|
||||
),
|
||||
)
|
||||
if (!mine?.value) return success(c, "0")
|
||||
// 「近两年」按东八区日历算到当天零点
|
||||
const since = todayStart(shiftMonthsByCalendar(new Date(), -24))
|
||||
const [active, accepted] = await Promise.all([
|
||||
db
|
||||
.select({ value: count() })
|
||||
.from(schema.user)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.user.isDisabled, false),
|
||||
gte(schema.user.lastLogin, since),
|
||||
),
|
||||
),
|
||||
db
|
||||
.select({ value: countDistinct(schema.submission.userId) })
|
||||
.from(schema.submission)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.submission.problemId, id),
|
||||
inArray(schema.submission.result, [0, 10]),
|
||||
gte(schema.submission.createTime, since),
|
||||
),
|
||||
),
|
||||
])
|
||||
const total = active[0]?.value ?? 0
|
||||
const solved = accepted[0]?.value ?? 0
|
||||
return success(
|
||||
c,
|
||||
total > 0 && solved < total
|
||||
? (((total - solved) / total) * 100).toFixed(2)
|
||||
: "0",
|
||||
)
|
||||
})
|
||||
|
||||
problemRoutes.get("/problems/:displayId/similar", optionalAuth, async (c) => {
|
||||
const [target] = await db.select({ id: schema.problem.id }).from(schema.problem)
|
||||
.where(and(sql`lower(${schema.problem.displayId}) = lower(${c.req.param("displayId")})`, isNull(schema.problem.contestId))).limit(1)
|
||||
if (!target) return failure(c, 404, "problem-not-found", "Problem not found")
|
||||
const targetTags = await db.select({ id: schema.problemTags.problemtagId }).from(schema.problemTags).where(eq(schema.problemTags.problemId, target.id))
|
||||
if (targetTags.length === 0) return success(c, [])
|
||||
const rows = await db.select({ problem: schema.problem, user: schema.user, realName: schema.userProfile.realName })
|
||||
.from(schema.problem)
|
||||
.innerJoin(schema.user, eq(schema.problem.createdById, schema.user.id))
|
||||
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
|
||||
.where(and(
|
||||
eq(schema.problem.visible, true), isNull(schema.problem.contestId), sql`${schema.problem.id} <> ${target.id}`,
|
||||
inArray(schema.problem.id, db.select({ id: schema.problemTags.problemId }).from(schema.problemTags)
|
||||
.where(inArray(schema.problemTags.problemtagId, targetTags.map((tag) => tag.id)))),
|
||||
)).groupBy(schema.problem.id, schema.user.id, schema.userProfile.realName).orderBy(asc(schema.problem.difficulty)).limit(5)
|
||||
const [tags, statuses] = await Promise.all([getProblemTags(rows.map((row) => row.problem.id)), getProblemStatuses(c.get("user")?.id)])
|
||||
const filtered = rows.filter((row) => toObject(statuses[String(row.problem.id)]).status !== JudgeStatus.ACCEPTED)
|
||||
return success(c, filtered.map((row) => listItem(row, tags, statuses)))
|
||||
const [target] = await db
|
||||
.select({ id: schema.problem.id })
|
||||
.from(schema.problem)
|
||||
.where(
|
||||
and(
|
||||
sql`lower(${schema.problem.displayId}) = lower(${c.req.param("displayId")})`,
|
||||
isNull(schema.problem.contestId),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
if (!target) return failure(c, 404, "problem-not-found", "Problem not found")
|
||||
const targetTags = await db
|
||||
.select({ id: schema.problemTags.problemtagId })
|
||||
.from(schema.problemTags)
|
||||
.where(eq(schema.problemTags.problemId, target.id))
|
||||
if (targetTags.length === 0) return success(c, [])
|
||||
const rows = await db
|
||||
.select({
|
||||
problem: schema.problem,
|
||||
user: schema.user,
|
||||
realName: schema.userProfile.realName,
|
||||
})
|
||||
.from(schema.problem)
|
||||
.innerJoin(schema.user, eq(schema.problem.createdById, schema.user.id))
|
||||
.leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
|
||||
.where(
|
||||
and(
|
||||
eq(schema.problem.visible, true),
|
||||
isNull(schema.problem.contestId),
|
||||
sql`${schema.problem.id} <> ${target.id}`,
|
||||
inArray(
|
||||
schema.problem.id,
|
||||
db
|
||||
.select({ id: schema.problemTags.problemId })
|
||||
.from(schema.problemTags)
|
||||
.where(
|
||||
inArray(
|
||||
schema.problemTags.problemtagId,
|
||||
targetTags.map((tag) => tag.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.groupBy(schema.problem.id, schema.user.id, schema.userProfile.realName)
|
||||
.orderBy(asc(schema.problem.difficulty))
|
||||
.limit(5)
|
||||
const [tags, statuses] = await Promise.all([
|
||||
getProblemTags(rows.map((row) => row.problem.id)),
|
||||
getProblemStatuses(c.get("user")?.id),
|
||||
])
|
||||
const filtered = rows.filter(
|
||||
(row) =>
|
||||
toObject(statuses[String(row.problem.id)]).status !==
|
||||
JudgeStatus.ACCEPTED,
|
||||
)
|
||||
return success(
|
||||
c,
|
||||
filtered.map((row) => listItem(row, tags, statuses)),
|
||||
)
|
||||
})
|
||||
|
||||
problemRoutes.get("/problems/:displayId/yearly-ac", async (c) => {
|
||||
const [problem] = await db.select({ id: schema.problem.id }).from(schema.problem)
|
||||
.where(and(sql`lower(${schema.problem.displayId}) = lower(${c.req.param("displayId")})`, isNull(schema.problem.contestId), eq(schema.problem.visible, true))).limit(1)
|
||||
if (!problem) return failure(c, 404, "problem-not-found", "Problem does not exist")
|
||||
const year = sql<number>`extract(year from ${localTime(schema.submission.createTime)})::int`
|
||||
const rows = await db.select({
|
||||
year,
|
||||
total: count(),
|
||||
accepted: sql<number>`count(*) filter (where ${schema.submission.result} in (0, 10))::int`,
|
||||
}).from(schema.submission).where(and(eq(schema.submission.problemId, problem.id), isNull(schema.submission.contestId), notInArray(schema.submission.result, [6, 7])))
|
||||
.groupBy(year).orderBy(year)
|
||||
return success(c, rows.map((row) => ({ ...row, acRate: row.total > 0 ? Math.round(row.accepted / row.total * 10_000) / 100 : 0 } satisfies YearlyAc)))
|
||||
const [problem] = await db
|
||||
.select({ id: schema.problem.id })
|
||||
.from(schema.problem)
|
||||
.where(
|
||||
and(
|
||||
sql`lower(${schema.problem.displayId}) = lower(${c.req.param("displayId")})`,
|
||||
isNull(schema.problem.contestId),
|
||||
eq(schema.problem.visible, true),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
if (!problem)
|
||||
return failure(c, 404, "problem-not-found", "Problem does not exist")
|
||||
const year = sql<number>`extract(year from ${localTime(schema.submission.createTime)})::int`
|
||||
const rows = await db
|
||||
.select({
|
||||
year,
|
||||
total: count(),
|
||||
accepted: sql<number>`count(*) filter (where ${schema.submission.result} in (0, 10))::int`,
|
||||
})
|
||||
.from(schema.submission)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.submission.problemId, problem.id),
|
||||
isNull(schema.submission.contestId),
|
||||
notInArray(schema.submission.result, [6, 7]),
|
||||
),
|
||||
)
|
||||
.groupBy(year)
|
||||
.orderBy(year)
|
||||
return success(
|
||||
c,
|
||||
rows.map(
|
||||
(row) =>
|
||||
({
|
||||
...row,
|
||||
acRate:
|
||||
row.total > 0
|
||||
? Math.round((row.accepted / row.total) * 10_000) / 100
|
||||
: 0,
|
||||
}) satisfies YearlyAc,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
problemRoutes.get("/problems/:displayId", optionalAuth, async (c) => {
|
||||
const [row] = await db
|
||||
.select({
|
||||
problem: schema.problem,
|
||||
creatorId: schema.user.id,
|
||||
creatorUsername: schema.user.username,
|
||||
})
|
||||
.from(schema.problem)
|
||||
.innerJoin(schema.user, eq(schema.problem.createdById, schema.user.id))
|
||||
.where(
|
||||
and(
|
||||
eq(schema.problem.displayId, c.req.param("displayId")),
|
||||
eq(schema.problem.visible, true),
|
||||
isNull(schema.problem.contestId),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
const [row] = await db
|
||||
.select({
|
||||
problem: schema.problem,
|
||||
creatorId: schema.user.id,
|
||||
creatorUsername: schema.user.username,
|
||||
})
|
||||
.from(schema.problem)
|
||||
.innerJoin(schema.user, eq(schema.problem.createdById, schema.user.id))
|
||||
.where(
|
||||
and(
|
||||
eq(schema.problem.displayId, c.req.param("displayId")),
|
||||
eq(schema.problem.visible, true),
|
||||
isNull(schema.problem.contestId),
|
||||
),
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (!row) return failure(c, 404, "problem-not-found", "Problem does not exist")
|
||||
if (!row)
|
||||
return failure(c, 404, "problem-not-found", "Problem does not exist")
|
||||
|
||||
const tagRows = await db
|
||||
.select({ name: schema.problemTag.name })
|
||||
.from(schema.problemTags)
|
||||
.innerJoin(
|
||||
schema.problemTag,
|
||||
eq(schema.problemTags.problemtagId, schema.problemTag.id),
|
||||
)
|
||||
.where(eq(schema.problemTags.problemId, row.problem.id))
|
||||
const tagRows = await db
|
||||
.select({ name: schema.problemTag.name })
|
||||
.from(schema.problemTags)
|
||||
.innerJoin(
|
||||
schema.problemTag,
|
||||
eq(schema.problemTags.problemtagId, schema.problemTag.id),
|
||||
)
|
||||
.where(eq(schema.problemTags.problemId, row.problem.id))
|
||||
|
||||
const user = c.get("user")
|
||||
let myStatus: number | null = null
|
||||
let myFailedCount = 0
|
||||
if (user) {
|
||||
const [profile] = await db
|
||||
.select({ status: schema.userProfile.acmProblemsStatus })
|
||||
.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
|
||||
if (typeof problemStatus === "number") myStatus = problemStatus
|
||||
const user = c.get("user")
|
||||
let myStatus: number | null = null
|
||||
let myFailedCount = 0
|
||||
if (user) {
|
||||
const [profile] = await db
|
||||
.select({ status: schema.userProfile.acmProblemsStatus })
|
||||
.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
|
||||
if (typeof problemStatus === "number") myStatus = problemStatus
|
||||
|
||||
// 前端拿这个数决定「让 AI 分析我的代码」露不露面,口径必须和 POST /ai/hint
|
||||
// 的服务端闸门一致,所以两边共用 countFailedSubmissions
|
||||
myFailedCount = await countFailedSubmissions(user.id, row.problem.id)
|
||||
}
|
||||
// 前端拿这个数决定「让 AI 分析我的代码」露不露面,口径必须和 POST /ai/hint
|
||||
// 的服务端闸门一致,所以两边共用 countFailedSubmissions
|
||||
myFailedCount = await countFailedSubmissions(user.id, row.problem.id)
|
||||
}
|
||||
|
||||
const samples = Array.isArray(row.problem.samples) ? row.problem.samples : []
|
||||
const data = {
|
||||
id: row.problem.id,
|
||||
_id: row.problem.displayId,
|
||||
title: row.problem.title,
|
||||
description: row.problem.description,
|
||||
inputDescription: row.problem.inputDescription,
|
||||
outputDescription: row.problem.outputDescription,
|
||||
samples,
|
||||
hint: row.problem.hint,
|
||||
languages: row.problem.languages,
|
||||
template: publicTemplates(row.problem.template),
|
||||
createTime: row.problem.createTime,
|
||||
lastUpdateTime: row.problem.lastUpdateTime,
|
||||
timeLimit: row.problem.timeLimit,
|
||||
memoryLimit: row.problem.memoryLimit,
|
||||
difficulty: row.problem.difficulty,
|
||||
source: row.problem.source,
|
||||
prompt: row.problem.prompt,
|
||||
submissionNumber: row.problem.submissionNumber,
|
||||
acceptedNumber: row.problem.acceptedNumber,
|
||||
statisticInfo: objectValue(row.problem.statisticInfo),
|
||||
contestId: row.problem.contestId,
|
||||
tags: tagRows.map((tag) => tag.name),
|
||||
createdBy: sampleUser({ id: row.creatorId, username: row.creatorUsername }, null),
|
||||
myStatus,
|
||||
myFailedCount,
|
||||
allowFlowchart: row.problem.allowFlowchart,
|
||||
showFlowchart: row.problem.showFlowchart,
|
||||
mermaidCode: row.problem.allowFlowchart ? null : row.problem.mermaidCode,
|
||||
flowchartData: row.problem.allowFlowchart
|
||||
? null
|
||||
: objectValue(row.problem.flowchartData),
|
||||
flowchartHint: row.problem.flowchartHint,
|
||||
sqlConfig: row.problem.sqlConfig,
|
||||
sqlDisplay: row.problem.sqlDisplay,
|
||||
// 代码要求:只给渲染好的文案,规则原文不下发给学生
|
||||
astRequirements: astRequirements(row.problem.astRules),
|
||||
} satisfies ProblemDetail
|
||||
const samples = Array.isArray(row.problem.samples) ? row.problem.samples : []
|
||||
const data = {
|
||||
id: row.problem.id,
|
||||
_id: row.problem.displayId,
|
||||
title: row.problem.title,
|
||||
description: row.problem.description,
|
||||
inputDescription: row.problem.inputDescription,
|
||||
outputDescription: row.problem.outputDescription,
|
||||
samples,
|
||||
hint: row.problem.hint,
|
||||
languages: row.problem.languages,
|
||||
template: publicTemplates(row.problem.template),
|
||||
createTime: row.problem.createTime,
|
||||
lastUpdateTime: row.problem.lastUpdateTime,
|
||||
timeLimit: row.problem.timeLimit,
|
||||
memoryLimit: row.problem.memoryLimit,
|
||||
difficulty: row.problem.difficulty,
|
||||
source: row.problem.source,
|
||||
prompt: row.problem.prompt,
|
||||
submissionNumber: row.problem.submissionNumber,
|
||||
acceptedNumber: row.problem.acceptedNumber,
|
||||
statisticInfo: objectValue(row.problem.statisticInfo),
|
||||
contestId: row.problem.contestId,
|
||||
tags: tagRows.map((tag) => tag.name),
|
||||
createdBy: sampleUser(
|
||||
{ id: row.creatorId, username: row.creatorUsername },
|
||||
null,
|
||||
),
|
||||
myStatus,
|
||||
myFailedCount,
|
||||
allowFlowchart: row.problem.allowFlowchart,
|
||||
showFlowchart: row.problem.showFlowchart,
|
||||
mermaidCode: row.problem.allowFlowchart ? null : row.problem.mermaidCode,
|
||||
flowchartData: row.problem.allowFlowchart
|
||||
? null
|
||||
: objectValue(row.problem.flowchartData),
|
||||
flowchartHint: row.problem.flowchartHint,
|
||||
sqlConfig: row.problem.sqlConfig,
|
||||
sqlDisplay: row.problem.sqlDisplay,
|
||||
// 代码要求:只给渲染好的文案,规则原文不下发给学生
|
||||
astRequirements: astRequirements(row.problem.astRules),
|
||||
} satisfies ProblemDetail
|
||||
|
||||
return success(c, data)
|
||||
return success(c, data)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user