学习报告不只是“统计”,还会给出:
- 当前最薄弱的知识点
- 为什么判定它薄弱
- 建议先练什么
- 自动推荐对应练习题
后端 schema 新增
修改 backend/app/schemas.py
class WeakKnowledgeItem(BaseModel):
name: str
wrong_count: int
total_count: int
wrong_rate: float
suggestion: str
class StudySuggestionResponse(BaseModel):
weak_knowledge_points: List[WeakKnowledgeItem]
overall_suggestion: str
新增建议分析服务
新增 backend/app/suggestion_service.py
import json
from collections import defaultdict
from sqlalchemy.orm import Session
from app.models import QuestionHistory
def build_study_suggestion(db: Session):
rows = db.query(QuestionHistory).all()
stat_map = defaultdict(lambda: {"total": 0, "wrong": 0})
for row in rows:
try:
knowledge_points = json.loads(row.knowledge_points or "[]")
except Exception:
knowledge_points = []
for kp in knowledge_points:
if not kp:
continue
stat_map[kp]["total"] += 1
if row.is_wrong:
stat_map[kp]["wrong"] += 1
weak_list = []
for name, stat in stat_map.items():
total = stat["total"]
wrong = stat["wrong"]
wrong_rate = round((wrong / total * 100), 2) if total > 0 else 0.0
if wrong > 0:
if wrong_rate >= 60:
suggestion = f"建议优先强化“{name}”基础概念,并连续练习 3~5 道同类题。"
elif wrong_rate >= 30:
suggestion = f"建议针对“{name}”做专项复习,并配合 2~3 道练习题巩固。"
else:
suggestion = f"“{name}”有少量错误,建议复盘错题并适量练习。"
weak_list.append({
"name": name,
"wrong_count": wrong,
"total_count": total,
"wrong_rate": wrong_rate,
"suggestion": suggestion,
})
weak_list.sort(key=lambda x: (x["wrong_rate"], x["wrong_count"]), reverse=True)
weak_list = weak_list[:5]
if not weak_list:
overall_suggestion = "当前暂无明显薄弱知识点,建议继续保持练习,并适度拓展更高难度题目。"
else:
top_name = weak_list[0]["name"]
overall_suggestion = f"当前最需要优先突破的知识点是“{top_name}”,建议先复习核心方法,再进行针对性训练。"
return {
"weak_knowledge_points": weak_list,
"overall_suggestion": overall_suggestion,
}
后端主接口新增
修改 backend/app/main.py
1)补充 import
新增:
from app.suggestion_service import build_study_suggestion
from app.schemas import StudySuggestionResponse
2)新增学习建议接口
把这个接口加到 main.py 里:
@app.get("/api/study-suggestion", response_model=StudySuggestionResponse)
def get_study_suggestion(db: Session = Depends(get_db)):
try:
result = build_study_suggestion(db)
return StudySuggestionResponse(**result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
前端 API 新增
修改 frontend/src/api/math.ts
新增类型:
export interface WeakKnowledgeItem {
name: string
wrong_count: number
total_count: number
wrong_rate: number
suggestion: string
}
export interface StudySuggestionResponse {
weak_knowledge_points: WeakKnowledgeItem[]
overall_suggestion: string
}
新增请求方法:
export function getStudySuggestion() {
return request.get<StudySuggestionResponse>('/api/study-suggestion')
}
前端页面状态新增
修改 frontend/src/App.vue
1)补充 import
把 api import 补上:
import {
solveMathQuestion,
solveMathImage,
getHistoryList,
getWrongQuestionList,
markWrongQuestion,
generatePracticeByKnowledge,
regenerateQuestion,
getLearningReport,
getStudySuggestion,
type SolveResponse,
type HistoryItem,
type PracticeQuestionItem,
type LearningReportResponse,
type StudySuggestionResponse,
} from './api/math'
2)activeTab 扩展
改成:
const activeTab = ref<'solve' | 'history' | 'wrong' | 'report' | 'suggestion'>('solve')
3)新增状态
在 script setup 里新增:
const suggestionLoading = ref(false)
const studySuggestion = ref<StudySuggestionResponse | null>(null)
4)新增方法
const loadStudySuggestion = async () => {
suggestionLoading.value = true
try {
const { data } = await getStudySuggestion()
studySuggestion.value = data
} catch (error: any) {
console.error('加载学习建议失败:', error)
alert(error?.response?.data?.detail || '加载学习建议失败')
} finally {
suggestionLoading.value = false
}
}
const switchToSuggestion = async () => {
activeTab.value = 'suggestion'
await loadStudySuggestion()
}
5)这些操作成功后顺手刷新建议
在下面几个方法成功后追加:
handleSubmit
await loadStudySuggestion()
handleImageChange
await loadStudySuggestion()
toggleWrong
await loadStudySuggestion()
前端 tabs 增加入口
修改 frontend/src/App.vue
在 tabs 按钮区域新增:
<button
:class="['tab-btn', activeTab === 'suggestion' ? 'active' : '']"
@click="switchToSuggestion"
>
学习建议
</button>
前端模板新增“学习建议”页
修改 frontend/src/App.vue
现在已经有:
- solve
- history
- wrong
- report
所以需要把 report 分支改成 v-else-if="activeTab === 'report'" ,
然后最后新增一个 v-else 作为 suggestion 页面。
1)先把 report 分支改成:
<template v-else-if="activeTab === 'report'">
2)最后新增 suggestion 分支:
<template v-else>
<div v-if="suggestionLoading" class="empty">学习建议加载中...</div>
<div v-else-if="studySuggestion" class="report-panel">
<div class="result-card">
<h2>整体学习建议</h2>
<p>{{ studySuggestion.overall_suggestion }}</p>
</div>
<div class="result-card">
<h2>薄弱知识点分析</h2>
<div v-if="studySuggestion.weak_knowledge_points.length === 0" class="empty">
暂无薄弱知识点
</div>
<div
v-for="(item, index) in studySuggestion.weak_knowledge_points"
:key="index"
class="weak-item"
>
<div class="weak-header">
<strong>{{ item.name }}</strong>
<span class="weak-rate">错误率 {{ item.wrong_rate }}%</span>
</div>
<div class="weak-meta">
错误 {{ item.wrong_count }} 次 / 共出现 {{ item.total_count }} 次
</div>
<div class="weak-suggestion">
{{ item.suggestion }}
</div>
<button
class="retry-btn"
@click="
practiceKnowledge = item.name;
activeTab = 'solve';
handleGeneratePractice();
"
>
生成该知识点练习题
</button>
</div>
</div>
</div>
</template>
前端样式补充
修改 frontend/src/App.vue
在 style scoped 里新增:
.weak-item {
padding: 16px 0;
border-bottom: 1px solid #eee;
}
.weak-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.weak-rate {
color: #d03050;
font-weight: 600;
}
.weak-meta {
color: #666;
font-size: 14px;
margin-bottom: 8px;
}
.weak-suggestion {
margin-bottom: 12px;
color: #333;
line-height: 1.7;
}
初始化时顺手加载建议
修改 onMounted
改成:
onMounted(async () => {
await loadHistory()
await loadWrongList()
await loadReport()
await loadStudySuggestion()
})
启动查看效果
重启后端
uvicorn app.main:app --reload --port 8000
重启前端
npm run dev
学习建议来源于错题