fix(前端): 我改 /api2 前缀时漏了 5 处;修 4 个字段名不匹配;收紧 .gitignore
浏览器走查 + 真实 AI 调用暴露出来的,都是「接口好的、界面坏的」,
只靠脚本打 API 永远发现不了。
## 一、AI 功能和测试用例下载全都会 404(我今天造成的)
把 api2.ts 的 baseURL 从 /api2 改成 /api 时,只改了走 axios 实例的调用,
漏掉 5 处不走它的:
oj/store/ai.ts AI 题解分析 原生 fetch("/api2/...")
oj/problem/.../SubmissionResult.vue AI 提示 原生 fetch
oj/rank/list.vue AI 班级分析 原生 fetch
oj/class/pk.vue AI 班级 PK 原生 fetch
utils/download.ts 测试用例下载 独立 axios,baseURL "/api2/admin"
生产的 Caddy 只认 /api,这 5 条全是 404。阶段 0 的端点清单里就写着
「盲点 2:4 处用原生 fetch」「盲点 3:download.ts 是独立 axios 实例」——
我今天读过那份文档,还是漏了。
修完实测四条 AI 链路全通(真实 DeepSeek 调用):
/api/ai/analysis 200 SSE 176 分片 5.2s start→delta→done→end 完整
/api/ai/hint 200 2.4s 且未泄露参考答案
/api/ai/class-analysis 200 12.6s
/api/ai/class-pk-analysis 200 12.6s
落库确认(ai_analysis 新增,model=deepseek-v4-flash 与旧后端一致)。
hint 对别人的提交回 404 是**正确**的,它用 userId 限定只能看自己的。
## 二、4 个字段名永久对不上(重写引入的回归)
适配器只在**大写字母**前插下划线:`top10Avg` → `top10_avg`;
而旧 Django 给的是 `top_10_avg`(数字前也有)。前端还按旧名读。
影响比"三列空白"大:`row.top_10_avg.toFixed(2)` 在 undefined 上抛异常,
Vue 放弃整个子树 —— 班级PK 和班级排名页的**整个「分层统计」面板都不显示**
(Q1/Q3/四分位距/标准差/前10%/中间80%/后10%/人数 全没了),控制台还不报错。
修完实测:前10%均值 194.80、中间80%均值 93.79、后10%均值 49.40 全部出现。
写脚本按这个规律全仓扫过,确认只有这 4 个,改完复查归零。
## 三、.gitignore 差一点把生产库提交上去
原来只忽略 data/test_case/ 和 data/judge_server/,而 compose 会在
data/postgres/ 生成**整个数据库**(含 raw_password 明文列)、data/redis/、
data/backend/(学生上传的文件)。这次 `git add -A` 报权限错误才发现 ——
只因为 postgres 容器用别的 uid、目录读不了才没提交成功。改成忽略整个 data/。
已确认历史里从没提交过这些。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,9 +57,9 @@ interface ClassComparison {
|
||||
q3_ac: number
|
||||
iqr: number
|
||||
std_dev: number
|
||||
top_10_avg: number
|
||||
middle_80_avg: number
|
||||
bottom_10_avg: number
|
||||
top10_avg: number
|
||||
middle80_avg: number
|
||||
bottom10_avg: number
|
||||
excellent_rate: number
|
||||
pass_rate: number
|
||||
active_rate: number
|
||||
@@ -68,7 +68,7 @@ interface ClassComparison {
|
||||
recent_total_ac?: number
|
||||
recent_avg_ac?: number
|
||||
recent_median_ac?: number
|
||||
recent_top_10_avg?: number
|
||||
recent_top10_avg?: number
|
||||
recent_active_count?: number
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ async function analyzeWithAI() {
|
||||
if (csrfToken) headers["X-CSRFToken"] = csrfToken
|
||||
|
||||
try {
|
||||
const response = await fetch("/api2/ai/class-pk-analysis", {
|
||||
const response = await fetch("/api/ai/class-pk-analysis", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
@@ -382,7 +382,7 @@ const top10AvgChartData = computed(() => {
|
||||
const datasets = [
|
||||
{
|
||||
label: "前10%平均",
|
||||
data: comparisons.value.map((c) => c.top_10_avg),
|
||||
data: comparisons.value.map((c) => c.top10_avg),
|
||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -400,7 +400,7 @@ const bottom10AvgChartData = computed(() => {
|
||||
const datasets = [
|
||||
{
|
||||
label: "后10%平均",
|
||||
data: comparisons.value.map((c) => c.bottom_10_avg),
|
||||
data: comparisons.value.map((c) => c.bottom10_avg),
|
||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -418,7 +418,7 @@ const middle80AvgChartData = computed(() => {
|
||||
const datasets = [
|
||||
{
|
||||
label: "中间80%均值",
|
||||
data: comparisons.value.map((c) => c.middle_80_avg),
|
||||
data: comparisons.value.map((c) => c.middle80_avg),
|
||||
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||
borderWidth: 2,
|
||||
@@ -640,35 +640,35 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
||||
},
|
||||
{
|
||||
title: "前10%均值",
|
||||
key: "top_10_avg",
|
||||
key: "top10_avg",
|
||||
width: 100,
|
||||
render: (row) =>
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#cf1322", fontWeight: "600" } },
|
||||
row.top_10_avg.toFixed(2),
|
||||
row.top10_avg.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "中间80%均值",
|
||||
key: "middle_80_avg",
|
||||
key: "middle80_avg",
|
||||
width: 110,
|
||||
render: (row) =>
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#389e0d", fontWeight: "600" } },
|
||||
row.middle_80_avg.toFixed(2),
|
||||
row.middle80_avg.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "后10%均值",
|
||||
key: "bottom_10_avg",
|
||||
key: "bottom10_avg",
|
||||
width: 100,
|
||||
render: (row) =>
|
||||
h(
|
||||
"span",
|
||||
{ style: { color: "#096dd9", fontWeight: "500" } },
|
||||
row.bottom_10_avg.toFixed(2),
|
||||
row.bottom10_avg.toFixed(2),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -956,17 +956,17 @@ const radarChartOptions = {
|
||||
<!-- 分层统计 -->
|
||||
<n-descriptions-item label="前10%均值">
|
||||
<span style="color: #cf1322; font-weight: 600">{{
|
||||
classData.top_10_avg.toFixed(2)
|
||||
classData.top10_avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="中间80%均值">
|
||||
<span style="color: #389e0d; font-weight: 600">{{
|
||||
classData.middle_80_avg.toFixed(2)
|
||||
classData.middle80_avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="后10%均值">
|
||||
<span style="color: #096dd9; font-weight: 500">{{
|
||||
classData.bottom_10_avg.toFixed(2)
|
||||
classData.bottom10_avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
|
||||
@@ -1049,7 +1049,7 @@ const radarChartOptions = {
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="时间段前10名平均">
|
||||
<span style="color: #ff4d4f; font-weight: 600">{{
|
||||
classData.recent_top_10_avg?.toFixed(2)
|
||||
classData.recent_top10_avg?.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="活跃学生数" :span="2">
|
||||
|
||||
@@ -83,7 +83,7 @@ async function fetchHint(submissionId: string) {
|
||||
headers["X-CSRFToken"] = csrfToken
|
||||
}
|
||||
|
||||
const response = await fetch("/api2/ai/hint", {
|
||||
const response = await fetch("/api/ai/hint", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ submissionId }),
|
||||
|
||||
@@ -95,7 +95,7 @@ async function analyzeSingleClassWithAI() {
|
||||
if (csrfToken) headers["X-CSRFToken"] = csrfToken
|
||||
|
||||
try {
|
||||
const response = await fetch("/api2/ai/class-analysis", {
|
||||
const response = await fetch("/api/ai/class-analysis", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ comparison: classDetailData.value }),
|
||||
@@ -158,9 +158,9 @@ interface ClassComparison {
|
||||
q3_ac: number
|
||||
iqr: number
|
||||
std_dev: number
|
||||
top_10_avg: number
|
||||
middle_80_avg: number
|
||||
bottom_10_avg: number
|
||||
top10_avg: number
|
||||
middle80_avg: number
|
||||
bottom10_avg: number
|
||||
excellent_rate: number
|
||||
pass_rate: number
|
||||
active_rate: number
|
||||
@@ -719,17 +719,17 @@ watch(
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="前10%均值">
|
||||
<span style="color: #cf1322; font-weight: 600">{{
|
||||
classDetailData.top_10_avg.toFixed(2)
|
||||
classDetailData.top10_avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="中间80%均值">
|
||||
<span style="color: #389e0d; font-weight: 600">{{
|
||||
classDetailData.middle_80_avg.toFixed(2)
|
||||
classDetailData.middle80_avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="后10%均值">
|
||||
<span style="color: #096dd9; font-weight: 500">{{
|
||||
classDetailData.bottom_10_avg.toFixed(2)
|
||||
classDetailData.bottom10_avg.toFixed(2)
|
||||
}}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="人数">
|
||||
|
||||
@@ -104,7 +104,7 @@ export const useAIStore = defineStore("ai", () => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api2/ai/analysis", {
|
||||
const response = await fetch("/api/ai/analysis", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user