智教助手平台:完整初始化
- 前端:Vue3 + TS + Element Plus,24 个页面路由(课件/组题/教案/动画/思维导图/作文批改/命题/课堂/资源/社区等) - 后端:FastAPI + SQLAlchemy + SQLite,17 个路由模块,AI 服务层含降级模板 - AI:glm-5.x 推理模型已禁用思维链,确保输出真实内容 - 修复:ai_service 两处请求体注入 enable_thinking/thinking disabled - 测试账号:13900999999 / test1234
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user