Compare commits
2
Commits
e00ab7b876
...
8861393529
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8861393529 | ||
|
|
e921506f40 |
@@ -330,7 +330,22 @@ flowchartRoutes.post("/flowcharts/:id/retry", requireAuth, async (c) => {
|
|||||||
status: 0, aiScore: null, aiGrade: null, aiFeedback: null, aiSuggestions: null,
|
status: 0, aiScore: null, aiGrade: null, aiFeedback: null, aiSuggestions: null,
|
||||||
aiCriteriaDetails: {}, processingTime: null, evaluationTime: null,
|
aiCriteriaDetails: {}, processingTime: null, evaluationTime: null,
|
||||||
}).where(eq(schema.flowchartSubmission.id, row.flowchart.id))
|
}).where(eq(schema.flowchartSubmission.id, row.flowchart.id))
|
||||||
await flowchartQueue.add("evaluate", { submissionId: row.flowchart.id }, { jobId: `${row.flowchart.id}:${Date.now()}` })
|
try {
|
||||||
|
// jobId 必须**正好三段**:bullmq 对含 `:` 的自定义 id 有一条兼容老的可重复
|
||||||
|
// 任务的校验(job.js 的 `split(':').length !== 3`),两段会直接抛
|
||||||
|
// `Custom Id cannot contain :`。原来写的是 `${id}:${时间戳}`,于是这个接口
|
||||||
|
// 从来没成功过 —— 而清空评分在入队之前,每点一次就把原来的分数永久清掉、
|
||||||
|
// 提交卡在 PENDING 且没有任何任务会来救它。
|
||||||
|
await flowchartQueue.add(
|
||||||
|
"evaluate",
|
||||||
|
{ submissionId: row.flowchart.id },
|
||||||
|
{ jobId: `${row.flowchart.id}:retry:${Date.now()}` },
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
// 入队失败就落 FAILED,别把提交丢在 PENDING 上 —— 和 POST /flowcharts 同一处理
|
||||||
|
await db.update(schema.flowchartSubmission).set({ status: 3 }).where(eq(schema.flowchartSubmission.id, row.flowchart.id))
|
||||||
|
return failure(c, 502, "queue-unavailable", "Evaluation queue is unavailable")
|
||||||
|
}
|
||||||
return success(c, createFlowchartResponseSchema.parse({ submissionId: row.flowchart.id, status: "pending" }))
|
return success(c, createFlowchartResponseSchema.parse({ submissionId: row.flowchart.id, status: "pending" }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { toRefs } from "vue"
|
|||||||
|
|
||||||
// 工具函数
|
// 工具函数
|
||||||
import { atou, utoa } from "utils/functions"
|
import { atou, utoa } from "utils/functions"
|
||||||
|
import { sortFlowchartCriteria } from "utils/constants"
|
||||||
|
|
||||||
// 组合式函数
|
// 组合式函数
|
||||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||||
@@ -79,6 +80,11 @@ const suggestionLines = computed(() =>
|
|||||||
splitSuggestionLines(evaluation.value.suggestions),
|
splitSuggestionLines(evaluation.value.suggestions),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// jsonb 不保留键序,直接遍历会把 40 分的「逻辑正确性」排到最后
|
||||||
|
const sortedCriteria = computed(() =>
|
||||||
|
sortFlowchartCriteria(evaluation.value.criteria_details),
|
||||||
|
)
|
||||||
|
|
||||||
function splitSuggestionLines(suggestions?: string | null) {
|
function splitSuggestionLines(suggestions?: string | null) {
|
||||||
return suggestions
|
return suggestions
|
||||||
? suggestions
|
? suggestions
|
||||||
@@ -251,9 +257,17 @@ async function submitFlowchartData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message.success("流程图已提交,请耐心等待评分")
|
message.success("流程图已提交,请耐心等待评分")
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
// 按错误码分支(见 utils/api.ts 的约定)。限流是最容易撞上的一种:
|
||||||
|
// 只说「提交失败」的话,学生会以为是自己的图有问题,然后反复点,越点越久
|
||||||
|
if (error?.error === "too-many-submissions") {
|
||||||
|
message.warning("提交太频繁了,缓一会儿再交")
|
||||||
|
} else if (error?.error === "flowchart-not-allowed") {
|
||||||
|
message.error("这道题不接受流程图提交")
|
||||||
|
} else {
|
||||||
message.error("流程图提交失败")
|
message.error("流程图提交失败")
|
||||||
|
}
|
||||||
console.error("提交流程图失败:", error)
|
console.error("提交流程图失败:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,12 +510,12 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<!-- 详细评分 -->
|
<!-- 详细评分 -->
|
||||||
<n-card
|
<n-card
|
||||||
v-if="evaluation.criteria_details"
|
v-if="sortedCriteria.length"
|
||||||
size="small"
|
size="small"
|
||||||
title="详细评分"
|
title="详细评分"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(detail, key) in evaluation.criteria_details"
|
v-for="[key, detail] in sortedCriteria"
|
||||||
:key="key"
|
:key="key"
|
||||||
style="margin-bottom: 12px"
|
style="margin-bottom: 12px"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -70,12 +70,12 @@
|
|||||||
|
|
||||||
<!-- 详细评分 -->
|
<!-- 详细评分 -->
|
||||||
<n-card
|
<n-card
|
||||||
v-if="Object.keys(criteriaDetails).length > 0"
|
v-if="sortedCriteria.length > 0"
|
||||||
size="small"
|
size="small"
|
||||||
title="详细评分"
|
title="详细评分"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(detail, key) in criteriaDetails"
|
v-for="[key, detail] in sortedCriteria"
|
||||||
:key="key"
|
:key="key"
|
||||||
style="margin-bottom: 12px"
|
style="margin-bottom: 12px"
|
||||||
>
|
>
|
||||||
@@ -109,6 +109,7 @@
|
|||||||
import { Icon } from "@iconify/vue"
|
import { Icon } from "@iconify/vue"
|
||||||
import type { FlowchartSubmission } from "utils/types"
|
import type { FlowchartSubmission } from "utils/types"
|
||||||
import { useMermaid } from "shared/composables/useMermaid"
|
import { useMermaid } from "shared/composables/useMermaid"
|
||||||
|
import { sortFlowchartCriteria } from "utils/constants"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
submissionId: string
|
submissionId: string
|
||||||
@@ -146,6 +147,9 @@ const criteriaDetails = computed<
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
// jsonb 不保留键序,直接遍历会把 40 分的「逻辑正确性」排到最后
|
||||||
|
const sortedCriteria = computed(() => sortFlowchartCriteria(criteriaDetails.value))
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const rendering = ref(false)
|
const rendering = ref(false)
|
||||||
const showLargeImage = ref(false)
|
const showLargeImage = ref(false)
|
||||||
|
|||||||
@@ -162,7 +162,7 @@
|
|||||||
import { formatISO, sub, type Duration } from "date-fns"
|
import { formatISO, sub, type Duration } from "date-fns"
|
||||||
import type { FlowchartStatistics } from "@oj2/contract"
|
import type { FlowchartStatistics } from "@oj2/contract"
|
||||||
import { getFlowchartStatistics } from "oj/api"
|
import { getFlowchartStatistics } from "oj/api"
|
||||||
import { DURATION_OPTIONS } from "utils/constants"
|
import { DURATION_OPTIONS, FLOWCHART_CRITERIA_ORDER } from "utils/constants"
|
||||||
import { Doughnut, Radar, Bar } from "vue-chartjs"
|
import { Doughnut, Radar, Bar } from "vue-chartjs"
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
@@ -366,7 +366,8 @@ const doughnutOptions = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const CRITERIA_ORDER = ["逻辑正确性", "完整性", "规范性", "清晰度"]
|
// 与评分明细共用同一份顺序,见 utils/constants
|
||||||
|
const CRITERIA_ORDER = FLOWCHART_CRITERIA_ORDER
|
||||||
|
|
||||||
const hasRadarData = computed(() =>
|
const hasRadarData = computed(() =>
|
||||||
CRITERIA_ORDER.some((k) => k in data.criteriaAverages),
|
CRITERIA_ORDER.some((k) => k in data.criteriaAverages),
|
||||||
|
|||||||
@@ -304,6 +304,31 @@ export const RARITY_TEXT_COLOR: Record<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 时间范围配置
|
// 时间范围配置
|
||||||
|
/**
|
||||||
|
* 流程图评分项的展示顺序(按分值从高到低)。
|
||||||
|
*
|
||||||
|
* `ai_criteria_details` 存在 jsonb 列里,而 Postgres 的 jsonb **不保留键序** ——
|
||||||
|
* 它按「键长度 + 字节序」重排,读出来会变成 完整性 / 清晰度 / 规范性 / 逻辑正确性,
|
||||||
|
* 40 分的那项排到最后。凡是要展示评分明细的地方都按这个顺序排,别直接遍历对象。
|
||||||
|
*/
|
||||||
|
export const FLOWCHART_CRITERIA_ORDER = [
|
||||||
|
"逻辑正确性",
|
||||||
|
"完整性",
|
||||||
|
"规范性",
|
||||||
|
"清晰度",
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 按 FLOWCHART_CRITERIA_ORDER 排序,表里没有的键排在后面并保持原有相对顺序 */
|
||||||
|
export function sortFlowchartCriteria<T>(
|
||||||
|
details: Record<string, T>,
|
||||||
|
): [string, T][] {
|
||||||
|
const rank = (key: string) => {
|
||||||
|
const i = FLOWCHART_CRITERIA_ORDER.indexOf(key)
|
||||||
|
return i === -1 ? Number.MAX_SAFE_INTEGER : i
|
||||||
|
}
|
||||||
|
return Object.entries(details).sort(([a], [b]) => rank(a) - rank(b))
|
||||||
|
}
|
||||||
|
|
||||||
export const DURATION_OPTIONS = [
|
export const DURATION_OPTIONS = [
|
||||||
{ label: "本节课内", value: "hours:1" },
|
{ label: "本节课内", value: "hours:1" },
|
||||||
{ label: "两节课内", value: "hours:2" },
|
{ label: "两节课内", value: "hours:2" },
|
||||||
|
|||||||
Reference in New Issue
Block a user