# 自然写教学数据分析与学情诊断系统软件 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