智教助手平台:完整初始化
- 前端: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,317 @@
|
||||
import html
|
||||
import re
|
||||
import secrets
|
||||
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 typing import Optional
|
||||
from database import get_db
|
||||
from models.courseware import Courseware, CoursewareShare
|
||||
from models.resource import Resource
|
||||
from models.user import User
|
||||
from schemas.courseware import CoursewareCreate, CoursewareAIGenerate, CoursewareShareOut, CoursewareUpdate, CoursewareOut
|
||||
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/coursewares", 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 _plain_text_from_html(raw_html: str) -> str:
|
||||
text = str(raw_html or "")
|
||||
text = re.sub(r"<(script|style)[\s\S]*?</\1>", " ", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r"<br\s*/?>", "\n", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r"</(p|div|section|article|li|h[1-6]|tr)>", "\n", text, flags=re.IGNORECASE)
|
||||
text = re.sub(r"<[^>]+>", " ", text)
|
||||
text = html.unescape(text)
|
||||
text = re.sub(r"[ \t\u00a0]+", " ", text)
|
||||
text = re.sub(r"\n\s+", "\n", text)
|
||||
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||
return text.strip()
|
||||
|
||||
|
||||
def _courseware_or_404(
|
||||
courseware_id: int,
|
||||
current_user: User | None,
|
||||
db: Session,
|
||||
) -> Courseware:
|
||||
cw = db.query(Courseware).filter(Courseware.id == courseware_id).first()
|
||||
if not cw or cw.status == "archived":
|
||||
raise HTTPException(status_code=404, detail="课件不存在")
|
||||
if cw.status != "published" and (not current_user or cw.user_id != current_user.id):
|
||||
raise HTTPException(status_code=403, detail="无权访问该课件")
|
||||
return cw
|
||||
|
||||
|
||||
def _add_if_present(document: Document, label: str, value: str | None) -> None:
|
||||
if value:
|
||||
document.add_paragraph(f"{label}:{value}")
|
||||
|
||||
|
||||
def _share_payload(share: CoursewareShare, request: Request | None = None) -> dict:
|
||||
data = CoursewareShareOut.model_validate(share).model_dump()
|
||||
data["share_url"] = f"/preview/share/{share.token}"
|
||||
return data
|
||||
|
||||
|
||||
@limiter.limit("10/minute")
|
||||
@router.post("/ai-generate", response_model=dict)
|
||||
async def ai_generate_courseware(
|
||||
request: Request,
|
||||
data: CoursewareAIGenerate,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
account = spend_credits(db, current_user, "courseware_generate", f"生成互动课件:{data.prompt[:80]}")
|
||||
db.commit()
|
||||
result = await ai_service.generate_courseware(
|
||||
prompt=data.prompt, subject=data.subject, grade=data.grade, page_count=data.page_count,
|
||||
aspect_ratio=data.aspect_ratio,
|
||||
)
|
||||
return {"success": True, "data": result, "credits": credits_payload(account, "courseware_generate")}
|
||||
|
||||
|
||||
@router.post("/", response_model=CoursewareOut, status_code=201)
|
||||
def create_courseware(data: CoursewareCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
cw = Courseware(user_id=current_user.id, **data.model_dump())
|
||||
db.add(cw)
|
||||
db.commit()
|
||||
db.refresh(cw)
|
||||
return cw
|
||||
|
||||
|
||||
@router.get("/", response_model=list[CoursewareOut])
|
||||
def list_coursewares(
|
||||
subject: Optional[str] = None,
|
||||
grade: Optional[str] = None,
|
||||
status: Optional[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(Courseware).filter(Courseware.user_id == current_user.id)
|
||||
if subject:
|
||||
query = query.filter(Courseware.subject == subject)
|
||||
if grade:
|
||||
query = query.filter(Courseware.grade == grade)
|
||||
if status:
|
||||
query = query.filter(Courseware.status == status)
|
||||
else:
|
||||
query = query.filter(Courseware.status != "archived")
|
||||
return query.order_by(Courseware.updated_at.desc()).offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
@router.get("/shares/mine/list", response_model=list[CoursewareShareOut])
|
||||
def list_my_courseware_shares(
|
||||
request: Request,
|
||||
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),
|
||||
):
|
||||
shares = (
|
||||
db.query(CoursewareShare)
|
||||
.filter(CoursewareShare.user_id == current_user.id, CoursewareShare.status == "active")
|
||||
.order_by(CoursewareShare.updated_at.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [_share_payload(share, request) for share in shares]
|
||||
|
||||
|
||||
@router.delete("/shares/mine/{share_id}", status_code=204)
|
||||
def revoke_courseware_share(
|
||||
share_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
share = db.query(CoursewareShare).filter(
|
||||
CoursewareShare.id == share_id,
|
||||
CoursewareShare.user_id == current_user.id,
|
||||
CoursewareShare.status == "active",
|
||||
).first()
|
||||
if not share:
|
||||
raise HTTPException(status_code=404, detail="分享不存在")
|
||||
share.status = "revoked"
|
||||
db.commit()
|
||||
|
||||
|
||||
@router.get("/shares/{token}", response_model=CoursewareShareOut)
|
||||
def get_courseware_share(
|
||||
token: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
share = db.query(CoursewareShare).filter(
|
||||
CoursewareShare.token == token,
|
||||
CoursewareShare.status == "active",
|
||||
).first()
|
||||
if not share:
|
||||
raise HTTPException(status_code=404, detail="分享不存在或已失效")
|
||||
share.views = (share.views or 0) + 1
|
||||
db.commit()
|
||||
db.refresh(share)
|
||||
return _share_payload(share, request)
|
||||
|
||||
|
||||
@router.get("/{courseware_id}", response_model=CoursewareOut)
|
||||
def get_courseware(
|
||||
courseware_id: int,
|
||||
current_user: User | None = Depends(get_optional_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
return _courseware_or_404(courseware_id, current_user, db)
|
||||
|
||||
|
||||
@router.post("/{courseware_id}/share", response_model=CoursewareShareOut, status_code=201)
|
||||
def create_courseware_share(
|
||||
courseware_id: int,
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
cw = db.query(Courseware).filter(
|
||||
Courseware.id == courseware_id,
|
||||
Courseware.user_id == current_user.id,
|
||||
Courseware.status != "archived",
|
||||
).first()
|
||||
if not cw:
|
||||
raise HTTPException(status_code=404, detail="课件不存在")
|
||||
share = db.query(CoursewareShare).filter(
|
||||
CoursewareShare.courseware_id == courseware_id,
|
||||
CoursewareShare.user_id == current_user.id,
|
||||
CoursewareShare.status == "active",
|
||||
).first()
|
||||
if not share:
|
||||
share = CoursewareShare(
|
||||
courseware_id=courseware_id,
|
||||
user_id=current_user.id,
|
||||
token=secrets.token_urlsafe(18),
|
||||
)
|
||||
db.add(share)
|
||||
share.title = cw.title
|
||||
share.subject = cw.subject or ""
|
||||
share.grade = cw.grade or ""
|
||||
share.description = cw.description or ""
|
||||
share.content = cw.content or []
|
||||
share.tags = list(cw.tags or [])
|
||||
share.status = "active"
|
||||
db.commit()
|
||||
db.refresh(share)
|
||||
return _share_payload(share, request)
|
||||
|
||||
|
||||
@router.get("/{courseware_id}/export-docx")
|
||||
def export_courseware_docx(
|
||||
courseware_id: int,
|
||||
current_user: User | None = Depends(get_optional_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
cw = _courseware_or_404(courseware_id, current_user, db)
|
||||
document = Document()
|
||||
document.add_heading(cw.title or "互动课件讲义", level=1)
|
||||
|
||||
meta_parts = [part for part in [cw.subject, cw.grade] if part]
|
||||
if meta_parts:
|
||||
document.add_paragraph(" / ".join(meta_parts))
|
||||
_add_if_present(document, "课件说明", cw.description)
|
||||
if cw.tags:
|
||||
document.add_paragraph("标签:" + "、".join(str(tag) for tag in cw.tags if str(tag).strip()))
|
||||
|
||||
pages = cw.content or []
|
||||
document.add_paragraph(f"页数:{len(pages)}")
|
||||
|
||||
for index, page in enumerate(pages, start=1):
|
||||
if not isinstance(page, dict):
|
||||
continue
|
||||
page_title = str(page.get("title") or f"第 {index} 页").strip()
|
||||
document.add_heading(f"第 {index} 页:{page_title}", level=2)
|
||||
page_type = page.get("type")
|
||||
if page_type:
|
||||
document.add_paragraph(f"页面类型:{page_type}")
|
||||
text = _plain_text_from_html(page.get("content") or "")
|
||||
document.add_paragraph(text or "该页暂无可提取文本。")
|
||||
notes = str(page.get("notes") or "").strip()
|
||||
if notes:
|
||||
document.add_paragraph(f"教师备注:{notes}")
|
||||
|
||||
buffer = BytesIO()
|
||||
document.save(buffer)
|
||||
buffer.seek(0)
|
||||
filename = _safe_filename(f"{cw.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("/{courseware_id}", response_model=CoursewareOut)
|
||||
def update_courseware(
|
||||
courseware_id: int, data: CoursewareUpdate,
|
||||
current_user: User = Depends(get_current_user), db: Session = Depends(get_db),
|
||||
):
|
||||
cw = db.query(Courseware).filter(Courseware.id == courseware_id, Courseware.user_id == current_user.id).first()
|
||||
if not cw:
|
||||
raise HTTPException(status_code=404, detail="课件不存在")
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(cw, key, value)
|
||||
db.commit()
|
||||
db.refresh(cw)
|
||||
return cw
|
||||
|
||||
|
||||
@router.post("/{courseware_id}/remix", response_model=CoursewareOut, status_code=201)
|
||||
def remix_courseware(
|
||||
courseware_id: int,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
source = db.query(Courseware).filter(Courseware.id == courseware_id).first()
|
||||
if not source or source.status == "archived":
|
||||
raise HTTPException(status_code=404, detail="课件不存在")
|
||||
if source.status != "published" and source.user_id != current_user.id:
|
||||
raise HTTPException(status_code=403, detail="无权改编该课件")
|
||||
|
||||
clone = Courseware(
|
||||
user_id=current_user.id,
|
||||
title=f"{source.title}(改编)",
|
||||
subject=source.subject or "",
|
||||
grade=source.grade or "",
|
||||
description=source.description or "",
|
||||
content=source.content or [],
|
||||
cover_image=source.cover_image or "",
|
||||
tags=list(source.tags or []),
|
||||
status="draft",
|
||||
)
|
||||
db.add(clone)
|
||||
db.commit()
|
||||
db.refresh(clone)
|
||||
return clone
|
||||
|
||||
|
||||
@router.delete("/{courseware_id}", status_code=204)
|
||||
def delete_courseware(courseware_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
|
||||
cw = db.query(Courseware).filter(Courseware.id == courseware_id, Courseware.user_id == current_user.id).first()
|
||||
if not cw:
|
||||
raise HTTPException(status_code=404, detail="课件不存在")
|
||||
cw.status = "archived"
|
||||
db.query(Resource).filter(
|
||||
Resource.user_id == current_user.id,
|
||||
Resource.content_ref == f"courseware:{courseware_id}",
|
||||
Resource.status == "active",
|
||||
).update({"status": "archived"}, synchronize_session=False)
|
||||
db.commit()
|
||||
Reference in New Issue
Block a user