添加 功能
-
每次解析题目时,自动归类到某个知识点
-
统计每个学生在各知识点下:
- 做题次数
- 错题次数
- 正确次数
- 错误率
1)修改 backend/app/models.py
新增模型
ini
class StudentKnowledgeStat(Base):
__tablename__ = "student_knowledge_stat"
id = Column(Integer, primary_key=True, index=True)
student_id = Column(Integer, ForeignKey("student.id"), nullable=False, index=True)
knowledge_name = Column(Text, nullable=False, index=True)
total_count = Column(Integer, nullable=False, default=0)
wrong_count = Column(Integer, nullable=False, default=0)
correct_count = Column(Integer, nullable=False, default=0)
2)修改 backend/app/schemas.py
新增结构
python
class KnowledgeGraphItem(BaseModel):
knowledge_name: str
total_count: int
wrong_count: int
correct_count: int
wrong_rate: float
class KnowledgeGraphResponse(BaseModel):
student_id: int
items: List[KnowledgeGraphItem]
3)新增 backend/app/knowledge_graph_service.py
ini
import json
from sqlalchemy.orm import Session
from app.models import StudentKnowledgeStat, QuestionHistory
def _normalize_knowledge_points(knowledge_points: list[str]) -> list[str]:
result = []
seen = set()
for item in knowledge_points:
name = (item or "").strip()
if not name or name in seen:
continue
seen.add(name)
result.append(name)
return result
def update_student_knowledge_stat(
db: Session,
student_id: int,
knowledge_points: list[str],
is_wrong: bool,
):
kp_list = _normalize_knowledge_points(knowledge_points)
for kp in kp_list:
row = (
db.query(StudentKnowledgeStat)
.filter(
StudentKnowledgeStat.student_id == student_id,
StudentKnowledgeStat.knowledge_name == kp,
)
.first()
)
if not row:
row = StudentKnowledgeStat(
student_id=student_id,
knowledge_name=kp,
total_count=0,
wrong_count=0,
correct_count=0,
)
db.add(row)
db.flush()
row.total_count += 1
if is_wrong:
row.wrong_count += 1
else:
row.correct_count += 1
db.commit()
def rebuild_student_knowledge_stat(db: Session, student_id: int):
db.query(StudentKnowledgeStat).filter(
StudentKnowledgeStat.student_id == student_id
).delete()
rows = (
db.query(QuestionHistory)
.filter(QuestionHistory.student_id == student_id)
.order_by(QuestionHistory.id.asc())
.all()
)
for row in rows:
try:
knowledge_points = json.loads(row.knowledge_points or "[]")
except Exception:
knowledge_points = []
update_student_knowledge_stat(
db=db,
student_id=student_id,
knowledge_points=knowledge_points,
is_wrong=row.is_wrong,
)
def get_student_knowledge_graph(db: Session, student_id: int):
rows = (
db.query(StudentKnowledgeStat)
.filter(StudentKnowledgeStat.student_id == student_id)
.order_by(
StudentKnowledgeStat.wrong_count.desc(),
StudentKnowledgeStat.total_count.desc(),
StudentKnowledgeStat.knowledge_name.asc(),
)
.all()
)
items = []
for row in rows:
wrong_rate = round((row.wrong_count / row.total_count * 100), 2) if row.total_count > 0 else 0.0
items.append({
"knowledge_name": row.knowledge_name,
"total_count": row.total_count,
"wrong_count": row.wrong_count,
"correct_count": row.correct_count,
"wrong_rate": wrong_rate,
})
return {
"student_id": student_id,
"items": items,
}
4)修改 backend/app/main.py
补充 import
javascript
from app.knowledge_graph_service import (
update_student_knowledge_stat,
rebuild_student_knowledge_stat,
get_student_knowledge_graph,
)
from app.schemas import KnowledgeGraphResponse
在 /api/solve 里,追加
ini
update_student_knowledge_stat(
db=db,
student_id=student_id,
knowledge_points=result.get("knowledge_points", []),
is_wrong=False,
)
在 /api/solve-image 里,追加
ini
update_student_knowledge_stat(
db=db,
student_id=student_id,
knowledge_points=result.get("knowledge_points", []),
is_wrong=False,
)
/api/history/{question_id}/wrong
ini
@app.patch("/api/history/{question_id}/wrong", response_model=HistoryItem)
def mark_wrong(question_id: int, body: MarkWrongRequest, db: Session = Depends(get_db)):
row = db.query(QuestionHistory).filter(QuestionHistory.id == question_id).first()
if not row:
raise HTTPException(status_code=404, detail="记录不存在")
row.is_wrong = body.is_wrong
db.commit()
db.refresh(row)
rebuild_student_knowledge_stat(db, row.student_id)
return HistoryItem(
id=row.id,
question=row.question,
answer=row.answer,
steps=json.loads(row.steps),
knowledge_points=json.loads(row.knowledge_points),
similar_question=row.similar_question,
is_wrong=row.is_wrong,
matched_knowledge=json.loads(row.matched_knowledge or "[]"),
)
新增两个接口
less
@app.get("/api/knowledge-graph", response_model=KnowledgeGraphResponse)
def get_knowledge_graph(
student_id: int = Query(1),
db: Session = Depends(get_db),
):
try:
result = get_student_knowledge_graph(db, student_id)
return KnowledgeGraphResponse(**result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/api/knowledge-graph/rebuild")
def rebuild_knowledge_graph(
student_id: int = Query(1),
db: Session = Depends(get_db),
):
try:
rebuild_student_knowledge_stat(db, student_id)
return {"message": "知识图谱重建成功"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
5)修改 frontend/src/api/math.ts
新增类型
typescript
export interface KnowledgeGraphItem {
knowledge_name: string
total_count: number
wrong_count: number
correct_count: number
wrong_rate: number
}
export interface KnowledgeGraphResponse {
student_id: number
items: KnowledgeGraphItem[]
}
新增接口
javascript
export function getKnowledgeGraph(student_id: number) {
return request.get<KnowledgeGraphResponse>('/api/knowledge-graph', {
params: { student_id },
})
}
export function rebuildKnowledgeGraph(student_id: number) {
return request.post('/api/knowledge-graph/rebuild', null, {
params: { student_id },
})
}
6)修改 frontend/src/App.vue
补充 import
bash
getKnowledgeGraph,
rebuildKnowledgeGraph,
type KnowledgeGraphResponse,
修改 activeTab 类型
csharp
const activeTab = ref<'solve' | 'history' | 'wrong' | 'report' | 'suggestion' | 'graph'>('solve')
新增状态
csharp
const graphLoading = ref(false)
const knowledgeGraph = ref<KnowledgeGraphResponse | null>(null)
新增方法
typescript
const loadKnowledgeGraph = async () => {
graphLoading.value = true
try {
const { data } = await getKnowledgeGraph(currentStudentId.value)
knowledgeGraph.value = data
} catch (error: any) {
console.error('加载知识图谱失败:', error)
alert(error?.response?.data?.detail || '加载知识图谱失败')
} finally {
graphLoading.value = false
}
}
const switchToGraph = async () => {
activeTab.value = 'graph'
await loadKnowledgeGraph()
}
const handleRebuildKnowledgeGraph = async () => {
try {
await rebuildKnowledgeGraph(currentStudentId.value)
await loadKnowledgeGraph()
alert('知识图谱重建成功')
} catch (error: any) {
console.error('重建知识图谱失败:', error)
alert(error?.response?.data?.detail || '重建知识图谱失败')
}
}
handleSubmit
scss
await loadKnowledgeGraph()
handleImageChange
scss
await loadKnowledgeGraph()
toggleWrong
scss
await loadKnowledgeGraph()
refreshAllStudentData
scss
const refreshAllStudentData = async () => {
await Promise.all([
loadHistory(),
loadWrongList(),
loadReport(),
loadStudySuggestion(),
loadKnowledgeGraph(),
])
}
7)修改 src/components/TabNav.vue
css
defineProps<{
activeTab: 'solve' | 'history' | 'wrong' | 'report' | 'suggestion' | 'graph'
}>()
defineEmits<{
(e: 'change', value: 'solve' | 'history' | 'wrong' | 'report' | 'suggestion' | 'graph'): void
}>()
tabList 里新增
css
{ label: '知识图谱', value: 'graph' },
8)修改 src/App.vue
handleTabChange
csharp
const handleTabChange = async (
tab: 'solve' | 'history' | 'wrong' | 'report' | 'suggestion' | 'graph'
) => {
activeTab.value = tab
if (tab === 'history') {
await loadHistory()
} else if (tab === 'wrong') {
await loadWrongList()
} else if (tab === 'report') {
await loadReport()
} else if (tab === 'suggestion') {
await loadStudySuggestion()
} else if (tab === 'graph') {
await loadKnowledgeGraph()
}
}
在 template 里新增一个
xml
<template v-else-if="activeTab === 'graph'">
<div v-if="graphLoading" class="empty">知识图谱加载中...</div>
<div v-else-if="knowledgeGraph" class="report-panel">
<div class="result-card">
<div class="card-header">
<h2>学生知识图谱</h2>
<button class="retry-btn" @click="handleRebuildKnowledgeGraph">
重建图谱
</button>
</div>
<div v-if="knowledgeGraph.items.length === 0" class="empty">
暂无知识图谱数据
</div>
<div v-else class="graph-list">
<div v-for="(item, index) in knowledgeGraph.items" :key="index" class="graph-item">
<div class="graph-header">
<strong>{{ item.knowledge_name }}</strong>
<span class="weak-rate">错误率 {{ item.wrong_rate }}%</span>
</div>
<div class="graph-meta">
共练习 {{ item.total_count }} 次 / 正确 {{ item.correct_count }} 次 / 错误 {{ item.wrong_count }} 次
</div>
<div class="graph-bar">
<div
class="graph-bar-inner"
:style="{ width: `${Math.min(item.wrong_rate, 100)}%` }"
/>
</div>
</div>
</div>
</div>
</div>
</template>
9)补充 src/App.vue 样式
css
.graph-list {
margin-top: 12px;
}
.graph-item {
padding: 16px 0;
border-bottom: 1px solid #eee;
}
.graph-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.graph-meta {
color: #666;
font-size: 14px;
margin-bottom: 10px;
}
.graph-bar {
height: 10px;
background: #f0f0f0;
border-radius: 999px;
overflow: hidden;
}
.graph-bar-inner {
height: 100%;
background: linear-gradient(90deg, #f0a020, #d03050);
border-radius: 999px;
}
10)初始化补一行
onMounted 改成
scss
onMounted(async () => {
await loadStudents()
await refreshAllStudentData()
})
11)注意
改了表结构,开发阶段建议你删库重建(删库跑路):
bash
cd backend
rm math_tutor.db
然后重启后端:
lua
uvicorn app.main:app --reload --port 8000
前端:
arduino
npm run dev
