import os from django.db import models from django.conf import settings CATEGORY_CHOICES = [ ('frontend', '前端'), ('backend', '后端'), ('tools', '工具'), ('devops', '运维'), ('security', '安全'), ('basic', '基础'), ] LEVEL_CHOICES = [ ('beginner', '入门'), ('advanced', '进阶'), ] STATUS_CHOICES = [ ('published', '已发布'), ('draft', '草稿'), ] class Course(models.Model): title = models.CharField(max_length=200, verbose_name='标题') description = models.TextField(blank=True, default='', verbose_name='描述') category = models.CharField(max_length=20, choices=CATEGORY_CHOICES, default='frontend') level = models.CharField(max_length=10, choices=LEVEL_CHOICES, default='beginner') cover_image = models.ImageField(upload_to='learn/covers/%Y/%m/', blank=True, null=True) color = models.CharField(max_length=20, default='#667eea', verbose_name='主题色') icon_name = models.CharField(max_length=50, default='CodeOutlined', verbose_name='图标名称') author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='courses', verbose_name='作者' ) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft') sort_order = models.IntegerField(default=0) is_hot = models.BooleanField(default=False) is_new = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['-sort_order', '-created_at'] verbose_name = '课程' verbose_name_plural = '课程' def __str__(self): return self.title class Chapter(models.Model): VIDEO_SOURCE_CHOICES = [ ('bilibili', 'B站视频'), ('local', '本地视频'), ] course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='chapters', verbose_name='课程') title = models.CharField(max_length=200) sort_order = models.IntegerField(default=0) duration = models.CharField(max_length=50, blank=True, default='', verbose_name='时长') is_free = models.BooleanField(default=True) video_url = models.URLField(max_length=500, blank=True, default='', verbose_name='视频链接') video_source = models.CharField(max_length=20, choices=VIDEO_SOURCE_CHOICES, default='bilibili', verbose_name='视频来源') video_local = models.FileField(upload_to='learn/videos/%Y/%m/', blank=True, null=True, verbose_name='本地视频文件') video_poster = models.ImageField(upload_to='learn/video_posters/%Y/%m/', blank=True, null=True, verbose_name='视频封面') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['sort_order'] verbose_name = '章节' verbose_name_plural = '章节' def __str__(self): return self.title class ChapterContent(models.Model): chapter = models.OneToOneField(Chapter, on_delete=models.CASCADE, related_name='content', verbose_name='章节') content_md = models.TextField(blank=True, default='', verbose_name='Markdown内容') content_html = models.TextField(blank=True, default='', verbose_name='预渲染HTML') md_file_path = models.CharField(max_length=500, blank=True, default='', verbose_name='MD文件路径') updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = '章节内容' verbose_name_plural = '章节内容' def __str__(self): return f'{self.chapter.title} - 内容' def save(self, *args, **kwargs): super().save(*args, **kwargs) if self.content_md: try: import markdown self.content_html = markdown.markdown( self.content_md, extensions=['extra', 'codehilite', 'toc', 'tables'] ) except ImportError: self.content_html = self.content_md.replace('\n', '
') ChapterContent.objects.filter(pk=self.pk).update(content_html=self.content_html) self.export_md_file() def export_md_file(self): if not self.content_md: return course_id = self.chapter.course_id chapter_id = self.chapter_id file_path = os.path.join( settings.MEDIA_ROOT, 'learn', 'courses', str(course_id), 'chapters', f'{chapter_id}.md' ) os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'w', encoding='utf-8') as f: f.write(self.content_md) relative_path = os.path.relpath(file_path, settings.MEDIA_ROOT).replace('\\', '/') ChapterContent.objects.filter(pk=self.pk).update(md_file_path=relative_path) class ChapterRead(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='chapter_reads', verbose_name='用户' ) chapter = models.ForeignKey( Chapter, on_delete=models.CASCADE, related_name='chapter_reads', verbose_name='章节' ) course = models.ForeignKey( Course, on_delete=models.CASCADE, related_name='chapter_reads', verbose_name='课程' ) completed = models.BooleanField(default=True, verbose_name='是否已完成') completed_at = models.DateTimeField(auto_now_add=True, verbose_name='完成时间') updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间') class Meta: verbose_name = '章节学习记录' verbose_name_plural = '章节学习记录' unique_together = ['user', 'chapter'] ordering = ['-completed_at'] def __str__(self): return f'{self.user} read {self.chapter}' class CourseFavorite(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='course_favorites', verbose_name='用户' ) course = models.ForeignKey( Course, on_delete=models.CASCADE, related_name='course_favorites', verbose_name='课程' ) created_at = models.DateTimeField( auto_now_add=True, verbose_name='创建时间' ) class Meta: verbose_name = '课程收藏' verbose_name_plural = '课程收藏' unique_together = ['user', 'course'] ordering = ['-created_at'] def __str__(self): return f'{self.user} favorites {self.course}' class CourseMaterial(models.Model): FILE_TYPE_CHOICES = [ ('pdf', 'PDF'), ('code', '代码'), ('doc', '文档'), ('video', '视频'), ('dataset', '数据集'), ('zip', '压缩包'), ('other', '其他'), ] chapter = models.ForeignKey( Chapter, on_delete=models.CASCADE, related_name='materials', verbose_name='章节' ) title = models.CharField(max_length=200, verbose_name='资料名称') file = models.FileField(upload_to='learn/materials/%Y/%m/', verbose_name='文件') file_type = models.CharField(max_length=20, choices=FILE_TYPE_CHOICES, default='other', verbose_name='文件类型') file_size = models.PositiveIntegerField(default=0, verbose_name='文件大小(字节)') download_count = models.PositiveIntegerField(default=0, verbose_name='下载次数') sort_order = models.IntegerField(default=0, verbose_name='排序') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Meta: ordering = ['sort_order'] verbose_name = '课程资料' verbose_name_plural = '课程资料' def __str__(self): return self.title