From e01045b19de457f08288379a84c88fe39a92e783 Mon Sep 17 00:00:00 2001 From: thezzisu Date: Mon, 23 Mar 2026 19:34:23 +0800 Subject: [PATCH 1/3] feat: add pull rejudge mode for single solutions --- .../src/components/solution/SolutionView.ts | 4 +- .../src/components/solution/SolutionView.vue | 13 +++ .../src/routes/contest/solution/index.ts | 90 +++++++++++++------ apps/server/src/routes/problem/solution.ts | 70 +++++++++------ 4 files changed, 123 insertions(+), 54 deletions(-) diff --git a/apps/frontend/src/components/solution/SolutionView.ts b/apps/frontend/src/components/solution/SolutionView.ts index 658b200c..f6391b08 100644 --- a/apps/frontend/src/components/solution/SolutionView.ts +++ b/apps/frontend/src/components/solution/SolutionView.ts @@ -20,6 +20,7 @@ export interface ISolutionViewProps { export function useSolutionView(props: ISolutionViewProps) { const app = useAppState() const settings = props.contestId ? useContestSettings() : useProblemSettings() + const pull = ref(true) const showDetails = computed(() => { if (props.admin) return true if (solution.state.value?.userId === app.userId) { @@ -67,7 +68,7 @@ export function useSolutionView(props: ISolutionViewProps) { const url = props.contestId ? `contest/${props.contestId}/solution/${props.solutionId}/rejudge` : `problem/${props.problemId}/solution/${props.solutionId}/rejudge` - await http.post(url) + await http.post(url, { json: { pull: pull.value } }) solution.execute() autoRefresh.resume() }) @@ -88,6 +89,7 @@ export function useSolutionView(props: ISolutionViewProps) { solution, showDetails, showData, + pull, viewFile, downloadEndpoint, submit, diff --git a/apps/frontend/src/components/solution/SolutionView.vue b/apps/frontend/src/components/solution/SolutionView.vue index a3350828..92bd2ed9 100644 --- a/apps/frontend/src/components/solution/SolutionView.vue +++ b/apps/frontend/src/components/solution/SolutionView.vue @@ -89,9 +89,17 @@ @click="submit.execute()" :loading="submit.isLoading.value" /> + @@ -143,6 +151,7 @@ const { solution, showDetails, showData, + pull, viewFile, downloadEndpoint, submit, @@ -157,9 +166,13 @@ en: info: Info details: Details data: Data + rejudge-mode-pin: Fixed rejudge + rejudge-mode-pull: Pull rejudge zh-Hans: solution: info: 信息 details: 细节 data: 数据 + rejudge-mode-pin: 固定重测 + rejudge-mode-pull: 拉取重测 diff --git a/apps/server/src/routes/contest/solution/index.ts b/apps/server/src/routes/contest/solution/index.ts index f7b2c4d3..023dcfac 100644 --- a/apps/server/src/routes/contest/solution/index.ts +++ b/apps/server/src/routes/contest/solution/index.ts @@ -94,36 +94,74 @@ const solutionScopedRoutes = defineRoutes(async (s) => { } ) - s.post('/rejudge', {}, async (req, rep) => { - const ctx = req.inject(kContestContext) + s.post( + '/rejudge', + { + schema: { + body: T.Object({ + pull: T.Optional(T.Boolean()) + }) + } + }, + async (req, rep) => { + const ctx = req.inject(kContestContext) - const solutionId = loadUUID(req.params, 'solutionId', s.httpErrors.badRequest()) - const admin = hasCapability(ctx._contestCapability, CONTEST_CAPS.CAP_ADMIN) - if (!admin) return rep.forbidden() + const solutionId = loadUUID(req.params, 'solutionId', s.httpErrors.badRequest()) + const admin = hasCapability(ctx._contestCapability, CONTEST_CAPS.CAP_ADMIN) + if (!admin) return rep.forbidden() - const { modifiedCount } = await solutions.updateOne( - { - _id: solutionId, - contestId: ctx._contestId, - state: { $ne: SolutionState.CREATED } - }, - [ + const { pull } = req.body + let label: string | undefined + let problemDataHash: string | undefined + if (pull) { + const solution = await solutions.findOne( + { + _id: solutionId, + contestId: ctx._contestId, + state: { $ne: SolutionState.CREATED } + }, + { projection: { problemId: 1 } } + ) + if (!solution) return rep.notFound() + + const problem = await s.db.problems.findOne( + { _id: solution.problemId }, + { projection: { currentDataHash: 1, data: 1 } } + ) + if (!problem) return rep.notFound() + const currentData = problem.data.find(({ hash }) => hash === problem.currentDataHash) + if (!currentData) return rep.preconditionFailed('Current data not found') + + label = currentData.config.label + problemDataHash = problem.currentDataHash + } + + const { modifiedCount } = await solutions.updateOne( { - $set: { - state: SolutionState.PENDING, - score: 0, - status: '', - metrics: {}, - message: '' - } + _id: solutionId, + contestId: ctx._contestId, + state: { $ne: SolutionState.CREATED } }, - { $unset: ['taskId', 'runnerId'] } - ], - { ignoreUndefined: true } - ) - if (modifiedCount === 0) return rep.notFound() - return {} - }) + [ + { + $set: { + label, + problemDataHash, + state: SolutionState.PENDING, + score: 0, + status: '', + metrics: {}, + message: '' + } + }, + { $unset: ['taskId', 'runnerId'] } + ], + { ignoreUndefined: true } + ) + if (modifiedCount === 0) return rep.notFound() + return {} + } + ) s.get( '/', diff --git a/apps/server/src/routes/problem/solution.ts b/apps/server/src/routes/problem/solution.ts index 39b6b2f4..345bec20 100644 --- a/apps/server/src/routes/problem/solution.ts +++ b/apps/server/src/routes/problem/solution.ts @@ -62,37 +62,53 @@ const solutionScopedRoutes = defineRoutes(async (s) => { } ) - s.post('/rejudge', {}, async (req, rep) => { - const ctx = req.inject(kProblemContext) + s.post( + '/rejudge', + { + schema: { + body: T.Object({ + pull: T.Optional(T.Boolean()) + }) + } + }, + async (req, rep) => { + const ctx = req.inject(kProblemContext) - const solutionId = loadUUID(req.params, 'solutionId', s.httpErrors.badRequest()) - const admin = hasCapability(ctx._problemCapability, PROBLEM_CAPS.CAP_ADMIN) - if (!admin) return rep.forbidden() + const solutionId = loadUUID(req.params, 'solutionId', s.httpErrors.badRequest()) + const admin = hasCapability(ctx._problemCapability, PROBLEM_CAPS.CAP_ADMIN) + if (!admin) return rep.forbidden() - const { modifiedCount } = await solutions.updateOne( - { - _id: solutionId, - contestId: { $exists: false }, - problemId: ctx._problemId, - state: { $ne: SolutionState.CREATED } - }, - [ + const { pull } = req.body + const currentData = ctx._problem.data.find(({ hash }) => hash === ctx._problem.currentDataHash) + if (pull && !currentData) return rep.preconditionFailed('Current data not found') + + const { modifiedCount } = await solutions.updateOne( { - $set: { - state: SolutionState.PENDING, - score: 0, - status: '', - metrics: {}, - message: '' - } + _id: solutionId, + contestId: { $exists: false }, + problemId: ctx._problemId, + state: { $ne: SolutionState.CREATED } }, - { $unset: ['taskId', 'runnerId'] } - ], - { ignoreUndefined: true } - ) - if (modifiedCount === 0) return rep.notFound() - return {} - }) + [ + { + $set: { + label: pull ? currentData?.config.label : undefined, + problemDataHash: pull ? ctx._problem.currentDataHash : undefined, + state: SolutionState.PENDING, + score: 0, + status: '', + metrics: {}, + message: '' + } + }, + { $unset: ['taskId', 'runnerId'] } + ], + { ignoreUndefined: true } + ) + if (modifiedCount === 0) return rep.notFound() + return {} + } + ) s.get( '/', From d263e591e24cb95292210084ebfb83e54882af84 Mon Sep 17 00:00:00 2001 From: thezzisu Date: Mon, 23 Mar 2026 19:37:04 +0800 Subject: [PATCH 2/3] fix: execute tasks against snapshot problem data --- apps/server/src/routes/problem/data.ts | 21 ++++++++++++++++++++- apps/server/src/routes/problem/solution.ts | 4 +++- apps/server/src/routes/runner/instance.ts | 10 +++++----- apps/server/src/routes/runner/solution.ts | 10 +++++----- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/apps/server/src/routes/problem/data.ts b/apps/server/src/routes/problem/data.ts index b2359048..41a1b49f 100644 --- a/apps/server/src/routes/problem/data.ts +++ b/apps/server/src/routes/problem/data.ts @@ -9,7 +9,7 @@ import { getFileUrl, defineRoutes, paramSchemaMerger } from '../common/index.js' import { kProblemContext } from './inject.js' const dataScopedRoutes = defineRoutes(async (s) => { - const { problems } = s.db + const { instances, problems, solutions } = s.db s.addHook('onRoute', paramSchemaMerger(T.Object({ hash: T.Hash() }))) s.register(getFileUrl, { @@ -36,6 +36,25 @@ const dataScopedRoutes = defineRoutes(async (s) => { ensureCapability(ctx._problemCapability, PROBLEM_CAPS.CAP_CONTENT, s.httpErrors.forbidden()) const hash = (req.params as { hash: string }).hash + const [referencedSolution, referencedInstance] = await Promise.all([ + solutions.findOne( + { + problemId: ctx._problemId, + problemDataHash: hash + }, + { projection: { _id: 1 } } + ), + instances.findOne( + { + problemId: ctx._problemId, + problemDataHash: hash + }, + { projection: { _id: 1 } } + ) + ]) + if (referencedSolution || referencedInstance) { + return rep.conflict('Problem data is still referenced') + } const { modifiedCount } = await problems.updateOne( { _id: ctx._problemId, currentDataHash: { $ne: hash } }, { $pull: { data: { hash } } } diff --git a/apps/server/src/routes/problem/solution.ts b/apps/server/src/routes/problem/solution.ts index 345bec20..1e63b263 100644 --- a/apps/server/src/routes/problem/solution.ts +++ b/apps/server/src/routes/problem/solution.ts @@ -79,7 +79,9 @@ const solutionScopedRoutes = defineRoutes(async (s) => { if (!admin) return rep.forbidden() const { pull } = req.body - const currentData = ctx._problem.data.find(({ hash }) => hash === ctx._problem.currentDataHash) + const currentData = ctx._problem.data.find( + ({ hash }) => hash === ctx._problem.currentDataHash + ) if (pull && !currentData) return rep.preconditionFailed('Current data not found') const { modifiedCount } = await solutions.updateOne( diff --git a/apps/server/src/routes/runner/instance.ts b/apps/server/src/routes/runner/instance.ts index 0a5b67c7..cdcd5c6f 100644 --- a/apps/server/src/routes/runner/instance.ts +++ b/apps/server/src/routes/runner/instance.ts @@ -217,19 +217,19 @@ export const runnerInstanceRoutes = defineRoutes(async (s) => { if (!oss) return { ...info, errMsg: 'OSS not enabled' } const problem = await problems.findOne({ _id: instance.problemId }) if (!problem) return { ...info, errMsg: 'Problem not found' } - const currentData = problem.data.find(({ hash }) => hash === problem.currentDataHash) - if (!currentData) return { ...info, errMsg: 'Problem data not found' } + const problemData = problem.data.find(({ hash }) => hash === instance.problemDataHash) + if (!problemData) return { ...info, errMsg: 'Problem data not found' } const problemDataUrl = await getDownloadUrl( oss, - problemDataKey(problem._id, problem.currentDataHash) + problemDataKey(problem._id, instance.problemDataHash) ) return { ...info, - problemConfig: currentData.config, + problemConfig: problemData.config, problemDataUrl, - problemDataHash: problem.currentDataHash + problemDataHash: instance.problemDataHash } } ) diff --git a/apps/server/src/routes/runner/solution.ts b/apps/server/src/routes/runner/solution.ts index 98ee8a82..1ccdaa9d 100644 --- a/apps/server/src/routes/runner/solution.ts +++ b/apps/server/src/routes/runner/solution.ts @@ -222,20 +222,20 @@ export const runnerSolutionRoutes = defineRoutes(async (s) => { if (!oss) return { ...info, errMsg: 'OSS not enabled' } const problem = await s.db.problems.findOne({ _id: solution.problemId }) if (!problem) return { ...info, errMsg: 'Problem not found' } - const currentData = problem.data.find(({ hash }) => hash === problem.currentDataHash) - if (!currentData) return { ...info, errMsg: 'Problem data not found' } + const problemData = problem.data.find(({ hash }) => hash === solution.problemDataHash) + if (!problemData) return { ...info, errMsg: 'Problem data not found' } const problemDataUrl = await getDownloadUrl( oss, - problemDataKey(problem._id, problem.currentDataHash) + problemDataKey(problem._id, solution.problemDataHash) ) const solutionDataUrl = await getDownloadUrl(oss, solutionDataKey(solution._id)) return { ...info, - problemConfig: currentData.config, + problemConfig: problemData.config, problemDataUrl, - problemDataHash: problem.currentDataHash, + problemDataHash: solution.problemDataHash, solutionDataUrl, solutionDataHash: solution.solutionDataHash } From ce23630be3c2fd9d84ff637ff68f3962f85df959 Mon Sep 17 00:00:00 2001 From: thezzisu Date: Mon, 23 Mar 2026 20:23:26 +0800 Subject: [PATCH 3/3] feat: add pull mode to contest admin rejudge --- .../contest/[contestId]/admin/index.vue | 18 +++- apps/server/src/routes/contest/admin.ts | 101 ++++++++++++------ 2 files changed, 87 insertions(+), 32 deletions(-) diff --git a/apps/frontend/src/pages/org/[orgId]/contest/[contestId]/admin/index.vue b/apps/frontend/src/pages/org/[orgId]/contest/[contestId]/admin/index.vue index e6f01cb7..3e0128fc 100644 --- a/apps/frontend/src/pages/org/[orgId]/contest/[contestId]/admin/index.vue +++ b/apps/frontend/src/pages/org/[orgId]/contest/[contestId]/admin/index.vue @@ -88,10 +88,19 @@ /> + + {{ pull ? t('rejudge-mode-pull') : t('rejudge-mode-pin') }} + {{ t('action.rejudge-all') }} @@ -193,11 +202,14 @@ const rejudgeOptions = reactive({ submittedAtL: undefined as number | undefined, submittedAtR: undefined as number | undefined }) +const pull = ref(true) const rejudgeAllTask = useAsyncTask(async (ev: SubmitEventPromise) => { const result = await ev if (!result.valid) return noMessage() const { modifiedCount } = await http - .post(`contest/${props.contestId}/admin/rejudge-all`, { json: rejudgeOptions }) + .post(`contest/${props.contestId}/admin/rejudge-all`, { + json: { ...rejudgeOptions, pull: pull.value } + }) .json<{ modifiedCount: number }>() return withMessage(t('msg.rejudge-all-success', { count: modifiedCount })) }) @@ -258,6 +270,8 @@ const ifNotUndefined = (cond: unknown, value: T) => (cond !== undefined ? va en: ranklist-state: Ranklist State is {state} reset-runner: Switch Reset Runner + rejudge-mode-pin: Fixed rejudge + rejudge-mode-pull: Pull rejudge min-score: Min Score max-score: Max Score submitted-after: Submitted After @@ -270,6 +284,8 @@ en: zh-Hans: ranklist-state: '排行榜状态: {state}' reset-runner: 切换重置运行器 + rejudge-mode-pin: 固定重测 + rejudge-mode-pull: 拉取重测 min-score: 最小分数 max-score: 最大分数 submitted-after: 提交时间晚于 diff --git a/apps/server/src/routes/contest/admin.ts b/apps/server/src/routes/contest/admin.ts index 487a134c..a05463a1 100644 --- a/apps/server/src/routes/contest/admin.ts +++ b/apps/server/src/routes/contest/admin.ts @@ -21,7 +21,7 @@ import { import { kContestContext } from './inject.js' export const contestAdminRoutes = defineRoutes(async (s) => { - const { contests, solutions } = s.db + const { contests, problems, solutions } = s.db s.addHook('onRequest', async (req) => { ensureCapability( @@ -106,6 +106,7 @@ export const contestAdminRoutes = defineRoutes(async (s) => { description: 'Rejudge all solutions', body: T.Object({ problemId: T.Optional(T.UUID()), + pull: T.Optional(T.Boolean()), state: T.Optional(T.Integer({ minimum: 1, maximum: 4 })), status: T.Optional(T.String()), runnerId: T.Optional(T.String()), @@ -121,36 +122,74 @@ export const contestAdminRoutes = defineRoutes(async (s) => { } } }, - async (req) => { - const { modifiedCount } = await solutions.updateMany( - { - contestId: req.inject(kContestContext)._contestId, - problemId: req.body.problemId ? new UUID(req.body.problemId) : undefined, - state: req.body.state || { $ne: SolutionState.CREATED }, - status: req.body.status, - runnerId: - typeof req.body.runnerId === 'string' - ? req.body.runnerId - ? new UUID(req.body.runnerId) - : { $exists: false } - : undefined, - score: generateRangeQuery(req.body.scoreL, req.body.scoreR), - submittedAt: generateRangeQuery(req.body.submittedAtL, req.body.submittedAtR) - }, - [ - { - $set: { - state: SolutionState.PENDING, - score: 0, - status: '', - metrics: {}, - message: '' - } - }, - { $unset: ['taskId', 'runnerId'] } - ], - { ignoreUndefined: true } - ) + async (req, rep) => { + const baseQuery = { + contestId: req.inject(kContestContext)._contestId, + problemId: req.body.problemId ? new UUID(req.body.problemId) : undefined, + state: req.body.state || { $ne: SolutionState.CREATED }, + status: req.body.status, + runnerId: + typeof req.body.runnerId === 'string' + ? req.body.runnerId + ? new UUID(req.body.runnerId) + : { $exists: false } + : undefined, + score: generateRangeQuery(req.body.scoreL, req.body.scoreR), + submittedAt: generateRangeQuery(req.body.submittedAtL, req.body.submittedAtR) + } + + if (!req.body.pull) { + const { modifiedCount } = await solutions.updateMany( + baseQuery, + [ + { + $set: { + state: SolutionState.PENDING, + score: 0, + status: '', + metrics: {}, + message: '' + } + }, + { $unset: ['taskId', 'runnerId'] } + ], + { ignoreUndefined: true } + ) + return { modifiedCount } + } + + const matchedProblemIds = await solutions.distinct('problemId', baseQuery, { + ignoreUndefined: true + }) + if (!matchedProblemIds.length) return { modifiedCount: 0 } + + const matchedProblems = await problems + .find({ _id: { $in: matchedProblemIds } }, { projection: { currentDataHash: 1, data: 1 } }) + .toArray() + let modifiedCount = 0 + for (const problem of matchedProblems) { + const currentData = problem.data.find(({ hash }) => hash === problem.currentDataHash) + if (!currentData) return rep.preconditionFailed('Current data not found') + const result = await solutions.updateMany( + { ...baseQuery, problemId: problem._id }, + [ + { + $set: { + label: currentData.config.label, + problemDataHash: problem.currentDataHash, + state: SolutionState.PENDING, + score: 0, + status: '', + metrics: {}, + message: '' + } + }, + { $unset: ['taskId', 'runnerId'] } + ], + { ignoreUndefined: true } + ) + modifiedCount += result.modifiedCount + } return { modifiedCount } } )