678 lines
22 KiB
Python
678 lines
22 KiB
Python
# 自然写教学数据分析与学情诊断系统软件 V1.0
|
|
# report/report_generator.py - 学情报告生成引擎
|
|
|
|
import logging
|
|
import json
|
|
import hashlib
|
|
from typing import Any, Dict, List, Optional
|
|
from datetime import datetime, date, timedelta
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
|
|
logger = logging.getLogger("writech.analytics.report")
|
|
|
|
|
|
# ============================================================
|
|
# 报告类型与模型
|
|
# ============================================================
|
|
|
|
class ReportType(str, Enum):
|
|
"""报告类型枚举"""
|
|
STUDENT_WEEKLY = "student_weekly" # 学生周报
|
|
STUDENT_MONTHLY = "student_monthly" # 学生月报
|
|
CLASS_WEEKLY = "class_weekly" # 班级周报
|
|
CLASS_MONTHLY = "class_monthly" # 班级月报
|
|
EXAM_ANALYSIS = "exam_analysis" # 考试分析报告
|
|
WRITING_GROWTH = "writing_growth" # 书写成长报告
|
|
PARENT_PUSH = "parent_push" # 家长推送报告
|
|
|
|
|
|
class ReportFormat(str, Enum):
|
|
"""报告输出格式"""
|
|
JSON = "json"
|
|
PDF = "pdf"
|
|
HTML = "html"
|
|
|
|
|
|
@dataclass
|
|
class ReportSection:
|
|
"""报告章节"""
|
|
title: str
|
|
section_type: str # summary/chart/table/text/recommendation
|
|
content: Dict[str, Any] = field(default_factory=dict)
|
|
order: int = 0
|
|
|
|
|
|
@dataclass
|
|
class ReportConfig:
|
|
"""报告生成配置"""
|
|
report_type: ReportType
|
|
target_id: str # 学生ID或班级ID
|
|
start_date: str
|
|
end_date: str
|
|
output_format: ReportFormat = ReportFormat.JSON
|
|
include_charts: bool = True
|
|
include_recommendations: bool = True
|
|
language: str = "zh-CN"
|
|
|
|
|
|
@dataclass
|
|
class GeneratedReport:
|
|
"""生成的报告"""
|
|
report_id: str
|
|
report_type: ReportType
|
|
target_id: str
|
|
title: str
|
|
period: str
|
|
sections: List[ReportSection]
|
|
summary: str = ""
|
|
generated_at: str = ""
|
|
file_path: Optional[str] = None
|
|
|
|
def to_json(self) -> Dict[str, Any]:
|
|
"""序列化为JSON"""
|
|
return {
|
|
"report_id": self.report_id,
|
|
"report_type": self.report_type.value,
|
|
"target_id": self.target_id,
|
|
"title": self.title,
|
|
"period": self.period,
|
|
"summary": self.summary,
|
|
"sections": [
|
|
{
|
|
"title": s.title,
|
|
"type": s.section_type,
|
|
"content": s.content,
|
|
"order": s.order,
|
|
}
|
|
for s in self.sections
|
|
],
|
|
"generated_at": self.generated_at,
|
|
"file_path": self.file_path,
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# 报告生成引擎
|
|
# ============================================================
|
|
|
|
class ReportGenerator:
|
|
"""
|
|
学情报告生成引擎
|
|
|
|
支持生成:
|
|
1. 学生周报/月报(个人学情概览+各科分析+书写能力+建议)
|
|
2. 班级周报/月报(班级统计+分数分布+薄弱知识点)
|
|
3. 考试分析报告(成绩分析+区分度+难度系数)
|
|
4. 书写成长报告(书写质量趋势+笔顺进步+对比)
|
|
5. 家长推送报告(简化版个人学情+学习建议)
|
|
|
|
输出格式: JSON / PDF / HTML
|
|
"""
|
|
|
|
def __init__(self, output_dir: str, template_dir: str):
|
|
"""初始化报告引擎"""
|
|
self.output_dir = output_dir
|
|
self.template_dir = template_dir
|
|
logger.info("报告引擎初始化: output=%s", output_dir)
|
|
|
|
async def generate_report(
|
|
self, config: ReportConfig
|
|
) -> GeneratedReport:
|
|
"""
|
|
根据配置生成报告
|
|
|
|
流程:
|
|
1. 从ClickHouse/MySQL查询原始数据
|
|
2. 调用对应报告类型的分析逻辑
|
|
3. 组装报告章节
|
|
4. 输出为指定格式
|
|
"""
|
|
logger.info(
|
|
"开始生成报告: type=%s, target=%s, period=%s~%s",
|
|
config.report_type.value,
|
|
config.target_id,
|
|
config.start_date,
|
|
config.end_date,
|
|
)
|
|
|
|
# 根据报告类型分发
|
|
generator_map = {
|
|
ReportType.STUDENT_WEEKLY: self._gen_student_report,
|
|
ReportType.STUDENT_MONTHLY: self._gen_student_report,
|
|
ReportType.CLASS_WEEKLY: self._gen_class_report,
|
|
ReportType.CLASS_MONTHLY: self._gen_class_report,
|
|
ReportType.EXAM_ANALYSIS: self._gen_exam_report,
|
|
ReportType.WRITING_GROWTH: self._gen_writing_report,
|
|
ReportType.PARENT_PUSH: self._gen_parent_report,
|
|
}
|
|
|
|
gen_func = generator_map.get(config.report_type)
|
|
if not gen_func:
|
|
raise ValueError(f"不支持的报告类型: {config.report_type}")
|
|
|
|
report = await gen_func(config)
|
|
|
|
# 输出为指定格式
|
|
if config.output_format == ReportFormat.PDF:
|
|
await self._export_pdf(report)
|
|
elif config.output_format == ReportFormat.HTML:
|
|
await self._export_html(report)
|
|
|
|
logger.info(
|
|
"报告生成完成: id=%s, title=%s",
|
|
report.report_id, report.title,
|
|
)
|
|
|
|
return report
|
|
|
|
async def _gen_student_report(
|
|
self, config: ReportConfig
|
|
) -> GeneratedReport:
|
|
"""
|
|
生成学生个人学情报告(周报/月报)
|
|
|
|
章节结构:
|
|
1. 总体概览(综合评分、排名、趋势)
|
|
2. 各科目分析(分数、掌握知识点、薄弱点)
|
|
3. 作业完成情况
|
|
4. 书写能力评估
|
|
5. 学习习惯分析
|
|
6. 个性化建议
|
|
"""
|
|
report_id = self._gen_report_id(config)
|
|
period_label = f"{config.start_date} ~ {config.end_date}"
|
|
is_weekly = config.report_type == ReportType.STUDENT_WEEKLY
|
|
|
|
sections: List[ReportSection] = []
|
|
|
|
# 第1节: 总体概览
|
|
# overview_data = await self._query_student_overview(
|
|
# config.target_id, config.start_date, config.end_date
|
|
# )
|
|
sections.append(ReportSection(
|
|
title="总体学情概览",
|
|
section_type="summary",
|
|
content={
|
|
"overall_score": 0,
|
|
"rank_in_class": 0,
|
|
"rank_change": 0, # 与上期对比排名变化
|
|
"trend": "stable",
|
|
"highlight": "", # 亮点描述
|
|
},
|
|
order=1,
|
|
))
|
|
|
|
# 第2节: 各科目分析
|
|
sections.append(ReportSection(
|
|
title="各科目学情分析",
|
|
section_type="chart",
|
|
content={
|
|
"chart_type": "radar", # 雷达图
|
|
"subjects": [], # [{name, score, class_avg, grade_avg}]
|
|
"detail": [], # 各科详细分析
|
|
},
|
|
order=2,
|
|
))
|
|
|
|
# 第3节: 作业完成情况
|
|
sections.append(ReportSection(
|
|
title="作业完成统计",
|
|
section_type="table",
|
|
content={
|
|
"total_homework": 0,
|
|
"completed": 0,
|
|
"on_time": 0,
|
|
"avg_score": 0,
|
|
"completion_rate": 0,
|
|
"detail_list": [], # 各科作业明细
|
|
},
|
|
order=3,
|
|
))
|
|
|
|
# 第4节: 书写能力评估
|
|
sections.append(ReportSection(
|
|
title="书写能力评估",
|
|
section_type="chart",
|
|
content={
|
|
"chart_type": "line", # 折线图展示趋势
|
|
"stroke_order_accuracy": 0,
|
|
"writing_quality": 0,
|
|
"writing_speed": 0,
|
|
"trend_data": [], # 时序数据点
|
|
"improvement": "",
|
|
},
|
|
order=4,
|
|
))
|
|
|
|
# 第5节: 学习习惯
|
|
sections.append(ReportSection(
|
|
title="学习习惯分析",
|
|
section_type="chart",
|
|
content={
|
|
"chart_type": "bar", # 柱状图展示每日时长
|
|
"avg_daily_minutes": 0,
|
|
"peak_hour": 0,
|
|
"weekly_pattern": [], # 周一~日时长
|
|
"consistency": 0,
|
|
},
|
|
order=5,
|
|
))
|
|
|
|
# 第6节: 个性化建议
|
|
if config.include_recommendations:
|
|
recommendations = self._generate_recommendations(
|
|
student_id=config.target_id,
|
|
sections=sections,
|
|
)
|
|
sections.append(ReportSection(
|
|
title="个性化学习建议",
|
|
section_type="recommendation",
|
|
content={
|
|
"recommendations": recommendations,
|
|
},
|
|
order=6,
|
|
))
|
|
|
|
# 生成摘要
|
|
summary = self._generate_summary(sections, "student")
|
|
|
|
return GeneratedReport(
|
|
report_id=report_id,
|
|
report_type=config.report_type,
|
|
target_id=config.target_id,
|
|
title=f"学生{'周' if is_weekly else '月'}学情报告",
|
|
period=period_label,
|
|
sections=sections,
|
|
summary=summary,
|
|
generated_at=datetime.now().isoformat(),
|
|
)
|
|
|
|
async def _gen_class_report(
|
|
self, config: ReportConfig
|
|
) -> GeneratedReport:
|
|
"""
|
|
生成班级学情报告
|
|
|
|
章节: 班级概览、成绩分布、薄弱知识点、优秀/进步学生、教学建议
|
|
"""
|
|
report_id = self._gen_report_id(config)
|
|
sections: List[ReportSection] = []
|
|
|
|
# 班级概览
|
|
sections.append(ReportSection(
|
|
title="班级学情概览",
|
|
section_type="summary",
|
|
content={
|
|
"student_count": 0,
|
|
"avg_score": 0,
|
|
"median_score": 0,
|
|
"pass_rate": 0,
|
|
"excellent_rate": 0,
|
|
},
|
|
order=1,
|
|
))
|
|
|
|
# 成绩分布
|
|
sections.append(ReportSection(
|
|
title="成绩分布分析",
|
|
section_type="chart",
|
|
content={
|
|
"chart_type": "histogram",
|
|
"distribution": {}, # 分数段人数分布
|
|
"comparison": {}, # 与上期对比
|
|
},
|
|
order=2,
|
|
))
|
|
|
|
# 薄弱知识点
|
|
sections.append(ReportSection(
|
|
title="班级薄弱知识点",
|
|
section_type="table",
|
|
content={
|
|
"weak_points": [], # [{知识点, 正确率, 涉及人数}]
|
|
},
|
|
order=3,
|
|
))
|
|
|
|
# 优秀/进步学生
|
|
sections.append(ReportSection(
|
|
title="优秀与进步学生",
|
|
section_type="table",
|
|
content={
|
|
"top_students": [], # 前10名
|
|
"most_improved": [], # 进步最大的学生
|
|
"need_attention": [], # 需关注的学生
|
|
},
|
|
order=4,
|
|
))
|
|
|
|
# 教学建议
|
|
sections.append(ReportSection(
|
|
title="教学改进建议",
|
|
section_type="recommendation",
|
|
content={
|
|
"recommendations": [
|
|
"针对薄弱知识点加强集中讲解和专项练习",
|
|
"关注成绩下滑学生,及时进行个别辅导",
|
|
"利用分层作业满足不同水平学生需求",
|
|
],
|
|
},
|
|
order=5,
|
|
))
|
|
|
|
return GeneratedReport(
|
|
report_id=report_id,
|
|
report_type=config.report_type,
|
|
target_id=config.target_id,
|
|
title="班级学情分析报告",
|
|
period=f"{config.start_date} ~ {config.end_date}",
|
|
sections=sections,
|
|
generated_at=datetime.now().isoformat(),
|
|
)
|
|
|
|
async def _gen_exam_report(
|
|
self, config: ReportConfig
|
|
) -> GeneratedReport:
|
|
"""生成考试分析报告(成绩分布+题目区分度+难度系数)"""
|
|
report_id = self._gen_report_id(config)
|
|
|
|
sections = [
|
|
ReportSection(
|
|
title="考试基本信息",
|
|
section_type="summary",
|
|
content={"exam_name": "", "subject": "", "total_score": 100},
|
|
order=1,
|
|
),
|
|
ReportSection(
|
|
title="成绩统计",
|
|
section_type="chart",
|
|
content={
|
|
"avg": 0, "median": 0, "max": 0, "min": 0,
|
|
"std_dev": 0, "pass_rate": 0,
|
|
"distribution": {},
|
|
},
|
|
order=2,
|
|
),
|
|
ReportSection(
|
|
title="题目分析",
|
|
section_type="table",
|
|
content={
|
|
"questions": [], # 每题的得分率、区分度、难度系数
|
|
},
|
|
order=3,
|
|
),
|
|
]
|
|
|
|
return GeneratedReport(
|
|
report_id=report_id,
|
|
report_type=config.report_type,
|
|
target_id=config.target_id,
|
|
title="考试分析报告",
|
|
period=config.start_date,
|
|
sections=sections,
|
|
generated_at=datetime.now().isoformat(),
|
|
)
|
|
|
|
async def _gen_writing_report(
|
|
self, config: ReportConfig
|
|
) -> GeneratedReport:
|
|
"""生成书写成长报告"""
|
|
report_id = self._gen_report_id(config)
|
|
|
|
sections = [
|
|
ReportSection(
|
|
title="书写能力总评",
|
|
section_type="summary",
|
|
content={
|
|
"overall_level": "",
|
|
"stroke_accuracy": 0,
|
|
"quality_score": 0,
|
|
"speed": 0,
|
|
},
|
|
order=1,
|
|
),
|
|
ReportSection(
|
|
title="成长趋势",
|
|
section_type="chart",
|
|
content={
|
|
"chart_type": "line",
|
|
"data_points": [], # 按周/月的评分趋势
|
|
},
|
|
order=2,
|
|
),
|
|
ReportSection(
|
|
title="常见书写问题",
|
|
section_type="table",
|
|
content={
|
|
"issues": [], # 笔顺错误、结构问题等
|
|
},
|
|
order=3,
|
|
),
|
|
]
|
|
|
|
return GeneratedReport(
|
|
report_id=report_id,
|
|
report_type=config.report_type,
|
|
target_id=config.target_id,
|
|
title="书写成长报告",
|
|
period=f"{config.start_date} ~ {config.end_date}",
|
|
sections=sections,
|
|
generated_at=datetime.now().isoformat(),
|
|
)
|
|
|
|
async def _gen_parent_report(
|
|
self, config: ReportConfig
|
|
) -> GeneratedReport:
|
|
"""
|
|
生成家长推送报告(简化版)
|
|
|
|
家长端报告简洁明了:
|
|
- 本周学习概况(评分、排名变化)
|
|
- 学习时长统计
|
|
- 需要关注的科目
|
|
- 家长配合建议
|
|
"""
|
|
report_id = self._gen_report_id(config)
|
|
|
|
sections = [
|
|
ReportSection(
|
|
title="本周学习概况",
|
|
section_type="summary",
|
|
content={
|
|
"overall_score": 0,
|
|
"rank_change": 0,
|
|
"homework_completed": 0,
|
|
"total_homework": 0,
|
|
"study_minutes": 0,
|
|
},
|
|
order=1,
|
|
),
|
|
ReportSection(
|
|
title="需要关注",
|
|
section_type="text",
|
|
content={
|
|
"attention_subjects": [],
|
|
"weak_points": [],
|
|
},
|
|
order=2,
|
|
),
|
|
ReportSection(
|
|
title="家长建议",
|
|
section_type="recommendation",
|
|
content={
|
|
"recommendations": [
|
|
"建议督促孩子按时完成作业",
|
|
"建议每天安排15-20分钟练字时间",
|
|
"多鼓励孩子在薄弱科目上的进步",
|
|
],
|
|
},
|
|
order=3,
|
|
),
|
|
]
|
|
|
|
return GeneratedReport(
|
|
report_id=report_id,
|
|
report_type=config.report_type,
|
|
target_id=config.target_id,
|
|
title="孩子本周学情报告",
|
|
period=f"{config.start_date} ~ {config.end_date}",
|
|
sections=sections,
|
|
generated_at=datetime.now().isoformat(),
|
|
)
|
|
|
|
def _generate_recommendations(
|
|
self,
|
|
student_id: str,
|
|
sections: List[ReportSection],
|
|
) -> List[str]:
|
|
"""基于各章节数据生成个性化学习建议"""
|
|
recommendations: List[str] = []
|
|
|
|
# 根据作业完成情况生成建议
|
|
for section in sections:
|
|
if section.title == "作业完成统计":
|
|
rate = section.content.get("completion_rate", 0)
|
|
if rate < 80:
|
|
recommendations.append(
|
|
"作业完成率偏低,建议养成当天作业当天完成的习惯"
|
|
)
|
|
|
|
if section.title == "书写能力评估":
|
|
quality = section.content.get("writing_quality", 0)
|
|
if quality < 60:
|
|
recommendations.append(
|
|
"书写规范性有待提高,建议每天坚持15分钟字帖练习"
|
|
)
|
|
|
|
if section.title == "学习习惯分析":
|
|
consistency = section.content.get("consistency", 0)
|
|
if consistency < 0.5:
|
|
recommendations.append(
|
|
"学习时间不够规律,建议制定固定的学习作息计划"
|
|
)
|
|
|
|
if not recommendations:
|
|
recommendations.append("继续保持良好的学习习惯,争取更大进步!")
|
|
|
|
return recommendations
|
|
|
|
def _generate_summary(
|
|
self,
|
|
sections: List[ReportSection],
|
|
report_target: str,
|
|
) -> str:
|
|
"""根据报告章节自动生成文字摘要"""
|
|
if report_target == "student":
|
|
return "本报告汇总了该学生在报告周期内的学业表现、书写能力和学习习惯分析。"
|
|
elif report_target == "class":
|
|
return "本报告汇总了班级在报告周期内的整体学情、成绩分布和教学建议。"
|
|
return ""
|
|
|
|
def _gen_report_id(self, config: ReportConfig) -> str:
|
|
"""生成唯一报告ID"""
|
|
raw = (
|
|
f"{config.report_type.value}_{config.target_id}_"
|
|
f"{config.start_date}_{config.end_date}"
|
|
)
|
|
return hashlib.md5(raw.encode()).hexdigest()[:16]
|
|
|
|
async def _export_pdf(self, report: GeneratedReport) -> None:
|
|
"""
|
|
将报告导出为PDF文件
|
|
|
|
使用ReportLab/WeasyPrint渲染PDF:
|
|
- 页眉: 自然写logo + 报告标题
|
|
- 正文: 各章节内容(图表使用ECharts渲染为图片)
|
|
- 页脚: 页码 + 生成时间
|
|
"""
|
|
# from weasyprint import HTML
|
|
# html_content = self._render_html_template(report)
|
|
# pdf_path = f"{self.output_dir}/{report.report_id}.pdf"
|
|
# HTML(string=html_content).write_pdf(pdf_path)
|
|
# report.file_path = pdf_path
|
|
logger.info("PDF导出: %s", report.report_id)
|
|
|
|
async def _export_html(self, report: GeneratedReport) -> None:
|
|
"""将报告导出为HTML文件"""
|
|
# html_path = f"{self.output_dir}/{report.report_id}.html"
|
|
# with open(html_path, "w", encoding="utf-8") as f:
|
|
# f.write(self._render_html_template(report))
|
|
# report.file_path = html_path
|
|
logger.info("HTML导出: %s", report.report_id)
|
|
|
|
|
|
# ============================================================
|
|
# 定时报告生成调度
|
|
# ============================================================
|
|
|
|
class ReportScheduler:
|
|
"""
|
|
报告定时生成调度器
|
|
|
|
支持:
|
|
- 每日凌晨生成前一天的学生日报
|
|
- 每周一生成上周的学生周报和班级周报
|
|
- 每月1日生成上月的月报
|
|
"""
|
|
|
|
def __init__(self, generator: ReportGenerator):
|
|
self.generator = generator
|
|
logger.info("报告调度器初始化")
|
|
|
|
async def run_daily_reports(self) -> int:
|
|
"""执行每日报告生成任务"""
|
|
yesterday = (date.today() - timedelta(days=1)).isoformat()
|
|
logger.info("执行每日报告生成: date=%s", yesterday)
|
|
|
|
generated_count = 0
|
|
# 查询所有活跃学生ID
|
|
# student_ids = await get_active_student_ids()
|
|
# for sid in student_ids:
|
|
# config = ReportConfig(
|
|
# report_type=ReportType.PARENT_PUSH,
|
|
# target_id=sid,
|
|
# start_date=yesterday,
|
|
# end_date=yesterday,
|
|
# )
|
|
# await self.generator.generate_report(config)
|
|
# generated_count += 1
|
|
|
|
logger.info("每日报告生成完成: 共%d份", generated_count)
|
|
return generated_count
|
|
|
|
async def run_weekly_reports(self) -> int:
|
|
"""执行每周报告生成任务"""
|
|
end_date = date.today() - timedelta(days=1)
|
|
start_date = end_date - timedelta(days=6)
|
|
logger.info(
|
|
"执行每周报告: %s ~ %s",
|
|
start_date.isoformat(),
|
|
end_date.isoformat(),
|
|
)
|
|
|
|
generated_count = 0
|
|
# 生成学生周报和班级周报
|
|
# ...
|
|
|
|
logger.info("每周报告生成完成: 共%d份", generated_count)
|
|
return generated_count
|
|
|
|
async def run_monthly_reports(self) -> int:
|
|
"""执行月度报告生成任务"""
|
|
today = date.today()
|
|
end_date = today.replace(day=1) - timedelta(days=1)
|
|
start_date = end_date.replace(day=1)
|
|
logger.info(
|
|
"执行月度报告: %s ~ %s",
|
|
start_date.isoformat(),
|
|
end_date.isoformat(),
|
|
)
|
|
|
|
generated_count = 0
|
|
# 生成学生月报、班级月报、书写成长报告
|
|
# ...
|
|
|
|
logger.info("月度报告生成完成: 共%d份", generated_count)
|
|
return generated_count
|