From 25b86ec17e1c39091cccae7fe1e8eee4362252e0 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 7 Sep 2026 07:18:41 -0600 Subject: [PATCH] =?UTF-8?q?refactor(=E6=8F=90=E4=BA=A4):=20=E5=88=A0?= =?UTF-8?q?=E6=8E=89=E4=BB=8E=E6=9D=A5=E6=B2=A1=E7=94=A8=E8=B5=B7=E6=9D=A5?= =?UTF-8?q?=E7=9A=84=E3=80=8C=E6=8F=90=E4=BA=A4=E4=BA=92=E7=9B=B8=E5=8F=AF?= =?UTF-8?q?=E8=A7=81=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit problem.share_submission(题目级)和 submission.shared(单条)两个开关,连同 判定分支一起删掉。 生产备份(2026-08-07)里的实际用量:956 道题只有 2 道开过 share_submission, 还都是 contest_id=1 的比赛题 —— 比赛未结束时那条分支根本走不到,等于一天都没 生效过。123140 条提交里 shared=true 的有 40 条,39 条在 2022 年、1 条在 2023-03-30,之后近三年零使用;出题页从来没给过题目级开关,单条分享的入口在更早 那版前端上,ojnext 和 OJ2 都没搬过来,OJ2 里 PUT /submissions/:id 一个调用方 都没有。 canViewSubmission 剩下三条:本人 / 管理员(比赛中的 Student Admin 除外)/ 本题 作者,其余一律看不到。结尾的 `shareSubmission || shared` 没了;它上面那条「比赛 未结束一律不给」也一并去掉 —— 走到那里的必然不是这三种人,现在无论比赛与否都是 false,留着是重复的。allowShared 参数随之消失,它唯一的用途就是分享开关的归属 校验。 一并删掉:PUT /submissions/:id、响应里的 shared / canUnshare(列表、详情、站内 信内嵌三处)、题目详情与后台题目里的 shareSubmission、shareSubmissionRequestSchema, 以及新建提交时那句 shared: false(列上本来就有 default false)。 两个数据库列保留不删,只在 schema.ts 上注明已停用:删列是破坏性迁移,而留着不读 不写零成本,还留着那 40 条的历史痕迹。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XvmqDsZNyUo9P3sFQtoWVB --- apps/api/src/db/schema.ts | 11 ++++++ apps/api/src/routes/admin/problem.ts | 2 -- apps/api/src/routes/content.ts | 2 -- apps/api/src/routes/contest.ts | 1 - apps/api/src/routes/problem.ts | 1 - apps/api/src/routes/submission.ts | 49 ++++++--------------------- apps/web/src/admin/api.ts | 1 - apps/web/src/admin/problem/detail.vue | 2 -- packages/contract/src/admin.ts | 2 -- packages/contract/src/problem.ts | 1 - packages/contract/src/submission.ts | 6 ---- 11 files changed, 22 insertions(+), 56 deletions(-) diff --git a/apps/api/src/db/schema.ts b/apps/api/src/db/schema.ts index 8494b93..964f488 100644 --- a/apps/api/src/db/schema.ts +++ b/apps/api/src/db/schema.ts @@ -379,6 +379,12 @@ export const problem = pgTable("problem", { statisticInfo: jsonb("statistic_info").default({}).notNull(), contestId: integer("contest_id"), isPublic: boolean("is_public").default(false).notNull(), + /** + * 已停用。「提交互相可见」的两个开关(这个是题目级,submission.shared 是单条级) + * 连同判定分支一起删掉了:生产库 956 道题里只有 2 道打开过,还都是比赛题 + * (比赛未结束时那条分支根本走不到),出题页也从来没给过开关。 + * 列保留不删:删列是破坏性迁移,而留着不写不读没有任何代价。 + */ shareSubmission: boolean("share_submission").default(false).notNull(), prompt: text(), answers: jsonb(), @@ -446,6 +452,11 @@ export const submission = pgTable("submission", { result: integer().default(6).notNull(), info: jsonb().default({}).notNull(), language: text().notNull(), + /** + * 已停用,见 problem.share_submission 的说明。历史上 12.3 万条提交里有 40 条 + * 为真(2022 年 39 条、2023 年 1 条),入口在更早的那版前端上,ojnext 和 OJ2 + * 都没有把它搬过来。现在没有任何代码读写它,行里的历史值原样留着。 + */ shared: boolean().default(false).notNull(), statisticInfo: jsonb("statistic_info").default({}).notNull(), username: text().notNull(), diff --git a/apps/api/src/routes/admin/problem.ts b/apps/api/src/routes/admin/problem.ts index 0164ba8..451a606 100644 --- a/apps/api/src/routes/admin/problem.ts +++ b/apps/api/src/routes/admin/problem.ts @@ -153,7 +153,6 @@ async function serialize(row: ProblemRow) { submissionNumber: row.submissionNumber, acceptedNumber: row.acceptedNumber, statisticInfo: objectValue(row.statisticInfo), - shareSubmission: row.shareSubmission, contestId: row.contestId, createdBy: sampleUser(creator ?? { id: row.createdById, username: "" }, creator?.realName), isPublic: row.isPublic, @@ -247,7 +246,6 @@ function problemValues(data: ReturnType visible: data.visible, difficulty: data.difficulty, source: data.source, - shareSubmission: data.shareSubmission, allowFlowchart: data.allowFlowchart, showFlowchart: data.showFlowchart, mermaidCode: data.mermaidCode, diff --git a/apps/api/src/routes/content.ts b/apps/api/src/routes/content.ts index 653b889..bb85b7d 100644 --- a/apps/api/src/routes/content.ts +++ b/apps/api/src/routes/content.ts @@ -100,12 +100,10 @@ contentRoutes.get("/messages", requireAuth, async (c) => { // info / ip / contestId 三个字段不在 embeddedSubmissionSchema 里,故不传 —— // 对齐旧后端 SubmissionSafeModelSerializer 的 exclude,这三个键不出现在响应中 language: submission.language, - shared: submission.shared, statisticInfo: objectValue(submission.statisticInfo), // 展示用题号而非数字主键,站内信页面拿它拼 /problem/<题号> problem: displayId, showLink: true, - canUnshare: false, }), })), total: totalRows[0]?.value ?? 0, diff --git a/apps/api/src/routes/contest.ts b/apps/api/src/routes/contest.ts index e5b6b4e..4992b64 100644 --- a/apps/api/src/routes/contest.ts +++ b/apps/api/src/routes/contest.ts @@ -186,7 +186,6 @@ contestRoutes.get("/contests/:id/problems/:displayId", optionalAuth, requireCont submissionNumber: allowed ? row.problem.submissionNumber : 0, acceptedNumber: allowed ? row.problem.acceptedNumber : 0, statisticInfo: allowed ? objectValue(row.problem.statisticInfo) : {}, - shareSubmission: row.problem.shareSubmission, contestId: contest.id, tags: tags.get(row.problem.id) ?? [], createdBy: sampleUser(row.user, row.realName), diff --git a/apps/api/src/routes/problem.ts b/apps/api/src/routes/problem.ts index f97abe5..bed20ee 100644 --- a/apps/api/src/routes/problem.ts +++ b/apps/api/src/routes/problem.ts @@ -304,7 +304,6 @@ problemRoutes.get("/problems/:displayId", optionalAuth, async (c) => { submissionNumber: row.problem.submissionNumber, acceptedNumber: row.problem.acceptedNumber, statisticInfo: objectValue(row.problem.statisticInfo), - shareSubmission: row.problem.shareSubmission, contestId: row.problem.contestId, tags: tagRows.map((tag) => tag.name), createdBy: sampleUser({ id: row.creatorId, username: row.creatorUsername }, null), diff --git a/apps/api/src/routes/submission.ts b/apps/api/src/routes/submission.ts index 731eead..0c4f58f 100644 --- a/apps/api/src/routes/submission.ts +++ b/apps/api/src/routes/submission.ts @@ -5,7 +5,6 @@ import { createSubmissionResponseSchema, formatCodeRequestSchema, formatCodeResponseSchema, - shareSubmissionRequestSchema, submissionDetailSchema, submissionListItemSchema, submissionListSchema, @@ -141,7 +140,6 @@ submissionRoutes.post("/submissions", requireAuth, async (c) => { result: JudgeStatus.PENDING, info: {}, language: parsed.data.language, - shared: false, statisticInfo: {}, contestId, }) @@ -423,19 +421,15 @@ async function problemSetJoinTimes(userId: number, problemIds: number[]) { // 传不进完整行。完整行在结构上满足这两个窄类型,详情接口照旧调用不受影响。 function canViewSubmission( user: AuthUser | null, - row: { userId: number; shared: boolean; problemId: number; createTime: string }, - problem: { createdById: number; shareSubmission: boolean }, + row: { userId: number; problemId: number; createTime: string }, + problem: { createdById: number }, contest: typeof schema.contest.$inferSelect | null, - allowShared = true, problemSetJoinTime?: Map, ) { if (!user) return false // 题单防作弊,见 problemSetJoinTimes。只对学生自己的提交生效,管理员不受限,对齐旧后端 // `get_show_link` 里的 `obj.user_id == self.user.id and self.user.is_regular_user()`。 - // - // 只挡「看代码」这一路,不挡 allowShared=false 的那一路:后者是分享/取消分享的归属校验, - // 与作弊无关,挡了会让学生连自己旧提交的分享开关都动不了。 - if (allowShared && row.userId === user.id && !isAdminRole(user)) { + if (row.userId === user.id && !isAdminRole(user)) { const joinTime = problemSetJoinTime?.get(row.problemId) if (joinTime !== undefined && Date.parse(row.createTime) < Date.parse(joinTime)) return false } @@ -448,10 +442,12 @@ function canViewSubmission( // 他早就知道答案了,挡他没有意义。 const elevated = isAdminRole(user) && !(contest && contestStatus(contest) !== "-1" && user.adminType === "Student Admin") - if (row.userId === user.id || elevated || problem.createdById === user.id) return true - if (!allowShared) return false - if (contest && contestStatus(contest) !== "-1") return false - return problem.shareSubmission || row.shared + // 这三条就是全部:别人的代码谁都看不到,比赛内外一样。 + // 分享功能(problem.share_submission 题目级 / submission.shared 单条)已经删掉, + // 原来结尾的 `return problem.shareSubmission || row.shared` 随之消失;它上面那条 + // 「比赛未结束一律不给」也一并去掉 —— 走到那里的必然不是本人/管理员/作者, + // 现在无论比赛与否都是 false,留着是重复的。 + return row.userId === user.id || elevated || problem.createdById === user.id } /** @@ -470,7 +466,6 @@ const submissionListColumns = { username: schema.submission.username, result: schema.submission.result, language: schema.submission.language, - shared: schema.submission.shared, statisticInfo: schema.submission.statisticInfo, // 只取 id,题单标题按页单独查一次(见 /submissions)——把 problemset 一起 join 进来 // 会动到下面那条调过的分页查询,而每页最多两三个不同的题单,PK 查一次更便宜 @@ -479,7 +474,6 @@ const submissionListColumns = { problem: { displayId: schema.problem.displayId, title: schema.problem.title, - shareSubmission: schema.problem.shareSubmission, createdById: schema.problem.createdById, }, } as const @@ -496,7 +490,7 @@ async function submissionDetail(id: string, user: AuthUser) { const joinTimes = isAdminRole(user) || row.submission.userId !== user.id ? undefined : await problemSetJoinTimes(user.id, [row.submission.problemId]) - if (!canViewSubmission(user, row.submission, row.problem, row.contest, true, joinTimes)) return null + if (!canViewSubmission(user, row.submission, row.problem, row.contest, joinTimes)) return null // info(含每个测试点的 test_case 编号与 output_md5)只给管理员,对齐旧后端: // submission/views/oj.py 用 is_admin_role() 在 SubmissionModelSerializer 与 // SubmissionSafeModelSerializer 之间二选一,把关的是角色,不是「是不是自己的提交」。 @@ -510,7 +504,6 @@ async function submissionDetail(id: string, user: AuthUser) { result: row.submission.result, info: full ? row.submission.info : {}, language: row.submission.language, - shared: row.submission.shared, statisticInfo: objectValue(row.submission.statisticInfo), // contest 也在旧后端的排除名单里,同样只给管理员 contestId: full ? row.submission.contestId : null, @@ -518,7 +511,6 @@ async function submissionDetail(id: string, user: AuthUser) { // problem 表本来就 join 了,不额外查库 problemDisplayId: row.problem.displayId, showLink: true, - canUnshare: canViewSubmission(user, row.submission, row.problem, row.contest, false), }) } @@ -634,13 +626,12 @@ submissionRoutes.get("/submissions", optionalAuth, async (c) => { id: submission.id, problem: problem.displayId, problemTitle: problem.title, - showLink: user ? canViewSubmission(user, submission, problem, null, true, joinTimes) : false, + showLink: user ? canViewSubmission(user, submission, problem, null, joinTimes) : false, createTime: submission.createTime, userId: submission.userId, username: submission.username, result: submission.result, language: submission.language, - shared: submission.shared, statisticInfo: objectValue(submission.statisticInfo), // 题单被删掉之后外键把 problemset_id 置了空,这里自然就没标记了 problemSet: submission.problemsetId !== null && problemsetTitles.has(submission.problemsetId) @@ -693,7 +684,6 @@ submissionRoutes.get("/contests/:contestId/submissions", optionalAuth, requireCo username: submission.username, result: submission.result, language: submission.language, - shared: submission.shared, statisticInfo: objectValue(submission.statisticInfo), // 比赛提交没有来源题单:题单只收非比赛题(admin/problemset.ts 加题时卡了 // isNull(problem.contestId)),提交接口那边也只在 contestId 为空时才认这个字段 @@ -711,20 +701,3 @@ submissionRoutes.get("/submissions/:id", requireAuth, async (c) => { } return success(c, data) }) - -submissionRoutes.put("/submissions/:id", requireAuth, async (c) => { - const parsed = shareSubmissionRequestSchema.safeParse(await c.req.json().catch(() => null)) - if (!parsed.success) return failure(c, 400, "invalid-request", "Invalid share payload") - const [row] = await db.select({ submission: schema.submission, problem: schema.problem, contest: schema.contest }) - .from(schema.submission).innerJoin(schema.problem, eq(schema.submission.problemId, schema.problem.id)) - .leftJoin(schema.contest, eq(schema.submission.contestId, schema.contest.id)) - .where(eq(schema.submission.id, c.req.param("id"))).limit(1) - if (!row || !canViewSubmission(c.get("user")!, row.submission, row.problem, row.contest, false)) { - return failure(c, 404, "submission-not-found", "Submission does not exist") - } - if (row.contest && contestStatus(row.contest) === "0") { - return failure(c, 403, "contest-underway", "Can not share submission now") - } - await db.update(schema.submission).set({ shared: parsed.data.shared }).where(eq(schema.submission.id, row.submission.id)) - return success(c, null) -}) diff --git a/apps/web/src/admin/api.ts b/apps/web/src/admin/api.ts index 845e83f..29c8afb 100644 --- a/apps/web/src/admin/api.ts +++ b/apps/web/src/admin/api.ts @@ -274,7 +274,6 @@ function toProblemBody(problem: AdminProblem | BlankProblem) { source: p.source ?? null, prompt: p.prompt ?? null, answers: p.answers ?? [], - shareSubmission: p.shareSubmission ?? false, allowFlowchart: p.allowFlowchart ?? false, showFlowchart: p.showFlowchart ?? false, mermaidCode: p.mermaidCode ?? null, diff --git a/apps/web/src/admin/problem/detail.vue b/apps/web/src/admin/problem/detail.vue index 4193213..1cd24b9 100644 --- a/apps/web/src/admin/problem/detail.vue +++ b/apps/web/src/admin/problem/detail.vue @@ -62,7 +62,6 @@ const problem = useLocalStorage(STORAGE_KEY.ADMIN_PROBLEM, { memoryLimit: 64, difficulty: "Low", visible: false, - shareSubmission: false, tags: [], languages: ["Python3", "C"] as LANGUAGE[], template: {} as { [key in LANGUAGE]?: string }, @@ -241,7 +240,6 @@ async function getProblemDetail() { problem.value.memoryLimit = data.memoryLimit problem.value.difficulty = data.difficulty problem.value.visible = data.visible - problem.value.shareSubmission = data.shareSubmission problem.value.tags = normalizeTagNames(data.tags) problem.value.languages = data.languages problem.value.template = data.template diff --git a/packages/contract/src/admin.ts b/packages/contract/src/admin.ts index 2794e9a..037c0d7 100644 --- a/packages/contract/src/admin.ts +++ b/packages/contract/src/admin.ts @@ -678,7 +678,6 @@ export const adminProblemSchema = z.object({ submissionNumber: z.number().int(), acceptedNumber: z.number().int(), statisticInfo: z.record(z.string(), z.unknown()), - shareSubmission: z.boolean(), contestId: z.number().int().nullable(), createdBy: sampleUserSchema, isPublic: z.boolean(), @@ -726,7 +725,6 @@ export const createProblemRequestSchema = z.object({ source: z.string().max(256).nullable().default(null), prompt: z.string().nullable().default(null), answers: z.array(problemAnswerSchema).default([]), - shareSubmission: z.boolean(), allowFlowchart: z.boolean().default(false), showFlowchart: z.boolean().default(false), mermaidCode: z.string().nullable().default(null), diff --git a/packages/contract/src/problem.ts b/packages/contract/src/problem.ts index 9a926ca..bee9b98 100644 --- a/packages/contract/src/problem.ts +++ b/packages/contract/src/problem.ts @@ -308,7 +308,6 @@ export const problemDetailSchema = z.object({ submissionNumber: z.number().int(), acceptedNumber: z.number().int(), statisticInfo: z.record(z.string(), z.unknown()), - shareSubmission: z.boolean(), contestId: z.number().int().nullable(), tags: z.array(z.string()), createdBy: z.object({ diff --git a/packages/contract/src/submission.ts b/packages/contract/src/submission.ts index 148c708..3a19d5a 100644 --- a/packages/contract/src/submission.ts +++ b/packages/contract/src/submission.ts @@ -46,7 +46,6 @@ export const submissionDetailSchema = z.object({ result: judgeStatusSchema, info: z.unknown(), language: z.string(), - shared: z.boolean(), statisticInfo: z.record(z.string(), z.unknown()), contestId: z.number().int().nullable(), problemId: z.number().int(), @@ -58,7 +57,6 @@ export const submissionDetailSchema = z.object({ */ problemDisplayId: z.string(), showLink: z.boolean(), - canUnshare: z.boolean(), }) /** @@ -102,7 +100,6 @@ export const submissionListItemSchema = z.object({ username: z.string(), result: judgeStatusSchema, language: z.string(), - shared: z.boolean(), statisticInfo: z.record(z.string(), z.unknown()), /** * 来源题单,非题单入口提交的为 null。比赛提交恒为 null(比赛题不会进题单)。 @@ -114,8 +111,6 @@ export const submissionListItemSchema = z.object({ export const submissionListSchema = paginatedSchema(submissionListItemSchema) -export const shareSubmissionRequestSchema = z.object({ shared: z.boolean() }) - /** * 未完成学生。`realName` 是从用户名里剥掉 `ks<班级号>` 前缀后剩下的那一段, * 不是 user.real_name 列 —— 与 F2「真名默认不下发」不冲突:这里只有教师能看到, @@ -173,5 +168,4 @@ export type EmbeddedSubmission = z.infer export type CreateSubmissionResponse = z.infer export type FormatCodeResponse = z.infer -export type ShareSubmissionRequest = z.infer export type FormatCodeRequest = z.infer