All checks were successful
continuous-integration/drone/push Build is passing
- progress.py: get_db, get_current_user 从 app.core.deps 导入 - speech.py: 同上 - recommendation.py: 同上
470 lines
15 KiB
Python
470 lines
15 KiB
Python
"""
|
|
用户课程学习进度 API
|
|
"""
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, func, and_
|
|
from pydantic import BaseModel, Field
|
|
|
|
from app.core.deps import get_db, get_current_user
|
|
from app.models.user import User
|
|
from app.models.course import Course, CourseMaterial
|
|
from app.models.user_course_progress import (
|
|
UserCourseProgress,
|
|
UserMaterialProgress,
|
|
ProgressStatus,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# ============ Schemas ============
|
|
|
|
class MaterialProgressUpdate(BaseModel):
|
|
"""更新资料进度请求"""
|
|
progress_percent: float = Field(ge=0, le=100, description="进度百分比")
|
|
last_position: Optional[int] = Field(default=0, ge=0, description="播放位置(秒)")
|
|
study_time_delta: Optional[int] = Field(default=0, ge=0, description="本次学习时长(秒)")
|
|
is_completed: Optional[bool] = Field(default=None, description="是否标记完成")
|
|
|
|
|
|
class MaterialProgressResponse(BaseModel):
|
|
"""资料进度响应"""
|
|
material_id: int
|
|
material_name: str
|
|
is_completed: bool
|
|
progress_percent: float
|
|
last_position: int
|
|
study_time: int
|
|
first_accessed_at: Optional[datetime]
|
|
last_accessed_at: Optional[datetime]
|
|
completed_at: Optional[datetime]
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class CourseProgressResponse(BaseModel):
|
|
"""课程进度响应"""
|
|
course_id: int
|
|
course_name: str
|
|
status: str
|
|
progress_percent: float
|
|
completed_materials: int
|
|
total_materials: int
|
|
total_study_time: int
|
|
first_accessed_at: Optional[datetime]
|
|
last_accessed_at: Optional[datetime]
|
|
completed_at: Optional[datetime]
|
|
materials: List[MaterialProgressResponse] = []
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ProgressSummary(BaseModel):
|
|
"""进度统计摘要"""
|
|
total_courses: int
|
|
completed_courses: int
|
|
in_progress_courses: int
|
|
not_started_courses: int
|
|
total_study_time: int
|
|
average_progress: float
|
|
|
|
|
|
# ============ API Endpoints ============
|
|
|
|
@router.get("/summary", response_model=ProgressSummary)
|
|
async def get_progress_summary(
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""获取用户学习进度摘要"""
|
|
# 获取用户所有课程进度
|
|
result = await db.execute(
|
|
select(UserCourseProgress).where(
|
|
UserCourseProgress.user_id == current_user.id
|
|
)
|
|
)
|
|
progress_list = result.scalars().all()
|
|
|
|
total_courses = len(progress_list)
|
|
completed = sum(1 for p in progress_list if p.status == ProgressStatus.COMPLETED.value)
|
|
in_progress = sum(1 for p in progress_list if p.status == ProgressStatus.IN_PROGRESS.value)
|
|
not_started = sum(1 for p in progress_list if p.status == ProgressStatus.NOT_STARTED.value)
|
|
total_time = sum(p.total_study_time for p in progress_list)
|
|
avg_progress = sum(p.progress_percent for p in progress_list) / total_courses if total_courses > 0 else 0
|
|
|
|
return ProgressSummary(
|
|
total_courses=total_courses,
|
|
completed_courses=completed,
|
|
in_progress_courses=in_progress,
|
|
not_started_courses=not_started,
|
|
total_study_time=total_time,
|
|
average_progress=round(avg_progress, 2),
|
|
)
|
|
|
|
|
|
@router.get("/courses", response_model=List[CourseProgressResponse])
|
|
async def get_all_course_progress(
|
|
status: Optional[str] = Query(None, description="过滤状态"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""获取用户所有课程的学习进度"""
|
|
query = select(UserCourseProgress, Course).join(
|
|
Course, UserCourseProgress.course_id == Course.id
|
|
).where(
|
|
UserCourseProgress.user_id == current_user.id
|
|
)
|
|
|
|
if status:
|
|
query = query.where(UserCourseProgress.status == status)
|
|
|
|
result = await db.execute(query)
|
|
rows = result.all()
|
|
|
|
response = []
|
|
for progress, course in rows:
|
|
response.append(CourseProgressResponse(
|
|
course_id=course.id,
|
|
course_name=course.name,
|
|
status=progress.status,
|
|
progress_percent=progress.progress_percent,
|
|
completed_materials=progress.completed_materials,
|
|
total_materials=progress.total_materials,
|
|
total_study_time=progress.total_study_time,
|
|
first_accessed_at=progress.first_accessed_at,
|
|
last_accessed_at=progress.last_accessed_at,
|
|
completed_at=progress.completed_at,
|
|
))
|
|
|
|
return response
|
|
|
|
|
|
@router.get("/courses/{course_id}", response_model=CourseProgressResponse)
|
|
async def get_course_progress(
|
|
course_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""获取指定课程的详细学习进度"""
|
|
# 获取课程信息
|
|
course_result = await db.execute(
|
|
select(Course).where(Course.id == course_id)
|
|
)
|
|
course = course_result.scalar_one_or_none()
|
|
if not course:
|
|
raise HTTPException(status_code=404, detail="课程不存在")
|
|
|
|
# 获取或创建课程进度
|
|
progress_result = await db.execute(
|
|
select(UserCourseProgress).where(
|
|
and_(
|
|
UserCourseProgress.user_id == current_user.id,
|
|
UserCourseProgress.course_id == course_id,
|
|
)
|
|
)
|
|
)
|
|
progress = progress_result.scalar_one_or_none()
|
|
|
|
if not progress:
|
|
# 获取课程资料数量
|
|
materials_result = await db.execute(
|
|
select(func.count(CourseMaterial.id)).where(
|
|
and_(
|
|
CourseMaterial.course_id == course_id,
|
|
CourseMaterial.is_deleted == False,
|
|
)
|
|
)
|
|
)
|
|
total_materials = materials_result.scalar() or 0
|
|
|
|
# 创建新的进度记录
|
|
progress = UserCourseProgress(
|
|
user_id=current_user.id,
|
|
course_id=course_id,
|
|
status=ProgressStatus.NOT_STARTED.value,
|
|
progress_percent=0.0,
|
|
completed_materials=0,
|
|
total_materials=total_materials,
|
|
)
|
|
db.add(progress)
|
|
await db.commit()
|
|
await db.refresh(progress)
|
|
|
|
# 获取资料进度
|
|
material_progress_result = await db.execute(
|
|
select(UserMaterialProgress, CourseMaterial).join(
|
|
CourseMaterial, UserMaterialProgress.material_id == CourseMaterial.id
|
|
).where(
|
|
and_(
|
|
UserMaterialProgress.user_id == current_user.id,
|
|
UserMaterialProgress.course_id == course_id,
|
|
)
|
|
)
|
|
)
|
|
material_rows = material_progress_result.all()
|
|
|
|
materials = []
|
|
for mp, material in material_rows:
|
|
materials.append(MaterialProgressResponse(
|
|
material_id=material.id,
|
|
material_name=material.name,
|
|
is_completed=mp.is_completed,
|
|
progress_percent=mp.progress_percent,
|
|
last_position=mp.last_position,
|
|
study_time=mp.study_time,
|
|
first_accessed_at=mp.first_accessed_at,
|
|
last_accessed_at=mp.last_accessed_at,
|
|
completed_at=mp.completed_at,
|
|
))
|
|
|
|
return CourseProgressResponse(
|
|
course_id=course.id,
|
|
course_name=course.name,
|
|
status=progress.status,
|
|
progress_percent=progress.progress_percent,
|
|
completed_materials=progress.completed_materials,
|
|
total_materials=progress.total_materials,
|
|
total_study_time=progress.total_study_time,
|
|
first_accessed_at=progress.first_accessed_at,
|
|
last_accessed_at=progress.last_accessed_at,
|
|
completed_at=progress.completed_at,
|
|
materials=materials,
|
|
)
|
|
|
|
|
|
@router.post("/materials/{material_id}", response_model=MaterialProgressResponse)
|
|
async def update_material_progress(
|
|
material_id: int,
|
|
data: MaterialProgressUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""更新资料学习进度"""
|
|
# 获取资料信息
|
|
material_result = await db.execute(
|
|
select(CourseMaterial).where(CourseMaterial.id == material_id)
|
|
)
|
|
material = material_result.scalar_one_or_none()
|
|
if not material:
|
|
raise HTTPException(status_code=404, detail="资料不存在")
|
|
|
|
course_id = material.course_id
|
|
now = datetime.now()
|
|
|
|
# 获取或创建资料进度
|
|
mp_result = await db.execute(
|
|
select(UserMaterialProgress).where(
|
|
and_(
|
|
UserMaterialProgress.user_id == current_user.id,
|
|
UserMaterialProgress.material_id == material_id,
|
|
)
|
|
)
|
|
)
|
|
mp = mp_result.scalar_one_or_none()
|
|
|
|
if not mp:
|
|
mp = UserMaterialProgress(
|
|
user_id=current_user.id,
|
|
material_id=material_id,
|
|
course_id=course_id,
|
|
first_accessed_at=now,
|
|
)
|
|
db.add(mp)
|
|
|
|
# 更新进度
|
|
mp.progress_percent = data.progress_percent
|
|
mp.last_position = data.last_position or mp.last_position
|
|
mp.study_time += data.study_time_delta or 0
|
|
mp.last_accessed_at = now
|
|
|
|
# 处理完成状态
|
|
if data.is_completed is not None:
|
|
if data.is_completed and not mp.is_completed:
|
|
mp.is_completed = True
|
|
mp.completed_at = now
|
|
mp.progress_percent = 100.0
|
|
elif not data.is_completed:
|
|
mp.is_completed = False
|
|
mp.completed_at = None
|
|
elif data.progress_percent >= 100:
|
|
mp.is_completed = True
|
|
mp.completed_at = now
|
|
|
|
await db.commit()
|
|
|
|
# 更新课程整体进度
|
|
await _update_course_progress(db, current_user.id, course_id)
|
|
|
|
await db.refresh(mp)
|
|
|
|
return MaterialProgressResponse(
|
|
material_id=mp.material_id,
|
|
material_name=material.name,
|
|
is_completed=mp.is_completed,
|
|
progress_percent=mp.progress_percent,
|
|
last_position=mp.last_position,
|
|
study_time=mp.study_time,
|
|
first_accessed_at=mp.first_accessed_at,
|
|
last_accessed_at=mp.last_accessed_at,
|
|
completed_at=mp.completed_at,
|
|
)
|
|
|
|
|
|
@router.post("/materials/{material_id}/complete")
|
|
async def mark_material_complete(
|
|
material_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""标记资料为已完成"""
|
|
return await update_material_progress(
|
|
material_id=material_id,
|
|
data=MaterialProgressUpdate(progress_percent=100, is_completed=True),
|
|
db=db,
|
|
current_user=current_user,
|
|
)
|
|
|
|
|
|
@router.post("/courses/{course_id}/start")
|
|
async def start_course(
|
|
course_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: User = Depends(get_current_user),
|
|
):
|
|
"""开始学习课程(记录首次访问)"""
|
|
# 获取课程
|
|
course_result = await db.execute(
|
|
select(Course).where(Course.id == course_id)
|
|
)
|
|
course = course_result.scalar_one_or_none()
|
|
if not course:
|
|
raise HTTPException(status_code=404, detail="课程不存在")
|
|
|
|
now = datetime.now()
|
|
|
|
# 获取或创建进度
|
|
progress_result = await db.execute(
|
|
select(UserCourseProgress).where(
|
|
and_(
|
|
UserCourseProgress.user_id == current_user.id,
|
|
UserCourseProgress.course_id == course_id,
|
|
)
|
|
)
|
|
)
|
|
progress = progress_result.scalar_one_or_none()
|
|
|
|
if not progress:
|
|
# 获取资料数量
|
|
materials_result = await db.execute(
|
|
select(func.count(CourseMaterial.id)).where(
|
|
and_(
|
|
CourseMaterial.course_id == course_id,
|
|
CourseMaterial.is_deleted == False,
|
|
)
|
|
)
|
|
)
|
|
total_materials = materials_result.scalar() or 0
|
|
|
|
progress = UserCourseProgress(
|
|
user_id=current_user.id,
|
|
course_id=course_id,
|
|
status=ProgressStatus.IN_PROGRESS.value,
|
|
total_materials=total_materials,
|
|
first_accessed_at=now,
|
|
last_accessed_at=now,
|
|
)
|
|
db.add(progress)
|
|
else:
|
|
if progress.status == ProgressStatus.NOT_STARTED.value:
|
|
progress.status = ProgressStatus.IN_PROGRESS.value
|
|
if not progress.first_accessed_at:
|
|
progress.first_accessed_at = now
|
|
progress.last_accessed_at = now
|
|
|
|
await db.commit()
|
|
|
|
return {"code": 200, "message": "已开始学习", "data": {"course_id": course_id}}
|
|
|
|
|
|
# ============ Helper Functions ============
|
|
|
|
async def _update_course_progress(db: AsyncSession, user_id: int, course_id: int):
|
|
"""更新课程整体进度"""
|
|
now = datetime.now()
|
|
|
|
# 获取课程所有资料数量
|
|
materials_result = await db.execute(
|
|
select(func.count(CourseMaterial.id)).where(
|
|
and_(
|
|
CourseMaterial.course_id == course_id,
|
|
CourseMaterial.is_deleted == False,
|
|
)
|
|
)
|
|
)
|
|
total_materials = materials_result.scalar() or 0
|
|
|
|
# 获取已完成的资料数量和总学习时长
|
|
completed_result = await db.execute(
|
|
select(
|
|
func.count(UserMaterialProgress.id),
|
|
func.coalesce(func.sum(UserMaterialProgress.study_time), 0),
|
|
).where(
|
|
and_(
|
|
UserMaterialProgress.user_id == user_id,
|
|
UserMaterialProgress.course_id == course_id,
|
|
UserMaterialProgress.is_completed == True,
|
|
)
|
|
)
|
|
)
|
|
row = completed_result.one()
|
|
completed_materials = row[0]
|
|
total_study_time = row[1]
|
|
|
|
# 计算进度百分比
|
|
progress_percent = (completed_materials / total_materials * 100) if total_materials > 0 else 0
|
|
|
|
# 确定状态
|
|
if completed_materials == 0:
|
|
status = ProgressStatus.IN_PROGRESS.value # 已开始但未完成任何资料
|
|
elif completed_materials >= total_materials:
|
|
status = ProgressStatus.COMPLETED.value
|
|
else:
|
|
status = ProgressStatus.IN_PROGRESS.value
|
|
|
|
# 获取或创建课程进度
|
|
progress_result = await db.execute(
|
|
select(UserCourseProgress).where(
|
|
and_(
|
|
UserCourseProgress.user_id == user_id,
|
|
UserCourseProgress.course_id == course_id,
|
|
)
|
|
)
|
|
)
|
|
progress = progress_result.scalar_one_or_none()
|
|
|
|
if not progress:
|
|
progress = UserCourseProgress(
|
|
user_id=user_id,
|
|
course_id=course_id,
|
|
first_accessed_at=now,
|
|
)
|
|
db.add(progress)
|
|
|
|
# 更新进度
|
|
progress.status = status
|
|
progress.progress_percent = round(progress_percent, 2)
|
|
progress.completed_materials = completed_materials
|
|
progress.total_materials = total_materials
|
|
progress.total_study_time = total_study_time
|
|
progress.last_accessed_at = now
|
|
|
|
if status == ProgressStatus.COMPLETED.value and not progress.completed_at:
|
|
progress.completed_at = now
|
|
|
|
await db.commit()
|