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:
11
.gitignore
vendored
11
.gitignore
vendored
@@ -20,9 +20,14 @@ build/
|
|||||||
docs/spikes/node_modules/
|
docs/spikes/node_modules/
|
||||||
docs/spikes/endpoints-*.json
|
docs/spikes/endpoints-*.json
|
||||||
|
|
||||||
# 本地判题验收数据来自生产测试点,只读挂载,不进入版本库
|
# data/ 整个目录都是运行时数据,一律不进版本库。
|
||||||
data/test_case/
|
#
|
||||||
data/judge_server/
|
# 必须写成整目录:原来只忽略了 test_case/ 和 judge_server/,而
|
||||||
|
# docker/compose.*.yml 会在这里生成 data/postgres/(**整个数据库**,含
|
||||||
|
# raw_password 明文列)、data/redis/、data/backend/(学生上传的文件)。
|
||||||
|
# 阶段 5 演练时 `git add -A` 差一点就把生产库快照提交上去,只因为 postgres
|
||||||
|
# 容器用的是别的 uid、目录读不了才失败。别再收窄这条。
|
||||||
|
data/
|
||||||
|
|
||||||
# 生产题目样本只用于本地验收,不进入 git
|
# 生产题目样本只用于本地验收,不进入 git
|
||||||
*.csv
|
*.csv
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ interface ClassComparison {
|
|||||||
q3_ac: number
|
q3_ac: number
|
||||||
iqr: number
|
iqr: number
|
||||||
std_dev: number
|
std_dev: number
|
||||||
top_10_avg: number
|
top10_avg: number
|
||||||
middle_80_avg: number
|
middle80_avg: number
|
||||||
bottom_10_avg: number
|
bottom10_avg: number
|
||||||
excellent_rate: number
|
excellent_rate: number
|
||||||
pass_rate: number
|
pass_rate: number
|
||||||
active_rate: number
|
active_rate: number
|
||||||
@@ -68,7 +68,7 @@ interface ClassComparison {
|
|||||||
recent_total_ac?: number
|
recent_total_ac?: number
|
||||||
recent_avg_ac?: number
|
recent_avg_ac?: number
|
||||||
recent_median_ac?: number
|
recent_median_ac?: number
|
||||||
recent_top_10_avg?: number
|
recent_top10_avg?: number
|
||||||
recent_active_count?: number
|
recent_active_count?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ async function analyzeWithAI() {
|
|||||||
if (csrfToken) headers["X-CSRFToken"] = csrfToken
|
if (csrfToken) headers["X-CSRFToken"] = csrfToken
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api2/ai/class-pk-analysis", {
|
const response = await fetch("/api/ai/class-pk-analysis", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -382,7 +382,7 @@ const top10AvgChartData = computed(() => {
|
|||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "前10%平均",
|
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),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -400,7 +400,7 @@ const bottom10AvgChartData = computed(() => {
|
|||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "后10%平均",
|
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),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -418,7 +418,7 @@ const middle80AvgChartData = computed(() => {
|
|||||||
const datasets = [
|
const datasets = [
|
||||||
{
|
{
|
||||||
label: "中间80%均值",
|
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),
|
backgroundColor: comparisons.value.map((_, i) => getClassColor(i).bg),
|
||||||
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
borderColor: comparisons.value.map((_, i) => getClassColor(i).border),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
@@ -640,35 +640,35 @@ const tableColumns: DataTableColumn<ClassComparison>[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "前10%均值",
|
title: "前10%均值",
|
||||||
key: "top_10_avg",
|
key: "top10_avg",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#cf1322", fontWeight: "600" } },
|
{ style: { color: "#cf1322", fontWeight: "600" } },
|
||||||
row.top_10_avg.toFixed(2),
|
row.top10_avg.toFixed(2),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "中间80%均值",
|
title: "中间80%均值",
|
||||||
key: "middle_80_avg",
|
key: "middle80_avg",
|
||||||
width: 110,
|
width: 110,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#389e0d", fontWeight: "600" } },
|
{ style: { color: "#389e0d", fontWeight: "600" } },
|
||||||
row.middle_80_avg.toFixed(2),
|
row.middle80_avg.toFixed(2),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "后10%均值",
|
title: "后10%均值",
|
||||||
key: "bottom_10_avg",
|
key: "bottom10_avg",
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (row) =>
|
render: (row) =>
|
||||||
h(
|
h(
|
||||||
"span",
|
"span",
|
||||||
{ style: { color: "#096dd9", fontWeight: "500" } },
|
{ 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%均值">
|
<n-descriptions-item label="前10%均值">
|
||||||
<span style="color: #cf1322; font-weight: 600">{{
|
<span style="color: #cf1322; font-weight: 600">{{
|
||||||
classData.top_10_avg.toFixed(2)
|
classData.top10_avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="中间80%均值">
|
<n-descriptions-item label="中间80%均值">
|
||||||
<span style="color: #389e0d; font-weight: 600">{{
|
<span style="color: #389e0d; font-weight: 600">{{
|
||||||
classData.middle_80_avg.toFixed(2)
|
classData.middle80_avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="后10%均值">
|
<n-descriptions-item label="后10%均值">
|
||||||
<span style="color: #096dd9; font-weight: 500">{{
|
<span style="color: #096dd9; font-weight: 500">{{
|
||||||
classData.bottom_10_avg.toFixed(2)
|
classData.bottom10_avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
|
|
||||||
@@ -1049,7 +1049,7 @@ const radarChartOptions = {
|
|||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="时间段前10名平均">
|
<n-descriptions-item label="时间段前10名平均">
|
||||||
<span style="color: #ff4d4f; font-weight: 600">{{
|
<span style="color: #ff4d4f; font-weight: 600">{{
|
||||||
classData.recent_top_10_avg?.toFixed(2)
|
classData.recent_top10_avg?.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="活跃学生数" :span="2">
|
<n-descriptions-item label="活跃学生数" :span="2">
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ async function fetchHint(submissionId: string) {
|
|||||||
headers["X-CSRFToken"] = csrfToken
|
headers["X-CSRFToken"] = csrfToken
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch("/api2/ai/hint", {
|
const response = await fetch("/api/ai/hint", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({ submissionId }),
|
body: JSON.stringify({ submissionId }),
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ async function analyzeSingleClassWithAI() {
|
|||||||
if (csrfToken) headers["X-CSRFToken"] = csrfToken
|
if (csrfToken) headers["X-CSRFToken"] = csrfToken
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api2/ai/class-analysis", {
|
const response = await fetch("/api/ai/class-analysis", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({ comparison: classDetailData.value }),
|
body: JSON.stringify({ comparison: classDetailData.value }),
|
||||||
@@ -158,9 +158,9 @@ interface ClassComparison {
|
|||||||
q3_ac: number
|
q3_ac: number
|
||||||
iqr: number
|
iqr: number
|
||||||
std_dev: number
|
std_dev: number
|
||||||
top_10_avg: number
|
top10_avg: number
|
||||||
middle_80_avg: number
|
middle80_avg: number
|
||||||
bottom_10_avg: number
|
bottom10_avg: number
|
||||||
excellent_rate: number
|
excellent_rate: number
|
||||||
pass_rate: number
|
pass_rate: number
|
||||||
active_rate: number
|
active_rate: number
|
||||||
@@ -719,17 +719,17 @@ watch(
|
|||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="前10%均值">
|
<n-descriptions-item label="前10%均值">
|
||||||
<span style="color: #cf1322; font-weight: 600">{{
|
<span style="color: #cf1322; font-weight: 600">{{
|
||||||
classDetailData.top_10_avg.toFixed(2)
|
classDetailData.top10_avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="中间80%均值">
|
<n-descriptions-item label="中间80%均值">
|
||||||
<span style="color: #389e0d; font-weight: 600">{{
|
<span style="color: #389e0d; font-weight: 600">{{
|
||||||
classDetailData.middle_80_avg.toFixed(2)
|
classDetailData.middle80_avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="后10%均值">
|
<n-descriptions-item label="后10%均值">
|
||||||
<span style="color: #096dd9; font-weight: 500">{{
|
<span style="color: #096dd9; font-weight: 500">{{
|
||||||
classDetailData.bottom_10_avg.toFixed(2)
|
classDetailData.bottom10_avg.toFixed(2)
|
||||||
}}</span>
|
}}</span>
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="人数">
|
<n-descriptions-item label="人数">
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const useAIStore = defineStore("ai", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api2/ai/analysis", {
|
const response = await fetch("/api/ai/analysis", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
|
|
||||||
// 指向新后端的 /api2/admin。响应是 zip 二进制,不走 { error, data } 信封,
|
// 指向新后端的 /api/admin。响应是 zip 二进制,不走 { error, data } 信封,
|
||||||
// 所以不能复用 utils/api2 的拦截器(它会把 response.data.data 取出来)。
|
// 所以不能复用 utils/api2 的拦截器(它会把 response.data.data 取出来)。
|
||||||
const http = axios.create({
|
const http = axios.create({
|
||||||
baseURL: "/api2/admin",
|
baseURL: "/api/admin",
|
||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user