# 自然写教学数据分析与学情诊断系统软件 V1.0 # api/profile_api.py - 学情画像API接口 import logging from typing import Optional, List, Dict, Any from datetime import datetime, date, timedelta from enum import Enum from fastapi import APIRouter, Query, Path, Depends, HTTPException from pydantic import BaseModel, Field logger = logging.getLogger("writech.analytics.profile") router = APIRouter(tags=["学情画像"]) # ============================================================ # 数据模型定义 # ============================================================ class SubjectEnum(str, Enum): """学科枚举""" CHINESE = "chinese" MATH = "math" ENGLISH = "english" PHYSICS = "physics" CHEMISTRY = "chemistry" BIOLOGY = "biology" class KnowledgeMastery(BaseModel): """知识点掌握度模型""" knowledge_id: str = Field(..., description="知识点ID") knowledge_name: str = Field(..., description="知识点名称") chapter: str = Field("", description="所属章节") mastery_level: float = Field(0.0, ge=0.0, le=1.0, description="掌握度(0-1)") practice_count: int = Field(0, description="练习次数") correct_rate: float = Field(0.0, description="正确率") last_practice_at: Optional[str] = Field(None, description="最近练习时间") trend: str = Field("stable", description="趋势: improving/declining/stable") class WeakPoint(BaseModel): """薄弱知识点模型""" knowledge_id: str knowledge_name: str mastery_level: float error_count: int = Field(0, description="错误次数") suggested_exercises: List[str] = Field([], description="推荐练习题ID") related_knowledge: List[str] = Field([], description="关联知识点") class StudentProfile(BaseModel): """学生学情画像完整模型""" student_id: str student_name: str class_id: str grade: str school_id: str # 总体学业水平 overall_score: float = Field(0.0, description="综合评分(百分制)") overall_rank: int = Field(0, description="班级排名") overall_trend: str = Field("stable", description="总体趋势") # 各科目掌握度 subject_scores: Dict[str, float] = Field({}, description="各科目评分") # 知识点掌握度矩阵 knowledge_mastery: List[KnowledgeMastery] = Field([]) # 薄弱环节 weak_points: List[WeakPoint] = Field([]) # 书写能力评估 writing_quality_score: float = Field(0.0, description="书写规范性评分") stroke_order_accuracy: float = Field(0.0, description="笔顺正确率") writing_speed: float = Field(0.0, description="书写速度(字/分)") # 学习习惯统计 avg_daily_study_minutes: float = Field(0.0, description="日均学习时长(分)") homework_completion_rate: float = Field(0.0, description="作业完成率") homework_on_time_rate: float = Field(0.0, description="按时提交率") # 更新时间 updated_at: str = Field("", description="画像更新时间") class ClassProfile(BaseModel): """班级学情统计模型""" class_id: str class_name: str grade: str student_count: int # 班级整体指标 avg_score: float = Field(0.0, description="班级平均分") median_score: float = Field(0.0, description="班级中位分") max_score: float = Field(0.0, description="最高分") min_score: float = Field(0.0, description="最低分") std_deviation: float = Field(0.0, description="标准差") # 成绩分布(分数段人数) score_distribution: Dict[str, int] = Field( {}, description="分数段分布: {'90-100': 5, '80-89': 10, ...}" ) # 知识点班级掌握度 knowledge_avg_mastery: List[Dict[str, Any]] = Field([]) # 薄弱知识点(班级维度) class_weak_points: List[Dict[str, Any]] = Field([]) # 作业统计 homework_avg_completion: float = Field(0.0) homework_avg_score: float = Field(0.0) class ProfileCompareResponse(BaseModel): """学情对比响应""" student_profile: StudentProfile class_avg: Dict[str, float] grade_avg: Dict[str, float] percentile: float = Field(0.0, description="年级百分位排名") # ============================================================ # API接口实现 # ============================================================ @router.get("/student/{student_id}", response_model=StudentProfile) async def get_student_profile( student_id: str = Path(..., description="学生ID"), subject: Optional[SubjectEnum] = Query(None, description="筛选科目"), ): """ 获取学生个人学情画像 返回学生的知识掌握度、薄弱环节、书写能力、学习习惯等全面画像数据。 教师可查看本班学生,家长可查看自己子女。 """ logger.info("查询学生画像: student_id=%s, subject=%s", student_id, subject) try: # 从ClickHouse查询学生画像宽表数据 # profile_data = await query_student_profile(student_id) # 从Neo4j查询知识点掌握度和薄弱环节 # mastery = await query_knowledge_mastery(student_id, subject) # weak = await query_weak_points(student_id, subject) # 组装画像数据 profile = StudentProfile( student_id=student_id, student_name="", class_id="", grade="", school_id="", updated_at=datetime.now().isoformat(), ) return profile except Exception as e: logger.error("查询学生画像失败: %s", str(e)) raise HTTPException(status_code=500, detail=f"查询学生画像失败: {str(e)}") @router.get("/class/{class_id}", response_model=ClassProfile) async def get_class_profile( class_id: str = Path(..., description="班级ID"), subject: Optional[SubjectEnum] = Query(None, description="筛选科目"), start_date: Optional[str] = Query(None, description="起始日期"), end_date: Optional[str] = Query(None, description="结束日期"), ): """ 获取班级学情统计 返回班级平均分、分数分布、薄弱知识点等班级维度的统计数据。 仅班级教师和校管理员可查看。 """ logger.info("查询班级学情: class_id=%s, subject=%s", class_id, subject) try: # 从ClickHouse聚合查询班级统计数据 # class_stats = await aggregate_class_stats(class_id, subject, ...) class_profile = ClassProfile( class_id=class_id, class_name="", grade="", student_count=0, ) return class_profile except Exception as e: logger.error("查询班级学情失败: %s", str(e)) raise HTTPException(status_code=500, detail=f"查询班级学情失败: {str(e)}") @router.get("/compare/{student_id}", response_model=ProfileCompareResponse) async def compare_student_with_class( student_id: str = Path(..., description="学生ID"), subject: Optional[SubjectEnum] = Query(None), ): """ 学生与班级/年级对比分析 将学生各项指标与班级平均和年级平均对比,计算百分位排名。 """ logger.info("学情对比分析: student_id=%s", student_id) try: # 查询学生个人画像 # student = await query_student_profile(student_id) # 查询班级和年级平均值 # class_avg = await query_class_avg(student.class_id, subject) # grade_avg = await query_grade_avg(student.grade, subject) # 计算百分位排名 # percentile = await calc_percentile(student_id, student.grade) return ProfileCompareResponse( student_profile=StudentProfile( student_id=student_id, student_name="", class_id="", grade="", school_id="", ), class_avg={}, grade_avg={}, percentile=0.0, ) except Exception as e: logger.error("学情对比失败: %s", str(e)) raise HTTPException(status_code=500, detail=str(e)) @router.get("/knowledge-map/{student_id}") async def get_knowledge_map( student_id: str = Path(..., description="学生ID"), subject: SubjectEnum = Query(..., description="科目"), ): """ 获取知识图谱掌握度可视化数据 从Neo4j查询该科目知识图谱结构,叠加学生个人掌握度, 生成可供前端ECharts渲染的图谱JSON数据。 """ logger.info( "查询知识图谱: student_id=%s, subject=%s", student_id, subject ) try: # 从Neo4j查询知识点节点和边 # nodes = await neo4j_query_knowledge_nodes(subject) # edges = await neo4j_query_knowledge_edges(subject) # 查询学生对各知识点的掌握度 # mastery_map = await query_mastery_map(student_id, subject) # 组装ECharts图谱数据格式 graph_data = { "nodes": [], # [{id, name, mastery, category, ...}] "edges": [], # [{source, target, relation_type}] "categories": [ {"name": "已掌握"}, {"name": "部分掌握"}, {"name": "未掌握"}, {"name": "未学习"}, ], } return { "code": 0, "message": "success", "data": graph_data, } except Exception as e: logger.error("查询知识图谱失败: %s", str(e)) raise HTTPException(status_code=500, detail=str(e)) @router.get("/weak-analysis/{student_id}") async def analyze_weak_points( student_id: str = Path(..., description="学生ID"), subject: Optional[SubjectEnum] = Query(None), top_n: int = Query(10, ge=1, le=50, description="返回前N个薄弱点"), ): """ 薄弱知识点深度分析 结合错题归因和知识图谱前驱关系,分析薄弱根因并给出学习建议。 """ logger.info( "薄弱分析: student_id=%s, subject=%s, top=%d", student_id, subject, top_n, ) try: # 查询错题记录及关联知识点 # errors = await query_error_records(student_id, subject) # 利用Neo4j知识图谱进行根因分析 # 如果某知识点正确率低,检查其前驱知识点是否也未掌握 # root_causes = await trace_knowledge_prerequisites(errors) # 生成学习建议 weak_analysis = { "weak_points": [], # 薄弱知识点列表 "root_causes": [], # 根因知识点 "suggestions": [], # 学习建议 "recommended_exercises": [], # 推荐练习 } return { "code": 0, "message": "success", "data": weak_analysis, } except Exception as e: logger.error("薄弱分析失败: %s", str(e)) raise HTTPException(status_code=500, detail=str(e))