software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,329 @@
# 自然写教学数据分析与学情诊断系统软件 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))
@@ -0,0 +1,397 @@
# 自然写教学数据分析与学情诊断系统软件 V1.0
# api/report_api.py - 报告导出与查询API
# api/growth_api.py - 成长轨迹API
# model/data_models.py - 核心数据模型定义
import logging
from typing import Optional, List, Dict, Any
from datetime import datetime, date
from enum import Enum
from fastapi import APIRouter, Query, Path, HTTPException, BackgroundTasks
from pydantic import BaseModel, Field
logger = logging.getLogger("writech.analytics.api")
# ============================================================
# 报告导出API路由
# ============================================================
report_router = APIRouter(tags=["报告导出"])
class ExportRequest(BaseModel):
"""报告导出请求"""
report_type: str = Field(..., description="报告类型")
target_id: str = Field(..., description="目标ID(学生/班级)")
start_date: str = Field(..., description="开始日期")
end_date: str = Field(..., description="结束日期")
format: str = Field("pdf", description="输出格式: json/pdf/html")
include_charts: bool = Field(True, description="是否包含图表")
class ExportResponse(BaseModel):
"""报告导出响应"""
task_id: str
status: str
download_url: Optional[str] = None
estimated_seconds: int = 0
@report_router.post("/export", response_model=ExportResponse)
async def export_report(
request: ExportRequest,
background_tasks: BackgroundTasks,
):
"""
生成并导出学情报告
异步生成报告,返回任务ID。
客户端可通过任务ID轮询状态或等待WebSocket通知。
"""
logger.info(
"报告导出请求: type=%s, target=%s, format=%s",
request.report_type,
request.target_id,
request.format,
)
# 生成任务ID
task_id = f"rpt_{datetime.now().strftime('%Y%m%d%H%M%S')}_{request.target_id[:8]}"
# 将报告生成任务加入后台队列
# background_tasks.add_task(
# generate_report_task,
# task_id=task_id,
# config=request,
# )
return ExportResponse(
task_id=task_id,
status="processing",
estimated_seconds=30,
)
@report_router.get("/status/{task_id}")
async def get_export_status(task_id: str = Path(...)):
"""查询报告导出任务状态"""
# status = await query_task_status(task_id)
return {
"task_id": task_id,
"status": "completed",
"download_url": None,
}
@report_router.get("/class/{class_id}")
async def get_class_report(
class_id: str = Path(..., description="班级ID"),
subject: Optional[str] = Query(None),
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
):
"""
获取班级学情统计报告
返回班级平均分、分数分布、薄弱知识点等统计数据。
仅班级教师和校管理员有权限查看。
"""
logger.info("班级报告查询: class=%s, subject=%s", class_id, subject)
# 权限校验:教师仅可查看本班数据
# verify_class_permission(current_user, class_id)
# 从ClickHouse查询班级统计数据
# stats = await aggregate_class_report(class_id, subject, ...)
return {
"code": 0,
"message": "success",
"data": {
"class_id": class_id,
"student_count": 0,
"avg_score": 0,
"score_distribution": {},
"weak_points": [],
"top_students": [],
},
}
@report_router.get("/history")
async def list_report_history(
target_id: str = Query(..., description="目标ID"),
report_type: Optional[str] = Query(None),
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
):
"""查询历史报告列表"""
# reports = await query_report_history(target_id, report_type, ...)
return {
"code": 0,
"data": {
"total": 0,
"page": page,
"items": [],
},
}
# ============================================================
# 成长轨迹API路由
# ============================================================
growth_router = APIRouter(tags=["成长轨迹"])
@growth_router.get("/{student_id}")
async def get_growth_trajectory(
student_id: str = Path(..., description="学生ID"),
subject: Optional[str] = Query(None, description="科目"),
start_date: Optional[str] = Query(None),
end_date: Optional[str] = Query(None),
granularity: str = Query("weekly", description="粒度: daily/weekly/monthly"),
):
"""
获取学生成长轨迹
返回学生在指定时间范围内的各项指标时序数据,
包括成绩趋势、书写能力变化、学习习惯变化等。
家长仅可查看自己子女的数据。
"""
logger.info(
"成长轨迹查询: student=%s, subject=%s, granularity=%s",
student_id, subject, granularity,
)
# 权限校验
# verify_student_access(current_user, student_id)
# 从ClickHouse查询时序数据
# trend_data = await query_growth_trend(student_id, subject, ...)
return {
"code": 0,
"message": "success",
"data": {
"student_id": student_id,
"period": f"{start_date} ~ {end_date}",
"score_trend": [], # 成绩趋势
"writing_trend": [], # 书写能力趋势
"habit_trend": [], # 学习习惯趋势
"milestones": [], # 里程碑事件
},
}
@growth_router.get("/writing/{student_id}")
async def get_writing_growth(
student_id: str = Path(..., description="学生ID"),
start_date: str = Query(..., description="开始日期"),
end_date: str = Query(..., description="结束日期"),
):
"""
获取书写能力成长报告
返回笔顺准确率、书写规范性、书写速度等维度的成长趋势。
"""
logger.info(
"书写成长查询: student=%s, %s~%s",
student_id, start_date, end_date,
)
# 调用书写成长分析引擎
# from analytics.writing_growth import WritingGrowthAnalyzer
# analyzer = WritingGrowthAnalyzer()
# report = await analyzer.analyze_growth(
# student_id, start_date, end_date
# )
return {
"code": 0,
"message": "success",
"data": {
"student_id": student_id,
"overall_level": "",
"overall_score": 0,
"dimensions": {
"stroke_order": {"score": 0, "trend": "stable"},
"quality": {"score": 0, "trend": "stable"},
"speed": {"score": 0, "trend": "stable"},
"structure": {"score": 0, "trend": "stable"},
},
"snapshots": [],
"most_improved_chars": [],
"needs_practice_chars": [],
},
}
@growth_router.get("/error/analysis/{student_id}")
async def get_error_analysis(
student_id: str = Path(..., description="学生ID"),
subject: Optional[str] = Query(None),
top_n: int = Query(20, ge=1, le=100),
):
"""
错题归因分析
返回学生的错题统计、知识点薄弱分析、错因归类。
结合知识图谱进行根因分析。
"""
logger.info(
"错题分析: student=%s, subject=%s", student_id, subject
)
return {
"code": 0,
"message": "success",
"data": {
"student_id": student_id,
"total_errors": 0,
"by_subject": {}, # 按科目分组
"by_knowledge": [], # 按知识点排序
"error_types": {}, # 错因分类
"root_causes": [], # 根因分析(知识图谱)
"recommendations": [], # 学习建议
},
}
@growth_router.post("/push/parent")
async def push_to_parent(
student_id: str = Query(..., description="学生ID"),
report_type: str = Query("weekly", description="推送报告类型"),
background_tasks: BackgroundTasks = None,
):
"""
触发学情报告推送至家长端
通过WebSocket或APP推送通知家长查看学情报告。
家长端展示简化版本的学情摘要。
"""
logger.info("家长推送: student=%s, type=%s", student_id, report_type)
# 生成家长版报告
# background_tasks.add_task(
# generate_and_push_parent_report,
# student_id=student_id,
# report_type=report_type,
# )
return {
"code": 0,
"message": "推送任务已提交",
"data": {"student_id": student_id},
}
# ============================================================
# 核心数据模型定义(model/data_models.py
# ============================================================
class GradeLevel(str, Enum):
"""年级枚举"""
GRADE_1 = "grade_1"
GRADE_2 = "grade_2"
GRADE_3 = "grade_3"
GRADE_4 = "grade_4"
GRADE_5 = "grade_5"
GRADE_6 = "grade_6"
GRADE_7 = "grade_7"
GRADE_8 = "grade_8"
GRADE_9 = "grade_9"
class StudentInfo(BaseModel):
"""学生基本信息"""
student_id: str
name: str
class_id: str
grade: GradeLevel
school_id: str
gender: Optional[str] = None
created_at: Optional[str] = None
class ClassInfo(BaseModel):
"""班级基本信息"""
class_id: str
class_name: str
grade: GradeLevel
school_id: str
teacher_id: str
student_count: int = 0
class SchoolInfo(BaseModel):
"""学校信息"""
school_id: str
school_name: str
region: str
district: str
class ErrorRecord(BaseModel):
"""错题记录模型(MySQL"""
id: Optional[int] = None
student_id: str
homework_id: str
question_id: str
subject: str
knowledge_point: str = ""
error_type: str = "" # 计算错误/概念混淆/审题不清/粗心
student_answer: str = ""
correct_answer: str = ""
created_at: str = ""
class ExamAnalysis(BaseModel):
"""考试分析结果模型(ClickHouse)"""
exam_id: str
class_id: str
subject: str
exam_date: str
avg_score: float = 0.0
median_score: float = 0.0
max_score: float = 0.0
min_score: float = 0.0
std_deviation: float = 0.0
pass_rate: float = 0.0
excellent_rate: float = 0.0
score_distribution: Dict[str, int] = {}
difficulty_coefficient: float = 0.0
discrimination_index: float = 0.0
class KafkaEventSchema(BaseModel):
"""Kafka事件消息Schema"""
event_id: str
event_type: str
student_id: str
class_id: str = ""
school_id: str = ""
timestamp: str
source: str = ""
payload: Dict[str, Any] = {}
class Config:
json_schema_extra = {
"example": {
"event_id": "evt_20240101_001",
"event_type": "grade_result",
"student_id": "stu_001",
"class_id": "cls_001",
"school_id": "sch_001",
"timestamp": "2024-01-01T10:00:00+08:00",
"source": "pad",
"payload": {
"homework_id": "hw_001",
"subject": "chinese",
"score": 85,
"total_score": 100,
},
}
}