From 2a2ef429b4443255e9624a799c90f97c9566d3c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9C=B1=E6=BD=AE?= Date: Sun, 30 Nov 2025 20:11:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8F=90=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bruno/survey/survey_report.bru | 2 +- enhanced_survey_system.py | 27 +- main.py | 1170 +++++++++++++++++++++++++++++++- public/index.html | 31 +- public/survey.html | 217 +++++- 5 files changed, 1380 insertions(+), 67 deletions(-) diff --git a/bruno/survey/survey_report.bru b/bruno/survey/survey_report.bru index 479dc4b..f98b4ec 100644 --- a/bruno/survey/survey_report.bru +++ b/bruno/survey/survey_report.bru @@ -5,7 +5,7 @@ meta { } post { - url: http://120.26.23.172:5678/webhook-test/survey_report + url: https://n8n.aitravelmaster.com/webhook-test/survey_report body: json auth: inherit } diff --git a/enhanced_survey_system.py b/enhanced_survey_system.py index aa4b60d..4557a2b 100644 --- a/enhanced_survey_system.py +++ b/enhanced_survey_system.py @@ -286,15 +286,34 @@ class ReportGenerator: print(f"成功提取JSON格式内容,长度: {len(json_content)} 字符") try: - return json.loads(json_content) + parsed_data = json.loads(json_content) + + # 如果返回的是数组,提取第一个元素 + if isinstance(parsed_data, list): + print(f"检测到数组响应,长度: {len(parsed_data)},提取下标为0的数据") + if len(parsed_data) > 0: + return parsed_data[0] + else: + print(f"数组为空,返回空字典") + return {} + + # 如果已经是字典格式,直接返回 + elif isinstance(parsed_data, dict): + return parsed_data + + # 其他情况,将数据包装成字典 + else: + print(f"响应不是字典或数组格式,将数据包装为字典") + return {"data": parsed_data} + except json.JSONDecodeError as e: print(f"JSON解析失败: {e}") - # 如果都不是,返回原始内容 - return json_content + # 如果解析失败,返回原始内容包装的字典 + return {"raw_content": json_content} except Exception as e: print(f"解析JSON响应失败: {e}") - return json_content + return {"error": str(e), "raw_content": json_content} def save_report_to_db(self, session_id, report_data, analysis_data): """保存报告到数据库""" diff --git a/main.py b/main.py index 6f5e03b..dd5ce59 100644 --- a/main.py +++ b/main.py @@ -50,6 +50,8 @@ class CreateSessionRequest(BaseModel): selectedTag: str = "" selectedTagsList: List[str] = [] questionsConfig: Dict[str, int] + totalQuestions: int = 0 # 总题目数 + totalScore: int = 0 # 总分数 class SaveAnswersRequest(BaseModel): sessionId: str @@ -114,10 +116,23 @@ async def create_session(request: CreateSessionRequest): # 创建答题会话 session_id = str(uuid.uuid4()) + + # 如果前端没有传递总题目数和总分数,则从配置中计算 + if request.totalQuestions == 0 or request.totalScore == 0: + # 根据题目配置计算总题目数和总分数 + total_questions = sum(questions_config.values()) + # 基础题5分,进阶题10分,竞赛题15分 + total_score = (questions_config.get('基础题', 0) * 5 + + questions_config.get('进阶题', 0) * 10 + + questions_config.get('竞赛题', 0) * 15) + else: + total_questions = request.totalQuestions + total_score = request.totalScore + cursor.execute(''' - INSERT INTO quiz_sessions (id, student_id, questions_config, status) - VALUES (?, ?, ?, 'created') - ''', (session_id, student_id, json.dumps(questions_config))) + INSERT INTO quiz_sessions (id, student_id, questions_config, status, total_questions, max_score) + VALUES (?, ?, ?, 'created', ?, ?) + ''', (session_id, student_id, json.dumps(questions_config), total_questions, total_score)) conn.commit() conn.close() @@ -444,6 +459,137 @@ async def get_filtered_questions(session_id: str): raise HTTPException(status_code=500, detail=f"获取筛选题目失败: {str(e)}") +@app.get("/api/quiz-results/{session_id}") +async def get_quiz_results(session_id: str): + """获取答题结果详情""" + try: + conn = sqlite3.connect('data/survey.db') + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # 获取答题结果 + cursor.execute(''' + SELECT qa.*, s.name, s.school, s.grade, qs.questions_config, qs.status, + qs.total_questions, qs.max_score + FROM quiz_answers qa + JOIN students s ON qa.student_name = s.name AND qa.student_school = s.school AND qa.student_grade = s.grade + JOIN quiz_sessions qs ON qa.session_id = qs.id + WHERE qa.session_id = ? + ''', (session_id,)) + + result = cursor.fetchone() + if not result: + raise HTTPException(status_code=404, detail="答题结果不存在") + + # 解析答题数据 + answers_data = json.loads(result['answers_data']) + questions_config = json.loads(result['questions_config']) + + # 获取原始题目数据以便显示选项 + cursor.execute(''' + SELECT s.selected_tag + FROM students s + JOIN quiz_sessions qs ON s.id = qs.student_id + WHERE qs.id = ? + ''', (session_id,)) + + session_info = cursor.fetchone() + selected_tag = session_info['selected_tag'] if session_info else '' + + # 解析标签,提取筛选条件 + subject = "" + grade = "" + unit = "" + + if selected_tag and "学科:" in selected_tag: + filters = selected_tag.split(' | ') + for filter_item in filters: + if filter_item.startswith("学科:"): + subject = filter_item[3:] + elif filter_item.startswith("年级:"): + grade = filter_item[3:] + elif filter_item.startswith("单元:"): + unit = filter_item[3:] + + # 获取筛选后的题目 + filtered_questions = get_questions_by_filters( + subject=subject, + grade=grade, + unit=unit + ) + + # 重新选择题目以获取完整的题目数据 + selected_questions = select_questions_by_config(filtered_questions, questions_config) + + # 合并题目数据和答题结果 + detailed_results = [] + for i, answer in enumerate(answers_data): + # 找到对应的题目 + question = None + for q in selected_questions: + if q['questionId'] == answer['questionId']: + question = q + break + + # 如果找不到完整题目数据,使用答题数据中的基本信息 + if not question: + detailed_results.append({ + 'questionNumber': i + 1, + 'questionText': answer['questionText'], + 'questionType': answer['questionType'], + 'options': {}, # 空选项,因为没有完整题目数据 + 'userAnswer': answer['userAnswer'], + 'correctAnswer': answer['correctAnswer'], + 'isCorrect': answer['isCorrect'], + 'score': answer['score'], + 'questionId': answer['questionId'] + }) + else: + # 获取所有选项 + options = {} + labels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] + for label in labels: + for key in question: + # 使用正则表达式来匹配选项键 + import re + if re.sub(r'\s+', '', key).find(f'选项{label}') != -1: + options[label] = question[key] + break + + detailed_results.append({ + 'questionNumber': i + 1, + 'questionText': question['题干'], + 'questionType': question['questionType'], + 'options': options, + 'userAnswer': answer['userAnswer'], + 'correctAnswer': answer['correctAnswer'], + 'isCorrect': answer['isCorrect'], + 'score': answer['score'], + 'questionId': answer['questionId'] + }) + + conn.close() + + return { + 'success': True, + 'studentInfo': { + 'name': result['name'], + 'school': result['school'], + 'grade': result['grade'] + }, + 'totalScore': result['total_score'], + 'maxScore': result['max_score'], + 'totalQuestions': result['total_questions'], + 'selectedTag': selected_tag, + 'results': detailed_results, + 'sessionStatus': result['status'] + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"获取答题结果失败: {str(e)}") + def select_questions_by_config(filtered_questions: Dict, questions_config: Dict) -> List[Dict]: """根据配置从筛选后的题目中选择题目""" selected = [] @@ -516,6 +662,36 @@ async def report_page(request: Request): except FileNotFoundError: raise HTTPException(status_code=404, detail="报告页面文件不存在") +@app.get("/quiz-results/{session_id}") +async def quiz_results_page(session_id: str, request: Request): + """提供答题结果展示页面""" + try: + conn = sqlite3.connect('data/survey.db') + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute(''' + SELECT s.*, qs.questions_config, qs.status, qs.total_score, qs.completed_at + FROM students s + JOIN quiz_sessions qs ON s.id = qs.student_id + WHERE qs.id = ? + ''', (session_id,)) + + session_data = cursor.fetchone() + conn.close() + + if not session_data: + raise HTTPException(status_code=404, detail="会话不存在") + + # 生成答题结果页面 + html_content = generate_quiz_results_page(session_data, session_id) + return Response(content=html_content, media_type="text/html; charset=utf-8") + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"生成答题结果页面失败: {str(e)}") + @app.get("/quiz/{session_id}") async def quiz_page(session_id: str, request: Request): """处理答题页面""" @@ -570,7 +746,6 @@ def generate_quiz_page(session_data: sqlite3.Row, session_id: str) -> str: body {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; - padding: 20px; }} .container {{ @@ -769,22 +944,179 @@ def generate_quiz_page(session_data: sqlite3.Row, session_id: str) -> str: .result-section {{ text-align: center; - padding: 50px 30px; + padding: 40px 30px; display: none; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); + border-radius: 20px; + margin: 20px 0; }} .score-display {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; - padding: 30px; - border-radius: 15px; - margin: 20px 0; + padding: 40px; + border-radius: 20px; + margin: 0 auto 30px; + max-width: 500px; + box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3); + position: relative; + overflow: hidden; + }} + + .score-display::before {{ + content: ''; + position: absolute; + top: -50%; + right: -50%; + width: 200%; + height: 200%; + background: linear-gradient(45deg, rgba(255,255,255,0.1) 0%, transparent 50%, rgba(255,255,255,0.1) 100%); + animation: shimmer 3s ease-in-out infinite; + }} + + @keyframes shimmer {{ + 0% {{ transform: translateX(-100%) translateY(-100%) rotate(45deg); }} + 50% {{ transform: translateX(100%) translateY(100%) rotate(45deg); }} + 100% {{ transform: translateX(-100%) translateY(-100%) rotate(45deg); }} }} .score-value {{ font-size: 48px; font-weight: 700; margin-bottom: 10px; + position: relative; + z-index: 2; + }} + + .score-label {{ + font-size: 18px; + opacity: 0.9; + margin-bottom: 20px; + position: relative; + z-index: 2; + }} + + .success-icon {{ + font-size: 60px; + margin-bottom: 20px; + animation: bounce 2s ease-in-out infinite; + position: relative; + z-index: 2; + }} + + @keyframes bounce {{ + 0%, 20%, 50%, 80%, 100% {{ transform: translateY(0); }} + 40% {{ transform: translateY(-10px); }} + 60% {{ transform: translateY(-5px); }} + }} + + .report-status-container {{ + background: white; + padding: 30px; + border-radius: 15px; + margin: 0 auto; + max-width: 600px; + box-shadow: 0 5px 20px rgba(0,0,0,0.1); + }} + + .status-header {{ + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 25px; + }} + + .status-icon {{ + font-size: 40px; + margin-right: 15px; + animation: pulse 2s ease-in-out infinite; + }} + + @keyframes pulse {{ + 0% {{ transform: scale(1); opacity: 1; }} + 50% {{ transform: scale(1.1); opacity: 0.8; }} + 100% {{ transform: scale(1); opacity: 1; }} + }} + + .status-title {{ + font-size: 24px; + font-weight: 600; + color: #333; + margin-bottom: 10px; + }} + + .status-message {{ + font-size: 16px; + color: #666; + margin-bottom: 25px; + line-height: 1.6; + }} + + .loading-indicator {{ + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 25px; + }} + + .loading-dots {{ + display: inline-flex; + gap: 8px; + }} + + .loading-dot {{ + width: 12px; + height: 12px; + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); + border-radius: 50%; + animation: bounce-dots 1.4s ease-in-out infinite both; + }} + + .loading-dot:nth-child(1) {{ animation-delay: -0.32s; }} + .loading-dot:nth-child(2) {{ animation-delay: -0.16s; }} + .loading-dot:nth-child(3) {{ animation-delay: 0s; }} + + @keyframes bounce-dots {{ + 0%, 80%, 100% {{ transform: scale(0); }} + 40% {{ transform: scale(1); }} + }} + + .action-buttons {{ + display: flex; + flex-direction: column; + gap: 15px; + align-items: center; + }} + + .primary-action {{ + background: linear-gradient(135deg, #4CAF50, #66BB6A); + color: white; + padding: 15px 30px; + border: none; + border-radius: 12px; + font-size: 16px; + font-weight: 600; + cursor: pointer; + text-decoration: none; + display: inline-block; + transition: all 0.3s ease; + box-shadow: 0 4px 15px rgba(76, 175, 80, 0.3); + }} + + .primary-action:hover {{ + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(76, 175, 80, 0.4); + }} + + .secondary-info {{ + font-size: 14px; + color: #888; + text-align: center; + margin-top: 15px; + padding: 12px; + background: #f8f9fa; + border-radius: 8px; + border-left: 4px solid #4facfe; }} .report-link {{ @@ -843,10 +1175,40 @@ def generate_quiz_page(session_data: sqlite3.Row, session_id: str) -> str:
+
🎉
0分
-
测评完成!正在生成报告...
+
测评完成!
+
+ +
+
+
+
+
+
智能分析报告生成中
+
系统正在基于您的答题情况,AI正在深度分析并生成个性化学习建议...
+
+
+ +
+
+
+
+
+
+ AI正在分析中... +
+ +
+ + 📊 立即查看答题结果 + +
+ 💡 您现在就可以查看详细的答题情况,AI报告生成完成后将在这里显示 +
+
+
-
请稍候,系统正在为您生成详细的测评报告...
@@ -1031,10 +1393,46 @@ def generate_quiz_page(session_data: sqlite3.Row, session_id: str) -> str: showResults(totalScore) {{ document.getElementById('questionsContainer').style.display = 'none'; document.getElementById('resultSection').style.display = 'block'; - document.getElementById('scoreValue').textContent = totalScore + '分'; + + // 获取总分信息并显示为"分数/总分"格式 + this.fetchSessionInfo().then(sessionInfo => {{ + const maxScore = sessionInfo.maxScore || 100; + document.getElementById('scoreValue').innerHTML = + `${{totalScore}}` + + ` / ${{maxScore}}分`; + }}).catch(error => {{ + // 如果获取失败,使用默认值 + document.getElementById('scoreValue').innerHTML = + `${{totalScore}}` + + ` / 100分`; + }}); + + this.checkReportStatus(); + }} + + async fetchSessionInfo() {{ + try {{ + const response = await fetch(`/api/quiz-results/${{this.sessionId}}`); + if (response.ok) {{ + const result = await response.json(); + if (result.success) {{ + return {{ + maxScore: result.maxScore, + totalQuestions: result.totalQuestions + }}; + }} + }} + // 如果API不可用,返回默认值 + return {{ maxScore: 100, totalQuestions: 14 }}; + }} catch (error) {{ + console.warn('获取会话信息失败,使用默认值:', error); + return {{ maxScore: 100, totalQuestions: 14 }}; + }} }} checkReportStatus() {{ + // HTML已经包含完整的加载状态,不需要在这里设置 + // 继续检查报告生成状态 let checkCount = 0; const maxChecks = 30; @@ -1052,9 +1450,7 @@ def generate_quiz_page(session_data: sqlite3.Row, session_id: str) -> str: this.showReportLink(latestReport.id); }} else if (checkCount >= maxChecks) {{ clearInterval(checkInterval); - document.getElementById('reportStatus').innerHTML = - '报告生成时间较长,请稍后前往报告列表查看。' + - '返回首页'; + this.showTimeoutMessage(); }} }} catch (error) {{ console.error('检查报告状态失败:', error); @@ -1064,8 +1460,45 @@ def generate_quiz_page(session_data: sqlite3.Row, session_id: str) -> str: showReportLink(reportId) {{ document.getElementById('reportStatus').innerHTML = - '✅ 测评报告已生成!' + - `查看详细报告`; + `
+
🎯
+
+
测评完成!
+
🎉 恭喜您完成测评!AI已为您生成了个性化的学习建议报告。
+
+
+ +
+ + 📊 查看答题结果 + + + 🧠 查看AI智能报告 + +
+ 💡 建议先查看答题结果了解具体错题,再查看AI报告获得深度学习建议 +
+
`; + }} + + showTimeoutMessage() {{ + document.getElementById('reportStatus').innerHTML = + `
+
+
+
报告生成时间较长
+
当前用户较多,AI报告正在加急处理中,您可以先查看答题结果。
+
+
+ +
+ + 📊 查看答题结果 + +
+ ⏱️ AI报告完成后可在首页报告列表中查看,给您带来不便敬请谅解 +
+
`; }} showError(message) {{ @@ -1322,17 +1755,76 @@ def generate_completion_page(session_data: sqlite3.Row, session_id: str) -> str: @media (max-width: 768px) {{ .container {{ - padding: 20px; + padding: 15px; }} h1 {{ font-size: 24px; }} - .score-number {{ + .result-section {{ + padding: 25px 15px; + }} + + .score-display {{ + padding: 30px 20px; + margin-bottom: 20px; + }} + + .score-value {{ font-size: 36px; }} + .success-icon {{ + font-size: 45px; + }} + + .report-status-container {{ + padding: 25px 20px; + margin: 0 10px; + }} + + .status-header {{ + flex-direction: column; + text-align: center; + }} + + .status-icon {{ + font-size: 35px; + margin-right: 0; + margin-bottom: 10px; + }} + + .status-title {{ + font-size: 20px; + }} + + .status-message {{ + font-size: 14px; + }} + + .primary-action {{ + width: 100%; + max-width: 280px; + padding: 14px 25px; + font-size: 15px; + }} + + .secondary-info {{ + font-size: 13px; + margin-top: 12px; + padding: 10px; + }} + + .loading-dots {{ + gap: 6px; + }} + + .loading-dot {{ + width: 10px; + height: 10px; + }} + .btn {{ display: block; margin: 10px auto; @@ -1377,7 +1869,7 @@ def generate_completion_page(session_data: sqlite3.Row, session_id: str) -> str:
返回首页 - + 查看答题结果
+ +''' + if __name__ == "__main__": import uvicorn uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file diff --git a/public/index.html b/public/index.html index 4dcedee..b8eed75 100644 --- a/public/index.html +++ b/public/index.html @@ -394,16 +394,24 @@ font-size: 12px; } - .delete-btn { - padding: 6px 12px; - background: linear-gradient(135deg, #e74c3c, #c0392b); - color: white; - border: none; - border-radius: 4px; - font-size: 12px; - cursor: pointer; - transition: all 0.3s ease; - margin-left: 5px; + .delete-btn, .action-cell .view-btn { + padding: 6px 8px; + font-size: 11px; + margin: 2px; + } + + .action-cell { + min-width: auto; + flex-direction: column; + align-items: stretch; + } + + .action-cell .view-btn:nth-child(1) { + background: linear-gradient(135deg, #ff7e5f, #feb47b); + } + + .action-cell .view-btn:nth-child(2) { + background: linear-gradient(135deg, #4CAF50, #66BB6A); } .delete-btn:hover { @@ -583,9 +591,11 @@ .action-cell { display: flex; + flex-wrap: wrap; gap: 5px; justify-content: center; align-items: center; + min-width: 200px; } @@ -869,6 +879,7 @@ ${report.school} 查看报告 + 查看答题结果 diff --git a/public/survey.html b/public/survey.html index 87a76bc..46eb970 100644 --- a/public/survey.html +++ b/public/survey.html @@ -4,6 +4,7 @@ 学科能力测评问卷 +