Files
chunyu_project/learn/views.py
T

809 lines
32 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 os
from django.conf import settings
from django.db.models import Count
from django.utils import timezone
from django.http import FileResponse, Http404
from adrf import generics
from adrf.generics import aget_object_or_404
from adrf.mixins import get_data
from adrf.views import APIView
from asgiref.sync import sync_to_async
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from chunyu_project.common_schemas import success_response, error_response, unauthorized_response, not_found_response
from utils.response_codes import ResponseCode, create_standardized_response, create_standardized_error_response
from .models import Course, Chapter, ChapterContent, CourseFavorite, ChapterRead, CourseMaterial
from .serializers import (
CourseListSerializer, CourseDetailSerializer,
CourseCreateUpdateSerializer, CourseManageSerializer,
ChapterSerializer, ChapterContentSerializer, MaterialSerializer,
)
class CourseListCreateView(generics.ListCreateAPIView):
parser_classes = [JSONParser, MultiPartParser, FormParser]
def get_permissions(self):
if self.request.method == 'GET':
return [AllowAny()]
return [IsAuthenticated()]
def get_serializer_class(self):
if self.request.method == 'POST':
return CourseCreateUpdateSerializer
return CourseListSerializer
def get_queryset(self):
qs = Course.objects.filter(status='published')
category = self.request.query_params.get('category')
if category and category != 'all':
qs = qs.filter(category=category)
level = self.request.query_params.get('level')
if level and level != 'all':
qs = qs.filter(level=level)
sort = self.request.query_params.get('sort', '')
if sort == 'created_at':
qs = qs.order_by('created_at')
elif sort == '-created_at':
qs = qs.order_by('-created_at')
elif sort == 'updated_at':
qs = qs.order_by('-updated_at')
elif sort == '-updated_at':
qs = qs.order_by('-updated_at')
elif sort == 'sort_order':
qs = qs.order_by('-sort_order')
elif sort == '-sort_order':
qs = qs.order_by('sort_order')
elif sort == 'popularity':
qs = qs.annotate(favorites_count=Count('course_favorites')).order_by('favorites_count')
elif sort == '-popularity':
qs = qs.annotate(favorites_count=Count('course_favorites')).order_by('-favorites_count')
return qs
async def get(self, request, *args, **kwargs):
return await self.list(request, *args, **kwargs)
async def post(self, request, *args, **kwargs):
return await self.create(request, *args, **kwargs)
@swagger_auto_schema(
tags=['学习'],
operation_summary='获取课程列表',
operation_description='获取已发布课程列表,支持按分类筛选和排序,登录用户可查看收藏状态',
manual_parameters=[
openapi.Parameter('category', openapi.IN_QUERY, description='课程分类筛选(frontend/backend/tools/devops/security/basic)', type=openapi.TYPE_STRING),
openapi.Parameter('level', openapi.IN_QUERY, description='课程难度筛选(beginner=入门/advanced=进阶)', type=openapi.TYPE_STRING),
openapi.Parameter('sort', openapi.IN_QUERY, description='排序方式:created_at(正序)、-created_at(倒序,默认最新)、-updated_at(最近更新)、-popularity(最热,按收藏数)', type=openapi.TYPE_STRING),
],
responses={200: success_response},
)
async def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
data = await get_data(serializer)
if request.user.is_authenticated:
favorite_course_ids = set([
v async for v in CourseFavorite.objects.filter(user=request.user).values_list('course_id', flat=True)
])
for item in data:
item['is_favorited'] = item['id'] in favorite_course_ids
item['favorites_count'] = await CourseFavorite.objects.filter(course_id=item['id']).acount()
else:
for item in data:
item['is_favorited'] = False
item['favorites_count'] = await CourseFavorite.objects.filter(course_id=item['id']).acount()
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
@swagger_auto_schema(
tags=['学习'],
operation_summary='创建课程',
operation_description='创建新课程,需要登录',
request_body=CourseCreateUpdateSerializer,
responses={
201: success_response,
400: error_response,
401: unauthorized_response,
},
)
async def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
await serializer.asave()
data = await get_data(serializer)
return create_standardized_response(
data=data,
code=ResponseCode.SUCCESS,
message='课程创建成功',
status_code=status.HTTP_201_CREATED
)
return create_standardized_error_response(
data=serializer.errors,
code=ResponseCode.VALIDATION_ERROR,
message='提交失败',
status_code=status.HTTP_400_BAD_REQUEST
)
class CourseDetailView(APIView):
parser_classes = [JSONParser, MultiPartParser, FormParser]
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['学习'],
operation_summary='获取课程详情',
operation_description='根据课程ID获取课程详细信息,包含章节列表',
manual_parameters=[
openapi.Parameter('pk', openapi.IN_PATH, description='课程ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={
200: success_response,
404: not_found_response,
},
)
async def get(self, request, pk):
course = await aget_object_or_404(Course, pk=pk)
serializer = CourseDetailSerializer(course, context={'request': request})
data = await get_data(serializer)
if request.user.is_authenticated:
data['is_favorited'] = await CourseFavorite.objects.filter(
user=request.user, course=course
).aexists()
else:
data['is_favorited'] = False
data['favorites_count'] = await CourseFavorite.objects.filter(course=course).acount()
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
@swagger_auto_schema(
tags=['学习'],
operation_summary='更新课程',
operation_description='更新课程信息,仅课程作者可操作',
manual_parameters=[
openapi.Parameter('pk', openapi.IN_PATH, description='课程ID', type=openapi.TYPE_INTEGER, required=True),
],
request_body=CourseCreateUpdateSerializer,
responses={
200: success_response,
400: error_response,
401: unauthorized_response,
403: error_response,
404: not_found_response,
},
)
async def put(self, request, pk):
course = await aget_object_or_404(Course, pk=pk)
if course.author != request.user:
return create_standardized_error_response(
code=ResponseCode.VALIDATION_ERROR,
message='无权操作',
status_code=status.HTTP_403_FORBIDDEN
)
serializer = CourseCreateUpdateSerializer(course, data=request.data, partial=True, context={'request': request})
if serializer.is_valid():
await serializer.asave()
data = await get_data(serializer)
return create_standardized_response(data=data, code=ResponseCode.SUCCESS, message='课程更新成功')
return create_standardized_error_response(
data=serializer.errors,
code=ResponseCode.VALIDATION_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
@swagger_auto_schema(
tags=['学习'],
operation_summary='删除课程',
operation_description='删除课程,仅课程作者可操作',
manual_parameters=[
openapi.Parameter('pk', openapi.IN_PATH, description='课程ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={
204: success_response,
401: unauthorized_response,
403: error_response,
404: not_found_response,
},
)
async def delete(self, request, pk):
course = await aget_object_or_404(Course, pk=pk)
if course.author != request.user:
return create_standardized_error_response(
code=ResponseCode.VALIDATION_ERROR,
message='无权操作',
status_code=status.HTTP_403_FORBIDDEN
)
await course.adelete()
return create_standardized_response(code=ResponseCode.SUCCESS, message='课程删除成功', status_code=status.HTTP_204_NO_CONTENT)
class ChapterListCreateView(generics.ListCreateAPIView):
parser_classes = [JSONParser, MultiPartParser]
def get_permissions(self):
if self.request.method == 'GET':
return [AllowAny()]
return [IsAuthenticated()]
serializer_class = ChapterSerializer
def get_queryset(self):
course_id = self.kwargs['course_id']
return Chapter.objects.filter(course_id=course_id)
async def get(self, request, *args, **kwargs):
return await self.list(request, *args, **kwargs)
async def post(self, request, *args, **kwargs):
return await self.create(request, *args, **kwargs)
@swagger_auto_schema(
tags=['学习'],
operation_summary='获取章节列表',
operation_description='获取指定课程下的所有章节',
manual_parameters=[
openapi.Parameter('course_id', openapi.IN_PATH, description='课程ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={200: success_response, 404: not_found_response},
)
async def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
data = await get_data(serializer)
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
@swagger_auto_schema(
tags=['学习'],
operation_summary='创建章节',
operation_description='为指定课程创建新章节,需要登录',
manual_parameters=[
openapi.Parameter('course_id', openapi.IN_PATH, description='课程ID', type=openapi.TYPE_INTEGER, required=True),
],
request_body=ChapterSerializer,
responses={
201: success_response,
400: error_response,
401: unauthorized_response,
404: not_found_response,
},
)
async def create(self, request, *args, **kwargs):
course_id = self.kwargs['course_id']
course = await aget_object_or_404(Course, pk=course_id)
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
await serializer.asave(course=course)
data = await get_data(serializer)
return create_standardized_response(
data=data,
code=ResponseCode.SUCCESS,
message='章节创建成功',
status_code=status.HTTP_201_CREATED
)
return create_standardized_error_response(
data=serializer.errors,
code=ResponseCode.VALIDATION_ERROR,
message='提交失败',
status_code=status.HTTP_400_BAD_REQUEST
)
class ChapterDetailView(APIView):
parser_classes = [JSONParser, MultiPartParser, FormParser]
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
tags=['学习'],
operation_summary='获取章节详情',
operation_description='根据章节ID获取章节详细信息,需要登录',
manual_parameters=[
openapi.Parameter('pk', openapi.IN_PATH, description='章节ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={
200: success_response,
401: unauthorized_response,
404: not_found_response,
},
)
async def get(self, request, pk):
chapter = await aget_object_or_404(Chapter, pk=pk)
serializer = ChapterSerializer(chapter)
data = await get_data(serializer)
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
@swagger_auto_schema(
tags=['学习'],
operation_summary='更新章节',
operation_description='更新章节信息,需要登录',
manual_parameters=[
openapi.Parameter('pk', openapi.IN_PATH, description='章节ID', type=openapi.TYPE_INTEGER, required=True),
],
request_body=ChapterSerializer,
responses={
200: success_response,
400: error_response,
401: unauthorized_response,
404: not_found_response,
},
)
async def put(self, request, pk):
chapter = await aget_object_or_404(Chapter, pk=pk)
serializer = ChapterSerializer(chapter, data=request.data, partial=True)
if serializer.is_valid():
await serializer.asave()
data = await get_data(serializer)
return create_standardized_response(data=data, code=ResponseCode.SUCCESS, message='章节更新成功')
return create_standardized_error_response(
data=serializer.errors,
code=ResponseCode.VALIDATION_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
@swagger_auto_schema(
tags=['学习'],
operation_summary='删除章节',
operation_description='删除章节,需要登录',
manual_parameters=[
openapi.Parameter('pk', openapi.IN_PATH, description='章节ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={
204: success_response,
401: unauthorized_response,
404: not_found_response,
},
)
async def delete(self, request, pk):
chapter = await aget_object_or_404(Chapter, pk=pk)
await chapter.adelete()
return create_standardized_response(code=ResponseCode.SUCCESS, message='章节删除成功', status_code=status.HTTP_204_NO_CONTENT)
class ChapterContentView(APIView):
parser_classes = [JSONParser, MultiPartParser, FormParser]
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
tags=['学习'],
operation_summary='获取章节内容',
operation_description='获取指定章节的Markdown和HTML内容,需要登录',
manual_parameters=[
openapi.Parameter('chapter_id', openapi.IN_PATH, description='章节ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={
200: success_response,
401: unauthorized_response,
404: not_found_response,
},
)
async def get(self, request, chapter_id):
chapter = await aget_object_or_404(Chapter, pk=chapter_id)
content, created = await ChapterContent.objects.aget_or_create(
chapter=chapter,
defaults={'content_md': '', 'content_html': ''}
)
serializer = ChapterContentSerializer(content)
data = await get_data(serializer)
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
@swagger_auto_schema(
tags=['学习'],
operation_summary='更新章节内容',
operation_description='保存或更新章节的Markdown和HTML内容,需要登录',
manual_parameters=[
openapi.Parameter('chapter_id', openapi.IN_PATH, description='章节ID', type=openapi.TYPE_INTEGER, required=True),
],
request_body=ChapterContentSerializer,
responses={
200: success_response,
400: error_response,
401: unauthorized_response,
404: not_found_response,
},
)
async def put(self, request, chapter_id):
chapter = await aget_object_or_404(Chapter, pk=chapter_id)
content, created = await ChapterContent.objects.aget_or_create(
chapter=chapter,
defaults={'content_md': '', 'content_html': ''}
)
serializer = ChapterContentSerializer(content, data=request.data, partial=True)
if serializer.is_valid():
await serializer.asave()
data = await get_data(serializer)
return create_standardized_response(data=data, code=ResponseCode.SUCCESS, message='内容保存成功')
return create_standardized_error_response(
data=serializer.errors,
code=ResponseCode.VALIDATION_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
class MyCourseListView(generics.ListAPIView):
permission_classes = [IsAuthenticated]
serializer_class = CourseManageSerializer
parser_classes = [JSONParser]
def get_queryset(self):
qs = Course.objects.filter(author=self.request.user)
st = self.request.query_params.get('status')
if st:
qs = qs.filter(status=st)
return qs.order_by('-updated_at')
async def get(self, request, *args, **kwargs):
return await self.list(request, *args, **kwargs)
@swagger_auto_schema(
tags=['学习'],
operation_summary='获取我的课程列表',
operation_description='获取当前登录用户创建的课程列表,支持按状态筛选',
manual_parameters=[
openapi.Parameter('status', openapi.IN_QUERY, description='课程状态筛选(draft/published)', type=openapi.TYPE_STRING),
],
responses={
200: success_response,
401: unauthorized_response,
},
)
async def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
data = await get_data(serializer)
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
class MyCourseBatchView(APIView):
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
@swagger_auto_schema(
tags=['学习'],
operation_summary='批量操作课程',
operation_description='批量发布、撤回草稿或删除课程,需要登录',
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
required=['ids', 'action'],
properties={
'ids': openapi.Schema(type=openapi.TYPE_ARRAY, items=openapi.Schema(type=openapi.TYPE_INTEGER), description='课程ID列表'),
'action': openapi.Schema(type=openapi.TYPE_STRING, description='操作类型', enum=['publish', 'draft', 'delete']),
},
),
responses={
200: success_response,
400: error_response,
401: unauthorized_response,
},
)
async def post(self, request):
ids = request.data.get('ids', [])
action = request.data.get('action', '')
if not ids or action not in ('publish', 'draft', 'delete'):
return create_standardized_error_response(
code=ResponseCode.VALIDATION_ERROR,
message='参数错误',
status_code=status.HTTP_400_BAD_REQUEST
)
qs = Course.objects.filter(id__in=ids, author=request.user)
if action == 'delete':
count = (await qs.adelete())[0]
elif action == 'publish':
count = await qs.aupdate(status='published')
elif action == 'draft':
count = await qs.aupdate(status='draft')
return create_standardized_response(
data={'affected': count},
code=ResponseCode.SUCCESS,
message=f'批量操作成功,影响 {count} 个课程'
)
class CDNStaticFileView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
tags=['学习'],
operation_summary='获取章节静态文件',
operation_description='获取课程章节的Markdown静态文件,支持CDN缓存',
manual_parameters=[
openapi.Parameter('course_id', openapi.IN_PATH, description='课程ID', type=openapi.TYPE_INTEGER, required=True),
openapi.Parameter('chapter_id', openapi.IN_PATH, description='章节ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={
200: openapi.Response(description='Markdown文件内容', schema=openapi.Schema(type=openapi.TYPE_FILE)),
404: not_found_response,
},
)
async def get(self, request, course_id, chapter_id):
file_path = os.path.join(
settings.MEDIA_ROOT,
'learn', 'courses', str(course_id), 'chapters', f'{chapter_id}.md'
)
def _open_file():
if not os.path.exists(file_path):
raise Http404
fh = open(file_path, "rb")
try:
response = FileResponse(fh, content_type="text/markdown; charset=utf-8")
response["Cache-Control"] = "max-age=86400"
return response
except Exception:
fh.close()
raise
# TODO: aiohttp 化(当前用 sync_to_async 兜底避免阻塞事件循环)
return await sync_to_async(_open_file)()
class CourseFavoriteToggleView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
tags=['学习'],
operation_summary='切换课程收藏状态',
operation_description='收藏或取消收藏课程,需要登录',
manual_parameters=[
openapi.Parameter('pk', openapi.IN_PATH, description='课程ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={
200: success_response,
401: unauthorized_response,
404: not_found_response,
},
)
async def post(self, request, pk):
course = await Course.objects.filter(pk=pk, status='published').afirst()
if not course:
return create_standardized_error_response(
code=ResponseCode.VALIDATION_ERROR,
message='课程不存在',
status_code=status.HTTP_404_NOT_FOUND
)
fav, created = await CourseFavorite.objects.aget_or_create(user=request.user, course=course)
if not created:
await fav.adelete()
count = await CourseFavorite.objects.filter(course=course).acount()
return create_standardized_response(
data={'favorited': False, 'favorites_count': count}
)
count = await CourseFavorite.objects.filter(course=course).acount()
return create_standardized_response(
data={'favorited': True, 'favorites_count': count}
)
class ChapterMarkCompletedView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
tags=['学习'],
operation_summary='标记章节已读/已完成',
operation_description='将指定章节标记为已读或取消标记,需要登录',
manual_parameters=[
openapi.Parameter('pk', openapi.IN_PATH, description='章节ID', type=openapi.TYPE_INTEGER, required=True),
],
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'completed': openapi.Schema(type=openapi.TYPE_BOOLEAN, description='是否已完成,默认true', default=True),
},
),
responses={
200: success_response,
401: unauthorized_response,
404: not_found_response,
},
)
async def post(self, request, pk):
chapter = await Chapter.objects.filter(pk=pk).select_related('course').afirst()
if not chapter:
return create_standardized_error_response(
code=ResponseCode.VALIDATION_ERROR,
message='章节不存在',
status_code=status.HTTP_404_NOT_FOUND
)
completed = request.data.get('completed', True)
if not isinstance(completed, bool):
completed = str(completed).lower() in ('true', '1', 'yes')
read_record, created = await ChapterRead.objects.aget_or_create(
user=request.user,
chapter=chapter,
defaults={
'course': chapter.course,
'completed': completed,
'completed_at': timezone.now() if completed else None,
}
)
if not created:
read_record.completed = completed
read_record.completed_at = timezone.now() if completed else None
await read_record.asave(update_fields=['completed', 'completed_at', 'updated_at'])
total_chapters = await chapter.course.chapters.acount()
completed_chapters = await ChapterRead.objects.filter(
user=request.user, course=chapter.course, completed=True
).acount()
progress = round((completed_chapters / total_chapters) * 100) if total_chapters > 0 else 0
return create_standardized_response(
data={
'chapter_id': chapter.id,
'completed': completed,
'completed_chapters': completed_chapters,
'total_chapters': total_chapters,
'progress': progress,
}
)
class CourseProgressView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
tags=['学习'],
operation_summary='获取课程学习进度',
operation_description='获取当前用户在该课程下的所有章节完成状态,需要登录',
manual_parameters=[
openapi.Parameter('course_id', openapi.IN_PATH, description='课程ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={
200: success_response,
401: unauthorized_response,
404: not_found_response,
},
)
async def get(self, request, course_id):
course = await Course.objects.filter(pk=course_id, status='published').afirst()
if not course:
return create_standardized_error_response(
code=ResponseCode.VALIDATION_ERROR,
message='课程不存在',
status_code=status.HTTP_404_NOT_FOUND
)
completed_ids = set([
v async for v in ChapterRead.objects.filter(
user=request.user, course=course, completed=True
).values_list('chapter_id', flat=True)
])
total_chapters = await course.chapters.acount()
completed_count = len(completed_ids)
progress = round((completed_count / total_chapters) * 100) if total_chapters > 0 else 0
return create_standardized_response(
data={
'course_id': course.id,
'completed_chapter_ids': list(completed_ids),
'completed_chapters': completed_count,
'total_chapters': total_chapters,
'progress': progress,
}
)
class MaterialListView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
tags=['学习'],
operation_summary='获取章节资料列表',
operation_description='获取指定章节的可下载资料列表',
manual_parameters=[
openapi.Parameter('chapter_id', openapi.IN_QUERY, description='章节ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={200: success_response},
)
async def get(self, request):
chapter_id = request.query_params.get('chapter_id')
if not chapter_id:
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
message='缺少章节ID',
status_code=status.HTTP_400_BAD_REQUEST
)
materials = CourseMaterial.objects.filter(chapter_id=chapter_id)
serializer = MaterialSerializer(materials, many=True, context={'request': request})
data = await get_data(serializer)
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
class MaterialDownloadView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
tags=['学习'],
operation_summary='下载课程资料',
operation_description='下载指定ID的课程资料文件,并增加下载次数统计',
manual_parameters=[
openapi.Parameter('pk', openapi.IN_PATH, description='资料ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={200: openapi.Response(description='文件流'), 404: not_found_response},
)
async def get(self, request, pk):
material = await CourseMaterial.objects.filter(pk=pk).afirst()
if not material or not material.file:
raise Http404
material.download_count += 1
await material.asave(update_fields=['download_count'])
response = FileResponse(material.file.open('rb'), content_type='application/octet-stream')
response['Content-Disposition'] = f'attachment; filename="{material.title}"'
return response
class ChapterVideoDownloadView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
tags=['学习'],
operation_summary='下载章节视频',
operation_description='下载指定章节的本地视频文件',
manual_parameters=[
openapi.Parameter('pk', openapi.IN_PATH, description='章节ID', type=openapi.TYPE_INTEGER, required=True),
],
responses={200: openapi.Response(description='文件流'), 404: not_found_response},
)
async def get(self, request, pk):
chapter = await Chapter.objects.filter(pk=pk).afirst()
if not chapter or not chapter.video_local:
raise Http404
response = FileResponse(chapter.video_local.open('rb'), content_type='application/octet-stream')
response['Content-Disposition'] = f'attachment; filename="{chapter.title}.mp4"'
return response
class MyProgressView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
tags=['学习'],
operation_summary='获取当前用户学习进度',
operation_description='获取当前用户所有课程的学习进度列表,需要登录',
responses={200: success_response, 401: unauthorized_response},
)
async def get(self, request):
user = request.user
reads = ChapterRead.objects.filter(user=user, completed=True).select_related('course', 'chapter')
course_map = {}
async for read in reads:
course_id = read.course.id
if course_id not in course_map:
course_map[course_id] = {
'id': course_id,
'title': read.course.title,
'cover_image': read.course.cover_image.url if read.course.cover_image else '',
'completed_chapters': 0,
'total_chapters': await read.course.chapters.acount(),
'last_studied_at': read.completed_at,
}
course_map[course_id]['completed_chapters'] += 1
if read.completed_at and (
not course_map[course_id]['last_studied_at'] or
read.completed_at > course_map[course_id]['last_studied_at']
):
course_map[course_id]['last_studied_at'] = read.completed_at
courses = []
for c in course_map.values():
total = c['total_chapters']
completed = c['completed_chapters']
c['progress'] = round((completed / total) * 100) if total > 0 else 0
c['last_studied_at'] = c['last_studied_at'].strftime('%Y-%m-%d %H:%M:%S') if c['last_studied_at'] else None
courses.append(c)
courses.sort(key=lambda x: x['last_studied_at'] or '', reverse=True)
total_completed = sum(1 for c in courses if c['progress'] >= 100)
return create_standardized_response(
data={
'courses': courses,
'total_courses': len(courses),
'total_completed': total_completed,
}
)