# 问题配置路由 - 完整CRUD import os import shutil from datetime import datetime from flask import request, jsonify, render_template, current_app, session, redirect from app.routes import main_bp from app.config import load_api_config, save_api_config from app.routes.auth import login_required_json, admin_required @main_bp.route("/settings") @login_required_json def settings(): """问题配置页面 - 所有登录用户可访问""" return render_template("settings.html") @main_bp.route("/api-settings") @admin_required def api_settings_page(): """API设置页面 - 仅管理员""" return render_template("api_settings.html") # ==================== API配置接口 ==================== @main_bp.route("/api/config", methods=["GET"]) @admin_required def get_api_config(): """获取API配置""" config = load_api_config(current_app.config) # 不返回完整的api_key,只返回前5位和后5位 if config.get("api_key"): api_key = config["api_key"] if len(api_key) > 10: config["api_key_preview"] = api_key[:5] + "..." + api_key[-5:] else: config["api_key_preview"] = "****" return jsonify(config) @main_bp.route("/api/config", methods=["POST"]) @admin_required def update_api_config(): """更新API配置""" data = request.get_json() # 验证必填字段 if not data.get("api_key"): return jsonify({"error": "API Key不能为空"}), 400 # 根据provider设置默认endpoint provider = data.get("provider", "volcengine") default_endpoints = { "minimax": "https://api.minimaxi.com/anthropic/v1", "volcengine": "https://ark.cn-beijing.volces.com/api/coding/v3", "deepseek": "https://api.deepseek.com/v1", "openrouter": "https://openrouter.ai/api/v1", } default_endpoint = default_endpoints.get(provider, "https://ark.cn-beijing.volces.com/api/coding/v3") # 保存配置 config = { "provider": provider, "api_key": data.get("api_key", ""), "base_url": data.get("base_url", default_endpoint), "model": data.get("model", "doubao-seed-2.0-pro"), "temperature": float(data.get("temperature", 0.7)), "prompt_template": data.get("prompt_template", ""), } save_api_config(config, current_app.config) return jsonify({"message": "配置保存成功"}) @main_bp.route("/api/problems", methods=["GET"]) @login_required_json def get_problems(): """获取问题列表(从文件夹动态读取)""" problems_dir = current_app.config.get("PROBLEMS_DIR") problems = [] if problems_dir and os.path.exists(problems_dir): for filename in sorted(os.listdir(problems_dir)): if ( filename.endswith(".md") and not filename.startswith("模板") and not filename.startswith("针对性练习建议") ): name = filename.replace(".md", "") if "汇总" in name: continue parts = name.split("_", 1) if len(parts) == 2: problem_id = parts[0] problem_name = parts[1] category = "技术类" filepath = os.path.join(problems_dir, filename) try: with open(filepath, "r", encoding="utf-8") as f: content = f.read() if "认知类" in content: category = "认知类" elif "节奏类" in content: category = "节奏类" elif "表现类" in content: category = "表现类" elif "习惯类" in content: category = "习惯类" elif "综合类" in content: category = "综合类" except: pass try: mtime = os.path.getmtime(filepath) except: mtime = 0 problems.append( { "id": problem_id, "name": problem_name, "category": category, "file": filename, "mtime": mtime, } ) return jsonify(sorted(problems, key=lambda x: x["id"])) @main_bp.route("/api/problems/", methods=["GET"]) @login_required_json def get_problem_detail(problem_id): """获取单个问题的详细信息""" problems_dir = current_app.config.get("PROBLEMS_DIR") if problems_dir and os.path.exists(problems_dir): for filename in os.listdir(problems_dir): if filename.startswith(f"{problem_id}_") and filename.endswith(".md"): filepath = os.path.join(problems_dir, filename) with open(filepath, "r", encoding="utf-8") as f: content = f.read() return jsonify( {"id": problem_id, "filename": filename, "content": content} ) return jsonify({"error": "问题不存在"}), 404 @main_bp.route("/api/problems", methods=["POST"]) @admin_required def create_problem(): """创建新问题 - 仅管理员""" """创建新问题""" data = request.get_json() problem_id = data.get("id", "").strip() problem_name = data.get("name", "").strip() category = data.get("category", "技术类") if not problem_id or not problem_name: return jsonify({"error": "ID和名称不能为空"}), 400 # 格式化ID problem_id = problem_id.zfill(2) problems_dir = current_app.config.get("PROBLEMS_DIR") filename = f"{problem_id}_{problem_name}.md" filepath = os.path.join(problems_dir, filename) if os.path.exists(filepath): return jsonify({"error": "问题已存在"}), 400 # 生成默认内容 content = f"""# {problem_name} > 所属系列:钢琴学习常见问题针对性练习建议 > 配合目标体系使用,针对性补齐短板 --- ## 问题表现 - 请描述具体表现症状 ## 原因分析 - 可能的原因1 - 可能的原因2 ## 针对性练习方案 ### 日常基础练习 | 练习名称 | 时长 | 频率 | 目的 | |---------|------|------|------| | 练习1 | 10分钟 | 每天 | 目的1 | | 练习2 | 5分钟 | 每天 | 目的2 | ### 具体操作 ``` 练习1:练习名称 - 步骤1 - 步骤2 - 步骤3 ``` ## 练习提醒 ### ⚠️ 禁忌 - 禁忌1 - 禁忌2 ### ✓ 正确做法 - 正确做法1 - 正确做法2 ## 评估标准 | 等级 | 标准 | |------|------| | 入门 | 达到的标准 | | 进阶 | 达到的标准 | | 熟练 | 达到的标准 | | 精通 | 达到的标准 | --- > **版本**:V1.0 > **创建时间**:{datetime.now().strftime("%Y-%m-%d")} > **适用场景**:成人钢琴集体课学员个性化辅导""" with open(filepath, "w", encoding="utf-8") as f: f.write(content) return jsonify({"message": "创建成功", "id": problem_id, "name": problem_name}) @main_bp.route("/api/problems/", methods=["PUT"]) @login_required_json def update_problem(problem_id): """更新问题""" data = request.get_json() new_name = data.get("name", "").strip() new_content = data.get("content", "").strip() if not new_name: return jsonify({"error": "名称不能为空"}), 400 problems_dir = current_app.config.get("PROBLEMS_DIR") # 找到旧文件 old_filename = None for f in os.listdir(problems_dir): if f.startswith(f"{problem_id}_") and f.endswith(".md"): old_filename = f break if not old_filename: return jsonify({"error": "问题不存在"}), 404 old_filepath = os.path.join(problems_dir, old_filename) new_filename = f"{problem_id}_{new_name}.md" new_filepath = os.path.join(problems_dir, new_filename) # 如果名称改变,需要重命名文件 if old_filename != new_filename: if os.path.exists(new_filepath): return jsonify({"error": "同名问题已存在"}), 400 os.rename(old_filepath, new_filepath) filepath = new_filepath else: filepath = old_filepath # 更新内容 with open(filepath, "w", encoding="utf-8") as f: f.write(new_content) return jsonify({"message": "更新成功"}) @main_bp.route("/api/problems/", methods=["DELETE"]) @admin_required def delete_problem(problem_id): """删除问题""" problems_dir = current_app.config.get("PROBLEMS_DIR") # 找到文件 filename = None for f in os.listdir(problems_dir): if f.startswith(f"{problem_id}_") and f.endswith(".md"): filename = f break if not filename: return jsonify({"error": "问题不存在"}), 404 filepath = os.path.join(problems_dir, filename) # 移动到备份目录 trash_dir = os.path.join(problems_dir, "bk") os.makedirs(trash_dir, exist_ok=True) import time backup_name = f"{filename.replace('.md', '')}_{int(time.time())}.md" shutil.move(filepath, os.path.join(trash_dir, backup_name)) return jsonify({"message": "删除成功"}) # ==================== AI测试接口 ==================== @main_bp.route("/api/config/test", methods=["POST"]) @admin_required def test_api_connection(): """测试API连接""" import requests config = load_api_config(current_app.config) provider = config.get("provider", "volcengine") headers = { "Authorization": f"Bearer {config.get('api_key', '')}", "Content-Type": "application/json", } payload = { "model": config.get("model", "doubao-seed-2.0-pro"), "messages": [{"role": "user", "content": "你好"}], "max_tokens": 50, } try: # MiniMax 使用 Anthropic 格式的 API if provider == "minimax": endpoint = f"{config.get('base_url')}/messages" payload["max_tokens"] = 50 else: endpoint = f"{config.get('base_url')}/chat/completions" response = requests.post( endpoint, headers=headers, json=payload, timeout=30, ) if response.status_code == 200: return jsonify({"success": True, "message": "连接成功"}) else: return jsonify( {"success": False, "error": f"API返回错误: {response.status_code}"} ) except Exception as e: return jsonify({"success": False, "error": str(e)})