nex_math/backend/routers/analytics.py

96 lines
3.4 KiB
Python

"""学习分析。"""
from __future__ import annotations
import json
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from database import get_db
from dependencies import get_current_user, user_permissions
from models import AttemptSession, ErrorEntry, User
from schemas import AnalyticsOut, DiagnosisOut, MetricItem
from services.mastery_engine import action_for, snapshot
router = APIRouter(tags=["analytics"])
def build_analytics(db: Session, user_id: int) -> AnalyticsOut:
"""按用户聚合学习分析:本人分析与管理员查看指定用户共用同一份逻辑。"""
sessions = (
db.query(AttemptSession)
.filter(AttemptSession.user_id == user_id)
.order_by(AttemptSession.id.asc())
.all()
)
total_answers = sum(s.total for s in sessions)
total_correct = sum(s.correct for s in sessions)
accuracy = total_correct / total_answers * 100 if total_answers else 0.0
last = sessions[-1] if sessions else None
errors_count = (
db.query(ErrorEntry).filter(ErrorEntry.user_id == user_id).count()
)
knowledge_rows = snapshot(db, user_id)
weak_count = sum(1 for k in knowledge_rows if k.mastery < 60)
metrics = [
MetricItem(
label="历史正确率",
value=f"{accuracy:.1f}%" if total_answers else "0%",
),
MetricItem(
label="最近一次",
value=f"{last.correct}/{last.total}" if last else "—",
),
MetricItem(label="错题数量", value=str(errors_count)),
MetricItem(label="需重点干预", value=str(weak_count)),
]
error_counts: dict[str, int] = {}
for row in (
db.query(ErrorEntry.knowledge_name, ErrorEntry.knowledge_names)
.filter(ErrorEntry.user_id == user_id)
.all()
):
try:
names = json.loads(row[1] or "[]")
if not isinstance(names, list) or not names:
names = [row[0]]
except (TypeError, ValueError):
names = [row[0]]
for name in {str(name) for name in names}:
error_counts[name] = error_counts.get(name, 0) + 1
diagnosis = [
DiagnosisOut(
name=k.name,
mastery=k.mastery,
error_count=error_counts.get(k.name, 0),
action=action_for(k.mastery),
)
for k in sorted(knowledge_rows, key=lambda x: x.mastery)
]
return AnalyticsOut(metrics=metrics, diagnosis=diagnosis)
@router.get("/analytics", response_model=AnalyticsOut)
def get_analytics(
user_id: int | None = None,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
"""默认返回本人学习分析;管理员可用 user_id 查看指定用户(用户管理子页面)。"""
permissions = user_permissions(db, user)
target_id = user.id
if user_id is not None and user_id != user.id:
if "users:manage" not in permissions:
raise HTTPException(status_code=403, detail="没有权限查看该用户的学习分析")
target_id = user_id
elif "learning:use" not in permissions and "users:manage" not in permissions:
raise HTTPException(status_code=403, detail="没有权限执行该操作")
if db.get(User, target_id) is None:
raise HTTPException(status_code=404, detail="用户不存在")
return build_analytics(db, target_id)