from adrf.serializers import ( ModelSerializer, SerializerMethodField, ) from asgiref.sync import sync_to_async from .models import Course, Chapter, ChapterContent, CourseMaterial def _author_display(obj): """同步取作者展示名(跑在 sync_to_async 线程里,避免 ASGI 下 SynchronousOnlyOperation)。""" try: author = obj.author except Exception: return '' if author is None: return '' try: full = author.get_full_name() except Exception: full = '' try: username = author.username except Exception: username = '' return full or username or '' def parse_duration_minutes(raw): """把章节 duration 字符串解析为分钟数;解析不出返回 None。 存量格式为中文字符串(如 '30分钟'),纯数字视为分钟。 total_minutes 只对可解析章节求和,不可解析章节不计入(不伪造)。 """ import re if raw is None: return None s = str(raw).strip() if not s: return None m = re.search(r'(\d+(?:\.\d+)?)', s) if not m: return None try: return int(float(m.group(1))) except (ValueError, OverflowError): return None def _course_total_minutes(obj): """同步聚合课程总分钟数(跑在 sync_to_async 线程里)。""" try: durations = list(obj.chapters.values_list('duration', flat=True)) except Exception: return None total = 0 any_parsed = False for d in durations: m = parse_duration_minutes(d) if m is not None: total += m any_parsed = True return total if any_parsed else None class ChapterSerializer(ModelSerializer): video_poster_url = SerializerMethodField() video_local_url = SerializerMethodField() class Meta: model = Chapter fields = ['id', 'title', 'sort_order', 'duration', 'is_free', 'video_url', 'video_source', 'video_local_url', 'video_poster_url', 'created_at', 'updated_at'] async def get_video_poster_url(self, obj): if obj.video_poster: request = self.context.get('request') if request: return request.build_absolute_uri(obj.video_poster.url) return obj.video_poster.url return '' async def get_video_local_url(self, obj): if obj.video_local: request = self.context.get('request') if request: return request.build_absolute_uri(obj.video_local.url) return obj.video_local.url return '' class ChapterContentSerializer(ModelSerializer): class Meta: model = ChapterContent fields = ['id', 'chapter', 'content_md', 'content_html', 'md_file_path', 'updated_at'] class MaterialSerializer(ModelSerializer): file_url = SerializerMethodField() file_size_display = SerializerMethodField() file_type_display = SerializerMethodField() class Meta: model = CourseMaterial fields = ['id', 'title', 'file_url', 'file_type', 'file_type_display', 'file_size', 'file_size_display', 'download_count', 'sort_order', 'created_at'] async def get_file_url(self, obj): if obj.file: request = self.context.get('request') if request: return request.build_absolute_uri(obj.file.url) return obj.file.url return '' async def get_file_size_display(self, obj): size = obj.file_size if size < 1024: return f'{size} B' elif size < 1024 * 1024: return f'{size / 1024:.1f} KB' else: return f'{size / (1024 * 1024):.1f} MB' async def get_file_type_display(self, obj): type_map = { 'pdf': 'PDF', 'code': '代码', 'doc': '文档', 'video': '视频', 'dataset': '数据集', 'zip': '压缩包', 'other': '其他', } return type_map.get(obj.file_type, '其他') class ChapterSerializer(ModelSerializer): video_poster_url = SerializerMethodField() class Meta: model = Chapter fields = ['id', 'title', 'sort_order', 'duration', 'is_free', 'video_url', 'video_poster_url', 'created_at', 'updated_at'] async def get_video_poster_url(self, obj): if obj.video_poster: request = self.context.get('request') if request: return request.build_absolute_uri(obj.video_poster.url) return obj.video_poster.url return '' class ChapterContentSerializer(ModelSerializer): class Meta: model = ChapterContent fields = ['id', 'chapter', 'content_md', 'content_html', 'md_file_path', 'updated_at'] class CourseListSerializer(ModelSerializer): author_name = SerializerMethodField() chapters_count = SerializerMethodField() students_count = SerializerMethodField() total_minutes = SerializerMethodField() cover_image_url = SerializerMethodField() class Meta: model = Course fields = [ 'id', 'title', 'description', 'category', 'level', 'cover_image_url', 'color', 'icon_name', 'author_name', 'chapters_count', 'students_count', 'total_minutes', 'status', 'sort_order', 'is_hot', 'is_new', 'created_at', 'updated_at', ] async def get_chapters_count(self, obj): return await obj.chapters.acount() async def get_students_count(self, obj): # 从章节阅读记录去重统计真实在学人数 from user.models import FUser return await FUser.objects.filter( chapter_reads__course=obj ).distinct().acount() async def get_total_minutes(self, obj): # 可解析章节 duration 求和;全不可解析返回 None(前端不显示,不伪造) return await sync_to_async(_course_total_minutes)(obj) async def get_cover_image_url(self, obj): if obj.cover_image: request = self.context.get('request') if request: return request.build_absolute_uri(obj.cover_image.url) return obj.cover_image.url return '' async def get_author_name(self, obj): return await sync_to_async(_author_display)(obj) class CourseDetailSerializer(ModelSerializer): author_name = SerializerMethodField() chapters = ChapterSerializer(many=True, read_only=True) chapters_count = SerializerMethodField() students_count = SerializerMethodField() total_minutes = SerializerMethodField() cover_image_url = SerializerMethodField() class Meta: model = Course fields = [ 'id', 'title', 'description', 'category', 'level', 'cover_image_url', 'color', 'icon_name', 'author_name', 'chapters', 'chapters_count', 'students_count', 'total_minutes', 'status', 'sort_order', 'is_hot', 'is_new', 'created_at', 'updated_at', ] async def get_chapters_count(self, obj): return await obj.chapters.acount() async def get_students_count(self, obj): # 从章节阅读记录去重统计真实在学人数 from user.models import FUser return await FUser.objects.filter( chapter_reads__course=obj ).distinct().acount() async def get_total_minutes(self, obj): return await sync_to_async(_course_total_minutes)(obj) async def get_cover_image_url(self, obj): if obj.cover_image: request = self.context.get('request') if request: return request.build_absolute_uri(obj.cover_image.url) return obj.cover_image.url return '' async def get_author_name(self, obj): return await sync_to_async(_author_display)(obj) class CourseCreateUpdateSerializer(ModelSerializer): class Meta: model = Course fields = ['title', 'description', 'category', 'level', 'cover_image', 'color', 'icon_name', 'status', 'sort_order', 'is_hot', 'is_new'] async def acreate(self, validated_data): validated_data['author'] = self.context['request'].user return await super().acreate(validated_data) class CourseManageSerializer(ModelSerializer): author_name = SerializerMethodField() chapters_count = SerializerMethodField() class Meta: model = Course fields = [ 'id', 'title', 'category', 'level', 'status', 'sort_order', 'is_hot', 'is_new', 'chapters_count', 'author_name', 'created_at', 'updated_at', ] async def get_chapters_count(self, obj): return await obj.chapters.acount() async def get_author_name(self, obj): if obj.author: return obj.author.get_full_name() or obj.author.username return ''