653 lines
25 KiB
Python
653 lines
25 KiB
Python
"""培训与技能认证体系 API 路由。
|
||
|
||
覆盖:课程浏览、报名、学习进度、结业证书、运营端课程管理。
|
||
完整闭环:参加培训 → 完成学习 → 通过考核 → 获得证书 → 授予徽章 → 信用加分。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from pydantic import BaseModel
|
||
from typing import Optional
|
||
|
||
from ...infrastructure.repositories import Database, new_id, utcnow_iso
|
||
from ...infrastructure.models import (
|
||
TrainingCourse, TrainingChapter, TrainingLesson,
|
||
TrainingCourseEnrollment, TrainingLessonProgress, TrainingCertificate,
|
||
)
|
||
import json
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
# ── 依赖 ──────────────────────────────────────────────────────────────
|
||
|
||
async def get_db() -> Database:
|
||
db = Database()
|
||
try:
|
||
yield db
|
||
finally:
|
||
await db.close()
|
||
|
||
|
||
def get_current_user(authorization: str = Query(default="")) -> dict:
|
||
"""简化的用户获取(实际项目中从 JWT 解析)。"""
|
||
# 这里返回一个模拟用户,实际应从 auth 依赖获取
|
||
return {"id": "demo_user", "username": "demo"}
|
||
|
||
|
||
# ── 请求模型 ──────────────────────────────────────────────────────────
|
||
|
||
class EnrollRequest(BaseModel):
|
||
pass
|
||
|
||
|
||
class LessonProgressRequest(BaseModel):
|
||
watch_seconds: int = 0
|
||
|
||
|
||
class LessonCompleteRequest(BaseModel):
|
||
quiz_score: Optional[int] = None
|
||
|
||
|
||
class CourseCreateRequest(BaseModel):
|
||
title: str
|
||
subtitle: str = ""
|
||
description: str = ""
|
||
cover_url: str = ""
|
||
category: str = ""
|
||
level: str = "beginner"
|
||
price: int = 0
|
||
duration_minutes: int = 0
|
||
instructor_name: str = ""
|
||
instructor_title: str = ""
|
||
instructor_avatar: str = ""
|
||
tags: list = []
|
||
prerequisites: str = ""
|
||
learning_outcomes: str = ""
|
||
status: str = "draft"
|
||
is_featured: int = 0
|
||
sort_order: int = 0
|
||
|
||
|
||
class ChapterCreateRequest(BaseModel):
|
||
course_id: str
|
||
title: str
|
||
description: str = ""
|
||
sort_order: int = 0
|
||
|
||
|
||
class LessonCreateRequest(BaseModel):
|
||
chapter_id: str
|
||
course_id: str
|
||
title: str
|
||
type: str = "video"
|
||
content: str = ""
|
||
duration_minutes: int = 0
|
||
sort_order: int = 0
|
||
is_free: int = 0
|
||
is_exam: int = 0
|
||
|
||
|
||
# ── 课程分类常量 ───────────────────────────────────────────────────────
|
||
|
||
COURSE_CATEGORIES = [
|
||
{"code": "ui_design", "name": "UI设计", "icon": "PaletteFilled"},
|
||
{"code": "graphic_design", "name": "平面设计", "icon": "ImageFilled"},
|
||
{"code": "frontend_dev", "name": "前端开发", "icon": "CodeFilled"},
|
||
{"code": "backend_dev", "name": "后端开发", "icon": "ServerFilled"},
|
||
{"code": "copywriting", "name": "文案写作", "icon": "PenFilled"},
|
||
{"code": "translation", "name": "翻译", "icon": "LanguagesFilled"},
|
||
{"code": "data_analysis", "name": "数据分析", "icon": "ChartBarFilled"},
|
||
{"code": "project_management", "name": "项目管理", "icon": "TrendingUpFilled"},
|
||
]
|
||
|
||
LEVEL_LABELS = {
|
||
"beginner": "入门",
|
||
"intermediate": "进阶",
|
||
"advanced": "高级",
|
||
"expert": "专家",
|
||
}
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
# 公开:课程浏览
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
|
||
@router.get("/courses")
|
||
async def list_courses(
|
||
category: str = "",
|
||
level: str = "",
|
||
search: str = "",
|
||
featured_only: bool = False,
|
||
db: Database = Depends(get_db),
|
||
):
|
||
"""课程列表(公开,仅返回已发布课程)。"""
|
||
courses = await db.courses.list(
|
||
category=category, level=level, status="published",
|
||
search=search, featured_only=featured_only,
|
||
)
|
||
# 附加分类名称和等级名称
|
||
for c in courses:
|
||
cat = next((x for x in COURSE_CATEGORIES if x["code"] == c["category"]), None)
|
||
c["category_name"] = cat["name"] if cat else c["category"]
|
||
c["level_name"] = LEVEL_LABELS.get(c["level"], c["level"])
|
||
return {"items": courses, "total": len(courses)}
|
||
|
||
|
||
@router.get("/courses/categories")
|
||
async def list_course_categories(db: Database = Depends(get_db)):
|
||
"""课程分类列表(含课程数统计)。"""
|
||
courses = await db.courses.list(status="published")
|
||
cat_counts = {}
|
||
for c in courses:
|
||
cat_counts[c["category"]] = cat_counts.get(c["category"], 0) + 1
|
||
result = []
|
||
for cat in COURSE_CATEGORIES:
|
||
result.append({
|
||
**cat,
|
||
"course_count": cat_counts.get(cat["code"], 0),
|
||
})
|
||
return {"items": result}
|
||
|
||
|
||
@router.get("/courses/{course_id}")
|
||
async def get_course_detail(course_id: str, db: Database = Depends(get_db)):
|
||
"""课程详情(含章节和课时列表)。"""
|
||
course = await db.courses.get(course_id)
|
||
if not course:
|
||
raise HTTPException(status_code=404, detail="课程不存在")
|
||
cat = next((x for x in COURSE_CATEGORIES if x["code"] == course["category"]), None)
|
||
course["category_name"] = cat["name"] if cat else course["category"]
|
||
course["level_name"] = LEVEL_LABELS.get(course["level"], course["level"])
|
||
|
||
# 获取章节和课时
|
||
chapters = await db.course_chapters.list_by_course(course_id)
|
||
for ch in chapters:
|
||
ch["lessons"] = await db.course_lessons.list_by_chapter(ch["id"])
|
||
course["chapters"] = chapters
|
||
course["total_lessons"] = sum(len(ch["lessons"]) for ch in chapters)
|
||
return course
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
# 我的课程:报名、学习进度
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
|
||
@router.get("/me/courses")
|
||
async def list_my_courses(
|
||
status: str = "",
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
"""我报名的课程列表。"""
|
||
enrollments = await db.course_enrollments.list_by_user(user["id"], status=status)
|
||
# 附加课程信息
|
||
for e in enrollments:
|
||
course = await db.courses.get(e["course_id"])
|
||
if course:
|
||
e["course"] = course
|
||
cat = next((x for x in COURSE_CATEGORIES if x["code"] == course["category"]), None)
|
||
e["course"]["category_name"] = cat["name"] if cat else course["category"]
|
||
return {"items": enrollments, "total": len(enrollments)}
|
||
|
||
|
||
@router.post("/courses/{course_id}/enroll")
|
||
async def enroll_course(
|
||
course_id: str,
|
||
req: EnrollRequest,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
"""报名课程(免费课程直接报名,付费课程需先支付)。"""
|
||
course = await db.courses.get(course_id)
|
||
if not course:
|
||
raise HTTPException(status_code=404, detail="课程不存在")
|
||
if course["status"] != "published":
|
||
raise HTTPException(status_code=400, detail="课程未发布")
|
||
|
||
# 检查是否已报名
|
||
existing = await db.course_enrollments.get(user["id"], course_id)
|
||
if existing:
|
||
return {"ok": True, "item": existing, "already_enrolled": True}
|
||
|
||
# 付费课程需要支付(这里简化,实际应对接支付)
|
||
if course["price"] > 0:
|
||
# TODO: 对接微信支付,返回支付参数
|
||
return {"ok": False, "message": "付费课程需先完成支付", "price": course["price"]}
|
||
|
||
# 统计课程总课时数
|
||
total_lessons = await db.course_lessons.count_by_course(course_id)
|
||
|
||
enrollment = await db.course_enrollments.create(
|
||
user_id=user["id"],
|
||
course_id=course_id,
|
||
status="enrolled",
|
||
progress_percent=0,
|
||
completed_lessons=0,
|
||
total_lessons=total_lessons,
|
||
price_paid=0,
|
||
)
|
||
# 增加课程报名数
|
||
await db.courses.increment_enroll(course_id)
|
||
# 实时通知:报名成功
|
||
from ...services.notification_service import notify as _n
|
||
await _n(db, user["id"], "training", "课程报名成功",
|
||
f"你已报名课程「{course['title']}」,开始学习吧",
|
||
event_code="course.enrolled", level="success", link="/opc/training",
|
||
ref_type="course", ref_id=course_id)
|
||
return {"ok": True, "item": enrollment, "already_enrolled": False}
|
||
|
||
|
||
@router.get("/me/courses/{course_id}/progress")
|
||
async def get_course_progress(
|
||
course_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
"""课程学习进度详情(含每个课时的完成状态)。"""
|
||
enrollment = await db.course_enrollments.get(user["id"], course_id)
|
||
if not enrollment:
|
||
raise HTTPException(status_code=404, detail="未报名该课程")
|
||
|
||
# 获取所有课时的进度
|
||
lessons = await db.course_lessons.list_by_course(course_id)
|
||
progress_list = await db.course_lesson_progress.list_by_enrollment(enrollment["id"])
|
||
progress_map = {p["lesson_id"]: p for p in progress_list}
|
||
|
||
lesson_statuses = []
|
||
for lesson in lessons:
|
||
p = progress_map.get(lesson["id"])
|
||
lesson_statuses.append({
|
||
"lesson_id": lesson["id"],
|
||
"title": lesson["title"],
|
||
"type": lesson["type"],
|
||
"status": p["status"] if p else "not_started",
|
||
"watch_seconds": p["watch_seconds"] if p else 0,
|
||
"completed_at": p["completed_at"] if p else None,
|
||
})
|
||
|
||
return {
|
||
"enrollment": enrollment,
|
||
"lessons": lesson_statuses,
|
||
"completed_count": enrollment["completed_lessons"],
|
||
"total_count": enrollment["total_lessons"],
|
||
"progress_percent": enrollment["progress_percent"],
|
||
}
|
||
|
||
|
||
@router.post("/courses/lessons/{lesson_id}/start")
|
||
async def start_lesson(
|
||
lesson_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
"""开始学习课时。"""
|
||
lesson = await db.course_lessons.get(lesson_id)
|
||
if not lesson:
|
||
raise HTTPException(status_code=404, detail="课时不存在")
|
||
|
||
enrollment = await db.course_enrollments.get(user["id"], lesson["course_id"])
|
||
if not enrollment:
|
||
raise HTTPException(status_code=403, detail="请先报名课程")
|
||
|
||
progress = await db.course_lesson_progress.get_or_create(
|
||
enrollment["id"], user["id"], lesson_id, lesson["course_id"],
|
||
)
|
||
if progress["status"] == "not_started":
|
||
progress = await db.course_lesson_progress.update(
|
||
progress["id"], {"status": "in_progress", "started_at": utcnow_iso()},
|
||
)
|
||
# 更新报名记录的上次学习
|
||
await db.course_enrollments.update(
|
||
enrollment["id"],
|
||
{"last_lesson_id": lesson_id, "last_studied_at": utcnow_iso(), "status": "in_progress"},
|
||
)
|
||
return {"ok": True, "item": progress, "lesson": lesson}
|
||
|
||
|
||
@router.post("/courses/lessons/{lesson_id}/progress")
|
||
async def update_lesson_progress(
|
||
lesson_id: str,
|
||
req: LessonProgressRequest,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
"""更新课时学习进度(watch_seconds 心跳上报)。"""
|
||
lesson = await db.course_lessons.get(lesson_id)
|
||
if not lesson:
|
||
raise HTTPException(status_code=404, detail="课时不存在")
|
||
|
||
enrollment = await db.course_enrollments.get(user["id"], lesson["course_id"])
|
||
if not enrollment:
|
||
raise HTTPException(status_code=403, detail="请先报名课程")
|
||
|
||
progress = await db.course_lesson_progress.get_or_create(
|
||
enrollment["id"], user["id"], lesson_id, lesson["course_id"],
|
||
)
|
||
progress = await db.course_lesson_progress.update(
|
||
progress["id"],
|
||
{"watch_seconds": req.watch_seconds, "status": "in_progress"},
|
||
)
|
||
# 更新总学习时长
|
||
total_watch = enrollment["total_watch_seconds"] + 30 # 每次心跳+30秒
|
||
await db.course_enrollments.update(enrollment["id"], {"total_watch_seconds": total_watch})
|
||
return {"ok": True, "item": progress}
|
||
|
||
|
||
@router.post("/courses/lessons/{lesson_id}/complete")
|
||
async def complete_lesson(
|
||
lesson_id: str,
|
||
req: LessonCompleteRequest,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
"""完成课时(自动更新课程进度)。"""
|
||
lesson = await db.course_lessons.get(lesson_id)
|
||
if not lesson:
|
||
raise HTTPException(status_code=404, detail="课时不存在")
|
||
|
||
enrollment = await db.course_enrollments.get(user["id"], lesson["course_id"])
|
||
if not enrollment:
|
||
raise HTTPException(status_code=403, detail="请先报名课程")
|
||
|
||
progress = await db.course_lesson_progress.get_or_create(
|
||
enrollment["id"], user["id"], lesson_id, lesson["course_id"],
|
||
)
|
||
if progress["status"] != "completed":
|
||
progress = await db.course_lesson_progress.update(
|
||
progress["id"],
|
||
{"status": "completed", "completed_at": utcnow_iso(), "quiz_score": req.quiz_score or 0},
|
||
)
|
||
|
||
# 重新计算课程进度
|
||
completed = await db.course_lesson_progress.count_completed(enrollment["id"])
|
||
total = enrollment["total_lessons"] or 1
|
||
progress_percent = int((completed / total) * 100)
|
||
status = "completed" if progress_percent >= 100 else "in_progress"
|
||
await db.course_enrollments.update(
|
||
enrollment["id"],
|
||
{"completed_lessons": completed, "progress_percent": progress_percent, "status": status},
|
||
)
|
||
return {"ok": True, "item": progress, "course_progress": progress_percent, "completed": completed, "total": total}
|
||
|
||
|
||
@router.post("/courses/{course_id}/complete")
|
||
async def complete_course(
|
||
course_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
"""完成课程(检查全部课时完成,颁发结业证书)。"""
|
||
course = await db.courses.get(course_id)
|
||
if not course:
|
||
raise HTTPException(status_code=404, detail="课程不存在")
|
||
|
||
enrollment = await db.course_enrollments.get(user["id"], course_id)
|
||
if not enrollment:
|
||
raise HTTPException(status_code=403, detail="请先报名课程")
|
||
|
||
# 检查是否所有课时都完成
|
||
completed = await db.course_lesson_progress.count_completed(enrollment["id"])
|
||
total = enrollment["total_lessons"] or 0
|
||
if completed < total:
|
||
raise HTTPException(status_code=400, detail=f"还有 {total - completed} 个课时未完成")
|
||
|
||
# 检查是否已有证书
|
||
existing_cert = await db.course_certificates.get_by_user_course(user["id"], course_id)
|
||
if existing_cert:
|
||
return {"ok": True, "certificate": existing_cert, "already_issued": True}
|
||
|
||
# 生成证书编号
|
||
now = utcnow_iso()
|
||
date_str = now[:10].replace("-", "")
|
||
cert_no = f"OPC-CC-{date_str}-{new_id('')[:4].upper()}"
|
||
|
||
certificate = await db.course_certificates.create(
|
||
certificate_no=cert_no,
|
||
user_id=user["id"],
|
||
course_id=course_id,
|
||
course_title=course["title"],
|
||
instructor_name=course["instructor_name"],
|
||
duration_minutes=enrollment["total_watch_seconds"] // 60,
|
||
final_score=0,
|
||
issued_at=now,
|
||
is_verified=1,
|
||
)
|
||
|
||
# 更新报名状态
|
||
await db.course_enrollments.update(
|
||
enrollment["id"],
|
||
{"status": "completed", "completed_at": now, "progress_percent": 100},
|
||
)
|
||
|
||
# 信用事件:课程结业(+30,按课程去重)+ 结业证书(+10)
|
||
from ...services.credit_engine import on_credit_event
|
||
await on_credit_event(
|
||
db, user_id=user["id"], event_code="training.completed",
|
||
ref_type="course", ref_id=course_id,
|
||
reason=f"完成培训课程「{course['title']}」",
|
||
)
|
||
await on_credit_event(
|
||
db, user_id=user["id"], event_code="training.cert_issued",
|
||
ref_type="training_cert", ref_id=certificate["id"],
|
||
reason=f"获得结业证书 {cert_no}",
|
||
)
|
||
|
||
# 实时通知:结业 + 证书 + 徽章
|
||
from ...services.notification_service import notify as _n
|
||
await _n(db, user["id"], "training", "课程结业",
|
||
f"你已完成「{course['title']}」全部课时,结业证书已颁发({cert_no})",
|
||
event_code="course.graduated", level="success", link="/opc/training",
|
||
ref_type="course", ref_id=course_id)
|
||
await _n(db, user["id"], "training", "获得结业证书",
|
||
f"证书编号 {cert_no},可在培训中心查看",
|
||
event_code="training.cert_issued", level="success", link="/opc/training",
|
||
ref_type="training_cert", ref_id=certificate["id"])
|
||
|
||
return {"ok": True, "certificate": certificate, "already_issued": False}
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
# 结业证书
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
|
||
@router.get("/me/certificates")
|
||
async def list_my_certificates(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(get_current_user),
|
||
):
|
||
"""我的结业证书列表。"""
|
||
certs = await db.course_certificates.list_by_user(user["id"])
|
||
return {"items": certs, "total": len(certs)}
|
||
|
||
|
||
@router.get("/certificates/{certificate_id}")
|
||
async def get_certificate(certificate_id: str, db: Database = Depends(get_db)):
|
||
"""证书详情(公开可验证)。"""
|
||
cert = await db.course_certificates.get(certificate_id)
|
||
if not cert:
|
||
raise HTTPException(status_code=404, detail="证书不存在")
|
||
return cert
|
||
|
||
|
||
@router.get("/certificates/verify/{certificate_no}")
|
||
async def verify_certificate(certificate_no: str, db: Database = Depends(get_db)):
|
||
"""公开验证证书。"""
|
||
cert = await db.course_certificates.get_by_no(certificate_no)
|
||
if not cert:
|
||
return {"valid": False, "message": "证书不存在"}
|
||
return {"valid": True, "certificate": cert}
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
# 运营端:课程管理
|
||
# ══════════════════════════════════════════════════════════════════════
|
||
|
||
@router.get("/admin/courses")
|
||
async def admin_list_courses(
|
||
status: str = "",
|
||
category: str = "",
|
||
db: Database = Depends(get_db),
|
||
):
|
||
"""运营端:课程列表(含草稿)。"""
|
||
courses = await db.courses.list(category=category, status=status if status else "")
|
||
return {"items": courses, "total": len(courses)}
|
||
|
||
|
||
@router.post("/admin/courses")
|
||
async def admin_create_course(
|
||
req: CourseCreateRequest,
|
||
db: Database = Depends(get_db),
|
||
):
|
||
"""运营端:创建课程。"""
|
||
course = await db.courses.create(**req.model_dump())
|
||
return {"ok": True, "item": course}
|
||
|
||
|
||
@router.put("/admin/courses/{course_id}")
|
||
async def admin_update_course(
|
||
course_id: str,
|
||
req: CourseCreateRequest,
|
||
db: Database = Depends(get_db),
|
||
):
|
||
"""运营端:编辑课程。"""
|
||
course = await db.courses.update(course_id, req.model_dump())
|
||
if not course:
|
||
raise HTTPException(status_code=404, detail="课程不存在")
|
||
return {"ok": True, "item": course}
|
||
|
||
|
||
@router.delete("/admin/courses/{course_id}")
|
||
async def admin_delete_course(course_id: str, db: Database = Depends(get_db)):
|
||
"""运营端:删除课程(软删除,改为 archived)。"""
|
||
course = await db.courses.update(course_id, {"status": "archived"})
|
||
if not course:
|
||
raise HTTPException(status_code=404, detail="课程不存在")
|
||
return {"ok": True, "item": course}
|
||
|
||
|
||
@router.post("/admin/courses/{course_id}/publish")
|
||
async def admin_publish_course(course_id: str, db: Database = Depends(get_db)):
|
||
"""运营端:发布课程。"""
|
||
course = await db.courses.update(course_id, {"status": "published"})
|
||
if not course:
|
||
raise HTTPException(status_code=404, detail="课程不存在")
|
||
return {"ok": True, "item": course}
|
||
|
||
|
||
# ── 章节管理 ──────────────────────────────────────────────────────────
|
||
|
||
@router.post("/admin/courses/{course_id}/chapters")
|
||
async def admin_create_chapter(
|
||
course_id: str,
|
||
req: ChapterCreateRequest,
|
||
db: Database = Depends(get_db),
|
||
):
|
||
"""运营端:添加章节。"""
|
||
chapter = await db.course_chapters.create(course_id=course_id, **req.model_dump(exclude={"course_id"}))
|
||
# 更新课程章节数
|
||
course = await db.courses.get(course_id)
|
||
if course:
|
||
chapters = await db.course_chapters.list_by_course(course_id)
|
||
await db.courses.update(course_id, {"chapter_count": len(chapters)})
|
||
return {"ok": True, "item": chapter}
|
||
|
||
|
||
@router.put("/admin/chapters/{chapter_id}")
|
||
async def admin_update_chapter(
|
||
chapter_id: str,
|
||
req: ChapterCreateRequest,
|
||
db: Database = Depends(get_db),
|
||
):
|
||
"""运营端:编辑章节。"""
|
||
chapter = await db.course_chapters.update(chapter_id, req.model_dump())
|
||
if not chapter:
|
||
raise HTTPException(status_code=404, detail="章节不存在")
|
||
return {"ok": True, "item": chapter}
|
||
|
||
|
||
@router.delete("/admin/chapters/{chapter_id}")
|
||
async def admin_delete_chapter(chapter_id: str, db: Database = Depends(get_db)):
|
||
"""运营端:删除章节。"""
|
||
chapter = await db.course_chapters.get(chapter_id)
|
||
if not chapter:
|
||
raise HTTPException(status_code=404, detail="章节不存在")
|
||
course_id = chapter["course_id"]
|
||
await db.course_chapters.delete(chapter_id)
|
||
# 更新课程章节数
|
||
chapters = await db.course_chapters.list_by_course(course_id)
|
||
await db.courses.update(course_id, {"chapter_count": len(chapters)})
|
||
return {"ok": True}
|
||
|
||
|
||
# ── 课时管理 ──────────────────────────────────────────────────────────
|
||
|
||
@router.post("/admin/chapters/{chapter_id}/lessons")
|
||
async def admin_create_lesson(
|
||
chapter_id: str,
|
||
req: LessonCreateRequest,
|
||
db: Database = Depends(get_db),
|
||
):
|
||
"""运营端:添加课时。"""
|
||
lesson = await db.course_lessons.create(chapter_id=chapter_id, **req.model_dump(exclude={"chapter_id"}))
|
||
# 更新课程课时数
|
||
course = await db.courses.get(req.course_id)
|
||
if course:
|
||
total = await db.course_lessons.count_by_course(req.course_id)
|
||
await db.courses.update(req.course_id, {"lesson_count": total})
|
||
return {"ok": True, "item": lesson}
|
||
|
||
|
||
@router.put("/admin/lessons/{lesson_id}")
|
||
async def admin_update_lesson(
|
||
lesson_id: str,
|
||
req: LessonCreateRequest,
|
||
db: Database = Depends(get_db),
|
||
):
|
||
"""运营端:编辑课时。"""
|
||
lesson = await db.course_lessons.update(lesson_id, req.model_dump())
|
||
if not lesson:
|
||
raise HTTPException(status_code=404, detail="课时不存在")
|
||
return {"ok": True, "item": lesson}
|
||
|
||
|
||
@router.delete("/admin/lessons/{lesson_id}")
|
||
async def admin_delete_lesson(lesson_id: str, db: Database = Depends(get_db)):
|
||
"""运营端:删除课时。"""
|
||
lesson = await db.course_lessons.get(lesson_id)
|
||
if not lesson:
|
||
raise HTTPException(status_code=404, detail="课时不存在")
|
||
course_id = lesson["course_id"]
|
||
await db.course_lessons.delete(lesson_id)
|
||
# 更新课程课时数
|
||
total = await db.course_lessons.count_by_course(course_id)
|
||
await db.courses.update(course_id, {"lesson_count": total})
|
||
return {"ok": True}
|
||
|
||
|
||
# ── 运营端统计 ────────────────────────────────────────────────────────
|
||
|
||
@router.get("/admin/courses/{course_id}/enrollments")
|
||
async def admin_course_enrollments(
|
||
course_id: str,
|
||
db: Database = Depends(get_db),
|
||
):
|
||
"""运营端:课程报名统计。"""
|
||
enrollments = await db.course_enrollments.list_by_course(course_id)
|
||
completed = sum(1 for e in enrollments if e["status"] == "completed")
|
||
in_progress = sum(1 for e in enrollments if e["status"] == "in_progress")
|
||
return {
|
||
"items": enrollments,
|
||
"total": len(enrollments),
|
||
"completed": completed,
|
||
"in_progress": in_progress,
|
||
"enrolled": len(enrollments) - completed - in_progress,
|
||
}
|
||
|
||
|
||
@router.get("/admin/certificates")
|
||
async def admin_list_certificates(db: Database = Depends(get_db)):
|
||
"""运营端:证书列表。"""
|
||
certs = await db.course_certificates.list_all()
|
||
return {"items": certs, "total": len(certs)}
|