import os
import sys
import json
import re
import tempfile
import urllib.request
import urllib.error
import urllib.parse
import mysql.connector
import datetime
import random
import base64
import socket
from typing import Optional, List
from flask import Flask, render_template, request, jsonify, url_for, send_file, redirect
from html.parser import HTMLParser
from urllib.parse import urljoin, urlparse

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

def _load_dotenv_if_exists():
    """
    允许把数据库/AI 等配置放在同目录 config.env 里（不改代码即可生效）。

    重要：需要在读取 os.getenv(...) 之前执行。
    """
    try:
        from dotenv import load_dotenv  # type: ignore
    except Exception:
        return

    # 兼容源码运行 / PyInstaller 打包运行
    candidates = [
        os.path.join(BASE_DIR, "config.env"),
        os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), "config.env"),
    ]
    for p in candidates:
        if os.path.exists(p):
            load_dotenv(p, override=True)
            break


_load_dotenv_if_exists()

def resource_path(*parts: str) -> str:
    """
    资源路径兼容：
    - 源码运行：基于当前文件目录
    - PyInstaller 打包：基于 _MEIPASS
    """
    base = getattr(sys, "_MEIPASS", BASE_DIR)
    return os.path.join(base, *parts)

app = Flask(
    __name__,
    template_folder=resource_path("templates"),
    static_folder=resource_path("static"),
)

# 可能的主键列名（不同库/表可能不一致）
_PK_CANDIDATE_COLS = ["id", "question_id", "pk_id", "qid"]
_PK_COLS_CACHE: Optional[List[str]] = None

# 数据库配置
# 默认值：按你刚刚给的 JDBC 配置来（也支持用环境变量覆盖）
DB_CONFIG = {
    "host": os.getenv("DB_HOST", "rm-f8ze60yirdj8786u2wo.mysql.rds.aliyuncs.com"),
    "port": int(os.getenv("DB_PORT", "3306")),
    "database": os.getenv("DB_DATABASE", "math-conten-online2"),
    "user": os.getenv("DB_USERNAME", "root"),
    "password": os.getenv("DB_PASSWORD", "csqz@20255"),
    "charset": "utf8mb4",
}

# 远程 PDF 生成接口（你提供的）
PDF_API_URL = os.getenv("PDF_API_URL", "https://teaching-content.chunsunqiuzhu.com/api/questions/pdf")
PDF_STUDENT_ID = os.getenv("PDF_STUDENT_ID", "44")  # 默认写死 44（也支持用环境变量覆盖）

# AI 优化题干（通过环境变量配置，避免把 key 写进代码）
AI_API_KEY = os.getenv("AI_API_KEY", "").strip()
AI_BASE_URL = (os.getenv("AI_BASE_URL") or "").strip()  # 为空则使用官方默认
AI_MODEL_NAME = (os.getenv("AI_MODEL_NAME") or "gpt-5.2").strip()

# 难度评分配置（使用相同的 AI 配置）
DIFFICULTY_SCORING_API_KEY = os.getenv("DIFFICULTY_SCORING_API_KEY", AI_API_KEY).strip()
DIFFICULTY_SCORING_MODEL = os.getenv("DIFFICULTY_SCORING_MODEL", AI_MODEL_NAME).strip()
DIFFICULTY_SCORING_TEMPERATURE = float(os.getenv("DIFFICULTY_SCORING_TEMPERATURE", "0.0"))
DIFFICULTY_SCORING_TOP_P = float(os.getenv("DIFFICULTY_SCORING_TOP_P", "0.3"))
DIFFICULTY_SCORING_PRESENCE_PENALTY = float(os.getenv("DIFFICULTY_SCORING_PRESENCE_PENALTY", "0.0"))
DIFFICULTY_SCORING_FREQUENCY_PENALTY = float(os.getenv("DIFFICULTY_SCORING_FREQUENCY_PENALTY", "0.0"))

# 默认难度评分提示词
DIFFICULTY_SCORING_PROMPT = """你是一名数学题库难度建模专家。你的任务不是解题，而是基于题目文本、公式及图像信息，对题目难度进行结构化量化评估，用于企业级题库难度算法。本评估包含四个评分维度，四个维度等权重参与最终得分，但在判断时需遵循重要性顺序：推理与运算步数最高，知识点数量第二，抽象与构造要求第三，题型常规性最低。每个维度的取值只能是 0.33、0.66 或 1.00，不允许出现其他数值。（1）推理与运算步数：直接或少量步骤取 0.33，中等连续步骤取 0.66，多步链式推理或存在中间结论取 1.00；（2）知识点数量：单一核心知识点取 0.33，两个知识点取 0.66，三个及以上或跨模块综合取 1.00；（3）抽象与构造要求：无需抽象或构造取 0.33，需要一定抽象或简单构造取 0.66，需要明显构造或较高层次建模取 1.00；（4）题型常规性：高度常规模板题取 0.33，存在一定变式取 0.66，非典型或创新题型取 1.00。四个维度得分相加后需归一化为 0~1 的 total_score，并按以下规则映射难度等级：0~0.25 为"筑基"，0.25~0.5 为"提分"，0.5 以上为"培优"。请仅以严格 JSON 格式输出 dimension_scores、total_score 和 difficulty_level，不得输出其他字段，不得展开解题过程。"""

AI_STEM_PROMPT_DEFAULT = r"""你是“题干格式修复器”，不是解题老师。
你的任务：把输入题干（stem）做最小幅度的格式清洗，使其更适合 PDF / 系统展示。

【核心原则：原文已兼容就不要改】（最重要，违反此条视为失败）
- 如果原文已经是 PDF 兼容格式（例如：数字、单位、上标³、普通文本混合），就不要做任何改动。
- **严禁添加**：反斜杠 `\`、逗号 `,`、LaTeX 命令（如 `\,`、`\n`、`^3` 等）、换行符 `\n` 等原本不存在的内容。
- **严禁改动**：不要把上标³改成 `^3`，不要把普通文本改成 LaTeX 格式，不要把 Unicode 上标改成 LaTeX 上标。
- **严禁添加换行**：不要添加 `\n` 或任何换行符，保持原文的换行格式不变。如果原文是连续文本，输出也必须是连续文本。

【严禁解题/严禁新增内容】（非常重要）
- 不要解题：不要推导、不要解释、不要补充答案、不要给出步骤、不要增加任何原文中不存在的内容。
- 不要把题干改写成解析/答案。

【必须保留】（非常重要）
1) 保留原题干的结构与排版：不调整顺序、不合并/拆分段落、不新增/删除句子。
2) 保留所有 HTML 标签与结构原样不动（例如 <p>、<br>、<span> 等）。
3) 题干中出现的所有 <svg>...</svg> 片段必须在输出中**逐字复制**（字符、空格、换行、属性顺序、大小写都必须完全一致），不得省略、重排、格式化。
4) **保留上标格式**：如果原文是 `dm³`、`cm³`、`m²` 等 Unicode 上标，必须保持原样，严禁改成 `dm^3`、`cm^3`、`m^2` 等 LaTeX 格式。
5) **保留单位格式**：如果原文是 `2.5 dm³` 这种格式（数字+空格+单位），保持原样，不要添加 `\,`、`\n` 等 LaTeX 命令。

【允许的唯一改动】（只允许做下面这些替换；除此之外任何字符都不要改）
6) 公式包裹：对行内公式 `$...$`，仅去掉外层 `$`，并保留内部内容的原顺序与原字符（不要删改里面的数字/变量/符号）。
7) 符号替换（只替换命中的这些 LaTeX 命令，其它所有内容保持原样）：
   - \triangle → △
   - \text{cm} → cm
   - \text{cm²} → cm²
   - \neq → !=
   - \ge → ≥
   - \le → ≤
   - \Rightarrow => =>
   - \sqrt → sqrt
   - \perp → ⟂
8) 填空线：把所有“连续下划线填空线”（长度不固定）统一成：________（8 个下划线）。
9) **问号填空**：把题干中的问号 `?`（用于表示填空）替换成：________（8 个下划线）。注意：不要删除问号，要替换成填空线。

【输出格式】
10) 输出必须是一段“可直接写回 stem 字段”的文本/HTML。
11) 只输出结果，不要加任何解释、标题、Markdown 标记（不要使用 **、列表编号等）。

下面是题干原文，请按规则输出清洗后的题干：
"""

_env_prompt = os.getenv("AI_STEM_PROMPT")
# 注意：如果环境变量存在但为空（例如 AI_STEM_PROMPT=），os.getenv 会返回空字符串，
# 这会把默认提示词覆盖掉，导致 AI 没有收到规则。这里显式做“空值回退默认”。
AI_STEM_PROMPT = (_env_prompt.strip() if isinstance(_env_prompt, str) else "").strip() or AI_STEM_PROMPT_DEFAULT

_RE_SVG_BLOCK = re.compile(r"<svg\b[\s\S]*?</svg>", re.IGNORECASE)

def _env_bool(name: str, default: bool) -> bool:
    val = os.getenv(name)
    if val is None:
        return default
    return str(val).strip().lower() in {"1", "true", "yes", "y", "on"}

def get_db_connection():
    """
    use_pure=True：避免 C 扩展在部分环境抛出 “RuntimeError: Failed raising error.”
    connection_timeout：避免卡死
    ssl：默认按 JDBC 里的 useSSL=true 走；如果你本地/网络环境不支持，也可以用环境变量关闭
        DB_USE_SSL=false
    """
    use_ssl = _env_bool("DB_USE_SSL", True)
    try:
        return mysql.connector.connect(
            **DB_CONFIG,
            use_pure=True,
            connection_timeout=5,
            ssl_disabled=(not use_ssl),
        )
    except Exception:
        # 兜底：有些环境 SSL 握手会失败，尝试降级不启用 SSL，让你至少能先用起来
        if use_ssl:
            return mysql.connector.connect(
                **DB_CONFIG,
                use_pure=True,
                connection_timeout=5,
                ssl_disabled=True,
            )
        raise

def render_db_error(e: Exception):
    return render_template(
        "db_error.html",
        error=str(e),
        db_host=DB_CONFIG.get("host"),
        db_port=DB_CONFIG.get("port"),
        db_name=DB_CONFIG.get("database"),
        db_user=DB_CONFIG.get("user"),
    ), 500


def get_pk_columns(conn) -> List[str]:
    """
    获取 questions_tem 表里"可能的主键列名"中实际存在的列。
    只要命中一个就能用主键 ID 搜索跳转。
    """
    global _PK_COLS_CACHE
    if _PK_COLS_CACHE is not None:
        return _PK_COLS_CACHE

    cols: List[str] = []
    try:
        cursor = conn.cursor()
        for c in _PK_CANDIDATE_COLS:
            cursor.execute("SHOW COLUMNS FROM questions_tem LIKE %s", (c,))
            if cursor.fetchone():
                cols.append(c)
    except Exception:
        cols = []

    _PK_COLS_CACHE = cols
    return cols


def get_question_pk(question: dict) -> Optional[str]:
    """
    从题目记录里提取“题目主键ID”（用于远程导出 PDF 接口的 question_ids）。
    兼容不同库的列名：id / question_id / pk_id / qid
    """
    for c in _PK_CANDIDATE_COLS:
        v = question.get(c)
        if v is None:
            continue
        s = str(v).strip()
        if s:
            return s
    return None


def request_remote_pdf_url(question_id: str, include_grading: bool = True) -> List[str]:
    """
    调用你给的接口生成 PDF，返回 pdf_url 列表（可能是一个或多个）。
    返回结构示例：
      {"success": true, "data": {"pdf_url": "https://...pdf"}}  # 单个
      或
      {"success": true, "data": {"pdf_url": ["https://...pdf1", "https://...pdf2"]}}  # 多个
    """
    payload = {
        "question_ids": [str(question_id)], 
        "student_id": str(PDF_STUDENT_ID),
        "include_grading": include_grading
    }
    data = json.dumps(payload, ensure_ascii=False).encode("utf-8")

    req = urllib.request.Request(
        PDF_API_URL,
        data=data,
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    try:
        with urllib.request.urlopen(req, timeout=20) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
    except urllib.error.HTTPError as e:
        detail = ""
        try:
            detail = e.read().decode("utf-8", errors="replace")
        except Exception:
            detail = ""
        raise RuntimeError(f"PDF 接口 HTTP 错误：{e.code} {e.reason}. {detail}".strip())
    except Exception as e:
        raise RuntimeError(f"调用 PDF 接口失败：{e}")

    try:
        obj = json.loads(raw)
    except Exception:
        raise RuntimeError(f"PDF 接口返回不是合法 JSON：{raw[:300]}")

    if not isinstance(obj, dict) or not obj.get("success"):
        msg = obj.get("message") if isinstance(obj, dict) else ""
        raise RuntimeError(f"PDF 生成失败：{msg or raw[:300]}")

    data_obj = obj.get("data") if isinstance(obj, dict) else None
    if not isinstance(data_obj, dict):
        raise RuntimeError(f"PDF 接口返回的 data 不是对象：{raw[:300]}")
    
    # 接口返回格式：{"pdf_url": "...", "grading_pdf_url": "..."}
    # 只打开 grading_pdf_url（评分PDF），不打开 pdf_url（题目PDF）
    grading_pdf_url = data_obj.get("grading_pdf_url")
    if not grading_pdf_url or not isinstance(grading_pdf_url, str) or not grading_pdf_url.strip():
        raise RuntimeError(f"PDF 接口未返回有效的 grading_pdf_url：{raw[:300]}")
    
    return [str(grading_pdf_url).strip()]


def _extract_svg_blocks(text: str) -> List[str]:
    if not text:
        return []
    return _RE_SVG_BLOCK.findall(text)


def _svg_blocks_equal(a: str, b: str) -> bool:
    """
    严格校验：新旧题干中的 <svg>...</svg> 片段列表必须完全一致（数量、顺序、内容都一致）。
    """
    return _extract_svg_blocks(a) == _extract_svg_blocks(b)


def call_ai_optimize_stem(stem_html: str) -> str:
    """
    调用 AI，把 stem 原文优化成“安全版 stem”，返回一段可直接写入 stem 的文本/HTML。
    注意：AI 返回的是一段文本，我们直接当作新的 stem 处理。
    """
    if not AI_API_KEY:
        raise RuntimeError("缺少 AI_API_KEY：请在 config.env 里配置 AI_API_KEY")

    base = AI_BASE_URL.rstrip("/")
    if not base:
        base = "https://api.openai.com/v1"

    url = f"{base}/chat/completions"

    # 把“规则”放 system，把题干原文放 user
    payload = {
        "model": AI_MODEL_NAME,
        "messages": [
            {"role": "system", "content": AI_STEM_PROMPT},
            {"role": "user", "content": stem_html or ""},
        ],
        # 关键：不设置时，部分兼容实现会默认只生成很短的内容（看起来像“没读 prompt”）。
        # 为了让模型能完整输出“带 SVG 的题干”，这里给足 token 预算。
        # 注意：部分 API 端点只支持 max_completion_tokens，不支持 max_tokens，所以只传前者。
        "max_completion_tokens": int(os.getenv("AI_MAX_TOKENS", "4096")),
        "temperature": float(os.getenv("AI_TEMPERATURE", "0.0")),
    }

    data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=data,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {AI_API_KEY}",
        },
        method="POST",
    )

    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            raw = resp.read().decode("utf-8", errors="replace")
    except urllib.error.HTTPError as e:
        detail = ""
        try:
            detail = e.read().decode("utf-8", errors="replace")
        except Exception:
            detail = ""
        raise RuntimeError(f"AI 接口 HTTP 错误：{e.code} {e.reason}. {detail}".strip())
    except Exception as e:
        raise RuntimeError(f"调用 AI 接口失败：{e}")

    try:
        obj = json.loads(raw)
    except Exception:
        raise RuntimeError(f"AI 接口返回不是合法 JSON：{raw[:300]}")

    try:
        content = obj["choices"][0]["message"]["content"]
    except Exception:
        raise RuntimeError(f"AI 接口返回结构不符合预期：{raw[:300]}")

    s = str(content or "").strip()
    if not s:
        raise RuntimeError("AI 返回为空")

    # 后处理：清理 AI 误加的无意义换行符 `\n`
    # 规则：只清理那些在连续文本中间的单个 `\n`（前后都是字母/数字/中文，没有空格或标点）
    # 保留 HTML 标签内的换行（如 <svg> 内的换行）和原本有意义的段落换行
    # 匹配：字母/数字/中文 + \n + 字母/数字/中文（这种通常是误加的）
    s = re.sub(r"([\w\u4e00-\u9fff])\n([\w\u4e00-\u9fff])", r"\1 \2", s)
    # 清理多余的连续空格（避免替换后产生多个空格）
    s = re.sub(r" +", " ", s)

    # 保护：如果输入很长但输出极短，通常是 token 上限/模型拒答/接口兼容问题。
    # 直接抛错，避免用户误以为“优化成功”。
    if len((stem_html or "").strip()) >= 200 and len(s) <= 40:
        raise RuntimeError(
            "AI 返回内容过短（疑似被 max_tokens 截断或模型未按要求输出）。请重试；"
            "如仍复现，可把 AI_MODEL_NAME / AI_BASE_URL 发我进一步定位。"
        )
    return s

# 加载知识点映射和层级结构
# ==================== 难度评分相关函数 ====================

class ImageExtractor(HTMLParser):
    """从HTML中提取图片URL"""
    def __init__(self):
        super().__init__()
        self.image_urls = []
    
    def handle_starttag(self, tag, attrs):
        if tag == 'img':
            for attr in attrs:
                if attr[0] in ['src', 'data-src']:
                    url = attr[1]
                    if url:
                        self.image_urls.append(url)

def extract_images_from_html(html_content, base_url=None):
    """从HTML内容中提取所有图片URL"""
    parser = ImageExtractor()
    parser.feed(html_content)
    
    image_urls = parser.image_urls
    
    # 如果是相对路径，转换为绝对路径
    if base_url and image_urls:
        image_urls = [urljoin(base_url, url) if not urlparse(url).netloc else url 
                     for url in image_urls]
    
    return image_urls

def extract_text_from_html(html_content):
    """从HTML中提取纯文本（去除标签）"""
    # 简单的HTML标签去除
    text = re.sub(r'<[^>]+>', '', html_content)
    # 去除多余的空白字符
    text = re.sub(r'\s+', ' ', text).strip()
    return text

def parse_json_response(text):
    """从AI响应中提取JSON"""
    # 尝试直接解析
    try:
        return json.loads(text)
    except:
        pass
    
    # 尝试提取JSON部分（如果响应包含其他文本）
    json_match = re.search(r'\{[\s\S]*\}', text)
    if json_match:
        try:
            return json.loads(json_match.group())
        except:
            pass
    
    # 如果都失败，返回None
    return None

def get_openai_client():
    """获取OpenAI客户端"""
    try:
        from openai import OpenAI
        api_key = DIFFICULTY_SCORING_API_KEY or AI_API_KEY
        if not api_key:
            return None
        
        base_url = AI_BASE_URL.rstrip("/") if AI_BASE_URL else None
        if base_url:
            return OpenAI(api_key=api_key, base_url=base_url)
        else:
            return OpenAI(api_key=api_key)
    except ImportError:
        return None
    except Exception as e:
        print(f"警告: OpenAI客户端初始化失败: {e}")
        return None

def load_kp_structure():
    """
    加载知识点结构，只保留三个层级：chapter、section、subsection
    返回：
    - kp_map: {id: label} 映射
    - kp_hierarchy: 层级结构列表，每个元素包含 chapter、section、subsection 信息
    """
    kp_map = {}
    kp_hierarchy = []
    
    # 优先加载 tree_new.json，如果不存在则从 D 盘复制
    tree_file = resource_path('知了数学题库', 'tree_new.json')
    if not os.path.exists(tree_file):
        # 尝试从 D 盘复制文件
        source_file = 'D:/tree_new.json'
        if os.path.exists(source_file):
            try:
                import shutil
                shutil.copy2(source_file, tree_file)
                print(f"已从 {source_file} 复制到 {tree_file}")
            except Exception as e:
                print(f"复制文件失败: {e}")
        # 如果复制失败或源文件不存在，使用旧的 tree(1)(1).json
        if not os.path.exists(tree_file):
            tree_file = resource_path('知了数学题库', 'tree(1)(1).json')
    
    if os.path.exists(tree_file):
        try:
            with open(tree_file, 'r', encoding='utf-8') as f:
                tree_data = json.load(f)
            
            def process_node(node, parent_chapter=None, parent_section=None):
                """递归处理节点，只保留 chapter、section、subsection 三个层级"""
                if not isinstance(node, dict):
                    return
                
                node_id = node.get("id")
                node_label = node.get("label", "")
                kp_level = node.get("kp_level", "")
                
                # 记录所有节点的映射
                if node_id and node_label:
                    kp_map[str(node_id)] = node_label
                
                # 只处理三个层级
                if kp_level == "chapter":
                    # 章节级别
                    chapter_info = {
                        "id": node_id,
                        "label": node_label,
                        "sections": []
                    }
                    kp_hierarchy.append(chapter_info)
                    
                    # 处理子节点（sections）
                    children = node.get("children", [])
                    for child in children:
                        process_node(child, parent_chapter=chapter_info, parent_section=None)
                
                elif kp_level == "section":
                    # 节级别
                    if parent_chapter:
                        section_info = {
                            "id": node_id,
                            "label": node_label,
                            "subsections": []
                        }
                        parent_chapter["sections"].append(section_info)
                        
                        # 处理子节点（subsections）
                        children = node.get("children", [])
                        for child in children:
                            process_node(child, parent_chapter=parent_chapter, parent_section=section_info)
                
                elif kp_level == "subsection":
                    # 小节级别
                    if parent_section:
                        subsection_info = {
                            "id": node_id,
                            "label": node_label
                        }
                        parent_section["subsections"].append(subsection_info)
                
                # 如果节点有 children，继续递归处理（但只处理符合层级的）
                children = node.get("children", [])
                if children and kp_level not in ["chapter", "section", "subsection"]:
                    # 对于其他层级（如 lesson），继续向下查找
                    for child in children:
                        process_node(child, parent_chapter=parent_chapter, parent_section=parent_section)
            
            # 处理根节点
            if isinstance(tree_data, dict):
                children = tree_data.get("children", [])
                for child in children:
                    process_node(child)
            elif isinstance(tree_data, list):
                for item in tree_data:
                    process_node(item)
                    
        except Exception as e:
            print(f"Error loading tree.json: {e}")
            import traceback
            traceback.print_exc()
    
    return kp_map, kp_hierarchy

def load_kp_structure_from_db():
    """
    从MySQL数据库加载知识点结构，构建树形结构
    返回：
    - kp_map: {kp_code: name} 映射
    - kp_tree: 树形结构列表，每个节点包含 children 列表和 question_count
    """
    kp_map = {}
    kp_tree = []
    
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        # 查询所有知识点及其题目数量
        cursor.execute("""
            SELECT 
                kp.kp_code, 
                kp.name, 
                kp.parent_kp_code, 
                kp.grade,
                COUNT(q.question_code) as question_count
            FROM knowledge_points_copy1 kp
            LEFT JOIN questions_tem q ON q.kp_code = kp.kp_code
            GROUP BY kp.kp_code, kp.name, kp.parent_kp_code, kp.grade
            ORDER BY kp.kp_code
        """)
        all_kps = cursor.fetchall()
        
        # 构建映射
        for kp in all_kps:
            kp_map[kp['kp_code']] = kp['name']
        
        # 构建节点字典
        nodes = {}
        for kp in all_kps:
            question_count = int(kp.get('question_count', 0) or 0)
            nodes[kp['kp_code']] = {
                'kp_code': kp['kp_code'],
                'name': kp['name'],
                'parent_kp_code': kp['parent_kp_code'],
                'grade': kp['grade'],
                'question_count': question_count,
                'children': []
            }
        
        # 构建树形结构
        for kp_code, node in nodes.items():
            parent_code = node['parent_kp_code']
            if parent_code and parent_code in nodes:
                # 有父节点，添加到父节点的children中
                nodes[parent_code]['children'].append(node)
            else:
                # 没有父节点，是根节点
                kp_tree.append(node)
        
        # 递归计算父节点的题目数量（包括子节点）
        def calculate_total_count(node):
            total = node.get('question_count', 0)
            for child in node['children']:
                total += calculate_total_count(child)
            node['total_question_count'] = total
            return total
        
        # 对每个节点的children按kp_code排序，并计算总题目数
        def sort_children(node):
            node['children'].sort(key=lambda x: x['kp_code'])
            for child in node['children']:
                sort_children(child)
            # 计算总题目数（包括子节点）
            calculate_total_count(node)
        
        for root in kp_tree:
            sort_children(root)
        
        conn.close()
        
    except Exception as e:
        print(f"Error loading knowledge points from database: {e}")
        import traceback
        traceback.print_exc()
    
    return kp_map, kp_tree

def load_kp_structure_with_pending_count():
    """
    从MySQL数据库加载知识点结构，构建树形结构，只包含有未审核题目的节点
    返回：
    - kp_map: {kp_code: name} 映射
    - kp_tree: 树形结构列表，每个节点包含 children 列表和 pending_count（未审核题目数量）
    - other_questions_count: 其他题目（未关联知识点）的未审核题目数量
    """
    kp_map = {}
    kp_tree = []
    other_questions_count = 0
    
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        # 查询所有知识点及其未审核题目数量（只查询有未审核题目的知识点）
        cursor.execute("""
            SELECT 
                kp.kp_code, 
                kp.name, 
                kp.parent_kp_code, 
                kp.grade,
                COUNT(q.question_code) as pending_count
            FROM knowledge_points_copy1 kp
            INNER JOIN questions_tem q ON q.kp_code = kp.kp_code
            WHERE (q.audit_reason IS NULL OR q.audit_reason = '')
            GROUP BY kp.kp_code, kp.name, kp.parent_kp_code, kp.grade
            HAVING pending_count > 0
            ORDER BY kp.kp_code
        """)
        all_kps = cursor.fetchall()
        
        # 查询其他题目（未关联知识点）的未审核题目数量
        cursor.execute("""
            SELECT COUNT(*) as count 
            FROM questions_tem 
            WHERE (kp_code IS NULL OR kp_code = '')
              AND (audit_reason IS NULL OR audit_reason = '')
        """)
        other_result = cursor.fetchone()
        other_questions_count = int(other_result['count'] or 0) if other_result else 0
        
        # 构建映射
        for kp in all_kps:
            kp_map[kp['kp_code']] = kp['name']
        
        # 构建节点字典
        nodes = {}
        for kp in all_kps:
            pending_count = int(kp.get('pending_count', 0) or 0)
            nodes[kp['kp_code']] = {
                'kp_code': kp['kp_code'],
                'name': kp['name'],
                'parent_kp_code': kp['parent_kp_code'],
                'grade': kp['grade'],
                'pending_count': pending_count,
                'children': []
            }
        
        # 构建树形结构（只包含有未审核题目的节点）
        for kp_code, node in nodes.items():
            parent_code = node['parent_kp_code']
            if parent_code and parent_code in nodes:
                # 有父节点，添加到父节点的children中
                nodes[parent_code]['children'].append(node)
            else:
                # 没有父节点，是根节点
                kp_tree.append(node)
        
        # 递归计算父节点的未审核题目数量（包括子节点）
        def calculate_total_pending_count(node):
            total = node.get('pending_count', 0)
            for child in node['children']:
                total += calculate_total_pending_count(child)
            node['total_pending_count'] = total
            return total
        
        # 对每个节点的children按kp_code排序，并计算总未审核题目数
        def sort_children(node):
            node['children'].sort(key=lambda x: x['kp_code'])
            for child in node['children']:
                sort_children(child)
            # 计算总未审核题目数（包括子节点）
            calculate_total_pending_count(node)
        
        for root in kp_tree:
            sort_children(root)
        
        conn.close()
        
    except Exception as e:
        print(f"Error loading knowledge points with pending count from database: {e}")
        import traceback
        traceback.print_exc()
    
    return kp_map, kp_tree, other_questions_count

KP_MAP, KP_HIERARCHY = load_kp_structure()

def get_tree_file_path():
    """获取 tree_new.json 文件路径"""
    tree_file = resource_path('知了数学题库', 'tree_new.json')
    if not os.path.exists(tree_file):
        # 尝试从 D 盘复制文件
        source_file = 'D:/tree_new.json'
        if os.path.exists(source_file):
            try:
                import shutil
                shutil.copy2(source_file, tree_file)
                print(f"已从 {source_file} 复制到 {tree_file}")
            except Exception as e:
                print(f"复制文件失败: {e}")
        # 如果复制失败或源文件不存在，使用旧的 tree(1)(1).json
        if not os.path.exists(tree_file):
            tree_file = resource_path('知了数学题库', 'tree(1)(1).json')
    return tree_file

def find_max_id(node):
    """递归查找树中的最大ID"""
    max_id = node.get('id', 0)
    for child in node.get('children', []):
        child_max = find_max_id(child)
        if child_max > max_id:
            max_id = child_max
    return max_id

def find_node_by_id(node, target_id):
    """递归查找指定ID的节点"""
    if node.get('id') == target_id:
        return node
    for child in node.get('children', []):
        result = find_node_by_id(child, target_id)
        if result:
            return result
    return None

def find_kp_hierarchy(kp_id):
    """
    根据知识点ID查找其层级信息（chapter, section, subsection）
    返回: {
        'chapter': {'id': xxx, 'label': 'xxx'},
        'section': {'id': xxx, 'label': 'xxx'} 或 None,
        'subsection': {'id': xxx, 'label': 'xxx'} 或 None
    }
    """
    kp_id_str = str(kp_id)
    result = {
        'chapter': None,
        'section': None,
        'subsection': None
    }
    
    for chapter in KP_HIERARCHY:
        # 检查是否是chapter本身
        if str(chapter["id"]) == kp_id_str:
            result['chapter'] = {'id': chapter["id"], 'label': chapter["label"]}
            return result
        
        # 检查sections
        for section in chapter.get("sections", []):
            # 检查是否是section本身
            if str(section["id"]) == kp_id_str:
                result['chapter'] = {'id': chapter["id"], 'label': chapter["label"]}
                result['section'] = {'id': section["id"], 'label': section["label"]}
                return result
            
            # 检查subsections
            for subsection in section.get("subsections", []):
                if str(subsection["id"]) == kp_id_str:
                    result['chapter'] = {'id': chapter["id"], 'label': chapter["label"]}
                    result['section'] = {'id': section["id"], 'label': section["label"]}
                    result['subsection'] = {'id': subsection["id"], 'label': subsection["label"]}
                    return result
    
    return result

@app.route('/question_management')
def question_management():
    """题目管理页面：显示知识点目录和题目列表"""
    # 从数据库加载知识点树形结构
    _, kp_tree = load_kp_structure_from_db()
    
    # 查询"其他题目"（未关联kp_code的题目）数量
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        cursor.execute("""
            SELECT COUNT(*) as count 
            FROM questions_tem 
            WHERE (kp_code IS NULL OR kp_code = '')
        """)
        other_questions_count = cursor.fetchone()['count'] if cursor.rowcount > 0 else 0
        conn.close()
    except Exception as e:
        print(f"查询其他题目数量失败: {e}")
        other_questions_count = 0
    
    return render_template('question_management.html', 
                         kp_tree=kp_tree, 
                         is_audit_mode=False,
                         other_questions_count=other_questions_count)

@app.route('/api/questions_by_kp/<kp_code>')
def api_questions_by_kp(kp_code):
    """根据知识点代码返回题目列表（JSON格式）"""
    return _api_questions_by_kp(kp_code, audit_only=False)

@app.route('/api/pending_questions_by_kp/<kp_code>')
def api_pending_questions_by_kp(kp_code):
    """根据知识点代码返回未审核题目列表（JSON格式，用于审核页面）"""
    return _api_questions_by_kp(kp_code, audit_only=True)

def _api_questions_by_kp(kp_code, audit_only=False):
    """根据知识点代码返回题目列表（JSON格式）"""
    try:
        conn = get_db_connection()
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500
    
    try:
        cursor = conn.cursor(dictionary=True)
        
        # 获取年级筛选参数（可选）
        grade_filter = request.args.get('grade', type=int)
        # 获取分页参数（可选）
        page = request.args.get('page', type=int, default=1)
        page_size = 20  # 每页显示20道题
        
        # 先获取知识点名称（如果kp_code不为空）
        kp_name = kp_code
        if kp_code != 'null' and kp_code != '':
            cursor.execute("SELECT name FROM knowledge_points_copy1 WHERE kp_code = %s", (kp_code,))
            kp_row = cursor.fetchone()
            kp_name = kp_row['name'] if kp_row else kp_code
        
        # 构建查询条件
        where_conditions = []
        params = []
        
        # 知识点条件
        if kp_code == 'null' or kp_code == '':
            where_conditions.append("(kp_code IS NULL OR kp_code = '' OR kp_code = 'null')")
            kp_name = "其他题目"
        else:
            where_conditions.append("kp_code = %s")
            params.append(kp_code)
        
        # 审核状态条件
        if audit_only:
            where_conditions.append("(audit_reason IS NULL OR audit_reason = '')")
        
        # 年级筛选条件（仅对"其他题目"有效，因为其他题目有grade字段）
        if grade_filter is not None and (kp_code == 'null' or kp_code == ''):
            where_conditions.append("grade = %s")
            params.append(grade_filter)
        
        where_clause = " AND ".join(where_conditions)
        
        # 先查询总数（用于分页）
        count_query = f"SELECT COUNT(*) as total FROM questions_tem WHERE {where_clause}"
        cursor.execute(count_query, params)
        total_count = cursor.fetchone()['total']
        total_pages = (total_count + page_size - 1) // page_size  # 向上取整
        
        # 确保页码有效
        if page < 1:
            page = 1
        if page > total_pages and total_pages > 0:
            page = total_pages
        
        # 计算偏移量
        offset = (page - 1) * page_size
        
        # 查询题目（带分页）
        query = f"""
            SELECT 
                question_code, 
                stem, 
                kp_code,
                kp_id,
                audit_reason,
                audit_status,
                difficulty,
                question_type,
                answer,
                solution,
                options,
                grade
            FROM questions_tem 
            WHERE {where_clause}
            ORDER BY question_code DESC
            LIMIT %s OFFSET %s
        """
        
        params_with_pagination = params + [page_size, offset]
        cursor.execute(query, params_with_pagination)
        
        questions = cursor.fetchall()
        
        # 为了统计，需要查询所有题目（不分页）
        stats_query = f"""
            SELECT 
                question_code,
                audit_reason,
                difficulty,
                question_type
            FROM questions_tem 
            WHERE {where_clause}
            ORDER BY question_code DESC
        """
        cursor.execute(stats_query, params)
        all_questions_for_stats = cursor.fetchall()
        
        # 处理选项JSON
        for q in questions:
            if q.get('options'):
                try:
                    opt_data = q['options']
                    if isinstance(opt_data, str):
                        opt_data = opt_data.replace("'", '"')
                        q['options'] = json.loads(opt_data)
                except:
                    q['options'] = {}
        
        # 计算统计数据（基于所有题目，不分页）
        # 审核统计
        pass_count = sum(1 for q in all_questions_for_stats if q.get('audit_reason') == '合格')
        fail_count = sum(1 for q in all_questions_for_stats if q.get('audit_reason') == '不合格')
        pending_count = sum(1 for q in all_questions_for_stats if not q.get('audit_reason') or q.get('audit_reason') == '')
        
        # 难度统计
        difficulty_jichu = 0  # 筑基 0.2
        difficulty_tifen = 0  # 提分 0.4
        difficulty_peiyou = 0  # 培优 0.7
        difficulty_unknown = 0  # 未设置难度
        
        for q in all_questions_for_stats:
            diff = q.get('difficulty')
            if diff is not None:
                try:
                    diff_float = float(diff)
                    if abs(diff_float - 0.2) < 0.1:
                        difficulty_jichu += 1
                    elif abs(diff_float - 0.4) < 0.1:
                        difficulty_tifen += 1
                    elif abs(diff_float - 0.7) < 0.1:
                        difficulty_peiyou += 1
                    else:
                        difficulty_unknown += 1
                except:
                    difficulty_unknown += 1
            else:
                difficulty_unknown += 1
        
        # 题型统计
        question_type_stats = {}
        for q in all_questions_for_stats:
            q_type = q.get('question_type') or '未分类'
            question_type_stats[q_type] = question_type_stats.get(q_type, 0) + 1
        
        conn.close()
        
        return jsonify({
            'success': True,
            'kp_code': kp_code,
            'kp_name': kp_name,
            'questions': questions,
            'count': total_count,
            'pagination': {
                'page': page,
                'page_size': page_size,
                'total_pages': total_pages,
                'total_count': total_count
            },
            'stats': {
                'total': total_count,
                'audit': {
                    'pass': pass_count,
                    'fail': fail_count,
                    'pending': pending_count,
                    'pass_rate': round((pass_count / (pass_count + fail_count) * 100) if (pass_count + fail_count) > 0 else 0, 1),
                    'audit_rate': round(((pass_count + fail_count) / total_count * 100) if total_count > 0 else 0, 1)
                },
                'difficulty': {
                    'jichu': difficulty_jichu,
                    'tifen': difficulty_tifen,
                    'peiyou': difficulty_peiyou,
                    'unknown': difficulty_unknown
                },
                'question_type': question_type_stats
            }
        })
    except Exception as e:
        try:
            conn.close()
        except:
            pass
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/')
def index():
    """首页：显示题目统计信息"""
    try:
        conn = get_db_connection()
    except Exception as e:
        return render_db_error(e)
    cursor = conn.cursor(dictionary=True)
    
    # 总体统计
    cursor.execute("""
        SELECT 
            COUNT(*) AS total_count,
            SUM(CASE WHEN audit_reason = '合格' THEN 1 ELSE 0 END) AS pass_count,
            SUM(CASE WHEN audit_reason = '不合格' OR audit_status = 1 THEN 1 ELSE 0 END) AS fail_count,
            SUM(CASE WHEN audit_reason IS NULL OR audit_reason = '' THEN 1 ELSE 0 END) AS pending_count
        FROM questions_tem
    """)
    overall_stats = cursor.fetchone()
    
    total = int(overall_stats['total_count'] or 0)
    pass_count = int(overall_stats['pass_count'] or 0)
    fail_count = int(overall_stats['fail_count'] or 0)
    pending_count = int(overall_stats['pending_count'] or 0)
    audited_count = pass_count + fail_count
    
    # 计算审核通过率
    pass_rate = (pass_count / audited_count * 100) if audited_count > 0 else 0
    audit_rate = (audited_count / total * 100) if total > 0 else 0
    
    # 教材系列统计
    cursor.execute("""
        SELECT 
            COUNT(*) AS total_series,
            SUM(CASE WHEN is_active = 1 THEN 1 ELSE 0 END) AS active_series
        FROM textbook_series_copy1
    """)
    series_stats = cursor.fetchone()
    total_series = int(series_stats['total_series'] or 0)
    active_series = int(series_stats['active_series'] or 0)
    
    # 教材统计
    cursor.execute("SELECT COUNT(*) AS total_textbooks FROM textbooks_copy1")
    textbook_stats = cursor.fetchone()
    total_textbooks = int(textbook_stats['total_textbooks'] or 0)
    
    # 知识点统计
    cursor.execute("SELECT COUNT(*) AS total_kp FROM knowledge_points_copy1")
    kp_stats = cursor.fetchone()
    total_kp = int(kp_stats['total_kp'] or 0)
    
    # 知识点层级统计（按级别统计）
    cursor.execute("""
        SELECT 
            CASE 
                WHEN parent_kp_code IS NULL OR parent_kp_code = '' THEN 'level_0'
                WHEN parent_kp_code IN (SELECT kp_code FROM knowledge_points_copy1 WHERE parent_kp_code IS NULL OR parent_kp_code = '') THEN 'level_1'
                ELSE 'level_2_plus'
            END AS level_type,
            COUNT(*) AS count
        FROM knowledge_points_copy1
        GROUP BY level_type
    """)
    kp_level_stats = cursor.fetchall()
    kp_level_0 = 0
    kp_level_1 = 0
    kp_level_2_plus = 0
    for row in kp_level_stats:
        if row['level_type'] == 'level_0':
            kp_level_0 = int(row['count'] or 0)
        elif row['level_type'] == 'level_1':
            kp_level_1 = int(row['count'] or 0)
        else:
            kp_level_2_plus = int(row['count'] or 0)
    
    # 最近添加的题目（如果有创建时间字段）
    cursor.execute("""
        SELECT question_code, stem, kp_id, audit_reason
        FROM questions_tem
        ORDER BY id DESC
        LIMIT 5
    """)
    recent_questions = cursor.fetchall()
    
    # 按审核状态统计
    status_stats = {
        'pass': pass_count,
        'fail': fail_count,
        'pending': pending_count,
    }
    
    conn.close()
    
    return render_template('index.html', 
                         total=total,
                         pass_count=pass_count,
                         fail_count=fail_count,
                         pending_count=pending_count,
                         audited_count=audited_count,
                         pass_rate=round(pass_rate, 1),
                         audit_rate=round(audit_rate, 1),
                         total_series=total_series,
                         active_series=active_series,
                         total_textbooks=total_textbooks,
                         total_kp=total_kp,
                         kp_level_0=kp_level_0,
                         kp_level_1=kp_level_1,
                         kp_level_2_plus=kp_level_2_plus,
                         recent_questions=recent_questions,
                         status_stats=status_stats)


@app.route('/audit_questions')
def audit_questions():
    """审核题目页面：显示所有未审核的题目，按知识点分类"""
    try:
        # 从数据库加载知识点树形结构（只包含有未审核题目的节点）
        _, kp_tree, other_questions_count = load_kp_structure_with_pending_count()
        
        # 查询总体统计
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        cursor.execute("""
            SELECT 
                COUNT(*) AS total_count,
                SUM(CASE WHEN audit_reason = '合格' THEN 1 ELSE 0 END) AS pass_count,
                SUM(CASE WHEN audit_reason = '不合格' OR audit_status = 1 THEN 1 ELSE 0 END) AS fail_count,
                SUM(CASE WHEN audit_reason IS NULL OR audit_reason = '' THEN 1 ELSE 0 END) AS pending_count
            FROM questions_tem
        """)
        overall_stats = cursor.fetchone()
        total = int(overall_stats['total_count'] or 0)
        pass_count = int(overall_stats['pass_count'] or 0)
        fail_count = int(overall_stats['fail_count'] or 0)
        pending_count = int(overall_stats['pending_count'] or 0)
        audited_count = pass_count + fail_count
        pass_rate = (pass_count / audited_count * 100) if audited_count > 0 else 0
        audit_rate = (audited_count / total * 100) if total > 0 else 0
        
        conn.close()
        
        return render_template('audit_questions.html', 
                             kp_tree=kp_tree, 
                             other_questions_count=other_questions_count,
                             total=total,
                             pass_count=pass_count,
                             fail_count=fail_count,
                             pending_count=pending_count,
                             audited_count=audited_count,
                             pass_rate=round(pass_rate, 1),
                             audit_rate=round(audit_rate, 1))
    except Exception as e:
        return render_db_error(e)


@app.route('/search')
def search_by_question_code():
    """
    输入 question_code 直接跳转题目详情。
    GET /search?q=xxxx
    """
    q = (request.args.get("q") or "").strip()
    if not q:
        return redirect(url_for("index"))

    try:
        conn = get_db_connection()
    except Exception as e:
        return render_db_error(e)

    cursor = conn.cursor(dictionary=True)
    cursor.execute("SELECT question_code FROM questions_tem WHERE question_code = %s LIMIT 1", (q,))
    row = cursor.fetchone()
    conn.close()

    if row:
        return redirect(url_for("detail", question_code=q))

    return render_template("search_not_found.html", q=q)


@app.route('/search_id')
def search_by_question_id():
    """
    输入主键ID（比如 id / question_id / pk_id / qid）查题。
    GET /search_id?id=123
    """
    raw = (request.args.get("id") or "").strip()
    if not raw:
        return redirect(url_for("index"))

    try:
        conn = get_db_connection()
    except Exception as e:
        return render_db_error(e)

    pk_cols = get_pk_columns(conn)
    if not pk_cols:
        conn.close()
        return render_template("search_id_not_supported.html", q=raw)

    cursor = conn.cursor(dictionary=True)
    found_code = None
    for col in pk_cols:
        try:
            cursor.execute(f"SELECT question_code FROM questions_tem WHERE {col} = %s LIMIT 1", (raw,))
            row = cursor.fetchone()
            if row and row.get("question_code"):
                found_code = row["question_code"]
                break
        except Exception:
            continue
    conn.close()

    if found_code:
        return redirect(url_for("detail", question_code=found_code))

    return render_template("search_id_not_found.html", q=raw, pk_cols=pk_cols)

@app.route('/questions/<kp_code>')
def question_list(kp_code):
    try:
        conn = get_db_connection()
    except Exception as e:
        return render_db_error(e)
    cursor = conn.cursor(dictionary=True)
    
    # 检查是否是审核模式（只显示未审核题目）
    audit_only = request.args.get('audit_only', '').lower() == 'true'
    
    # 处理 kp_code 为 'null' 或空字符串的情况（表示没有 kp_id 的题目）
    # 注意：URL参数仍使用 kp_code，但数据库字段已改为 kp_id
    # 使用别名 kp_id AS kp_code 以兼容模板代码
    if kp_code == 'null' or kp_code == '':
        if audit_only:
            cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE (kp_id IS NULL OR kp_id = '') AND (audit_reason IS NULL OR audit_reason = '') ORDER BY question_code DESC")
        else:
            cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE kp_id IS NULL OR kp_id = '' ORDER BY question_code DESC")
        kp_name = "未分类题目"
        hierarchy_info = {'chapter': None, 'section': None, 'subsection': None}
    else:
        # 查找层级信息，判断是否是章节级别
        hierarchy_info = find_kp_hierarchy(kp_code)
        
        # 如果是章节级别，需要查询该章节下所有 section 和 subsection 的题目
        if hierarchy_info['chapter'] and not hierarchy_info['section'] and not hierarchy_info['subsection']:
            # 这是章节级别，需要收集该章节下所有的 kp_id
            chapter_id = hierarchy_info['chapter']['id']
            kp_ids = [str(chapter_id)]  # 包含章节本身
            
            # 查找该章节下的所有 section 和 subsection
            for chapter in KP_HIERARCHY:
                if chapter["id"] == chapter_id:
                    for section in chapter.get("sections", []):
                        kp_ids.append(str(section["id"]))
                        for subsection in section.get("subsections", []):
                            kp_ids.append(str(subsection["id"]))
                    break
            
            # 使用 IN 查询该章节下所有知识点的题目
            placeholders = ','.join(['%s'] * len(kp_ids))
            if audit_only:
                query = f"SELECT *, kp_id AS kp_code FROM questions_tem WHERE kp_id IN ({placeholders}) AND (audit_reason IS NULL OR audit_reason = '') ORDER BY question_code DESC"
            else:
                query = f"SELECT *, kp_id AS kp_code FROM questions_tem WHERE kp_id IN ({placeholders}) ORDER BY question_code DESC"
            cursor.execute(query, tuple(kp_ids))
            kp_name = hierarchy_info['chapter']['label']
        else:
            # 普通的知识点查询（section 或 subsection）
            if audit_only:
                cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE kp_id = %s AND (audit_reason IS NULL OR audit_reason = '') ORDER BY question_code DESC", (kp_code,))
            else:
                cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE kp_id = %s ORDER BY question_code DESC", (kp_code,))
            kp_name = KP_MAP.get(str(kp_code), kp_code)
    
    questions = cursor.fetchall()
    
    # 构建录入题目页面的URL
    add_question_url = f'/add_question?kp_code={kp_code}'
    if hierarchy_info['chapter']:
        chapter_label_encoded = urllib.parse.quote(hierarchy_info['chapter']['label'])
        add_question_url += f"&chapter={hierarchy_info['chapter']['id']}&chapter_label={chapter_label_encoded}"
    if hierarchy_info['section']:
        section_label_encoded = urllib.parse.quote(hierarchy_info['section']['label'])
        add_question_url += f"&section={hierarchy_info['section']['id']}&section_label={section_label_encoded}"
    if hierarchy_info['subsection']:
        subsection_label_encoded = urllib.parse.quote(hierarchy_info['subsection']['label'])
        add_question_url += f"&subsection={hierarchy_info['subsection']['id']}&subsection_label={subsection_label_encoded}"
    
    conn.close()
    return render_template('questions.html', 
                          questions=questions, 
                          kp_code=kp_code, 
                          kp_name=kp_name, 
                          node_id=None,
                          hierarchy_info=hierarchy_info,
                          add_question_url=add_question_url,
                          audit_only=audit_only)

@app.route('/textbook/<node_id>')
def textbook_question_list(node_id):
    """教材节点题目列表"""
    try:
        conn = get_db_connection()
    except Exception as e:
        return render_db_error(e)
    cursor = conn.cursor(dictionary=True)
    
    # 获取教材节点标题
    try:
        cursor.execute("SELECT title FROM textbook_catalog_nodes WHERE id = %s", (node_id,))
        node_row = cursor.fetchone()
        title = node_row.get('title') if node_row else f'节点{node_id}'
    except Exception:
        # 如果表不存在，使用默认标题
        title = f'节点{node_id}'
    
    # 查询该节点下的所有题目（使用别名 kp_id AS kp_code 以兼容模板代码）
    cursor.execute(
        "SELECT *, kp_id AS kp_code FROM questions_tem WHERE textbook_catalog_nodes_id = %s ORDER BY question_code DESC",
        (node_id,)
    )
    questions = cursor.fetchall()
    conn.close()
    
    return render_template('questions.html', questions=questions, kp_code=None, kp_name=title, node_id=node_id)

@app.route('/detail/<question_code>')
def detail(question_code):
    try:
        conn = get_db_connection()
    except Exception as e:
        return render_db_error(e)
    cursor = conn.cursor(dictionary=True)
    # 使用别名 kp_id AS kp_code 以兼容模板代码
    cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE question_code = %s", (question_code,))
    question = cursor.fetchone()
    
    if not question:
        conn.close()
        return render_template("search_not_found.html", q=question_code), 404
    
    # 获取上下题索引（从 kp_id 字段读取知识点ID，但模板中使用 kp_code）
    # 优先使用 URL 参数中的 kp_code（用于返回列表时定位到正确的知识点）
    kp_code_from_url = request.args.get('kp_code')
    kp_code_db = kp_code_from_url or question.get('kp_id') or question.get('kp_code')  # 兼容处理
    textbook_node_id = question.get('textbook_catalog_nodes_id')
    prev_code = None
    next_code = None
    curr_idx = 0
    total = 1
    
    if kp_code_db:
        # 有 kp_id：查询同知识点下的所有题目
        cursor.execute("SELECT question_code FROM questions_tem WHERE kp_id = %s ORDER BY question_code DESC", (kp_code_db,))
        all_codes = [r['question_code'] for r in cursor.fetchall()]
        
        if question_code in all_codes:
            curr_idx = all_codes.index(question_code)
            prev_code = all_codes[curr_idx - 1] if curr_idx > 0 else None
            next_code = all_codes[curr_idx + 1] if curr_idx < len(all_codes) - 1 else None
            total = len(all_codes)
    elif textbook_node_id:
        # 没有 kp_code 但有 textbook_catalog_nodes_id：查询同教材节点下的所有题目
        cursor.execute(
            "SELECT question_code FROM questions_tem WHERE textbook_catalog_nodes_id = %s ORDER BY question_code DESC",
            (textbook_node_id,)
        )
        all_codes = [r['question_code'] for r in cursor.fetchall()]
        
        if question_code in all_codes:
            curr_idx = all_codes.index(question_code)
            prev_code = all_codes[curr_idx - 1] if curr_idx > 0 else None
            next_code = all_codes[curr_idx + 1] if curr_idx < len(all_codes) - 1 else None
            total = len(all_codes)
    else:
        # 既没有 kp_code 也没有 textbook_catalog_nodes_id：不显示上下题导航
        pass
    
    conn.close()
    
    # 处理选项 JSON
    options = []
    if question.get('options'):
        try:
            opt_data = question['options']
            if isinstance(opt_data, str):
                # 兼容单引号 JSON
                opt_data = opt_data.replace("'", '"')
                d = json.loads(opt_data)
            else:
                d = opt_data
            options = sorted(d.items())
        except:
            options = []

    # 处理知识点名称：如果没有 kp_code，显示"未分类"
    kp_name = "未分类题目"
    hierarchy_info = {'chapter': None, 'section': None, 'subsection': None}
    add_question_url = None
    
    if kp_code_db:
        kp_name = KP_MAP.get(str(kp_code_db), kp_code_db)
        # 查找层级信息
        hierarchy_info = find_kp_hierarchy(kp_code_db)
        # 构建录入题目页面的URL
        add_question_url = f'/add_question?kp_code={kp_code_db}'
        if hierarchy_info['chapter']:
            chapter_label_encoded = urllib.parse.quote(hierarchy_info['chapter']['label'])
            add_question_url += f"&chapter={hierarchy_info['chapter']['id']}&chapter_label={chapter_label_encoded}"
        if hierarchy_info['section']:
            section_label_encoded = urllib.parse.quote(hierarchy_info['section']['label'])
            add_question_url += f"&section={hierarchy_info['section']['id']}&section_label={section_label_encoded}"
        if hierarchy_info['subsection']:
            subsection_label_encoded = urllib.parse.quote(hierarchy_info['subsection']['label'])
            add_question_url += f"&subsection={hierarchy_info['subsection']['id']}&subsection_label={subsection_label_encoded}"
    elif textbook_node_id:
        # 查询教材节点标题
        try:
            cursor.execute("SELECT title FROM textbook_catalog_nodes WHERE id = %s", (textbook_node_id,))
            node_row = cursor.fetchone()
            if node_row:
                kp_name = node_row.get('title') or f'节点{textbook_node_id}'
            else:
                kp_name = f'节点{textbook_node_id}'
        except Exception as e:
            # 如果表不存在或查询失败，使用默认标题
            print(f"Warning: 无法查询教材节点标题: {e}")
            kp_name = f'节点{textbook_node_id}'

    return render_template('detail.html', 
                           q=question, 
                           options=options, 
                           kp_name=kp_name,
                           kp_code=kp_code_db,
                           node_id=str(textbook_node_id) if textbook_node_id else None,
                           prev_code=prev_code,
                           next_code=next_code,
                           total=total,
                           curr_num=curr_idx + 1 if total > 0 else 1,
                           add_question_url=add_question_url)

@app.route('/audit', methods=['POST'])
def audit():
    data = request.json
    q_code = data.get('question_code')
    status_text = data.get('audit_reason') # "合格" or "不合格"
    status_val = 1 if status_text == "不合格" else 0
    
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        # 更新 questions_tem 表的审核状态
        cursor.execute("UPDATE questions_tem SET audit_reason=%s, audit_status=%s WHERE question_code=%s", 
                       (status_text, status_val, q_code))
        
        # 如果审核通过（合格），将题目插入到 questions 表
        if status_text == "合格":
            # 从 questions_tem 表获取所有字段数据
            cursor.execute("SELECT * FROM questions_tem WHERE question_code = %s", (q_code,))
            question_data = cursor.fetchone()
            
            if question_data:
                # 检查 questions 表是否已存在该 question_code
                cursor.execute("SELECT question_code FROM questions WHERE question_code = %s", (q_code,))
                existing = cursor.fetchone()
                
                # 准备要插入/更新的字段（排除 id 和 is_repeat，因为 questions 表没有 is_repeat 字段）
                fields_to_copy = [
                    'question_code', 'kp_id', 'textbook_catalog_nodes_id', 'stem', 'options',
                    'answer', 'solution', 'difficulty', 'question_category', 'source', 'tags',
                    'question_type', 'source_file_id', 'source_paper_id', 'paper_part_id',
                    'textbook_id', 'meta', 'created_at', 'updated_at', 'audit_status',
                    'audit_reason', 'title_1', 'title_2', 'title_3', 'create_by',
                    'kp_code', 'kp_name', 'kp_reference'
                ]
                
                if existing:
                    # 如果已存在，更新数据
                    update_fields = []
                    update_values = []
                    for field in fields_to_copy:
                        if field in question_data:
                            update_fields.append(f"{field} = %s")
                            update_values.append(question_data[field])
                    update_values.append(q_code)
                    
                    update_sql = f"UPDATE questions SET {', '.join(update_fields)} WHERE question_code = %s"
                    cursor.execute(update_sql, tuple(update_values))
                else:
                    # 如果不存在，插入新数据
                    insert_fields = []
                    insert_values = []
                    placeholders = []
                    
                    for field in fields_to_copy:
                        if field in question_data:
                            insert_fields.append(field)
                            insert_values.append(question_data[field])
                            placeholders.append('%s')
                    
                    insert_sql = f"INSERT INTO questions ({', '.join(insert_fields)}) VALUES ({', '.join(placeholders)})"
                    cursor.execute(insert_sql, tuple(insert_values))
        
        conn.commit()
        conn.close()
        return jsonify({'success': True})
    except Exception as e:
        import traceback
        traceback.print_exc()
        return jsonify({'success': False, 'error': str(e)})

@app.route("/export_pdf_remote/<question_code>")
def export_pdf_remote(question_code):
    """
    远程导出：调用你给的接口拿到 pdf_url（可能是一个或多个），然后全部打开。
    """
    try:
        conn = get_db_connection()
    except Exception as e:
        return render_db_error(e)

    try:
        cursor = conn.cursor(dictionary=True)
        # 使用别名 kp_id AS kp_code 以兼容模板代码
        cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE question_code = %s", (question_code,))
        question = cursor.fetchone()
    finally:
        try:
            conn.close()
        except Exception:
            pass

    if not question:
        return render_template("search_not_found.html", q=question_code), 404

    qid = get_question_pk(question)
    if not qid:
        # 没有常见主键列名，无法给 question_ids
        return render_template("search_id_not_supported.html", q=str(question_code)), 400

    try:
        pdf_urls = request_remote_pdf_url(qid, include_grading=True)
    except Exception as e:
        # PDF接口失败，不显示数据库连接信息
        return render_template(
            "db_error.html",
            error=str(e),
            db_host=None,
            db_port=None,
            db_name=None,
            db_user=None,
        ), 500

    if not pdf_urls:
        # PDF接口未返回有效数据，不显示数据库连接信息
        return render_template(
            "db_error.html",
            error="PDF 接口未返回有效的 pdf_url",
            db_host=None,
            db_port=None,
            db_name=None,
            db_user=None,
        ), 500

    # 如果有多个 PDF，返回一个 HTML 页面，用 JavaScript 全部打开
    if len(pdf_urls) > 1:
        return render_template("open_multiple_pdfs.html", pdf_urls=pdf_urls)
    else:
        # 只有一个 PDF，直接重定向
        return redirect(pdf_urls[0])


@app.route("/api/optimize_stem/<question_code>", methods=["POST"])
def api_optimize_stem(question_code):
    """
    生成“优化后的 stem”，并返回左右对比所需内容。
    """
    try:
        conn = get_db_connection()
    except Exception as e:
        return jsonify({"success": False, "error": str(e)}), 500

    try:
        cursor = conn.cursor(dictionary=True)
        cursor.execute("SELECT question_code, stem FROM questions_tem WHERE question_code = %s", (question_code,))
        row = cursor.fetchone()
    finally:
        try:
            conn.close()
        except Exception:
            pass

    if not row:
        return jsonify({"success": False, "error": "题目不存在"}), 404

    old_stem = row.get("stem") or ""

    try:
        new_stem = call_ai_optimize_stem(old_stem)
    except Exception as e:
        return jsonify({"success": False, "error": str(e)}), 500

    svg_ok = _svg_blocks_equal(old_stem, new_stem)
    return jsonify(
        {
            "success": True,
            "question_code": question_code,
            "old_stem": old_stem,
            "new_stem": new_stem,
            "svg_ok": bool(svg_ok),
        }
    )


@app.route("/api/replace_stem/<question_code>", methods=["POST"])
def api_replace_stem(question_code):
    """
    将新 stem 覆写到 questions_tem.stem（仅改 stem）。
    额外安全：替换前校验 SVG 不被改动。
    """
    data = request.json or {}
    new_stem = data.get("new_stem")
    if new_stem is None:
        return jsonify({"success": False, "error": "缺少 new_stem"}), 400

    try:
        conn = get_db_connection()
    except Exception as e:
        return jsonify({"success": False, "error": str(e)}), 500

    try:
        cursor = conn.cursor(dictionary=True)
        cursor.execute("SELECT question_code, stem FROM questions_tem WHERE question_code = %s", (question_code,))
        row = cursor.fetchone()
        if not row:
            return jsonify({"success": False, "error": "题目不存在"}), 404

        old_stem = row.get("stem") or ""

        if not _svg_blocks_equal(old_stem, str(new_stem)):
            return jsonify({"success": False, "error": "安全拦截：检测到 SVG 被改动，已禁止替换。"}), 400

        cur2 = conn.cursor()
        cur2.execute("UPDATE questions_tem SET stem=%s WHERE question_code=%s", (str(new_stem), question_code))
        conn.commit()
        return jsonify({"success": True, "question_code": question_code})
    except Exception as e:
        try:
            conn.rollback()
        except Exception:
            pass
        return jsonify({"success": False, "error": str(e)}), 500
    finally:
        try:
            conn.close()
        except Exception:
            pass

@app.route('/api/score', methods=['POST'])
def api_score_question():
    """
    题目难度评分接口（支持文本、HTML和图片）
    
    请求格式:
    {
        "stem": "题目文本内容或HTML（可选，HTML中可包含<img>标签）",
        "image_url": "图片URL（可选）",
        "image_base64": "图片base64编码（可选，格式：data:image/png;base64,xxx）",
        "base_url": "基础URL（可选，用于处理HTML中相对路径的图片）",
        "custom_prompt": "可选的自定义提示词"
    }
    
    返回格式:
    {
        "success": true,
        "data": {
            "difficulty_level": "筑基|提分|培优",
            "final_score": 0.00,
            "dimension_scores": {
                "推理与运算步数": 0.33,
                "知识点数量": 0.33,
                "抽象与构造要求": 0.33,
                "题型常规性": 0.33
            }
        },
        "message": "评分成功"
    }
    """
    client = get_openai_client()
    if client is None:
        return jsonify({
            "success": False,
            "error": "OpenAI客户端未初始化，请检查配置"
        }), 500
    
    try:
        # 获取请求数据
        data = request.get_json()
        if not data:
            return jsonify({
                "success": False,
                "error": "请求体不能为空"
            }), 400
        
        stem = data.get('stem', '').strip()
        image_url = data.get('image_url', '').strip()
        image_base64 = data.get('image_base64', '').strip()
        base_url = data.get('base_url', '').strip()  # 用于处理相对路径的图片URL
        
        # 如果stem包含HTML标签，尝试提取图片
        html_images = []
        stem_text = stem
        if stem and ('<img' in stem.lower() or '<image' in stem.lower()):
            html_images = extract_images_from_html(stem, base_url)
            # 同时提取纯文本（保留LaTeX公式等数学符号）
            stem_text = extract_text_from_html(stem)
        
        # 至少需要提供一种输入
        if not stem_text and not image_url and not image_base64 and not html_images:
            return jsonify({
                "success": False,
                "error": "至少需要提供以下之一：stem（文本或HTML）、image_url（图片URL）或image_base64（图片base64）"
            }), 400
        
        # 获取提示词
        custom_prompt = data.get('custom_prompt', '').strip()
        prompt = custom_prompt if custom_prompt else DIFFICULTY_SCORING_PROMPT
        
        # 构建消息内容
        content = []
        
        # 添加文本提示词
        if stem_text:
            full_prompt = f"{prompt}\n\n题目内容：\n{stem_text}"
        else:
            full_prompt = prompt
        
        content.append({
            "type": "text",
            "text": full_prompt
        })
        
        # 添加图片（优先级：base64 > 直接URL > HTML中提取的图片）
        images_to_add = []
        
        if image_base64:
            # 处理base64图片
            if not image_base64.startswith('data:'):
                image_base64 = f"data:image/png;base64,{image_base64}"
            images_to_add.append(image_base64)
        elif image_url:
            # 使用直接提供的图片URL
            images_to_add.append(image_url)
        elif html_images:
            # 使用从HTML中提取的图片URL
            images_to_add.extend(html_images)
        
        # 添加所有图片到content
        for img in images_to_add:
            if img.startswith('data:'):
                # base64图片
                content.append({
                    "type": "image_url",
                    "image_url": {
                        "url": img
                    }
                })
            else:
                # URL图片
                content.append({
                    "type": "image_url",
                    "image_url": {
                        "url": img
                    }
                })
        
        # 调用OpenAI API
        model_name = DIFFICULTY_SCORING_MODEL or AI_MODEL_NAME
        try:
            response = client.chat.completions.create(
                model=model_name,
                messages=[
                    {
                        "role": "user",
                        "content": content
                    }
                ],
                temperature=DIFFICULTY_SCORING_TEMPERATURE,
                top_p=DIFFICULTY_SCORING_TOP_P,
                presence_penalty=DIFFICULTY_SCORING_PRESENCE_PENALTY,
                frequency_penalty=DIFFICULTY_SCORING_FREQUENCY_PENALTY
            )
        except Exception as api_error:
            print(f"OpenAI API调用失败: {str(api_error)}")
            return jsonify({
                "success": False,
                "error": f"AI服务调用失败: {str(api_error)}",
                "error_type": type(api_error).__name__
            }), 500
        
        if not response or not response.choices or len(response.choices) == 0:
            return jsonify({
                "success": False,
                "error": "AI服务返回空响应"
            }), 500
        
        result_text = response.choices[0].message.content
        
        # 调试：打印AI返回的原始内容（前500字符）
        print(f"AI返回原始内容（前500字符）: {result_text[:500] if result_text else 'None'}")
        
        # 解析JSON响应
        result_json = parse_json_response(result_text)
        
        if result_json is None:
            return jsonify({
                "success": False,
                "error": "AI返回格式不正确，无法解析JSON",
                "raw_response": result_text[:500] if result_text else "None"  # 只返回前500字符，避免响应过大
            }), 500
        
        # 验证返回的JSON结构
        required_fields = ["difficulty_level", "total_score", "dimension_scores"]
        for field in required_fields:
            if field not in result_json:
                return jsonify({
                    "success": False,
                    "error": f"返回JSON缺少必需字段: {field}",
                    "raw_response": result_text
                }), 500
        
        # 验证维度分值（放宽验证，只记录警告，不阻止返回）
        dimension_scores = result_json.get("dimension_scores", {})
        valid_dimension_values = [0.33, 0.66, 1.00]
        
        # 维度名称映射（支持中文和英文键名）
        dimension_name_mapping = {
            # 中文键名（标准）
            "推理与运算步数": "推理与运算步数",
            "知识点数量": "知识点数量",
            "抽象与构造要求": "抽象与构造要求",
            "题型常规性": "题型常规性",
            # 英文键名（兼容）
            "reasoning_steps": "推理与运算步数",
            "knowledge_points": "知识点数量",
            "abstraction_construction": "抽象与构造要求",
            "typicality": "题型常规性",
            # 其他可能的英文键名
            "reasoning": "推理与运算步数",
            "knowledge": "知识点数量",
            "abstraction": "抽象与构造要求",
            "conventionality": "题型常规性"
        }
        
        # 标准化维度分值（统一转换为中文键名）
        normalized_dimension_scores = {}
        for key, value in dimension_scores.items():
            # 查找对应的中文键名
            chinese_key = dimension_name_mapping.get(key, key)
            normalized_dimension_scores[chinese_key] = value
        
        # 更新 result_json 中的 dimension_scores 为标准格式
        result_json["dimension_scores"] = normalized_dimension_scores
        
        # 验证维度分值
        required_dimensions = ["推理与运算步数", "知识点数量", "抽象与构造要求", "题型常规性"]
        validation_errors = []
        
        for dim in required_dimensions:
            if dim not in normalized_dimension_scores:
                validation_errors.append(f"缺少维度评分: {dim}")
                continue
            
            dim_value = normalized_dimension_scores[dim]
            # 允许浮点数精度误差
            if not any(abs(dim_value - v) < 0.01 for v in valid_dimension_values):
                validation_errors.append(f"维度 {dim} 的值 {dim_value} 不在允许范围内（0.33、0.66、1.00）")
        
        # 验证 total_score 计算（四个维度相加后归一化）
        if len(normalized_dimension_scores) == 4:
            calculated_total = sum(normalized_dimension_scores.values()) / 4.0
            total_score = result_json.get("total_score", 0)
            
            # 允许小的浮点数误差（0.05，放宽验证）
            if abs(calculated_total - total_score) > 0.05:
                validation_errors.append(f"total_score ({total_score}) 与计算值 ({calculated_total:.2f}) 不一致")
        else:
            total_score = result_json.get("total_score", 0)
        
        # 验证 difficulty_level 映射（如果 total_score 有效）
        difficulty_level = result_json.get("difficulty_level", "")
        if total_score >= 0 and total_score <= 1:
            if total_score <= 0.25:
                expected_level = "筑基"
            elif total_score <= 0.5:
                expected_level = "提分"
            else:
                expected_level = "培优"
            
            if difficulty_level != expected_level:
                validation_errors.append(f"difficulty_level ({difficulty_level}) 与 total_score ({total_score}) 的映射不一致，应为 {expected_level}")
        
        # 如果有验证错误，记录但不阻止返回（放宽验证）
        if validation_errors:
            print(f"难度评分验证警告: {', '.join(validation_errors)}")
            # 仍然返回结果，但添加警告信息
            result_json["_validation_warnings"] = validation_errors
        
        return jsonify({
            "success": True,
            "data": result_json,
            "message": "评分成功"
        })
        
    except Exception as e:
        import traceback
        error_trace = traceback.format_exc()
        print(f"难度评分接口错误: {error_trace}")
        return jsonify({
            "success": False,
            "error": f"评分过程中出现错误: {str(e)}",
            "error_type": type(e).__name__
        }), 500

@app.route('/kp_management')
def kp_management():
    """知识点管理页面：从 knowledge_points_copy1 表读取数据，按层级结构展示"""
    try:
        conn = get_db_connection()
    except Exception as e:
        return render_db_error(e)
    cursor = conn.cursor(dictionary=True)
    
    # 获取所有知识点（只显示初中；注释掉小学和高中）
    cursor.execute("""
        SELECT 
            id, kp_code, name, subject, grade, parent_kp_code,
            prerequisite_kp_codes, dependent_kp_codes, related_kp_codes,
            stats, created_at, updated_at, skills, direct_score, related_score
        FROM knowledge_points_copy1
        WHERE grade = '初中'
        -- WHERE grade = '小学'  -- 小学（已注释）
        -- WHERE grade = '高中'  -- 高中（已注释）
        ORDER BY kp_code ASC
    """)
    all_kps = cursor.fetchall()
    
    # 处理JSON字段
    for kp in all_kps:
        for field in ['prerequisite_kp_codes', 'dependent_kp_codes', 'related_kp_codes', 'stats', 'skills', 'direct_score', 'related_score']:
            if kp[field] and isinstance(kp[field], str):
                try:
                    kp[field] = json.loads(kp[field])
                except:
                    kp[field] = None
    
    # 构建层级结构
    kp_dict = {kp['kp_code']: kp for kp in all_kps}
    root_kps = []
    
    # 为每个知识点添加children列表
    for kp in all_kps:
        kp['children'] = []
        kp['level'] = 0
    
    # 构建树形结构
    for kp in all_kps:
        if kp['parent_kp_code'] and kp['parent_kp_code'] in kp_dict:
            parent = kp_dict[kp['parent_kp_code']]
            parent['children'].append(kp)
            # 计算层级深度
            kp['level'] = parent['level'] + 1
        else:
            root_kps.append(kp)
    
    # 递归排序：按kp_code排序
    def sort_kp_tree(kp_list):
        kp_list.sort(key=lambda x: x['kp_code'])
        for kp in kp_list:
            if kp['children']:
                sort_kp_tree(kp['children'])
    
    sort_kp_tree(root_kps)
    
    # 扁平化列表（用于表格展示，保持层级顺序）
    # 默认显示：level 0 和 level 1，level 2 及以上默认隐藏
    def flatten_tree(kp_list, result=None, level=0):
        if result is None:
            result = []
        for kp in kp_list:
            kp['display_level'] = level
            kp['has_children'] = len(kp['children']) > 0
            # level 0 和 level 1 默认显示，level 2 及以上默认隐藏
            kp['default_visible'] = level <= 1
            result.append(kp)
            if kp['children']:
                flatten_tree(kp['children'], result, level + 1)
        return result
    
    knowledge_points = flatten_tree(root_kps)
    
    # 获取所有知识点代码和名称，用于下拉选择（只显示初中）
    cursor.execute("""
        SELECT kp_code, name 
        FROM knowledge_points_copy1 
        WHERE grade = '初中'
        -- WHERE grade = '小学'  -- 小学（已注释）
        -- WHERE grade = '高中'  -- 高中（已注释）
        ORDER BY kp_code ASC
    """)
    kp_options = cursor.fetchall()
    
    # 统计每个知识点关联的题目数量（只统计初中）
    # questions_tem 表使用 kp_code 字段，关联 knowledge_points_copy1 的 kp_code 字段
    cursor.execute("""
        SELECT 
            kp.id,
            kp.kp_code,
            COUNT(q.question_code) as question_count
        FROM knowledge_points_copy1 kp
        LEFT JOIN questions_tem q ON q.kp_code = kp.kp_code
        WHERE kp.grade = '初中'
        -- WHERE kp.grade = '小学'  -- 小学（已注释）
        -- WHERE kp.grade = '高中'  -- 高中（已注释）
        GROUP BY kp.id, kp.kp_code
    """)
    question_counts_raw = cursor.fetchall()
    question_counts = {row['kp_code']: row['question_count'] for row in question_counts_raw}
    
    # 递归计算父节点的题目数量（包括所有子节点的题目数量）
    def calculate_total_question_count(kp):
        # 先计算当前节点直接关联的题目数量
        direct_count = question_counts.get(kp['kp_code'], 0)
        
        # 递归计算所有子节点的题目数量总和
        children_count = 0
        if kp['children']:
            for child in kp['children']:
                children_count += calculate_total_question_count(child)
        
        # 父节点的题目数量 = 直接关联的题目 + 所有子节点的题目总和
        total_count = direct_count + children_count
        kp['question_count'] = total_count
        kp['direct_question_count'] = direct_count  # 保存直接关联的题目数量
        
        return total_count
    
    # 计算所有节点的题目数量（包括子节点）
    for kp in root_kps:
        calculate_total_question_count(kp)
    
    # 创建知识点代码到题目数量的映射（用于扁平列表）
    kp_code_to_question_count = {}
    def build_question_count_map(kp_list):
        for kp in kp_list:
            kp_code_to_question_count[kp['kp_code']] = kp.get('question_count', 0)
            if kp.get('children'):
                build_question_count_map(kp['children'])
    
    build_question_count_map(root_kps)
    
    # 更新扁平列表的题目数量
    for kp in knowledge_points:
        kp['question_count'] = kp_code_to_question_count.get(kp['kp_code'], 0)
    
    # 获取各学段的统计信息（只显示初中）
    cursor.execute("""
        SELECT 
            grade,
            COUNT(*) as count
        FROM knowledge_points_copy1
        WHERE grade = '初中'
        -- WHERE grade = '小学'  -- 小学（已注释）
        -- WHERE grade = '高中'  -- 高中（已注释）
        GROUP BY grade
    """)
    grade_stats_raw = cursor.fetchall()
    grade_stats = {row['grade']: row['count'] for row in grade_stats_raw}
    
    # 只显示初中（小学和高中已注释）
    grade_info = {
        # '小学': {'count': grade_stats.get('小学', 0), 'color': 'from-pink-500 to-rose-600', 'icon': 'ri-book-open-line', 'bg': 'bg-gradient-to-br from-pink-50 to-rose-50'},  # 已注释
        '初中': {'count': grade_stats.get('初中', 0), 'color': 'from-blue-500 to-indigo-600', 'icon': 'ri-graduation-cap-line', 'bg': 'bg-gradient-to-br from-blue-50 to-indigo-50'},
        # '高中': {'count': grade_stats.get('高中', 0), 'color': 'from-purple-500 to-violet-600', 'icon': 'ri-school-line', 'bg': 'bg-gradient-to-br from-purple-50 to-violet-50'}  # 已注释
    }
    
    conn.close()
    
    return render_template('kp_management.html', 
                         knowledge_points=knowledge_points,
                         kp_tree=root_kps,
                         kp_options=kp_options,
                         grade_info=grade_info)

@app.route('/textbook_management')
def textbook_management():
    """教材管理页面"""
    try:
        conn = get_db_connection()
    except Exception as e:
        return render_db_error(e)
    cursor = conn.cursor(dictionary=True)
    
    # 获取所有教材系列
    cursor.execute("SELECT * FROM textbook_series_copy1 ORDER BY sort_order ASC, id ASC")
    series_list = cursor.fetchall()
    
    # 获取所有教材
    cursor.execute("SELECT * FROM textbooks_copy1 ORDER BY series_id ASC, grade ASC, semester ASC")
    textbooks_list = cursor.fetchall()
    
    # 获取所有知识点（用于关联）
    cursor.execute("SELECT kp_code, name FROM knowledge_points_copy1 ORDER BY kp_code ASC")
    kp_options = cursor.fetchall()
    
    conn.close()
    
    return render_template('textbook_management.html',
                         series_list=series_list,
                         textbooks_list=textbooks_list,
                         kp_options=kp_options)

# ==================== 教材系列 API ====================
@app.route('/api/textbook/series/create', methods=['POST'])
def api_textbook_series_create():
    """创建教材系列"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        data = request.get_json()
        name = (data.get('name') or '').strip()
        slug = (data.get('slug') or '').strip() or None
        publisher = (data.get('publisher') or '').strip() or None
        region = (data.get('region') or '').strip() or None
        is_active = 1 if data.get('is_active') else 0
        
        if not name:
            return jsonify({'success': False, 'error': '系列名称不能为空'})
        
        cursor.execute("""
            INSERT INTO textbook_series_copy1 
            (name, slug, publisher, region, is_active, created_at, updated_at)
            VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
        """, (name, slug, publisher, region, is_active))
        
        conn.commit()
        new_id = cursor.lastrowid
        conn.close()
        return jsonify({'success': True, 'message': '创建成功', 'id': new_id})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/series/update/<int:series_id>', methods=['POST'])
def api_textbook_series_update(series_id):
    """更新教材系列"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        data = request.get_json()
        name = (data.get('name') or '').strip()
        slug = (data.get('slug') or '').strip() or None
        publisher = (data.get('publisher') or '').strip() or None
        region = (data.get('region') or '').strip() or None
        is_active = 1 if data.get('is_active') else 0
        
        if not name:
            return jsonify({'success': False, 'error': '系列名称不能为空'})
        
        cursor.execute("""
            UPDATE textbook_series_copy1 
            SET name = %s, slug = %s, publisher = %s, region = %s, is_active = %s, updated_at = NOW()
            WHERE id = %s
        """, (name, slug, publisher, region, is_active, series_id))
        
        conn.commit()
        conn.close()
        return jsonify({'success': True, 'message': '更新成功'})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/series/delete/<int:series_id>', methods=['POST'])
def api_textbook_series_delete(series_id):
    """删除教材系列"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        # 检查是否有关联的教材
        cursor.execute("SELECT COUNT(*) as count FROM textbooks_copy1 WHERE series_id = %s", (series_id,))
        result = cursor.fetchone()
        if result['count'] > 0:
            return jsonify({'success': False, 'error': '该系列下存在教材，无法删除'})
        
        cursor.execute("DELETE FROM textbook_series_copy1 WHERE id = %s", (series_id,))
        conn.commit()
        conn.close()
        return jsonify({'success': True, 'message': '删除成功'})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/series/get/<int:series_id>', methods=['GET'])
def api_textbook_series_get(series_id):
    """获取教材系列详情"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        cursor.execute("SELECT * FROM textbook_series_copy1 WHERE id = %s", (series_id,))
        series = cursor.fetchone()
        conn.close()
        
        if not series:
            return jsonify({'success': False, 'error': '系列不存在'})
        
        return jsonify({'success': True, 'data': series})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/series/toggle_active/<int:series_id>', methods=['POST'])
def api_textbook_series_toggle_active(series_id):
    """切换教材系列激活状态"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        # 获取当前状态
        cursor.execute("SELECT is_active FROM textbook_series_copy1 WHERE id = %s", (series_id,))
        series = cursor.fetchone()
        
        if not series:
            return jsonify({'success': False, 'error': '系列不存在'})
        
        # 切换状态：1变0，0变1
        new_status = 1 if series['is_active'] == 0 else 0
        
        cursor.execute("""
            UPDATE textbook_series_copy1 
            SET is_active = %s, updated_at = NOW()
            WHERE id = %s
        """, (new_status, series_id))
        
        conn.commit()
        conn.close()
        return jsonify({
            'success': True, 
            'message': '状态已更新',
            'is_active': new_status
        })
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

# ==================== 教材 API ====================
@app.route('/api/textbook/create', methods=['POST'])
def api_textbook_create():
    """创建教材"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        data = request.get_json()
        series_id = data.get('series_id')
        official_title = (data.get('official_title') or '').strip()
        stage = (data.get('stage') or '').strip() or None
        grade = (data.get('grade') or '').strip() or None
        semester = data.get('semester')
        
        if not series_id:
            return jsonify({'success': False, 'error': '教材系列不能为空'})
        if not official_title:
            return jsonify({'success': False, 'error': '教材名称不能为空'})
        
        cursor.execute("""
            INSERT INTO textbooks_copy1 
            (series_id, official_title, stage, grade, semester, created_at, updated_at)
            VALUES (%s, %s, %s, %s, %s, NOW(), NOW())
        """, (series_id, official_title, stage, grade, semester))
        
        conn.commit()
        new_id = cursor.lastrowid
        conn.close()
        return jsonify({'success': True, 'message': '创建成功', 'id': new_id})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/update/<int:textbook_id>', methods=['POST'])
def api_textbook_update(textbook_id):
    """更新教材"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        data = request.get_json()
        official_title = (data.get('official_title') or '').strip()
        stage = (data.get('stage') or '').strip() or None
        grade = (data.get('grade') or '').strip() or None
        semester = data.get('semester')
        
        if not official_title:
            return jsonify({'success': False, 'error': '教材名称不能为空'})
        
        cursor.execute("""
            UPDATE textbooks_copy1 
            SET official_title = %s, stage = %s, grade = %s, semester = %s, updated_at = NOW()
            WHERE id = %s
        """, (official_title, stage, grade, semester, textbook_id))
        
        conn.commit()
        conn.close()
        return jsonify({'success': True, 'message': '更新成功'})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/delete/<int:textbook_id>', methods=['POST'])
def api_textbook_delete(textbook_id):
    """删除教材"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        # 检查是否有目录节点
        cursor.execute("SELECT COUNT(*) as count FROM textbook_catalog_nodes_copy1 WHERE textbook_id = %s", (textbook_id,))
        result = cursor.fetchone()
        if result['count'] > 0:
            return jsonify({'success': False, 'error': '该教材下存在目录节点，无法删除'})
        
        cursor.execute("DELETE FROM textbooks_copy1 WHERE id = %s", (textbook_id,))
        conn.commit()
        conn.close()
        return jsonify({'success': True, 'message': '删除成功'})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/get/<int:textbook_id>', methods=['GET'])
def api_textbook_get(textbook_id):
    """获取教材详情"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        cursor.execute("SELECT * FROM textbooks_copy1 WHERE id = %s", (textbook_id,))
        textbook = cursor.fetchone()
        conn.close()
        
        if not textbook:
            return jsonify({'success': False, 'error': '教材不存在'})
        
        return jsonify({'success': True, 'data': textbook})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/list/<int:series_id>', methods=['GET'])
def api_textbook_list(series_id):
    """获取指定系列下的所有教材"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        cursor.execute("SELECT * FROM textbooks_copy1 WHERE series_id = %s ORDER BY grade ASC, semester ASC", (series_id,))
        textbooks = cursor.fetchall()
        conn.close()
        
        return jsonify({'success': True, 'data': textbooks})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})

# ==================== 目录节点 API ====================
@app.route('/api/textbook/catalog/create', methods=['POST'])
def api_textbook_catalog_create():
    """创建目录节点"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        data = request.get_json()
        textbook_id = data.get('textbook_id')
        parent_id = data.get('parent_id')  # 可以为None（顶级节点）
        node_type = (data.get('node_type') or 'chapter').strip()
        title = (data.get('title') or '').strip()
        display_no = (data.get('display_no') or '').strip() or None
        
        if not textbook_id:
            return jsonify({'success': False, 'error': '教材ID不能为空'})
        if not title:
            return jsonify({'success': False, 'error': '节点标题不能为空'})
        
        # 验证节点类型层级关系
        if parent_id:
            # 有父节点，需要验证节点类型
            cursor.execute("SELECT node_type FROM textbook_catalog_nodes_copy1 WHERE id = %s", (parent_id,))
            parent = cursor.fetchone()
            if not parent:
                return jsonify({'success': False, 'error': '父节点不存在'})
            
            parent_node_type = parent['node_type']
            # 章节下只能创建 section
            if parent_node_type == 'chapter' and node_type != 'section':
                return jsonify({'success': False, 'error': '章节下只能创建小节（section）'})
            # section 下只能创建 subsection
            elif parent_node_type == 'section' and node_type != 'subsection':
                return jsonify({'success': False, 'error': '小节下只能创建子小节（subsection）'})
            # subsection 下不能再创建子节点
            elif parent_node_type == 'subsection':
                return jsonify({'success': False, 'error': '子小节下不能再创建子节点'})
        else:
            # 没有父节点，只能创建顶级节点（chapter）
            if node_type != 'chapter':
                return jsonify({'success': False, 'error': '顶级节点只能是章节（chapter）'})
        
        # 计算depth
        depth = 1
        if parent_id:
            cursor.execute("SELECT depth FROM textbook_catalog_nodes_copy1 WHERE id = %s", (parent_id,))
            parent_depth_result = cursor.fetchone()
            if parent_depth_result:
                depth = parent_depth_result['depth'] + 1
        
        # 查询最大id并手动递增（因为id字段可能不是auto_increment）
        cursor.execute("SELECT MAX(id) as max_id FROM textbook_catalog_nodes_copy1")
        max_id_result = cursor.fetchone()
        new_id = (max_id_result['max_id'] or 0) + 1
        
        cursor.execute("""
            INSERT INTO textbook_catalog_nodes_copy1 
            (id, textbook_id, parent_id, node_type, title, display_no, depth, sort_order, created_at, updated_at)
            VALUES (%s, %s, %s, %s, %s, %s, %s, 0, NOW(), NOW())
        """, (new_id, textbook_id, parent_id, node_type, title, display_no, depth))
        
        conn.commit()
        conn.close()
        return jsonify({'success': True, 'message': '创建成功', 'id': new_id})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/catalog/update/<int:node_id>', methods=['POST'])
def api_textbook_catalog_update(node_id):
    """更新目录节点"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        data = request.get_json()
        title = (data.get('title') or '').strip()
        display_no = (data.get('display_no') or '').strip() or None
        node_type = (data.get('node_type') or '').strip() or None
        
        if not title:
            return jsonify({'success': False, 'error': '节点标题不能为空'})
        
        # 如果更新了节点类型，需要验证是否符合层级关系
        if node_type:
            # 获取当前节点的父节点信息
            cursor.execute("SELECT parent_id FROM textbook_catalog_nodes_copy1 WHERE id = %s", (node_id,))
            current_node = cursor.fetchone()
            if current_node and current_node.get('parent_id'):
                parent_id = current_node['parent_id']
                cursor.execute("SELECT node_type FROM textbook_catalog_nodes_copy1 WHERE id = %s", (parent_id,))
                parent = cursor.fetchone()
                if parent:
                    parent_node_type = parent['node_type']
                    # 验证节点类型层级关系
                    if parent_node_type == 'chapter' and node_type != 'section':
                        return jsonify({'success': False, 'error': '章节下只能创建小节（section）'})
                    elif parent_node_type == 'section' and node_type != 'subsection':
                        return jsonify({'success': False, 'error': '小节下只能创建子小节（subsection）'})
                    elif parent_node_type == 'subsection':
                        return jsonify({'success': False, 'error': '子小节下不能再创建子节点'})
            else:
                # 没有父节点，只能是顶级节点（chapter）
                if node_type != 'chapter':
                    return jsonify({'success': False, 'error': '顶级节点只能是章节（chapter）'})
        
        cursor.execute("""
            UPDATE textbook_catalog_nodes_copy1 
            SET title = %s, display_no = %s, node_type = COALESCE(%s, node_type), updated_at = NOW()
            WHERE id = %s
        """, (title, display_no, node_type, node_id))
        
        conn.commit()
        conn.close()
        return jsonify({'success': True, 'message': '更新成功'})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/catalog/delete/<int:node_id>', methods=['POST'])
def api_textbook_catalog_delete(node_id):
    """删除目录节点"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        # 检查是否有子节点
        cursor.execute("SELECT COUNT(*) as count FROM textbook_catalog_nodes_copy1 WHERE parent_id = %s", (node_id,))
        result = cursor.fetchone()
        if result['count'] > 0:
            return jsonify({'success': False, 'error': '该节点下存在子节点，无法删除'})
        
        # 检查是否有关联的知识点
        cursor.execute("SELECT COUNT(*) as count FROM textbook_chapter_knowledge_relation_copy1 WHERE catalog_chapter_id = %s AND is_deleted = 0", (node_id,))
        result = cursor.fetchone()
        if result['count'] > 0:
            return jsonify({'success': False, 'error': '该节点下存在关联的知识点，无法删除'})
        
        cursor.execute("DELETE FROM textbook_catalog_nodes_copy1 WHERE id = %s", (node_id,))
        conn.commit()
        conn.close()
        return jsonify({'success': True, 'message': '删除成功'})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/catalog/get/<int:node_id>', methods=['GET'])
def api_textbook_catalog_get(node_id):
    """获取目录节点详情"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        cursor.execute("SELECT * FROM textbook_catalog_nodes_copy1 WHERE id = %s", (node_id,))
        node = cursor.fetchone()
        conn.close()
        
        if not node:
            return jsonify({'success': False, 'error': '节点不存在'})
        
        return jsonify({'success': True, 'data': node})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/catalog/tree/<int:textbook_id>', methods=['GET'])
def api_textbook_catalog_tree(textbook_id):
    """获取教材的目录树"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        # 获取所有节点
        cursor.execute("""
            SELECT * FROM textbook_catalog_nodes_copy1 
            WHERE textbook_id = %s 
            ORDER BY depth ASC, sort_order ASC, id ASC
        """, (textbook_id,))
        all_nodes = cursor.fetchall()
        
        # 构建树形结构
        node_dict = {node['id']: node for node in all_nodes}
        root_nodes = []
        
        for node in all_nodes:
            node['children'] = []
            if node['parent_id'] and node['parent_id'] in node_dict:
                parent = node_dict[node['parent_id']]
                parent['children'].append(node)
            else:
                root_nodes.append(node)
        
        conn.close()
        return jsonify({'success': True, 'data': root_nodes})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})

# ==================== 章节-知识点关联 API ====================
@app.route('/api/textbook/relation/create', methods=['POST'])
def api_textbook_relation_create():
    """创建章节-知识点关联"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        data = request.get_json()
        catalog_chapter_id = data.get('catalog_chapter_id')
        kp_code = (data.get('kp_code') or '').strip()
        
        if not catalog_chapter_id:
            return jsonify({'success': False, 'error': '章节ID不能为空'})
        if not kp_code:
            return jsonify({'success': False, 'error': '知识点代码不能为空'})
        
        # 检查是否已存在
        cursor.execute("""
            SELECT id FROM textbook_chapter_knowledge_relation_copy1 
            WHERE catalog_chapter_id = %s AND kp_code = %s AND is_deleted = 0
        """, (catalog_chapter_id, kp_code))
        if cursor.fetchone():
            return jsonify({'success': False, 'error': '该关联已存在'})
        
        cursor.execute("""
            INSERT INTO textbook_chapter_knowledge_relation_copy1 
            (catalog_chapter_id, kp_code, is_deleted, gmt_create, gmt_modified)
            VALUES (%s, %s, 0, NOW(), NOW())
        """, (catalog_chapter_id, kp_code))
        
        conn.commit()
        new_id = cursor.lastrowid
        conn.close()
        return jsonify({'success': True, 'message': '创建成功', 'id': new_id})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/relation/delete/<int:relation_id>', methods=['POST'])
def api_textbook_relation_delete(relation_id):
    """删除章节-知识点关联（软删除）"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        cursor.execute("""
            UPDATE textbook_chapter_knowledge_relation_copy1 
            SET is_deleted = 1, gmt_modified = NOW()
            WHERE id = %s
        """, (relation_id,))
        
        conn.commit()
        conn.close()
        return jsonify({'success': True, 'message': '删除成功'})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/textbook/relation/list/<int:catalog_chapter_id>', methods=['GET'])
def api_textbook_relation_list(catalog_chapter_id):
    """获取章节关联的知识点列表"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        cursor.execute("""
            SELECT tckr.*, kp.name as kp_name
            FROM textbook_chapter_knowledge_relation_copy1 tckr
            LEFT JOIN knowledge_points_copy1 kp ON kp.kp_code = tckr.kp_code
            WHERE tckr.catalog_chapter_id = %s AND tckr.is_deleted = 0
            ORDER BY tckr.id ASC
        """, (catalog_chapter_id,))
        
        relations = cursor.fetchall()
        conn.close()
        return jsonify({'success': True, 'data': relations})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})

@app.route('/material_management')
def material_management():
    """资料管理页面"""
    return render_template('material_management.html')

@app.route('/add_question')
def add_question_page():
    """显示录入新题目页面"""
    # 从URL参数获取层级信息
    kp_code = request.args.get('kp_code')
    chapter_id = request.args.get('chapter')
    chapter_label = request.args.get('chapter_label', '')
    section_id = request.args.get('section')
    section_label = request.args.get('section_label', '')
    subsection_id = request.args.get('subsection')
    subsection_label = request.args.get('subsection_label', '')
    
    # 解码URL编码的标签
    if chapter_label:
        chapter_label = urllib.parse.unquote(chapter_label)
    if section_label:
        section_label = urllib.parse.unquote(section_label)
    if subsection_label:
        subsection_label = urllib.parse.unquote(subsection_label)
    
    return render_template('add_question.html',
                          kp_code=kp_code,
                          chapter_id=chapter_id,
                          chapter_label=chapter_label,
                          section_id=section_id,
                          section_label=section_label,
                          subsection_id=subsection_id,
                          subsection_label=subsection_label)

@app.route('/edit/<question_code>')
def edit_page(question_code):
    try:
        conn = get_db_connection()
    except Exception as e:
        return render_db_error(e)
    cursor = conn.cursor(dictionary=True)
    # 使用别名 kp_id AS kp_code 以兼容模板代码
    cursor.execute("SELECT *, kp_id AS kp_code FROM questions_tem WHERE question_code = %s", (question_code,))
    question = cursor.fetchone()
    conn.close()
    return render_template('edit.html', q=question)

@app.route('/update_question', methods=['POST'])
def update_question():
    data = request.json
    q_code = data.get('question_code')
    
    if not q_code:
        return jsonify({'success': False, 'error': '缺少 question_code'}), 400
    
    try:
        conn = get_db_connection()
        cursor = conn.cursor()
        
        # 只更新用户实际提供的字段（如果字段在 data 中存在，就更新）
        updates = []
        params = []
        
        if 'stem' in data:
            updates.append("stem=%s")
            params.append(data.get('stem', ''))
        
        if 'options' in data:
            # 如果 options 是空字符串，保存为 NULL
            # 如果 options 是无效 JSON（如 "Invalid value."），也保存为 NULL，避免数据库报错
            options_value = data.get('options')
            if options_value == '' or (isinstance(options_value, str) and options_value.strip().lower() in ['invalid value.', 'null', 'none']):
                options_value = None
            updates.append("options=%s")
            params.append(options_value)
        
        if 'answer' in data:
            updates.append("answer=%s")
            params.append(data.get('answer', ''))
        
        if 'solution' in data:
            updates.append("solution=%s")
            params.append(data.get('solution', ''))
        
        if 'question_type' in data:
            updates.append("question_type=%s")
            params.append(data.get('question_type', ''))
        
        if 'kp_code' in data:
            # 前端参数名是 kp_code，但数据库字段是 kp_id
            updates.append("kp_id=%s")
            params.append(data.get('kp_code'))
        
        if 'difficulty' in data:
            # 处理 difficulty 字段：转换为浮点数，保留两位小数
            difficulty_value = data.get('difficulty')
            if difficulty_value is not None and difficulty_value != '':
                try:
                    difficulty_value = round(float(difficulty_value), 2)
                    updates.append("difficulty=%s")
                    params.append(difficulty_value)
                except (ValueError, TypeError):
                    pass  # 如果转换失败，跳过该字段
        
        if not updates:
            conn.close()
            return jsonify({'success': False, 'error': '没有提供要更新的字段'}), 400
        
        # 添加 WHERE 条件
        params.append(q_code)
        query = f"UPDATE questions_tem SET {', '.join(updates)} WHERE question_code=%s"
        cursor.execute(query, tuple(params))
        conn.commit()
        conn.close()
        return jsonify({'success': True})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})

@app.route('/create_question', methods=['POST'])
def create_question():
    """创建新题目"""
    data = request.json
    
    try:
        conn = get_db_connection()
        cursor = conn.cursor()
        
        # 自动生成唯一的 question_code
        # 格式：Q + 年月日时分秒 + 3位随机数，例如：Q20250101120045123
        max_attempts = 10  # 最多尝试10次生成唯一编号
        q_code = None
        
        for _ in range(max_attempts):
            timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
            random_suffix = random.randint(100, 999)  # 3位随机数
            q_code = f"Q{timestamp}{random_suffix}"
            
            # 检查是否已存在
            cursor.execute("SELECT question_code FROM questions_tem WHERE question_code = %s", (q_code,))
            if not cursor.fetchone():
                break  # 找到唯一编号
            q_code = None
        
        if not q_code:
            conn.close()
            return jsonify({'success': False, 'error': '无法生成唯一的题号，请稍后重试'}), 500
        
        # 构建插入字段和值
        fields = []
        values = []
        placeholders = []
        
        # 必填字段：question_code（自动生成）
        fields.append('question_code')
        values.append(q_code)
        placeholders.append('%s')
        
        # 可选字段（前端参数 kp_code 保存到数据库的 kp_code 字段）
        optional_fields = {
            'stem': 'stem',
            'options': 'options',
            'answer': 'answer',
            'solution': 'solution',
            'question_type': 'question_type',
            'kp_code': 'kp_code',  # 前端参数名 kp_code，保存到数据库的 kp_code 字段（与查询保持一致）
            'textbook_catalog_nodes_id': 'textbook_catalog_nodes_id',
            'chapter': 'title_1',  # chapter 映射到 title_1
            'section': 'title_2',  # section 映射到 title_2
            'subsection': 'title_3',  # subsection 映射到 title_3
            'difficulty': 'difficulty',  # 难度字段
            'create_by': 'create_by',  # 创建者字段
            'grade': 'grade',  # 年级字段：1=小学，2=初中，3=高中
        }
        
        for key, field_name in optional_fields.items():
            if key in data and data[key] is not None:
                value = data[key]
                # 处理 options：如果是空字符串，设为 None
                if key == 'options' and (value == '' or (isinstance(value, str) and value.strip().lower() in ['null', 'none'])):
                    value = None
                # 处理 textbook_catalog_nodes_id：转换为整数
                elif key == 'textbook_catalog_nodes_id' and value:
                    try:
                        value = int(value)
                    except (ValueError, TypeError):
                        value = None
                # 处理 difficulty：转换为浮点数，保留两位小数
                elif key == 'difficulty' and value:
                    try:
                        value = round(float(value), 2)
                    except (ValueError, TypeError):
                        value = None
                # 处理 grade：转换为整数（1=小学，2=初中，3=高中）
                elif key == 'grade' and value:
                    try:
                        value = int(value)
                        # 验证年级值是否在有效范围内
                        if value not in [1, 2, 3]:
                            value = None
                    except (ValueError, TypeError):
                        value = None
                
                if value is not None and value != '':
                    fields.append(field_name)
                    values.append(value)
                    placeholders.append('%s')
        
        # 验证：所有题目都必须有年级字段
        has_grade = 'grade' in fields
        if not has_grade:
            conn.close()
            return jsonify({'success': False, 'error': '所有题目都必须选择年级'}), 400
        
        # 执行插入
        if len(fields) == 1:  # 只有 question_code
            conn.close()
            return jsonify({'success': False, 'error': '至少需要填写题号以外的其他字段'}), 400
        
        query = f"INSERT INTO questions_tem ({', '.join(fields)}) VALUES ({', '.join(placeholders)})"
        cursor.execute(query, tuple(values))
        # 获取插入的题目ID
        question_id = cursor.lastrowid
        conn.commit()
        conn.close()
        
        return jsonify({'success': True, 'question_code': q_code, 'question_id': question_id})
    except Exception as e:
        try:
            conn.rollback()
        except:
            pass
        try:
            conn.close()
        except:
            pass
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/question_by_id/<int:question_id>')
def api_question_by_id(question_id):
    """根据题目ID返回题目详情（用于查重比对）"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        # 查询题目详情
        cursor.execute("""
            SELECT 
                id,
                question_code, 
                stem, 
                kp_code,
                kp_id,
                audit_reason,
                audit_status,
                difficulty,
                question_type,
                answer,
                solution,
                options
            FROM questions_tem 
            WHERE id = %s
        """, (question_id,))
        
        question = cursor.fetchone()
        conn.close()
        
        if not question:
            return jsonify({'success': False, 'error': '题目不存在'}), 404
        
        # 处理选项JSON
        if question.get('options'):
            try:
                opt_data = question['options']
                if isinstance(opt_data, str):
                    opt_data = opt_data.replace("'", '"')
                    question['options'] = json.loads(opt_data)
            except:
                question['options'] = {}
        
        return jsonify({'success': True, 'question': question})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)}), 500

@app.route('/api/check_duplicate', methods=['POST', 'OPTIONS'])
def api_check_duplicate():
    """代理查重检测接口，解决CORS问题"""
    if request.method == 'OPTIONS':
        # 处理CORS预检请求
        response = jsonify({})
        response.headers.add('Access-Control-Allow-Origin', '*')
        response.headers.add('Access-Control-Allow-Methods', 'POST, OPTIONS')
        response.headers.add('Access-Control-Allow-Headers', 'Content-Type')
        return response
    
    try:
        # 获取请求数据
        data = request.get_json()
        
        # 转发请求到查重服务
        import urllib.request
        import urllib.parse
        
        url = 'http://47.77.199.85:8888/api/check_duplicate'
        req_data = json.dumps(data).encode('utf-8')
        
        req = urllib.request.Request(
            url,
            data=req_data,
            headers={
                'Content-Type': 'application/json',
                'Cookie': request.headers.get('Cookie', 'MATH-LOGIN-AUTH={{authToken}}'),
                'MATH-DEBUG': request.headers.get('MATH-DEBUG', '1'),
                'MATH-UID': request.headers.get('MATH-UID', '12')
            }
        )
        
        with urllib.request.urlopen(req, timeout=30) as response:
            result = json.loads(response.read().decode('utf-8'))
            
            # 返回结果，添加CORS头
            flask_response = jsonify(result)
            flask_response.headers.add('Access-Control-Allow-Origin', '*')
            return flask_response
            
    except urllib.error.HTTPError as e:
        error_body = e.read().decode('utf-8') if e.fp else 'Unknown error'
        return jsonify({'code': -1, 'error': f'查重服务错误: {e.code} - {error_body}'}), e.code
    except Exception as e:
        return jsonify({'code': -1, 'error': f'查重检测失败: {str(e)}'}), 500

@app.route('/api/confirm_repeat', methods=['POST', 'OPTIONS'])
def api_confirm_repeat():
    """代理确认查重结果接口，解决CORS问题"""
    if request.method == 'OPTIONS':
        # 处理CORS预检请求
        response = jsonify({})
        response.headers.add('Access-Control-Allow-Origin', '*')
        response.headers.add('Access-Control-Allow-Methods', 'POST, OPTIONS')
        response.headers.add('Access-Control-Allow-Headers', 'Content-Type')
        return response
    
    try:
        # 获取请求数据
        data = request.get_json()
        
        # 转发请求到查重服务
        import urllib.request
        
        url = 'http://47.77.199.85:8888/api/confirm_repeat'
        req_data = json.dumps(data).encode('utf-8')
        
        req = urllib.request.Request(
            url,
            data=req_data,
            headers={
                'Content-Type': 'application/json',
                'Cookie': request.headers.get('Cookie', 'MATH-LOGIN-AUTH={{authToken}}'),
                'MATH-DEBUG': request.headers.get('MATH-DEBUG', '1'),
                'MATH-UID': request.headers.get('MATH-UID', '12')
            }
        )
        
        with urllib.request.urlopen(req, timeout=30) as response:
            result = json.loads(response.read().decode('utf-8'))
            
            # 返回结果，添加CORS头
            flask_response = jsonify(result)
            flask_response.headers.add('Access-Control-Allow-Origin', '*')
            return flask_response
            
    except urllib.error.HTTPError as e:
        error_body = e.read().decode('utf-8') if e.fp else 'Unknown error'
        return jsonify({'code': -1, 'error': f'查重服务错误: {e.code} - {error_body}'}), e.code
    except Exception as e:
        return jsonify({'code': -1, 'error': f'确认查重结果失败: {str(e)}'}), 500

@app.route('/api/delete_question/<question_code>', methods=['POST'])
def delete_question(question_code):
    """
    删除题目（需要确认），返回下一题的 question_code（用于跳转）
    """
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        # 先获取题目信息（包括知识点和教材节点，使用 kp_id 字段）
        cursor.execute("SELECT question_code, kp_id, textbook_catalog_nodes_id FROM questions_tem WHERE question_code = %s", (question_code,))
        row = cursor.fetchone()
        if not row:
            conn.close()
            return jsonify({'success': False, 'error': '题目不存在'}), 404
        
        kp_code = row.get('kp_id')  # 数据库字段已改为 kp_id
        textbook_node_id = row.get('textbook_catalog_nodes_id')
        
        # 获取同分类下的所有题目（按 question_code 降序排列）
        all_codes = []
        if kp_code:
            # 有知识点：在同知识点内查找
            cursor.execute("SELECT question_code FROM questions_tem WHERE kp_id = %s ORDER BY question_code DESC", (kp_code,))
            all_codes = [r['question_code'] for r in cursor.fetchall()]
        elif textbook_node_id:
            # 有教材节点：在同教材节点内查找
            cursor.execute(
                "SELECT question_code FROM questions_tem WHERE textbook_catalog_nodes_id = %s ORDER BY question_code DESC",
                (textbook_node_id,)
            )
            all_codes = [r['question_code'] for r in cursor.fetchall()]
        
        # 找到当前题目在列表中的位置
        try:
            curr_idx = all_codes.index(question_code)
        except ValueError:
            curr_idx = -1
        
        # 删除题目
        cursor.execute("DELETE FROM questions_tem WHERE question_code = %s", (question_code,))
        conn.commit()
        
        # 确定跳转目标：优先下一题，没有则上一题，都没有则返回 None（前端跳转列表）
        next_code = None
        if curr_idx >= 0 and curr_idx < len(all_codes) - 1:
            # 有下一题
            next_code = all_codes[curr_idx + 1]
        elif curr_idx > 0:
            # 没有下一题，但有上一题
            next_code = all_codes[curr_idx - 1]
        
        conn.close()
        return jsonify({
            'success': True, 
            'message': '题目已删除',
            'next_code': next_code,
            'kp_code': kp_code,
            'node_id': str(textbook_node_id) if textbook_node_id else None
        })
    except Exception as e:
        try:
            conn.rollback()
        except Exception:
            pass
        return jsonify({'success': False, 'error': str(e)}), 500
    finally:
        try:
            conn.close()
        except Exception:
            pass

@app.route('/add_kp_node', methods=['POST'])
def add_kp_node():
    """添加新的知识点节点（section 或 subsection）"""
    try:
        data = request.json
        chapter_id = data.get('chapter_id')
        node_type = data.get('type')  # 'section' 或 'subsection'
        section_id = data.get('section_id')  # 仅当添加 subsection 时需要
        name = data.get('name', '').strip()
        
        if not chapter_id or not node_type or not name:
            return jsonify({'success': False, 'error': '缺少必要参数'}), 400
        
        if node_type not in ['section', 'subsection']:
            return jsonify({'success': False, 'error': '无效的节点类型'}), 400
        
        if node_type == 'subsection' and not section_id:
            return jsonify({'success': False, 'error': '添加小节时必须提供 section_id'}), 400
        
        # 获取 tree_new.json 文件路径
        tree_file = get_tree_file_path()
        if not os.path.exists(tree_file):
            return jsonify({'success': False, 'error': '找不到知识点结构文件'}), 500
        
        # 读取 JSON 文件
        with open(tree_file, 'r', encoding='utf-8') as f:
            tree_data = json.load(f)
        
        # 查找最大 ID
        max_id = find_max_id(tree_data)
        new_id = max_id + 1
        
        # 查找目标节点（chapter 或 section）
        target_node = None
        if node_type == 'section':
            # 查找 chapter
            target_node = find_node_by_id(tree_data, chapter_id)
        else:
            # 查找 section
            target_node = find_node_by_id(tree_data, section_id)
        
        if not target_node:
            return jsonify({'success': False, 'error': '找不到目标节点'}), 404
        
        # 确定 parent_id
        parent_id = chapter_id if node_type == 'section' else section_id
        
        # 创建新节点
        new_node = {
            "id": new_id,
            "label": name,
            "parent_id": parent_id,
            "kp_level": node_type,
            "skills": [],
            "direct_score": [],
            "related_score": [],
            "children": []
        }
        
        # 确保 target_node 有 children 数组
        if 'children' not in target_node:
            target_node['children'] = []
        
        # 添加新节点
        target_node['children'].append(new_node)
        
        # 保存回文件
        with open(tree_file, 'w', encoding='utf-8') as f:
            json.dump(tree_data, f, ensure_ascii=False, indent=2)
        
        # 重新加载结构（更新全局变量）
        global KP_MAP, KP_HIERARCHY
        KP_MAP, KP_HIERARCHY = load_kp_structure()
        
        return jsonify({
            'success': True,
            'message': '添加成功',
            'new_id': new_id
        })
        
    except Exception as e:
        import traceback
        traceback.print_exc()
        return jsonify({'success': False, 'error': str(e)}), 500

# 知识点管理 API（knowledge_points_copy1 表）
@app.route('/api/kp/create', methods=['POST'])
def api_kp_create():
    """创建知识点"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        data = request.get_json()
        kp_code = (data.get('kp_code') or '').strip()
        name = (data.get('name') or '').strip()
        subject = (data.get('subject') or '').strip() or None
        grade = (data.get('grade') or '').strip() or None
        parent_kp_code = (data.get('parent_kp_code') or '').strip() or None
        
        if not kp_code or not name:
            return jsonify({'success': False, 'error': '知识点代码和名称不能为空'})
        
        # 检查kp_code是否已存在
        cursor.execute("SELECT id FROM knowledge_points_copy1 WHERE kp_code = %s", (kp_code,))
        if cursor.fetchone():
            return jsonify({'success': False, 'error': '知识点代码已存在'})
        
        # 查询最大id并手动递增（确保id正确自增）
        cursor.execute("SELECT MAX(id) as max_id FROM knowledge_points_copy1")
        max_id_result = cursor.fetchone()
        new_id = (max_id_result['max_id'] or 0) + 1
        
        # 插入新知识点
        cursor.execute("""
            INSERT INTO knowledge_points_copy1 
            (id, kp_code, name, subject, grade, parent_kp_code, created_at, updated_at)
            VALUES (%s, %s, %s, %s, %s, %s, NOW(), NOW())
        """, (new_id, kp_code, name, subject, grade, parent_kp_code))
        
        conn.commit()
        conn.close()
        return jsonify({'success': True, 'message': '创建成功'})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/kp/update/<int:kp_id>', methods=['POST'])
def api_kp_update(kp_id):
    """更新知识点"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        data = request.get_json()
        name = (data.get('name') or '').strip()
        subject = (data.get('subject') or '').strip() or None
        grade = (data.get('grade') or '').strip() or None
        parent_kp_code = (data.get('parent_kp_code') or '').strip() or None
        
        if not name:
            return jsonify({'success': False, 'error': '知识点名称不能为空'})
        
        # 更新知识点
        cursor.execute("""
            UPDATE knowledge_points_copy1 
            SET name = %s, subject = %s, grade = %s, parent_kp_code = %s, updated_at = NOW()
            WHERE id = %s
        """, (name, subject, grade, parent_kp_code, kp_id))
        
        conn.commit()
        conn.close()
        return jsonify({'success': True, 'message': '更新成功'})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/kp/delete/<int:kp_id>', methods=['POST'])
def api_kp_delete(kp_id):
    """删除知识点"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        # 检查是否有子知识点
        cursor.execute("SELECT id FROM knowledge_points_copy1 WHERE parent_kp_code = (SELECT kp_code FROM knowledge_points_copy1 WHERE id = %s)", (kp_id,))
        if cursor.fetchone():
            return jsonify({'success': False, 'error': '该知识点下存在子知识点，无法删除'})
        
        # 删除知识点
        cursor.execute("DELETE FROM knowledge_points_copy1 WHERE id = %s", (kp_id,))
        
        conn.commit()
        conn.close()
        return jsonify({'success': True, 'message': '删除成功'})
    except Exception as e:
        if conn:
            conn.rollback()
            conn.close()
        return jsonify({'success': False, 'error': str(e)})

@app.route('/api/kp/get/<int:kp_id>', methods=['GET'])
def api_kp_get(kp_id):
    """获取单个知识点详情"""
    try:
        conn = get_db_connection()
        cursor = conn.cursor(dictionary=True)
        
        cursor.execute("""
            SELECT * FROM knowledge_points_copy1 WHERE id = %s
        """, (kp_id,))
        
        kp = cursor.fetchone()
        conn.close()
        
        if not kp:
            return jsonify({'success': False, 'error': '知识点不存在'})
        
        # 处理JSON字段
        for field in ['prerequisite_kp_codes', 'dependent_kp_codes', 'related_kp_codes', 'stats', 'skills', 'direct_score', 'related_score']:
            if kp[field] and isinstance(kp[field], str):
                try:
                    kp[field] = json.loads(kp[field])
                except:
                    kp[field] = None
        
        return jsonify({'success': True, 'data': kp})
    except Exception as e:
        return jsonify({'success': False, 'error': str(e)})

@app.route('/update_kp_node', methods=['POST'])
def update_kp_node():
    """更新知识点节点名称"""
    try:
        data = request.json
        node_id = data.get('node_id')
        node_type = data.get('node_type')  # 'chapter', 'section', 'subsection'
        name = data.get('name', '').strip()
        
        if not node_id or not node_type or not name:
            return jsonify({'success': False, 'error': '缺少必要参数'}), 400
        
        if node_type not in ['chapter', 'section', 'subsection']:
            return jsonify({'success': False, 'error': '无效的节点类型'}), 400
        
        # 获取 tree_new.json 文件路径
        tree_file = get_tree_file_path()
        if not os.path.exists(tree_file):
            return jsonify({'success': False, 'error': '找不到知识点结构文件'}), 500
        
        # 读取 JSON 文件
        with open(tree_file, 'r', encoding='utf-8') as f:
            tree_data = json.load(f)
        
        # 查找目标节点
        target_node = find_node_by_id(tree_data, node_id)
        
        if not target_node:
            return jsonify({'success': False, 'error': '找不到目标节点'}), 404
        
        # 验证节点类型是否匹配
        if target_node.get('kp_level') != node_type:
            return jsonify({'success': False, 'error': '节点类型不匹配'}), 400
        
        # 更新节点名称
        target_node['label'] = name
        
        # 保存回文件
        with open(tree_file, 'w', encoding='utf-8') as f:
            json.dump(tree_data, f, ensure_ascii=False, indent=2)
        
        # 重新加载结构（更新全局变量）
        global KP_MAP, KP_HIERARCHY
        KP_MAP, KP_HIERARCHY = load_kp_structure()
        
        return jsonify({
            'success': True,
            'message': '修改成功'
        })
        
    except Exception as e:
        import traceback
        traceback.print_exc()
        return jsonify({'success': False, 'error': str(e)}), 500

if __name__ == '__main__':
    # 自动创建缺失列
    try:
        conn = get_db_connection()
        cursor = conn.cursor()
        for field, ftype in [('audit_status', 'TINYINT DEFAULT 0'), ('audit_reason', 'TEXT')]:
            cursor.execute(f"SHOW COLUMNS FROM questions_tem LIKE '{field}'")
            if not cursor.fetchone():
                cursor.execute(f"ALTER TABLE questions_tem ADD COLUMN {field} {ftype}")
        conn.commit()
        conn.close()
    except: pass
    
    # 确保 static 目录存在，并复制小猫图片（如果根目录有的话）
    try:
        static_dir = resource_path("static")
        if not os.path.exists(static_dir):
            os.makedirs(static_dir, exist_ok=True)
        
        import shutil
        # 优先复制 PNG 格式，如果没有则复制 JPG（兼容旧版本）
        cat_img_src_png = os.path.join(BASE_DIR, "小猫.png")
        cat_img_dst_png = os.path.join(static_dir, "小猫.png")
        cat_img_src_jpg = os.path.join(BASE_DIR, "小猫.jpg")
        cat_img_dst_jpg = os.path.join(static_dir, "小猫.jpg")
        
        if os.path.exists(cat_img_src_png) and not os.path.exists(cat_img_dst_png):
            shutil.copy2(cat_img_src_png, cat_img_dst_png)
        elif os.path.exists(cat_img_src_jpg) and not os.path.exists(cat_img_dst_jpg):
            shutil.copy2(cat_img_src_jpg, cat_img_dst_jpg)
    except Exception:
        pass  # 如果复制失败，不影响启动
    
    app.run(host="0.0.0.0", port=5000, debug=True)

