# -*- coding: utf-8 -*- from flask import Flask, render_template, request, redirect, url_for, make_response, jsonify, session import requests import json import os from urllib.parse import quote from concurrent.futures import ThreadPoolExecutor import hashlib from webdav_sync import start_webdav_sync_if_configured # --- 应用配置 (Application Configuration) --- SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) TEMPLATE_DIR = os.path.join(SCRIPT_DIR, 'templates') HISTORY_FILE = os.path.join(SCRIPT_DIR, 'history.json') ENV_FILE = os.path.join(SCRIPT_DIR, '.env') # 在应用导入阶段拉取远端,并启动定时同步 _webdav_syncer = start_webdav_sync_if_configured(HISTORY_FILE) app = Flask(__name__, template_folder=TEMPLATE_DIR) app.secret_key = os.urandom(24) # --- 环境变量加载 (.env 支持) --- def _load_env_file(path: str = ENV_FILE): if not os.path.exists(path): return try: with open(path, 'r', encoding='utf-8') as f: for raw in f: line = raw.strip() if not line or line.startswith('#'): continue if line.startswith('export '): line = line[len('export '):].strip() if '=' not in line: continue key, value = line.split('=', 1) key = key.strip() value = value.strip().strip('\"').strip("'") # 仅在未设置时从文件填充,避免覆盖真实环境变量 if key and key not in os.environ: os.environ[key] = value except Exception as e: print(f"Warning: failed to load .env file: {e}") # 尝试在应用导入时加载 .env _load_env_file() # --- 数据处理与持久化 (Data Handling & Persistence) --- def _compute_entry_id(username: str, password: str, seid: str) -> str: """基于 (username, password, seid) 计算稳定ID,不写入文件,仅在内存中使用。""" raw = f"{username}|||{password}|||{seid}".encode('utf-8') return hashlib.sha1(raw).hexdigest() def load_history(): """从 JSON 文件加载查询历史(仅最小字段)。为每条记录计算内存态ID。""" if not os.path.exists(HISTORY_FILE): return [] try: with open(HISTORY_FILE, 'r', encoding='utf-8') as f: content = f.read() items = json.loads(content) if content else [] # 为每条记录补充计算得到的 id(不写回磁盘) enriched = [] for e in items: username = e.get('username') password = e.get('password') seid = e.get('seid') eid = _compute_entry_id(username or '', password or '', seid or '') enriched.append({**e, 'id': eid}) return enriched except (json.JSONDecodeError, IOError) as e: print(f"Error loading history file: {e}") return [] def save_history(history_data): """将查询历史保存到 JSON 文件,并进行去重,仅保留所需字段。 仅保存以下字段:seid、examName、username、studentName、password、totalScore、unionOrder。 """ ALLOWED_FIELDS = {"seid", "examName", "username", "studentName", "password", "totalScore", "unionOrder"} # 去重键:(username, password, seid) unique_entries = {} for entry in history_data: key = (entry.get('username'), entry.get('password'), entry.get('seid')) if key in unique_entries: continue # 仅保留允许的字段 minimal_entry = {k: entry.get(k) for k in ALLOWED_FIELDS} unique_entries[key] = minimal_entry deduplicated_history = list(unique_entries.values()) try: with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(deduplicated_history, f, indent=4, ensure_ascii=False) except IOError as e: print(f"Error saving history file: {e}") def process_segment_data(segments): """[优化] 处理分数段数据,过滤无效和首尾无人的分数段。 (Process score segment data, filtering invalid and empty-head/tail segments.)""" if not isinstance(segments, list) or not segments: return [] try: # Filter out segments with a max score below a reasonable threshold (e.g., 10) valid_segments = [s for s in segments if float(s.get('maxscore', 0)) > 10] except (ValueError, TypeError): valid_segments = segments # Fallback if conversion fails first_idx, last_idx = -1, -1 # Find the first and last segments that actually have people in them for i, segment in enumerate(valid_segments): if isinstance(segment.get('num'), (int, float)) and segment['num'] > 0: if first_idx == -1: first_idx = i last_idx = i # Return the slice of segments from the first person to the last person return valid_segments[first_idx : last_idx + 1] if first_idx != -1 else [] # --- 核心 API 逻辑 (Core API Logic) --- def login_and_get_session(user, pw): """登录并获取会话ID。 (Login and get session ID.)""" login_url = f"https://www.yunchengji.net/app/login?j_username={user}&j_password={pw}" student_login_url = "https://www.yunchengji.net/app/student/login" headers = {'User-Agent': "ycj/5.7.0(Android;12)()", 'Accept-Encoding': "gzip"} try: with requests.Session() as s: s.headers.update(headers) # First login request response1 = s.post(login_url, headers={'content-length': "0"}, allow_redirects=False, timeout=10) response1.raise_for_status() # Check if SESSIONID cookie is set if not s.cookies.get("SESSIONID"): return False, "登录失败:无法获取会话ID,请检查用户名和密码。" # Second request to finalize student login response2 = s.get(student_login_url, timeout=10) response2.raise_for_status() return True, s.cookies.get("SESSIONID") except requests.exceptions.RequestException as e: return False, f"登录过程中发生网络错误: {e}" def fetch_data(session_id, url, error_context, seid=None): """通用数据获取函数。 (Generic data fetching function.)""" headers = { 'User-Agent': "Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 Mobile Safari/537.36", 'Accept': "application/json, text/plain, */*", 'Cookie': f"SESSIONID={session_id}", } if seid: headers['Referer'] = f"https://www.yunchengji.net/app/student/report/html/report.html?seid={seid}" try: response = requests.get(url, headers=headers, timeout=10) response.raise_for_status() return True, response.json() except requests.exceptions.RequestException as e: return False, f"获取{error_context}时发生网络错误: {e}" except json.JSONDecodeError: return False, f"解析{error_context}失败:服务器返回的不是有效的JSON格式。" def get_processed_subject_details(session_id, seid, subject_id): """封装了获取和处理单个科目所有详细数据的逻辑。 (Encapsulates logic to fetch and process all details for a single subject.)""" base_url = "https://www.yunchengji.net/app/student/cj" # Fetch data from three different endpoints _, q_data = fetch_data(session_id, f"{base_url}/question-list?seid={seid}&subjectid={subject_id}", "题目列表", seid) _, a_data = fetch_data(session_id, f"{base_url}/report-subject?seid={seid}&subjectid={subject_id}", "科目分析", seid) _, lq_data = fetch_data(session_id, f"{base_url}/lose-question-list?seid={seid}&subjectid={subject_id}", "失分列表", seid) # Process question scores q_scores_list = q_data.get("desc", {}).get("questions", []) scores_dict = {q['title']: {'score': q.get('score'), 'totalScore': q.get('totalScore')} for q in q_scores_list if q.get('title')} # Process question rates and merge with scores quest_rates = a_data.get("desc", {}).get("questRates", []) processed_questions = [] for rate_info in quest_rates: if rate_info.get('bqFlag'): # This is a section header processed_questions.append({'is_header': True, 'data': rate_info, 'questions': []}) elif processed_questions: # This is a question under a header title = rate_info.get('title') score_details = scores_dict.get(title, {'score': 'N/A', 'totalScore': 'N/A'}) processed_questions[-1]['questions'].append({**rate_info, **score_details}) # Calculate section totals for section in processed_questions: if section['is_header']: my_total = sum(float(q.get('score', 0)) for q in section['questions'] if str(q.get('score')).replace('.', '', 1).isdigit()) section_total = sum(float(q.get('totalScore', 0)) for q in section['questions'] if str(q.get('totalScore')).replace('.', '', 1).isdigit()) section['data']['my_section_score'] = f"{my_total:.2f}".rstrip('0').rstrip('.') section['data']['section_total_score'] = f"{section_total:.2f}".rstrip('0').rstrip('.') return { "analysis": a_data.get("desc"), "lose_question_analysis": lq_data.get("desc"), "processed_questions": processed_questions } # --- 模板上下文 (Template Context) --- @app.context_processor def utility_processor(): """向模板注入实用函数。 (Inject utility functions into templates.)""" def get_rate_color_style(my_rate_str, avg_rate_str): """Generates a background color style based on the difference between my rate and the average rate.""" try: my_rate = float(str(my_rate_str).strip('%')) avg_rate = float(str(avg_rate_str).strip('%')) diff = my_rate - avg_rate if diff > 0.1: # Better than average intensity = min(diff / 30.0, 1.0) # Cap intensity at 30% difference return f"background-color: hsl(130, 80%, {92 - (intensity * 20):.0f}%) !important;" elif diff < -0.1: # Worse than average intensity = min(abs(diff) / 30.0, 1.0) # Cap intensity at 30% difference return f"background-color: hsl(0, 90%, {92 - (intensity * 15):.0f}%) !important;" return "" except (ValueError, TypeError): return "" return dict(get_rate_color_style=get_rate_color_style) # --- Flask 路由 (Flask Routes) --- @app.route('/', methods=['GET']) def index(): """渲染主页面。 (Render the main page.)""" # 不再加载或显示历史记录 return render_template('index.html', username='', password='', seid='') @app.route('/query', methods=['POST']) def query(): """处理新查询。 (Handle a new query.)""" form = request.form username, password, seid = form.get('username'), form.get('password'), form.get('seid') if not all([username, password, seid]): return render_template('index.html', error="所有字段均为必填项。", username=username, seid=seid) login_success, session_or_error = login_and_get_session(username, password) if not login_success: error_msg = f"查询失败:{session_or_error}
请检查用户名 (通常为手机号) 和密码是否正确。" return render_template('index.html', error=error_msg, username=username, seid=seid) session_id = session_or_error report_success, report_data = fetch_data(session_id, f"https://www.yunchengji.net/app/student/cj/report-total?seid={seid}", "主报告") if not report_success or report_data.get("result") != "1": error_msg_raw = report_data.get("desc", "获取报告失败或报告数据无效。") if isinstance(report_data, dict) else report_data error_msg = f"查询失败:{error_msg_raw}
请检查用户名 (通常为手机号) 和密码和考试编号 (seid) 是否正确。" return render_template('index.html', error=error_msg, username=username, seid=seid) # 提取所需信息 desc = report_data.get("desc", {}) stu_order = desc.get("stuOrder", {}) subjects = stu_order.get("subjects", []) total_score_info = next((s for s in subjects if s.get('name') == '总分'), {}) # 构造仅含必要字段的历史记录条目(磁盘只保存最小字段,不包含id) minimal_entry = { "seid": seid, "examName": desc.get("examName"), "username": username, "studentName": desc.get("studentname"), "password": password, "totalScore": total_score_info.get("score"), "unionOrder": total_score_info.get("unionOrder"), } history = load_history() history.insert(0, minimal_entry) save_history(history) # 计算本次查询的内存态ID并跳转到展示页 entry_id = _compute_entry_id(username, password, seid) return redirect(url_for('show_report', item_id=entry_id)) @app.route('/report/') def show_report(item_id): """显示单个报告结果。 (Display a single report result.)""" history = load_history() item = next((h for h in history if h['id'] == item_id), None) if not item: return redirect(url_for('index')) # 使用历史中的凭据重新拉取最新报告(不从历史中持久化原始报告数据) login_success, session_id = login_and_get_session(item['username'], item['password']) if not login_success: return render_template('index.html', error=session_id, username=item['username'], seid=item['seid']) report_ok, report_data = fetch_data(session_id, f"https://www.yunchengji.net/app/student/cj/report-total?seid={item['seid']}", "主报告") if not report_ok or report_data.get("result") != "1": error_msg_raw = report_data.get("desc", "获取报告失败或报告数据无效。") if isinstance(report_data, dict) else report_data return render_template('index.html', error=error_msg_raw, username=item['username'], seid=item['seid']) # 拉取总分分布(班级/学校/联考) base_url = "https://www.yunchengji.net/app/student/cj/segment-list" with ThreadPoolExecutor(max_workers=3) as executor: future_class = executor.submit(fetch_data, session_id, f"{base_url}?seid={item['seid']}&subjectid=-100&type=class", "班级分数段", item['seid']) future_school = executor.submit(fetch_data, session_id, f"{base_url}?seid={item['seid']}&subjectid=-100&type=school", "学校分数段", item['seid']) future_union = executor.submit(fetch_data, session_id, f"{base_url}?seid={item['seid']}&subjectid=-100&type=union", "联考分数段", item['seid']) _, class_data = future_class.result() _, school_data = future_school.result() _, union_data = future_union.result() total_dist = { 'class': process_segment_data(class_data.get("desc")), 'school': process_segment_data(school_data.get("desc")), 'union': process_segment_data(union_data.get("desc")) } return render_template('index.html', report=report_data, history_item_id=item['id'], username=item['username'], seid=item['seid'], total_score_distributions=total_dist) @app.route('/subject_details') def subject_details(): """显示科目详情页。 (Display subject details page.)""" args = request.args # 从查询参数中移除 username/password,仅保留必要参数 seid, subject_id, subject_name, history_item_id = \ args.get('seid'), args.get('subject_id'), args.get('subject_name'), args.get('history_item_id') if not all([seid, subject_id, subject_name, history_item_id]): return render_template('subject_details.html', error="缺少必要参数。") # 从历史记录中查找密码 history = load_history() item = next((h for h in history if h['id'] == history_item_id), None) if not item: return render_template('subject_details.html', error="无法找到相关的历史记录。") username = item.get('username') password = item.get('password') if not (username and password): return render_template('subject_details.html', error="无法获取凭据。") login_success, session_id = login_and_get_session(username, password) if not login_success: return render_template('subject_details.html', error=session_id) # Fetch all necessary data for the subject details page details = get_processed_subject_details(session_id, seid, subject_id) _, report_data = fetch_data(session_id, f"https://www.yunchengji.net/app/student/cj/report-total?seid={seid}", "总报告") all_subjects = report_data.get("desc", {}).get("stuOrder", {}).get("subjects", []) subject_info = next((s for s in all_subjects if str(s.get('id')) == subject_id), None) base_url = "https://www.yunchengji.net/app/student/cj/segment-list" _, seg_class_data = fetch_data(session_id, f"{base_url}?seid={seid}&subjectid={subject_id}&type=class", "班级分数段", seid) _, seg_school_data = fetch_data(session_id, f"{base_url}?seid={seid}&subjectid={subject_id}&type=school", "学校分数段", seid) _, seg_union_data = fetch_data(session_id, f"{base_url}?seid={seid}&subjectid={subject_id}&type=union", "联考分数段", seid) score_distributions = { 'class': process_segment_data(seg_class_data.get("desc")), 'school': process_segment_data(seg_school_data.get("desc")), 'union': process_segment_data(seg_union_data.get("desc")) } return render_template('subject_details.html', **details, score_distributions=score_distributions, subject_info=subject_info, subject_name=subject_name, history_item_id=history_item_id, exam_name=report_data.get("desc", {}).get("examName"), student_name=report_data.get("desc", {}).get("studentname")) @app.route('/print_report/') def print_report(item_id): """[性能优化] 使用并发请求为打印页面准备所有数据。 (Use concurrent requests to prepare all data for the print page.)""" item = next((h for h in load_history() if h['id'] == item_id), None) if not item: return "找不到指定的历史记录。", 404 username, password, seid = item['username'], item['password'], item['seid'] login_success, session_id = login_and_get_session(username, password) if not login_success: return f"无法生成报告:重新登录失败 - {session_id}", 500 report_ok, main_report_data = fetch_data(session_id, f"https://www.yunchengji.net/app/student/cj/report-total?seid={seid}", "主报告") if not report_ok or main_report_data.get("result") != "1": return f"无法生成报告:获取主报告失败。", 500 base_url = "https://www.yunchengji.net/app/student/cj/segment-list" def fetch_all_data_for_subject(subject): """Fetches all data for a single subject, to be run in a thread.""" subject_id = str(subject.get('id')) details = get_processed_subject_details(session_id, seid, subject_id) details['subject_info'] = subject _, seg_school = fetch_data(session_id, f"{base_url}?seid={seid}&subjectid={subject_id}&type=school", f"{subject.get('name')}学校分数段", seid) details['score_distribution'] = process_segment_data(seg_school.get("desc")) return details subjects_to_fetch = [s for s in main_report_data.get("desc", {}).get("stuOrder", {}).get("subjects", []) if s.get('name') != '总分'] all_subject_details = [] total_score_distribution = {} with ThreadPoolExecutor(max_workers=10) as executor: # Map futures to subjects to fetch details future_to_subject = {executor.submit(fetch_all_data_for_subject, s): s for s in subjects_to_fetch} # Fetch total score distribution concurrently future_total_dist = executor.submit(fetch_data, session_id, f"{base_url}?seid={seid}&subjectid=-100&type=school", "总分学校分数段", seid) for future in future_to_subject: try: all_subject_details.append(future.result()) except Exception as exc: print(f'Exception fetching subject data: {exc}') try: _, school_data = future_total_dist.result() total_score_distribution = process_segment_data(school_data.get("desc")) except Exception as exc: print(f'Exception fetching total score distribution data: {exc}') return render_template('print_report.html', report=main_report_data, all_subject_details=all_subject_details, total_score_distribution=total_score_distribution, exam_name=main_report_data.get("desc", {}).get("examName"), student_name=main_report_data.get("desc", {}).get("studentname")) # --- 管理后台 (Admin) --- def _admin_enabled(): return bool(os.environ.get('ADMIN_PASSWORD')) @app.route('/admin', methods=['GET', 'POST']) def admin(): # 登录处理 if request.method == 'POST': pw = request.form.get('password', '') real = os.environ.get('ADMIN_PASSWORD', '') if not real: return render_template('admin.html', logged_in=False, admin_enabled=False, error='未配置 ADMIN_PASSWORD,无法登录。') if pw == real: session['is_admin'] = True return redirect(url_for('admin')) else: return render_template('admin.html', logged_in=False, admin_enabled=True, error='密码错误。') # 非登录请求 if not session.get('is_admin'): return render_template('admin.html', logged_in=False, admin_enabled=_admin_enabled()) # 已登录:读取、排序和过滤 q = request.args.get('q', '').strip() sort_by = request.args.get('sort_by', 'date_desc') all_history = load_history() # 排序逻辑 if sort_by == 'date_asc': all_history.reverse() elif sort_by == 'score_desc': all_history.sort(key=lambda x: float(x.get('totalScore') or 0), reverse=True) elif sort_by == 'score_asc': all_history.sort(key=lambda x: float(x.get('totalScore') or 0)) # 'date_desc' 是默认顺序,无需处理 # 过滤逻辑 if q: qlower = q.lower() def _match(e): return ( (e.get('examName') or '').lower().find(qlower) != -1 or (e.get('username') or '').lower().find(qlower) != -1 or (e.get('studentName') or '').lower().find(qlower) != -1 or str(e.get('seid') or '').lower().find(qlower) != -1 ) entries = [e for e in all_history if _match(e)] else: entries = all_history return render_template('admin.html', logged_in=True, history=entries, search_term=q, sort_by=sort_by) @app.route('/admin/logout') def admin_logout(): session.pop('is_admin', None) return redirect(url_for('admin')) # --- 模板定义 (Template Definitions) --- def create_templates(): """在 'templates' 目录中创建或更新HTML模板文件。 (Create or update HTML template files in the 'templates' directory.)""" if not os.path.exists(TEMPLATE_DIR): os.makedirs(TEMPLATE_DIR) # 主页面模板 (index.html) index_html = """ 云成绩查询系统

云成绩查询系统

输入您的信息以获取最新的考试报告

查询成绩

如何获取考试的 seID?

1. 登录云成绩电脑版 https://www.yunchengji.net/

2. 点击进入需要查询的考试。

3. 此时浏览器地址栏中的链接会包含 ?seid=,等号后面的数字即为本次考试的 seID。

目前已知的seID:

  • 昆明市2025届高二下学期市统测 (理科): 316099
  • 昆明市2025届高二下学期市统测 (文科): 316097

免责声明:此页面仅供学习与技术研究使用,不提供任何破解、绕过等功能,请勿用于商业或非法用途。如有侵权请联系移除。

{% if error %} {% endif %} {% if report and report.result == "1" %} {% set desc = report.desc or {} %}{% set stu_order = desc.stuOrder or {} %}{% set subjects = stu_order.subjects or [] %}{% set score_gap = stu_order.scoreGap or {} %}{% set total_score_info = (subjects | selectattr('name', 'equalto', '总分') | first) or {} %}

{{ desc.examName or '考试报告' }}

{{ desc.studentname or '未知学生' }}

总分与排名情况

总分

{{ total_score_info.score or 'N/A' }} / {{ total_score_info.fullScore or 'N/A' }}

班级排名

{{ total_score_info.classOrder or 'N/A' }}

学校排名

{{ total_score_info.schoolOrder or 'N/A' }}

统考排名

{{ total_score_info.unionOrder or 'N/A' }}

{% if total_score_distributions and (total_score_distributions.class or total_score_distributions.school or total_score_distributions.union) %}

总分分数段分布

{% if total_score_distributions.class %}
{% endif %} {% if total_score_distributions.school %}
{% endif %} {% if total_score_distributions.union %}
{% endif %}
{% endif %}

各科成绩与排名详情

💡 点击科目名称可查看详细分析

{% for subject in subjects %}{% if subject.name != '总分' %}{% endif %}{% endfor %}
科目得分/赋分 (原始分)班级排名学校排名统考排名
{{ subject.name }} {% if subject.score != subject.paperScore %}{{ subject.score }} ({{ subject.paperScore }}){% else %}{{ subject.score }}{% endif %} {{ subject.classOrder or 'N/A' }}{{ subject.schoolOrder or 'N/A' }}{{ subject.unionOrder or 'N/A' }}

各科表现雷达图

统计数据

类别平均分最高分总人数
班级{{ score_gap.classAvg or 'N/A' }}{{ score_gap.classTop or 'N/A' }}{{ score_gap.classNum or 'N/A' }}
学校{{ score_gap.schoolAvg or 'N/A' }}{{ score_gap.schoolTop or 'N/A' }}{{ score_gap.schoolNum or 'N/A' }}
统考{{ score_gap.unionAvg or 'N/A' }}{{ score_gap.unionTop or 'N/A' }}{{ score_gap.unionNum or 'N/A' }}

免责声明:此页面仅供学习与技术研究使用,不提供任何破解、绕过等功能,请勿用于商业或非法用途。如有侵权请联系移除。

{% elif not error %}

暂无结果

请在上方输入信息进行查询。

{% endif %}
""" # 科目详情页模板 (subject_details.html) subject_details_html = """ {{ subject_name }} - 题目详情
{% if error %}

发生错误

{{ error | safe }}

{% elif analysis %}

{{ subject_name }} - 详细学情分析

{{ exam_name or '考试报告' }}

{{ student_name or '未知学生' }}

学情诊断

"{{ analysis.analysisDesc }}"

{% if subject_info %}

本科目成绩概览

本科目得分

{{ subject_info.score or 'N/A' }} / {{ subject_info.fullScore or 'N/A' }}

班级排名

{{ subject_info.classOrder or 'N/A' }}

学校排名

{{ subject_info.schoolOrder or 'N/A' }}

统考排名

{{ subject_info.unionOrder or 'N/A' }}

{% endif %} {% if score_distributions and (score_distributions.class or score_distributions.school or score_distributions.union) %}

分数段人数分布

{% if score_distributions.class %}
{% endif %} {% if score_distributions.school %}
{% endif %} {% if score_distributions.union %}
{% endif %}
{% endif %}

题目得分明细 (含得分率)

{% for section in processed_questions %} {% for q in section.questions %} {% endfor %} {% endfor %}
题号 我的得分 我的得分率 班级得分率 学校得分率 联考得分率
{{ section.data.title }} {{ section.data.my_section_score }} / {{ section.data.section_total_score }} {{ section.data.scoreRate }} {{ section.data.classScoreRate }} {{ section.data.schoolScoreRate }} {{ section.data.unionScoreRate }}
{{ q.title }} {{ q.score }} / {{ q.totalScore }} {{ q.scoreRate }} {{ q.classScoreRate }} {{ q.schoolScoreRate }} {{ q.unionScoreRate }}
{% if lose_question_analysis %}

失分题目详情

{% for category in lose_question_analysis %}

{{ category.name }} (得分: {{ category.score }} / {{ category.totalScore }})

{% if category.errors %}
{% for error in category.errors %} {% endfor %}
失分题号我的得分题目满分联考平均分
{{ error.title }} {{ error.studentScore }} {{ error.score }} {{ "%.2f"|format(error.average) }}
{% else %}

恭喜!该难度题目全部正确!

{% endif %}
{% endfor %}
{% endif %}

免责声明:此页面仅供学习与技术研究使用,不提供任何破解、绕过等功能,请勿用于商业或非法用途。如有侵权请联系移除。

{% else %}

未能加载科目详情

{{ analysis.desc or "返回数据格式不正确或为空。" }}

{% endif %}
""" # 管理后台模板 (admin.html) admin_html = """ 管理后台
{% if logged_in %}

查询历史记录

登出
{% if history %}
{% for item in history %}

{{ item.studentName }}

{{ item.examName }}

总分: {{ item.totalScore }} 统考排名: {{ item.unionOrder }}
用户名: {{ item.username }}
密码: {{ item.password }}
SEID: {{ item.seid }}
{% endfor %}
{% else %}

暂无历史记录

{% if search_term %}没有找到匹配 "{{ search_term }}" 的记录。{% else %}数据库中还没有任何查询记录。{% endif %}

{% endif %} {% else %}

管理后台登录

{% if not admin_enabled %} {% endif %} {% if error %} {% endif %}
{% endif %}
""" # [重构] 打印专用模板 (print_report.html) print_html = """ {{ report.desc.examName }} - {{ report.desc.studentname }} 的成绩报告
返回
{% if report and report.result == "1" %} {% set desc = report.desc or {} %} {% set stu_order = desc.stuOrder or {} %} {% set subjects = stu_order.subjects or [] %} {% set score_gap = stu_order.scoreGap or {} %} {% set total_score_info = (subjects | selectattr('name', 'equalto', '总分') | first) or {} %}

{{ desc.examName or '考试报告' }}

{{ desc.studentname or '未知学生' }}

总览

总分

{{ total_score_info.score or 'N/A' }}

班级排名

{{ total_score_info.classOrder or 'N/A' }}

学校排名

统考排名

{{ total_score_info.unionOrder or 'N/A' }}

各科成绩详情

{% for subject in subjects %} {% if subject.name != '总分' %} {% endif %} {% endfor %}
科目 得分/赋分 (原始分) 班级排名 学校排名 统考排名
{{ subject.name }} {% if subject.score != subject.paperScore %}{{ subject.score }} ({{ subject.paperScore }}){% else %}{{ subject.score }}{% endif %} {{ subject.classOrder or 'N/A' }} {{ subject.schoolOrder or 'N/A' }} {{ subject.unionOrder or 'N/A' }}

统计数据

类别 平均分 最高分 总人数
班级{{ score_gap.classAvg or 'N/A' }}{{ score_gap.classTop or 'N/A' }}{{ score_gap.classNum or 'N/A' }}
学校{{ score_gap.schoolAvg or 'N/A' }}{{ score_gap.schoolTop or 'N/A' }}{{ score_gap.schoolNum or 'N/A' }}
统考{{ score_gap.unionAvg or 'N/A' }}{{ score_gap.unionTop or 'N/A' }}{{ score_gap.unionNum or 'N/A' }}
{% else %}

无法加载报告数据。

{% endif %}
""" with open(os.path.join(TEMPLATE_DIR, 'index.html'), 'w', encoding='utf-8') as f: f.write(index_html) with open(os.path.join(TEMPLATE_DIR, 'subject_details.html'), 'w', encoding='utf-8') as f: f.write(subject_details_html) with open(os.path.join(TEMPLATE_DIR, 'admin.html'), 'w', encoding='utf-8') as f: f.write(admin_html) with open(os.path.join(TEMPLATE_DIR, 'print_report.html'), 'w', encoding='utf-8') as f: f.write(print_html) create_templates() if __name__ == '__main__': create_templates() try: save_history(load_history()) except Exception as _: pass # Run the Flask application app.run(host='0.0.0.0', port=7860, debug=False)