Compare commits
2 Commits
d1d071ff7b
...
25b86ec17e
| Author | SHA1 | Date | |
|---|---|---|---|
| 25b86ec17e | |||
| 2256597838 |
@@ -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(),
|
||||
|
||||
@@ -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<typeof createProblemRequestSchema.parse>
|
||||
visible: data.visible,
|
||||
difficulty: data.difficulty,
|
||||
source: data.source,
|
||||
shareSubmission: data.shareSubmission,
|
||||
allowFlowchart: data.allowFlowchart,
|
||||
showFlowchart: data.showFlowchart,
|
||||
mermaidCode: data.mermaidCode,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<number, string>,
|
||||
) {
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -84,6 +84,8 @@ export function deleteContestProblem(id: number) {
|
||||
return api.delete(`admin/problems/${id}`)
|
||||
}
|
||||
|
||||
// 比赛题也走这条:后端是同一条 PUT admin/problems/:id,比赛由题目自己推导,
|
||||
// 所以不像新建那样需要分两个函数
|
||||
export function editProblem(problem: AdminProblem | BlankProblem) {
|
||||
return api.put<AdminProblem>(
|
||||
`admin/problems/${(problem as AdminProblem).id}`,
|
||||
@@ -103,13 +105,6 @@ export function generateFlowchartFromPythonCode(python: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export function editContestProblem(problem: AdminProblem | BlankProblem) {
|
||||
return api.put<AdminProblem>(
|
||||
`admin/problems/${(problem as AdminProblem).id}`,
|
||||
toProblemBody(problem),
|
||||
)
|
||||
}
|
||||
|
||||
export function getProblem(id: string | number) {
|
||||
return api.get<AdminProblem>(`admin/problems/${id}`)
|
||||
}
|
||||
@@ -279,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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { PROBLEM_TAG_MAX_LENGTH } from "@oj2/contract"
|
||||
import { getProblemTagList } from "shared/api"
|
||||
import TextEditor from "shared/components/TextEditor.vue"
|
||||
import TestcaseGenerator from "./components/TestcaseGenerator.vue"
|
||||
@@ -15,7 +16,6 @@ import type { BlankProblem, LANGUAGE, Tag, Testcase } from "utils/types"
|
||||
import {
|
||||
createContestProblem,
|
||||
createProblem,
|
||||
editContestProblem,
|
||||
editProblem,
|
||||
generateFlowchartFromPythonCode,
|
||||
getProblem,
|
||||
@@ -62,7 +62,6 @@ const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
|
||||
memoryLimit: 64,
|
||||
difficulty: "Low",
|
||||
visible: false,
|
||||
shareSubmission: false,
|
||||
tags: [],
|
||||
languages: ["Python3", "C"] as LANGUAGE[],
|
||||
template: {} as { [key in LANGUAGE]?: string },
|
||||
@@ -136,6 +135,12 @@ function validateNewTags(v: string[]) {
|
||||
const existing = new Set(tagList.value.map((t) => t.name))
|
||||
const blanks: string[] = []
|
||||
for (const tag of unique(v)) {
|
||||
// 输入框已经挂了 maxlength,这里兜住粘贴等绕过的情况。只丢这一个标签、
|
||||
// 不像重复那支那样 break —— 后端撞上来只会回一句 zod 的英文
|
||||
if (tag.length > PROBLEM_TAG_MAX_LENGTH) {
|
||||
message.error(`标签最多 ${PROBLEM_TAG_MAX_LENGTH} 个字:` + tag)
|
||||
continue
|
||||
}
|
||||
if (existing.has(tag)) {
|
||||
message.error("已经存在标签:" + tag)
|
||||
break
|
||||
@@ -213,6 +218,11 @@ watch(
|
||||
|
||||
async function getProblemDetail() {
|
||||
if (!props.problemID) {
|
||||
// 草稿缓存和编辑页共用同一个 localStorage key:编辑页会把服务器数据连 id
|
||||
// 一起写进去,没保存就离开的话,这个 id 会跟着草稿漂到新建页 ——「下载测试点」
|
||||
// 下的是那道旧题的包,SQL 测试点编辑器也会去回显那道旧题的脚本。
|
||||
// 新建页不存在 id,进来先摘掉。
|
||||
delete problem.value.id
|
||||
syncTagInputsFromProblemTags()
|
||||
toggleReady(true)
|
||||
return
|
||||
@@ -230,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
|
||||
@@ -445,7 +454,7 @@ async function submit() {
|
||||
"admin problem create": createProblem,
|
||||
"admin problem edit": editProblem,
|
||||
"admin contest problem create": createContestProblem,
|
||||
"admin contest problem edit": editContestProblem,
|
||||
"admin contest problem edit": editProblem,
|
||||
}[route.name as string]
|
||||
if (
|
||||
route.name === "admin contest problem create" ||
|
||||
@@ -588,6 +597,7 @@ watch(
|
||||
</n-flex>
|
||||
<n-dynamic-tags
|
||||
v-model:value="newTags"
|
||||
:input-props="{ maxlength: PROBLEM_TAG_MAX_LENGTH }"
|
||||
@update:value="validateNewTags"
|
||||
/>
|
||||
</n-flex>
|
||||
|
||||
@@ -83,8 +83,12 @@ export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), "PUBLIC_")
|
||||
|
||||
// 开发时一律指向本机后端(apps/api,3000)。
|
||||
// **必须写 IP,不能写 localhost**:api 用 Bun.serve 起,只绑 IPv4 的
|
||||
// 0.0.0.0:3000,而 Node 解析 localhost 时 ::1 排在前面。别的项目的 dev server
|
||||
// 一旦占着 [::1]:3000,两边端口不冲突(一个 v4 一个 v6,谁都不报错),
|
||||
// 浏览器发出的 /api/* 就整个落到那个站上 —— 表现是后台一进就被弹回首页。
|
||||
const backend = {
|
||||
target: "http://localhost:3000",
|
||||
target: "http://127.0.0.1:3000",
|
||||
changeOrigin: true,
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
@@ -694,6 +693,18 @@ export const adminProblemSchema = z.object({
|
||||
sqlDisplay: sqlDisplaySchema.nullable(),
|
||||
})
|
||||
|
||||
/**
|
||||
* 出题页新建标签时的名字上限,沿用旧 CreateProblemSerializer 的 32。
|
||||
* 出题页的输入框拿它做 maxlength —— 不共享一个常量的话,超长标签只会在保存时
|
||||
* 撞出 zod 的英文报错("Too big: expected string to have <=32 characters"),
|
||||
* 老师看不懂也不知道该改哪里。
|
||||
*
|
||||
* 列本身是 text 没有长度限制,标签管理页(createTagRequestSchema /
|
||||
* renameTagRequestSchema)放到 64 —— 两处口径不同是旧后端就有的,这里只是把
|
||||
* 出题页这一侧的数字挪到一处,没有改任何校验的松紧。
|
||||
*/
|
||||
export const PROBLEM_TAG_MAX_LENGTH = 32
|
||||
|
||||
export const createProblemRequestSchema = z.object({
|
||||
_id: z.string().trim().min(1).max(32),
|
||||
title: z.string().trim().min(1).max(1024),
|
||||
@@ -709,12 +720,11 @@ export const createProblemRequestSchema = z.object({
|
||||
template: z.record(z.string(), z.string()),
|
||||
visible: z.boolean(),
|
||||
difficulty: z.enum(["Low", "Mid", "High"]),
|
||||
tags: z.array(z.string().max(32)).min(1),
|
||||
tags: z.array(z.string().max(PROBLEM_TAG_MAX_LENGTH)).min(1),
|
||||
hint: z.string().nullable().default(null),
|
||||
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),
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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<typeof embeddedSubmissionSchema>
|
||||
export type CreateSubmissionResponse = z.infer<typeof createSubmissionResponseSchema>
|
||||
export type FormatCodeResponse = z.infer<typeof formatCodeResponseSchema>
|
||||
|
||||
export type ShareSubmissionRequest = z.infer<typeof shareSubmissionRequestSchema>
|
||||
export type FormatCodeRequest = z.infer<typeof formatCodeRequestSchema>
|
||||
|
||||
Reference in New Issue
Block a user