Files
jiaoyu/backend/routers/lesson_plan.py
T
Zhang Jing Xuan 0841b1a103 智教助手平台:完整初始化
- 前端:Vue3 + TS + Element Plus,24 个页面路由(课件/组题/教案/动画/思维导图/作文批改/命题/课堂/资源/社区等)
- 后端:FastAPI + SQLAlchemy + SQLite,17 个路由模块,AI 服务层含降级模板
- AI:glm-5.x 推理模型已禁用思维链,确保输出真实内容
- 修复:ai_service 两处请求体注入 enable_thinking/thinking disabled
- 测试账号:13900999999 / test1234
2026-07-29 16:29:10 +08:00

246 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import re
from io import BytesIO
from urllib.parse import quote
from docx import Document
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import StreamingResponse
from sqlalchemy.orm import Session
from database import get_db
from models.lesson_plan import LessonPlan
from models.resource import Resource
from models.user import User
from schemas.lesson_plan import LessonPlanGenerate, LessonPlanCreate, LessonPlanOut, LessonPlanUpdate
from services.credits import credits_payload, spend_credits
from services.limiter import limiter
from services.auth import get_current_user, get_optional_current_user
from services.ai_service import AIService
router = APIRouter(prefix="/api/lesson-plans", tags=["教案管理"])
ai_service = AIService()
def _safe_filename(name: str, suffix: str) -> str:
stem = re.sub(r"[\\/:*?\"<>|\r\n]+", "_", name).strip(" .") or "教案"
return f"{stem[:80]}{suffix}"
def _as_list(value) -> list:
if isinstance(value, list):
return value
if value in (None, ""):
return []
return [value]
def _add_bullets(document: Document, items: list) -> None:
for item in items:
if isinstance(item, dict):
text = "".join(f"{key}{value}" for key, value in item.items() if value not in (None, "", []))
else:
text = str(item)
if text.strip():
document.add_paragraph(text.strip(), style="List Bullet")
def _lesson_plan_or_404(plan_id: int, current_user: User | None, db: Session) -> LessonPlan:
lp = db.query(LessonPlan).filter(LessonPlan.id == plan_id).first()
if not lp:
raise HTTPException(status_code=404, detail="教案不存在")
if (not current_user or lp.user_id != current_user.id) and not has_public_resource(db, plan_id):
raise HTTPException(status_code=403, detail="无权访问该教案")
return lp
def has_public_resource(db: Session, plan_id: int) -> bool:
return db.query(Resource).filter(
Resource.content_ref == f"lesson_plan:{plan_id}",
Resource.status == "active",
Resource.is_public == 1,
).first() is not None
@limiter.limit("10/minute")
@router.post("/ai-generate", response_model=dict)
async def ai_generate_lesson_plan(
request: Request,
data: LessonPlanGenerate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
account = spend_credits(db, current_user, "lesson_plan_generate", f"生成教案:{data.title[:80]}")
db.commit()
result = await ai_service.generate_lesson_plan(
title=data.title, subject=data.subject, grade=data.grade,
objectives=data.objectives, duration=data.duration,
extra_requirements=data.extra_requirements,
)
return {"success": True, "data": result, "credits": credits_payload(account, "lesson_plan_generate")}
@router.post("/", response_model=LessonPlanOut, status_code=201)
def create_lesson_plan(data: LessonPlanCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
lp = LessonPlan(user_id=current_user.id, **data.model_dump())
db.add(lp)
db.commit()
db.refresh(lp)
return lp
@router.get("/", response_model=list[LessonPlanOut])
def list_lesson_plans(
subject: str = None,
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
query = db.query(LessonPlan).filter(LessonPlan.user_id == current_user.id)
if subject:
query = query.filter(LessonPlan.subject == subject)
return query.order_by(LessonPlan.updated_at.desc()).offset(skip).limit(limit).all()
@router.get("/{plan_id}", response_model=LessonPlanOut)
def get_lesson_plan(
plan_id: int,
current_user: User | None = Depends(get_optional_current_user),
db: Session = Depends(get_db),
):
return _lesson_plan_or_404(plan_id, current_user, db)
@router.get("/{plan_id}/export-docx")
def export_lesson_plan_docx(
plan_id: int,
current_user: User | None = Depends(get_optional_current_user),
db: Session = Depends(get_db),
):
lp = _lesson_plan_or_404(plan_id, current_user, db)
content = lp.content or {}
document = Document()
document.add_heading(lp.title or content.get("title") or "大单元教案", level=1)
meta = " / ".join(str(part) for part in [lp.subject, lp.grade, f"{lp.duration or 45}分钟"] if part)
if meta:
document.add_paragraph(meta)
sections = [
("教学目标", lp.objectives or content.get("objectives")),
("教学重点", lp.key_points or content.get("key_points")),
("教学难点", lp.difficulties or content.get("difficulties")),
("教学材料", lp.materials or content.get("materials")),
]
for title, items in sections:
items = _as_list(items)
if items:
document.add_heading(title, level=2)
_add_bullets(document, items)
phases = _as_list(content.get("phases") or content.get("steps"))
if phases:
document.add_heading("教学过程", level=2)
for index, phase in enumerate(phases, start=1):
if not isinstance(phase, dict):
document.add_paragraph(f"{index}. {phase}")
continue
name = phase.get("name") or phase.get("title") or f"教学环节 {index}"
duration = phase.get("duration")
heading = f"{index}. {name}"
if duration:
heading += f"{duration}分钟)"
document.add_heading(heading, level=3)
for label, key in [
("教学活动", "activities"),
("教师行为", "teacher_actions"),
("学生行为", "student_actions"),
("资源材料", "resources"),
("评价方式", "assessment"),
]:
items = _as_list(phase.get(key))
if items:
document.add_paragraph(label)
_add_bullets(document, items)
homework = _as_list(lp.homework or content.get("homework"))
if homework:
document.add_heading("课后作业", level=2)
_add_bullets(document, homework)
reflection = content.get("reflection")
if reflection:
document.add_heading("教学反思", level=2)
document.add_paragraph(str(reflection))
buffer = BytesIO()
document.save(buffer)
buffer.seek(0)
filename = _safe_filename(f"{lp.title or '大单元教案'}-教案", ".docx")
return StreamingResponse(
buffer,
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{quote(filename)}"},
)
@router.put("/{plan_id}", response_model=LessonPlanOut)
def update_lesson_plan(
plan_id: int,
data: LessonPlanUpdate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
lp = db.query(LessonPlan).filter(LessonPlan.id == plan_id, LessonPlan.user_id == current_user.id).first()
if not lp:
raise HTTPException(status_code=404, detail="教案不存在")
for key, value in data.model_dump(exclude_unset=True).items():
setattr(lp, key, value)
db.commit()
db.refresh(lp)
return lp
@router.post("/{plan_id}/remix", response_model=LessonPlanOut, status_code=201)
def remix_lesson_plan(
plan_id: int,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
source = db.query(LessonPlan).filter(LessonPlan.id == plan_id).first()
if not source:
raise HTTPException(status_code=404, detail="教案不存在")
if source.user_id != current_user.id and not has_public_resource(db, plan_id):
raise HTTPException(status_code=403, detail="无权改编该教案")
clone = LessonPlan(
user_id=current_user.id,
title=f"{source.title}(改编)",
subject=source.subject or "",
grade=source.grade or "",
objectives=list(source.objectives or []),
key_points=list(source.key_points or []),
difficulties=list(source.difficulties or []),
content=dict(source.content or {}),
duration=source.duration or 45,
materials=list(source.materials or []),
homework=list(source.homework or []),
)
db.add(clone)
db.commit()
db.refresh(clone)
return clone
@router.delete("/{plan_id}", status_code=204)
def delete_lesson_plan(plan_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
lp = db.query(LessonPlan).filter(LessonPlan.id == plan_id, LessonPlan.user_id == current_user.id).first()
if not lp:
raise HTTPException(status_code=404, detail="教案不存在")
db.query(Resource).filter(
Resource.user_id == current_user.id,
Resource.content_ref == f"lesson_plan:{plan_id}",
Resource.status == "active",
).update({"status": "archived"}, synchronize_session=False)
db.delete(lp)
db.commit()