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/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 }
}
)
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/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 39b6b2f4..1e63b263 100644
--- a/apps/server/src/routes/problem/solution.ts
+++ b/apps/server/src/routes/problem/solution.ts
@@ -62,37 +62,55 @@ 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(
'/',
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
}