# views/lesson.py from flask import Blueprint, jsonify, request, render_template, session, current_app from werkzeug.exceptions import BadRequest from bson import ObjectId from datetime import datetime from ..auth.decorators import require_role from ..services.course_service import load_material,save_materials_markdown_cos from ..services.cos_service import upload_file import uuid bp = Blueprint('lesson', __name__) @bp.route("/api/upload", methods=["POST"]) def upload(): f = request.files['file'] url = upload_file(f, "image_" + str(uuid.uuid4())) return jsonify({"url": url}) # 返回课时元信息(含三份 markdown 的可访问链接或直接内容) @bp.route('/api/materials//chapters//lessons/', methods=['GET']) @require_role(roles="teacher") def edit_lesson_detail(material_id, chapter_name, lesson_name): teacher_uuid = session.get("user_uuid") teacher_id = current_app.extensions["uuid2username"][teacher_uuid] material = load_material(material_id) for chapter in material.chapters: if chapter.chapter_name == chapter_name: for lesson in chapter.lessons: if lesson.lesson_name == lesson_name: return render_template('lesson.html', lesson=lesson.model_dump(), role="teacher", material={**material.model_dump(), "material_id": material_id}, chapter=chapter.model_dump()) return jsonify({"message": "课时不存在"}), 404 # 返回某份 markdown 文件内容(你也可以直接返回 S3/OSS 的直链) @bp.route('/api/lesson_file////', methods=['GET']) def get_markdown_file(material_id, chapter_name, lesson_name, filename): # TODO: 真实环境从存储读取 demo_map = { "lesson.md": "# 主题\n## 阶段一\n### 步骤A\n内容A...\n### 步骤B\n内容B...\n", "prompt.md": "# 主题\n## 阶段一\n### 步骤A\n提示A...\n### 步骤B\n提示B...\n", "score_prompt.md": "# 主题\n## 阶段一\n### 步骤A\n评分A...\n### 步骤B\n评分B...\n" } return demo_map.get(filename, ""), 200 @bp.route("/api/materials/save///", methods=['POST']) @require_role(roles='teacher') def save_materials_markdown(material_id, chapter_name, lesson_name): payload = request.get_json(silent=True, force=True) if not payload: raise BadRequest("缺少 JSON payload") # 基础校验 required_keys = ("lesson", "prompt", "score") for k in required_keys: if k not in payload or not isinstance(payload[k], dict): raise BadRequest(f"缺少或非法字段: {k}") if "cdn_link" not in payload[k] or "content" not in payload[k]: raise BadRequest(f"{k} 需要包含 cdn_link 与 content 字段") if not isinstance(payload[k]["cdn_link"], str) or not isinstance(payload[k]["content"], str): raise BadRequest(f"{k}.cdn_link 与 {k}.content 必须为字符串") # 可选:版本/并发控制(若你前端传了) version = payload.get("version") docs = { "lesson": { "cdn_link": payload["lesson"]["cdn_link"].strip(), "content" : payload["lesson"]["content"] }, "prompt": { "cdn_link": payload["prompt"]["cdn_link"].strip(), "content" : payload["prompt"]["content"] }, "score": { "cdn_link": payload["score"]["cdn_link"].strip(), "content" : payload["score"]["content"] } } # 轻量防御:限制单次大小(自行按需调整) max_size = 4 * 1024 * 1024 # 2MB/份 for name, doc in docs.items(): if len(doc["content"].encode("utf-8")) > max_size: raise BadRequest(f"{name} 文档过大,超过 {max_size} 字节限制") # 调用服务层保存:写源站、刷新/回写 CDN、更新索引等 # 建议服务层返回新版本/新链接等元信息 result = save_materials_markdown_cos( material_id=material_id, chapter_name=chapter_name, lesson_name=lesson_name, docs=docs, version=version, # 可选,用于并发检测 actor=getattr(request, "user", None) # 若你的鉴权中注入了当前教师 ) # 统一响应 return jsonify({ "ok": True, "material_id": material_id, "chapter_name": chapter_name, "lesson_name": lesson_name, "result": result # e.g. {"lesson":{"cdn_link": "...", "version":"..."}, ...} }), 200