refactor(契约): 展示题号统一叫 problemDisplayId,题单 id 统一叫 problemSetId
Deploy / deploy (push) Canceled after 0s

前后端同时改,部署前开着的旧页面要刷新一次(题号列会是空的、提交筛选会失效)。

- 挂在非题目对象上的展示题号,原来叫 problem(字符串)或 displayId,统一成
  problemDisplayId:提交列表、流程图列表、统计的 lastFailure / items /
  今日最热题、站内信里的提交、后台题单的题目列表。别处的 problem 都是对象
  或数字 id,这几处是字符串,最容易看错
- 列表筛选的 query 参数 ?problemId= 实际传的是展示题号,改成
  ?problemDisplayId=(提交列表、流程图列表、三个统计接口)
- 契约里 problemsetId / problemSetId、problemset / problemSet 两种拼法并存,
  统一成 problemSetId / problemSet,和 ProblemSet* 类型名、problem-sets
  路径、前端路由参数一致。drizzle 的列键跟表名走,没动
- aiDetail.user(用户名字符串,而契约里别的 user 都是对象)→ username。
  ai_analysis.data 里存的历史快照仍是 user,没有代码读回它
- 后台题单题目表的列是按字符串键 'displayId' 取的,类型检查抓不到,一并改

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:31 +08:00
1 parent 5ae830a7f9
commit 9412ba3cf6
24 files changed
+80 -82

No files matched your search

+3 -3
View File
@@ -293,9 +293,9 @@ adminProblemSetRoutes.get(
({ item, problem }) => ({ item, problem }) =>
({ ({
id: item.id, id: item.id,
problemsetId: item.problemsetId, problemSetId: item.problemsetId,
problemId: item.problemId, problemId: item.problemId,
displayId: problem.displayId, problemDisplayId: problem.displayId,
title: problem.title, title: problem.title,
difficulty: problem.difficulty, difficulty: problem.difficulty,
order: item.order, order: item.order,
@@ -456,7 +456,7 @@ async function badgesWithCount(badges: BadgeRow[]) {
(badge) => (badge) =>
({ ({
id: badge.id, id: badge.id,
problemsetId: badge.problemsetId, problemSetId: badge.problemsetId,
name: badge.name, name: badge.name,
description: badge.description, description: badge.description,
icon: badge.icon, icon: badge.icon,
+1 -1
View File
@@ -169,7 +169,7 @@ contentRoutes.get("/messages", requireAuth, async (c) => {
language: submission.language, language: submission.language,
statisticInfo: asRecord(submission.statisticInfo), statisticInfo: asRecord(submission.statisticInfo),
// 展示用题号而非数字主键,站内信页面拿它拼 /problem/<题号> // 展示用题号而非数字主键,站内信页面拿它拼 /problem/<题号>
problem: displayId, problemDisplayId: displayId,
showLink: true, showLink: true,
} satisfies EmbeddedSubmission, } satisfies EmbeddedSubmission,
}) satisfies Message, }) satisfies Message,
+3 -3
View File
@@ -244,7 +244,7 @@ flowchartRoutes.get("/flowcharts", requireAuth, async (c) => {
const user = c.get("user")! const user = c.get("user")!
const limit = queryInteger(c.req.query("limit"), 10, { min: 1, max: 250 }) const limit = queryInteger(c.req.query("limit"), 10, { min: 1, max: 250 })
const offset = queryInteger(c.req.query("offset"), 0, { min: 0 }) const offset = queryInteger(c.req.query("offset"), 0, { min: 0 })
const displayId = c.req.query("problemId")?.trim() const displayId = c.req.query("problemDisplayId")?.trim()
const username = c.req.query("username")?.trim() const username = c.req.query("username")?.trim()
const grade = c.req.query("grade") const grade = c.req.query("grade")
// 与代码提交列表同一套口径(submission.ts 的 GET /submissions):关掉 // 与代码提交列表同一套口径(submission.ts 的 GET /submissions):关掉
@@ -298,7 +298,7 @@ flowchartRoutes.get("/flowcharts", requireAuth, async (c) => {
({ ({
id: flowchart.id, id: flowchart.id,
username, username,
problem: problem.displayId, problemDisplayId: problem.displayId,
problemTitle: problem.title, problemTitle: problem.title,
status: flowchart.status, status: flowchart.status,
createTime: flowchart.createTime, createTime: flowchart.createTime,
@@ -349,7 +349,7 @@ flowchartRoutes.get("/flowcharts/statistics", requireTeacher, async (c) => {
if (start) if (start)
filters.push(sql`${schema.flowchartSubmission.createTime} >= ${start}`) filters.push(sql`${schema.flowchartSubmission.createTime} >= ${start}`)
const displayId = c.req.query("problemId")?.trim() const displayId = c.req.query("problemDisplayId")?.trim()
if (displayId) { if (displayId) {
const [problem] = await db const [problem] = await db
.select({ id: schema.problem.id }) .select({ id: schema.problem.id })
+4 -4
View File
@@ -81,7 +81,7 @@ function badgeData(
) { ) {
return { return {
id: badge.id, id: badge.id,
problemsetId: badge.problemsetId, problemSetId: badge.problemsetId,
name: badge.name, name: badge.name,
description: badge.description, description: badge.description,
icon: badge.icon, icon: badge.icon,
@@ -306,7 +306,7 @@ problemsetRoutes.get("/problem-sets/:id/problems", optionalAuth, async (c) => {
({ link, problemId, displayId, title, difficulty }) => ({ link, problemId, displayId, title, difficulty }) =>
({ ({
id: link.id, id: link.id,
problemsetId: link.problemsetId, problemSetId: link.problemsetId,
problem: { id: problemId, _id: displayId, title, difficulty }, problem: { id: problemId, _id: displayId, title, difficulty },
order: link.order, order: link.order,
isRequired: link.isRequired, isRequired: link.isRequired,
@@ -435,7 +435,7 @@ problemsetRoutes.get("/users/:username/badges", optionalAuth, async (c) => {
userId: userBadge.userId, userId: userBadge.userId,
badge: badgeData(badge), badge: badgeData(badge),
earnedTime: userBadge.earnedTime, earnedTime: userBadge.earnedTime,
problemset: { id: problemSet.id, title: problemSet.title }, problemSet: { id: problemSet.id, title: problemSet.title },
}) satisfies UserBadge, }) satisfies UserBadge,
), ),
) )
@@ -572,7 +572,7 @@ problemsetRoutes.get(
({ progress, user: progressUser, realName }) => ({ progress, user: progressUser, realName }) =>
({ ({
id: progress.id, id: progress.id,
problemsetId: progress.problemsetId, problemSetId: progress.problemsetId,
user: sampleUser(progressUser, realName), user: sampleUser(progressUser, realName),
joinTime: progress.joinTime, joinTime: progress.joinTime,
completeTime: progress.completeTime, completeTime: progress.completeTime,
+5 -5
View File
@@ -151,7 +151,7 @@ submissionStatisticsRoutes.get(
count: row.value, count: row.value,
})), })),
problems: problemRows.map((row) => ({ problems: problemRows.map((row) => ({
problem: row.displayId, problemDisplayId: row.displayId,
problemTitle: row.title, problemTitle: row.title,
count: row.value, count: row.value,
acceptedCount: row.accepted, acceptedCount: row.accepted,
@@ -240,7 +240,7 @@ async function lastFailureByUser(where: SQL | undefined, userIds: number[]) {
number, number,
{ {
id: string id: string
problem: string problemDisplayId: string
result: JudgeStatusValue result: JudgeStatusValue
error: string | null error: string | null
} }
@@ -276,7 +276,7 @@ async function lastFailureByUser(where: SQL | undefined, userIds: number[]) {
for (const row of rows) { for (const row of rows) {
byUser.set(row.user_id, { byUser.set(row.user_id, {
id: row.id, id: row.id,
problem: row.problem, problemDisplayId: row.problem,
result: row.result, result: row.result,
error: row.error, error: row.error,
}) })
@@ -412,7 +412,7 @@ async function statisticsScope(c: {
if (range.start) if (range.start)
filters.push(sql`${schema.submission.createTime} >= ${range.start}`) filters.push(sql`${schema.submission.createTime} >= ${range.start}`)
const displayIds = parseDisplayIds(c.req.query("problemId") ?? "") const displayIds = parseDisplayIds(c.req.query("problemDisplayId") ?? "")
if (displayIds.length > STATISTICS_MAX_PROBLEMS) { if (displayIds.length > STATISTICS_MAX_PROBLEMS) {
return { return {
ok: false, ok: false,
@@ -682,7 +682,7 @@ submissionStatisticsRoutes.get(
id: schema.submission.id, id: schema.submission.id,
result: schema.submission.result, result: schema.submission.result,
createTime: schema.submission.createTime, createTime: schema.submission.createTime,
problem: schema.problem.displayId, problemDisplayId: schema.problem.displayId,
problemTitle: schema.problem.title, problemTitle: schema.problem.title,
}) })
.from(schema.submission) .from(schema.submission)
+4 -4
View File
@@ -627,7 +627,7 @@ submissionRoutes.get("/submissions", optionalAuth, async (c) => {
) { ) {
return success(c, { results: [], total: 0 } satisfies SubmissionList) return success(c, { results: [], total: 0 } satisfies SubmissionList)
} }
const displayId = c.req.query("problemId")?.trim() const displayId = c.req.query("problemDisplayId")?.trim()
const myself = c.req.query("myself") === "1" ? user : null const myself = c.req.query("myself") === "1" ? user : null
// 「只看自己」盖过用户名 // 「只看自己」盖过用户名
const username = myself ? undefined : c.req.query("username")?.trim() const username = myself ? undefined : c.req.query("username")?.trim()
@@ -673,7 +673,7 @@ submissionRoutes.get("/submissions", optionalAuth, async (c) => {
({ submission, problem }) => ({ submission, problem }) =>
({ ({
id: submission.id, id: submission.id,
problem: problem.displayId, problemDisplayId: problem.displayId,
problemTitle: problem.title, problemTitle: problem.title,
showLink: user showLink: user
? canViewSubmission(user, submission, problem, null, joinTimes) ? canViewSubmission(user, submission, problem, null, joinTimes)
@@ -708,7 +708,7 @@ submissionRoutes.get(
const limit = queryInteger(c.req.query("limit"), 10, { min: 1, max: 250 }) const limit = queryInteger(c.req.query("limit"), 10, { min: 1, max: 250 })
const offset = queryInteger(c.req.query("offset"), 0, { min: 0 }) const offset = queryInteger(c.req.query("offset"), 0, { min: 0 })
const user = c.get("user") const user = c.get("user")
const displayId = c.req.query("problemId")?.trim() const displayId = c.req.query("problemDisplayId")?.trim()
const myself = c.req.query("myself") === "1" ? user : null const myself = c.req.query("myself") === "1" ? user : null
const username = myself ? undefined : c.req.query("username")?.trim() const username = myself ? undefined : c.req.query("username")?.trim()
const result = c.req.query("result") const result = c.req.query("result")
@@ -757,7 +757,7 @@ submissionRoutes.get(
({ submission, problem }) => ({ submission, problem }) =>
({ ({
id: submission.id, id: submission.id,
problem: problem.displayId, problemDisplayId: problem.displayId,
problemTitle: problem.title, problemTitle: problem.title,
showLink: user showLink: user
? canViewSubmission(user, submission, problem, contest) ? canViewSubmission(user, submission, problem, contest)
+2 -2
View File
@@ -314,7 +314,7 @@ export async function buildDetail(user: AuthUser, start: string, end: string) {
const problemIds = firstAc.map((item) => item.problemId) const problemIds = firstAc.map((item) => item.problemId)
if (!problemIds.length) if (!problemIds.length)
return { return {
user: user.username, username: user.username,
className: user.className, className: user.className,
start, start,
end, end,
@@ -415,7 +415,7 @@ export async function buildDetail(user: AuthUser, start: string, end: string) {
b.latestSubmissionTime.localeCompare(a.latestSubmissionTime), b.latestSubmissionTime.localeCompare(a.latestSubmissionTime),
) )
return { return {
user: user.username, username: user.username,
className: user.className, className: user.className,
start, start,
end, end,
+1 -1
View File
@@ -134,7 +134,7 @@ async function viewSubmission(item: HelperItem) {
// 查询该用户在该竞赛该题目的 AC 提交 // 查询该用户在该竞赛该题目的 AC 提交
const res = await getSubmissions({ const res = await getSubmissions({
username: item.username, username: item.username,
problemId: item.problemDisplayId, problemDisplayId: item.problemDisplayId,
contestId: props.contestID, contestId: props.contestID,
result: "0", // ACCEPTED result: "0", // ACCEPTED
language: "", language: "",
@@ -27,7 +27,7 @@ defineEmits<Emits>()
</n-flex> </n-flex>
<n-data-table <n-data-table
:columns="[ :columns="[
{ title: '题目ID', key: 'displayId', width: 80 }, { title: '题目ID', key: 'problemDisplayId', width: 80 },
{ title: '题目标题', key: 'title', minWidth: 200 }, { title: '题目标题', key: 'title', minWidth: 200 },
{ title: '顺序', key: 'order', width: 80 }, { title: '顺序', key: 'order', width: 80 },
{ {
+3 -3
View File
@@ -161,15 +161,15 @@ watch(name, load)
object-fit="contain" object-fit="contain"
/> />
</template> </template>
<n-text v-if="b.problemset" depth="3" class="source"> <n-text v-if="b.problemSet" depth="3" class="source">
来自题单 来自题单
<router-link <router-link
:to="{ :to="{
name: 'problemset', name: 'problemset',
params: { problemSetId: b.problemset.id }, params: { problemSetId: b.problemSet.id },
}" }"
> >
{{ b.problemset.title }} {{ b.problemSet.title }}
</router-link> </router-link>
</n-text> </n-text>
</n-thing> </n-thing>
+7 -7
View File
@@ -174,20 +174,20 @@ export function adminRejudge(id: string) {
export function getSubmissionStatisticsItems( export function getSubmissionStatisticsItems(
duration: { start?: string; end: string }, duration: { start?: string; end: string },
username: string, username: string,
problemID?: string, problemDisplayId?: string,
) { ) {
return api.get<SubmissionStatisticsItems>("submissions/statistics/items", { return api.get<SubmissionStatisticsItems>("submissions/statistics/items", {
params: { ...duration, problemId: problemID, username }, params: { ...duration, problemDisplayId, username },
}) })
} }
export function getSubmissionStatistics( export function getSubmissionStatistics(
duration: { start?: string; end: string }, duration: { start?: string; end: string },
problemID?: string, problemDisplayId?: string,
username?: string, username?: string,
) { ) {
return api.get<SubmissionStatistics>("submissions/statistics", { return api.get<SubmissionStatistics>("submissions/statistics", {
params: { ...duration, problemId: problemID, username }, params: { ...duration, problemDisplayId, username },
}) })
} }
@@ -410,7 +410,7 @@ export function getFlowchartSubmission(id: string) {
export function getFlowchartSubmissions(params: { export function getFlowchartSubmissions(params: {
username?: string username?: string
problemId?: string problemDisplayId?: string
myself?: string myself?: string
offset?: number offset?: number
limit?: number limit?: number
@@ -422,11 +422,11 @@ export function getFlowchartSubmissions(params: {
export function getFlowchartStatistics( export function getFlowchartStatistics(
duration: { start?: string; end: string }, duration: { start?: string; end: string },
problemID?: string, problemDisplayId?: string,
username?: string, username?: string,
) { ) {
return api.get<FlowchartStatistics>("flowcharts/statistics", { return api.get<FlowchartStatistics>("flowcharts/statistics", {
params: { ...duration, problemId: problemID, username }, params: { ...duration, problemDisplayId, username },
}) })
} }
@@ -126,7 +126,7 @@ async function listSubmissions() {
...query, ...query,
myself: "1", myself: "1",
offset, offset,
problemId: (route.params.problemID as string) ?? "", problemDisplayId: (route.params.problemID as string) ?? "",
contestId: (route.params.contestID as string) ?? "", contestId: (route.params.contestID as string) ?? "",
}) })
submissions.value = res.results submissions.value = res.results
+1 -1
View File
@@ -49,7 +49,7 @@ async function loadUserBadges() {
const res = await getUserBadges() const res = await getUserBadges()
userBadges.value = res.filter( userBadges.value = res.filter(
(badge: UserBadgeType) => badge.badge.problemsetId === problemSetId.value, (badge: UserBadgeType) => badge.badge.problemSetId === problemSetId.value,
) )
} }
+1 -1
View File
@@ -16,7 +16,7 @@ export const useAIStore = defineStore("ai", () => {
const rangeEnd = ref("") const rangeEnd = ref("")
const durationData = ref<DurationData[]>([]) const durationData = ref<DurationData[]>([])
const detailsData = reactive<DetailsData>({ const detailsData = reactive<DetailsData>({
user: "", username: "",
start: "", start: "",
end: "", end: "",
grade: "", grade: "",
@@ -148,14 +148,18 @@ onMounted(async () => {
<template v-if="stats.problems.length"> <template v-if="stats.problems.length">
<n-divider style="margin: 16px 0">今天最热的题</n-divider> <n-divider style="margin: 16px 0">今天最热的题</n-divider>
<div class="rows"> <div class="rows">
<div class="row" v-for="row in stats.problems" :key="row.problem"> <div
class="row"
v-for="row in stats.problems"
:key="row.problemDisplayId"
>
<n-button <n-button
class="problem" class="problem"
text text
type="info" type="info"
@click="emit('openProblem', row.problem)" @click="emit('openProblem', row.problemDisplayId)"
> >
{{ row.problem }} {{ row.problemTitle }} {{ row.problemDisplayId }} {{ row.problemTitle }}
</n-button> </n-button>
<n-text class="row-count" depth="3"> <n-text class="row-count" depth="3">
{{ row.count }} 条 / 正确 {{ row.acceptedCount }} {{ row.count }} 条 / 正确 {{ row.acceptedCount }}
+11 -11
View File
@@ -160,7 +160,7 @@ async function listSubmissions() {
if (query.language === "Flowchart") { if (query.language === "Flowchart") {
const res = await getFlowchartSubmissions({ const res = await getFlowchartSubmissions({
username: query.username, username: query.username,
problemId: query.problem, problemDisplayId: query.problem,
myself: query.myself, myself: query.myself,
offset, offset,
limit: query.limit, limit: query.limit,
@@ -173,7 +173,7 @@ async function listSubmissions() {
const res = await getSubmissions({ const res = await getSubmissions({
...query, ...query,
offset, offset,
problemId: query.problem, problemDisplayId: query.problem,
contestId: (route.params.contestID as string) ?? "", contestId: (route.params.contestID as string) ?? "",
language: query.language, language: query.language,
today: query.today, today: query.today,
@@ -239,12 +239,12 @@ function problemClicked(row: SubmissionListItem | FlowchartSubmissionListItem) {
const path = router.resolve({ const path = router.resolve({
name: "contest problem", name: "contest problem",
params: { params: {
problemID: row.problem, problemID: row.problemDisplayId,
}, },
}) })
window.open(path.href, "_blank") window.open(path.href, "_blank")
} else { } else {
window.open("/problem/" + row.problem, "_blank") window.open("/problem/" + row.problemDisplayId, "_blank")
} }
} }
@@ -289,7 +289,7 @@ function consumePendingJump() {
message.info("这一页没有可以查看的代码") message.info("这一页没有可以查看的代码")
return return
} }
showCodePanel(row.id, row.problem) showCodePanel(row.id, row.problemDisplayId)
} }
function moveCodePanel(step: 1 | -1) { function moveCodePanel(step: 1 | -1) {
@@ -297,7 +297,7 @@ function moveCodePanel(step: 1 | -1) {
if (index === -1) return if (index === -1) return
const next = viewableSubmissions.value[index + step] const next = viewableSubmissions.value[index + step]
if (next) { if (next) {
showCodePanel(next.id, next.problem) showCodePanel(next.id, next.problemDisplayId)
return return
} }
const page = query.page + step const page = query.page + step
@@ -390,7 +390,7 @@ const columns = computed(() => {
render: (row) => render: (row) =>
h(SubmissionLink, { h(SubmissionLink, {
submission: row, submission: row,
onShowCode: () => showCodePanel(row.id, row.problem), onShowCode: () => showCodePanel(row.id, row.problemDisplayId),
}), }),
}, },
{ {
@@ -409,9 +409,9 @@ const columns = computed(() => {
{ {
type: "题目", type: "题目",
onClick: () => problemClicked(row), onClick: () => problemClicked(row),
onSearch: () => (query.problem = row.problem), onSearch: () => (query.problem = row.problemDisplayId),
}, },
() => `${row.problem} ${row.problemTitle}`, () => `${row.problemDisplayId} ${row.problemTitle}`,
) )
// 从题单入口做出来的提交才有这个标记(后端只在提交时记来源), // 从题单入口做出来的提交才有这个标记(后端只在提交时记来源),
// 同一道题从普通题库刷的不会带。老提交里只有当年首次 AC 那条有 // 同一道题从普通题库刷的不会带。老提交里只有当年首次 AC 那条有
@@ -507,9 +507,9 @@ const flowchartColumns = computed(() => {
{ {
type: "题目", type: "题目",
onClick: () => problemClicked(row), onClick: () => problemClicked(row),
onSearch: () => (query.problem = row.problem), onSearch: () => (query.problem = row.problemDisplayId),
}, },
() => `${row.problem} ${row.problemTitle}`, () => `${row.problemDisplayId} ${row.problemTitle}`,
), ),
}, },
{ {
+2 -2
View File
@@ -17,9 +17,9 @@
<n-button <n-button
text text
type="info" type="info"
@click="router.push('/problem/' + item.submission.problem)" @click="router.push('/problem/' + item.submission.problemDisplayId)"
> >
{{ item.submission.problem }} {{ item.submission.problemDisplayId }}
</n-button> </n-button>
<n-text :type="JUDGE_STATUS[item.submission.result]['type']"> <n-text :type="JUDGE_STATUS[item.submission.result]['type']">
{{ JUDGE_STATUS[item.submission.result]["name"] }} {{ JUDGE_STATUS[item.submission.result]["name"] }}
@@ -172,7 +172,7 @@
</template> </template>
<n-flex vertical size="small"> <n-flex vertical size="small">
<n-text depth="3"> <n-text depth="3">
最近一次:{{ item.failure?.problem }} · 最近一次:{{ item.failure?.problemDisplayId }} ·
{{ statusName(item.failure?.result) }} {{ statusName(item.failure?.result) }}
</n-text> </n-text>
<pre v-if="item.failure?.error" class="failure-error">{{ <pre v-if="item.failure?.error" class="failure-error">{{
@@ -252,17 +252,17 @@ function groupByProblem(list: SubmissionStatisticsItems["items"]) {
const groups = new Map< const groups = new Map<
string, string,
{ {
problem: string problemDisplayId: string
problemTitle: string problemTitle: string
items: SubmissionStatisticsItems["items"] items: SubmissionStatisticsItems["items"]
} }
>() >()
for (const item of list) { for (const item of list) {
const group = groups.get(item.problem) const group = groups.get(item.problemDisplayId)
if (group) group.items.push(item) if (group) group.items.push(item)
else else
groups.set(item.problem, { groups.set(item.problemDisplayId, {
problem: item.problem, problemDisplayId: item.problemDisplayId,
problemTitle: item.problemTitle, problemTitle: item.problemTitle,
items: [item], items: [item],
}) })
@@ -313,7 +313,7 @@ const columns: DataTableColumn<SubmissionStatisticsUser>[] = [
h( h(
NTag, NTag,
{ size: "small", bordered: false }, { size: "small", bordered: false },
() => group.problem, () => group.problemDisplayId,
), ),
h( h(
NText, NText,
+1 -1
View File
@@ -275,7 +275,7 @@ export interface SubmissionListPayload {
result?: string result?: string
username?: string username?: string
contestId?: string contestId?: string
problemId?: string problemDisplayId?: string
language: LANGUAGE | "" language: LANGUAGE | ""
today?: "1" | "0" today?: "1" | "0"
page: number page: number
+3 -3
View File
@@ -421,9 +421,9 @@ export const updateProblemSetStatusRequestSchema = z.object({
export const adminProblemSetProblemSchema = z.object({ export const adminProblemSetProblemSchema = z.object({
id: z.number().int(), id: z.number().int(),
problemsetId: z.number().int(), problemSetId: z.number().int(),
problemId: z.number().int(), problemId: z.number().int(),
displayId: z.string(), problemDisplayId: z.string(),
title: z.string(), title: z.string(),
difficulty: z.string(), difficulty: z.string(),
order: z.number().int(), order: z.number().int(),
@@ -450,7 +450,7 @@ export const updateProblemInSetRequestSchema = z.object({
export const adminProblemSetBadgeSchema = z.object({ export const adminProblemSetBadgeSchema = z.object({
id: z.number().int(), id: z.number().int(),
problemsetId: z.number().int(), problemSetId: z.number().int(),
name: z.string(), name: z.string(),
description: z.string(), description: z.string(),
icon: z.string(), icon: z.string(),
+1 -1
View File
@@ -59,7 +59,7 @@ export const activityBucketSchema = z.object({
}) })
export const aiDetailSchema = z.object({ export const aiDetailSchema = z.object({
user: z.string(), username: z.string(),
className: z.string().nullable(), className: z.string().nullable(),
start: z.string(), start: z.string(),
end: z.string(), end: z.string(),
+1 -1
View File
@@ -45,7 +45,7 @@ export const flowchartSubmissionSchema = z.object({
export const flowchartListItemSchema = z.object({ export const flowchartListItemSchema = z.object({
id: z.string(), id: z.string(),
username: z.string(), username: z.string(),
problem: z.string(), problemDisplayId: z.string(),
problemTitle: z.string(), problemTitle: z.string(),
status: flowchartStatusSchema, status: flowchartStatusSchema,
createTime: z.string(), createTime: z.string(),
+4 -4
View File
@@ -13,7 +13,7 @@ export const problemSetUserProgressSummarySchema = z.object({
export const problemSetBadgeSchema = z.object({ export const problemSetBadgeSchema = z.object({
id: z.number().int(), id: z.number().int(),
problemsetId: z.number().int(), problemSetId: z.number().int(),
name: z.string(), name: z.string(),
description: z.string(), description: z.string(),
icon: z.string(), icon: z.string(),
@@ -58,7 +58,7 @@ export const problemSetProblemItemSchema = z.object({
export const problemSetProblemSchema = z.object({ export const problemSetProblemSchema = z.object({
id: z.number().int(), id: z.number().int(),
problemsetId: z.number().int(), problemSetId: z.number().int(),
problem: problemSetProblemItemSchema, problem: problemSetProblemItemSchema,
order: z.number().int(), order: z.number().int(),
isRequired: z.boolean(), isRequired: z.boolean(),
@@ -79,7 +79,7 @@ export const completedProblemSchema = z.object({
export const problemSetProgressSchema = z.object({ export const problemSetProgressSchema = z.object({
id: z.number().int(), id: z.number().int(),
problemsetId: z.number().int(), problemSetId: z.number().int(),
user: sampleUserSchema, user: sampleUserSchema,
joinTime: z.string(), joinTime: z.string(),
completeTime: z.string().nullable(), completeTime: z.string().nullable(),
@@ -107,7 +107,7 @@ export const userBadgeSchema = z.object({
userId: z.number().int(), userId: z.number().int(),
badge: problemSetBadgeSchema, badge: problemSetBadgeSchema,
earnedTime: z.string(), earnedTime: z.string(),
problemset: z.object({ id: z.number().int(), title: z.string() }), problemSet: z.object({ id: z.number().int(), title: z.string() }),
}) })
export type ProblemSet = z.infer<typeof problemSetSchema> export type ProblemSet = z.infer<typeof problemSetSchema>
+7 -13
View File
@@ -191,20 +191,14 @@ export const submissionDetailSchema = z.object({
* 将来有人「顺手」把空值改成真值就不会变成泄露,因为这里压根没有这些字段。 * 将来有人「顺手」把空值改成真值就不会变成泄露,因为这里压根没有这些字段。
*/ */
export const embeddedSubmissionSchema = submissionDetailSchema export const embeddedSubmissionSchema = submissionDetailSchema
// problemDisplayId 也要去掉:下面的 problem 就是它,同一个值留两份, // 留下 problemDisplayId(展示用题号)而不是数字主键:站内信页面拿它拼
// 而路由只填了 problem —— 这里漏 omit 的那阵子,凡是收到过站内信的人 // `/problem/<题号>` 链接,给数字 id 会拼出打不开的地址。
// 打开消息页都是 500(parse 抛在缺失的 problemDisplayId 上,列表为空时才碰巧不炸)。
.omit({ .omit({
info: true, info: true,
contestId: true, contestId: true,
problemId: true, problemId: true,
problemDisplayId: true,
caseSummary: true, caseSummary: true,
}) })
// 旧 SubmissionSafeModelSerializer 里 problem 是
// `SlugRelatedField(slug_field="_id")`,即**展示用题号**而非数字主键。
// 站内信页面拿它拼 `/problem/<题号>` 链接,给数字 id 会拼出打不开的地址。
.extend({ problem: z.string() })
/** /**
* 判题进度推送。**只带前端真正要用的东西**:靠 submissionId 认领、靠 result / * 判题进度推送。**只带前端真正要用的东西**:靠 submissionId 认领、靠 result /
@@ -224,7 +218,7 @@ export const submissionUpdateSchema = z.object({
export const submissionListItemSchema = z.object({ export const submissionListItemSchema = z.object({
id: z.string(), id: z.string(),
problem: z.string(), problemDisplayId: z.string(),
problemTitle: z.string(), problemTitle: z.string(),
showLink: z.boolean(), showLink: z.boolean(),
createTime: z.string(), createTime: z.string(),
@@ -277,7 +271,7 @@ export const attemptedStudentSchema = unacceptedStudentSchema.extend({
.object({ .object({
id: z.string(), id: z.string(),
/** 题目的展示编号,用来告诉老师错在哪道题 */ /** 题目的展示编号,用来告诉老师错在哪道题 */
problem: z.string(), problemDisplayId: z.string(),
result: judgeStatusSchema, result: judgeStatusSchema,
error: z.string().nullable(), error: z.string().nullable(),
}) })
@@ -325,7 +319,7 @@ export const submissionStatisticsItemsSchema = z.object({
* 展开某个学生时列出他这段时间的提交。**带上题目**:一节课里学生往往在好几道题 * 展开某个学生时列出他这段时间的提交。**带上题目**:一节课里学生往往在好几道题
* 之间来回跳,一串只有编号的按钮看不出他卡在哪一道 —— 前端按题目分组展示。 * 之间来回跳,一串只有编号的按钮看不出他卡在哪一道 —— 前端按题目分组展示。
* *
* 字段名沿用 submissionListItemSchema 的口径:`problem` 是展示用题号(problem._id), * 字段名沿用 submissionListItemSchema 的口径:`problemDisplayId` 是展示用题号(problem._id),
* `problemTitle` 是标题。 * `problemTitle` 是标题。
*/ */
items: z.array( items: z.array(
@@ -333,7 +327,7 @@ export const submissionStatisticsItemsSchema = z.object({
id: z.string(), id: z.string(),
result: judgeStatusSchema, result: judgeStatusSchema,
createTime: z.string(), createTime: z.string(),
problem: z.string(), problemDisplayId: z.string(),
problemTitle: z.string(), problemTitle: z.string(),
}), }),
), ),
@@ -405,7 +399,7 @@ export const todaySubmissionStatisticsSchema = z.object({
*/ */
problems: z.array( problems: z.array(
z.object({ z.object({
problem: z.string(), problemDisplayId: z.string(),
problemTitle: z.string(), problemTitle: z.string(),
count: z.number().int(), count: z.number().int(),
acceptedCount: z.number().int(), acceptedCount: z.number().int(),