From 2edb8cfb0d09e87fd604a5cf1654c152a565473f Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sat, 8 Aug 2026 05:37:24 -0600 Subject: [PATCH] =?UTF-8?q?fix(=E5=89=8D=E7=AB=AF):=20=E6=88=91=E6=94=B9?= =?UTF-8?q?=20/api2=20=E5=89=8D=E7=BC=80=E6=97=B6=E6=BC=8F=E4=BA=86=205=20?= =?UTF-8?q?=E5=A4=84=EF=BC=9B=E4=BF=AE=204=20=E4=B8=AA=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=90=8D=E4=B8=8D=E5=8C=B9=E9=85=8D=EF=BC=9B=E6=94=B6=E7=B4=A7?= =?UTF-8?q?=20.gitignore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 浏览器走查 + 真实 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 --- .gitignore | 11 ++++-- apps/web/src/oj/class/pk.vue | 36 +++++++++---------- .../problem/components/SubmissionResult.vue | 2 +- apps/web/src/oj/rank/list.vue | 14 ++++---- apps/web/src/oj/store/ai.ts | 2 +- apps/web/src/utils/download.ts | 4 +-- 6 files changed, 37 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 18b1f9d..1013e8e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,9 +20,14 @@ build/ docs/spikes/node_modules/ docs/spikes/endpoints-*.json -# 本地判题验收数据来自生产测试点,只读挂载,不进入版本库 -data/test_case/ -data/judge_server/ +# data/ 整个目录都是运行时数据,一律不进版本库。 +# +# 必须写成整目录:原来只忽略了 test_case/ 和 judge_server/,而 +# docker/compose.*.yml 会在这里生成 data/postgres/(**整个数据库**,含 +# raw_password 明文列)、data/redis/、data/backend/(学生上传的文件)。 +# 阶段 5 演练时 `git add -A` 差一点就把生产库快照提交上去,只因为 postgres +# 容器用的是别的 uid、目录读不了才失败。别再收窄这条。 +data/ # 生产题目样本只用于本地验收,不进入 git *.csv diff --git a/apps/web/src/oj/class/pk.vue b/apps/web/src/oj/class/pk.vue index 76dbaec..eaf3cd2 100644 --- a/apps/web/src/oj/class/pk.vue +++ b/apps/web/src/oj/class/pk.vue @@ -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[] = [ }, { 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 = { {{ - classData.top_10_avg.toFixed(2) + classData.top10_avg.toFixed(2) }} {{ - classData.middle_80_avg.toFixed(2) + classData.middle80_avg.toFixed(2) }} {{ - classData.bottom_10_avg.toFixed(2) + classData.bottom10_avg.toFixed(2) }} @@ -1049,7 +1049,7 @@ const radarChartOptions = { {{ - classData.recent_top_10_avg?.toFixed(2) + classData.recent_top10_avg?.toFixed(2) }} diff --git a/apps/web/src/oj/problem/components/SubmissionResult.vue b/apps/web/src/oj/problem/components/SubmissionResult.vue index 25c6d4c..dc8bf66 100644 --- a/apps/web/src/oj/problem/components/SubmissionResult.vue +++ b/apps/web/src/oj/problem/components/SubmissionResult.vue @@ -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 }), diff --git a/apps/web/src/oj/rank/list.vue b/apps/web/src/oj/rank/list.vue index 505bc30..14e94c3 100644 --- a/apps/web/src/oj/rank/list.vue +++ b/apps/web/src/oj/rank/list.vue @@ -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( {{ - classDetailData.top_10_avg.toFixed(2) + classDetailData.top10_avg.toFixed(2) }} {{ - classDetailData.middle_80_avg.toFixed(2) + classDetailData.middle80_avg.toFixed(2) }} {{ - classDetailData.bottom_10_avg.toFixed(2) + classDetailData.bottom10_avg.toFixed(2) }} diff --git a/apps/web/src/oj/store/ai.ts b/apps/web/src/oj/store/ai.ts index 95ef9c5..2e66c95 100644 --- a/apps/web/src/oj/store/ai.ts +++ b/apps/web/src/oj/store/ai.ts @@ -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({ diff --git a/apps/web/src/utils/download.ts b/apps/web/src/utils/download.ts index d2f4e5c..62ebf40 100644 --- a/apps/web/src/utils/download.ts +++ b/apps/web/src/utils/download.ts @@ -1,9 +1,9 @@ import axios from "axios" -// 指向新后端的 /api2/admin。响应是 zip 二进制,不走 { error, data } 信封, +// 指向新后端的 /api/admin。响应是 zip 二进制,不走 { error, data } 信封, // 所以不能复用 utils/api2 的拦截器(它会把 response.data.data 取出来)。 const http = axios.create({ - baseURL: "/api2/admin", + baseURL: "/api/admin", responseType: "blob", withCredentials: true, })