diff --git a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/models.py b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/models.py index 083788bb..38a5c161 100644 --- a/djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/models.py +++ b/djangoblog/src/DjangoBlog-master/DjangoBlog-master/blog/models.py @@ -1,123 +1,161 @@ +# 导入日志模块,用于记录系统运行日志 import logging +# 导入正则表达式模块,用于处理文本中的匹配(如提取图片URL) import re +# 导入抽象基类相关工具,用于定义抽象方法 from abc import abstractmethod +# 导入Django配置、异常、模型、URL反转等核心功能 from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils.timezone import now -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_lazy as _ # 国际化翻译工具 +# 导入markdown编辑器字段,用于文章内容编辑 from mdeditor.fields import MDTextField +# 导入slug生成工具,用于生成URL友好的标识符 from uuslug import slugify +# 导入自定义工具:缓存装饰器、缓存操作、获取当前站点信息 from djangoblog.utils import cache_decorator, cache from djangoblog.utils import get_current_site +# 创建当前模块的日志记录器 logger = logging.getLogger(__name__) class LinkShowType(models.TextChoices): - I = ('i', _('index')) - L = ('l', _('list')) - P = ('p', _('post')) - A = ('a', _('all')) - S = ('s', _('slide')) + """ + 链接展示位置的枚举类 + 定义友情链接在网站中的展示位置选项 + """ + I = ('i', _('index')) # 首页展示 + L = ('l', _('list')) # 列表页展示 + P = ('p', _('post')) # 文章详情页展示 + A = ('a', _('all')) # 所有页面展示 + S = ('s', _('slide')) # 幻灯片展示 class BaseModel(models.Model): + """ + 模型基类,所有其他模型的父类 + 封装通用字段和方法,减少代码重复 + """ + # 自增主键ID id = models.AutoField(primary_key=True) + # 创建时间字段,默认为当前时间 creation_time = models.DateTimeField(_('creation time'), default=now) + # 最后修改时间字段,默认为当前时间 last_modify_time = models.DateTimeField(_('modify time'), default=now) def save(self, *args, **kwargs): + """ + 重写保存方法,扩展功能: + 1. 单独处理文章阅读量更新(避免更新其他字段) + 2. 自动生成slug(URL友好标识符) + """ + # 判断是否是更新文章阅读量的操作 is_update_views = isinstance( self, Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views'] + if is_update_views: + # 仅更新阅读量字段(优化性能) Article.objects.filter(pk=self.pk).update(views=self.views) else: + # 若模型包含slug字段,则自动生成slug(基于title或name字段) if 'slug' in self.__dict__: - slug = getattr( - self, 'title') if 'title' in self.__dict__ else getattr( - self, 'name') - setattr(self, 'slug', slugify(slug)) + # 优先使用title字段,否则使用name字段作为slug源 + slug_source = getattr(self, 'title') if 'title' in self.__dict__ else getattr(self, 'name') + setattr(self, 'slug', slugify(slug_source)) # 生成URL友好的slug + # 调用父类保存方法 super().save(*args, **kwargs) def get_full_url(self): - site = get_current_site().domain - url = "https://{site}{path}".format(site=site, - path=self.get_absolute_url()) - return url + """生成包含域名的完整URL(用于外部链接或分享)""" + site_domain = get_current_site().domain # 获取当前站点域名 + full_url = f"https://{site_domain}{self.get_absolute_url()}" + return full_url class Meta: - abstract = True + abstract = True # 声明为抽象基类,不生成数据库表 @abstractmethod def get_absolute_url(self): + """ + 抽象方法:获取模型实例的相对URL + 子类必须实现,用于生成详情页链接 + """ pass class Article(BaseModel): - """文章""" + """ + 文章模型 + 存储博客文章的核心信息 + """ + # 文章状态选项:草稿/已发布 STATUS_CHOICES = ( - ('d', _('Draft')), - ('p', _('Published')), + ('d', _('Draft')), # 草稿 + ('p', _('Published')), # 已发布 ) + # 评论状态选项:开启/关闭 COMMENT_STATUS = ( - ('o', _('Open')), - ('c', _('Close')), + ('o', _('Open')), # 允许评论 + ('c', _('Close')), # 关闭评论 ) + # 内容类型选项:文章/页面(如关于页、联系页) TYPE = ( - ('a', _('Article')), - ('p', _('Page')), + ('a', _('Article')), # 普通文章 + ('p', _('Page')), # 独立页面 ) - title = models.CharField(_('title'), max_length=200, unique=True) - body = MDTextField(_('body')) + + title = models.CharField(_('title'), max_length=200, unique=True) # 文章标题(唯一) + body = MDTextField(_('body')) # 文章内容(markdown格式) pub_time = models.DateTimeField( - _('publish time'), blank=False, null=False, default=now) + _('publish time'), blank=False, null=False, default=now) # 发布时间 status = models.CharField( - _('status'), - max_length=1, - choices=STATUS_CHOICES, - default='p') + _('status'), max_length=1, choices=STATUS_CHOICES, default='p') # 文章状态 comment_status = models.CharField( - _('comment status'), - max_length=1, - choices=COMMENT_STATUS, - default='o') - type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') - views = models.PositiveIntegerField(_('views'), default=0) + _('comment status'), max_length=1, choices=COMMENT_STATUS, default='o') # 评论状态 + type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 内容类型 + views = models.PositiveIntegerField(_('views'), default=0) # 阅读量 author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('author'), blank=False, null=False, - on_delete=models.CASCADE) + on_delete=models.CASCADE # 关联用户,用户删除时文章也删除 + ) article_order = models.IntegerField( - _('order'), blank=False, null=False, default=0) - show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) + _('order'), blank=False, null=False, default=0) # 文章排序权重 + show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False) # 是否显示目录 category = models.ForeignKey( 'Category', verbose_name=_('category'), on_delete=models.CASCADE, blank=False, - null=False) - tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) + null=False # 关联分类,分类删除时文章也删除 + ) + tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 多对多关联标签 def body_to_string(self): + """将文章内容转换为字符串(用于搜索等场景)""" return self.body def __str__(self): + """模型实例的字符串表示(用于后台显示)""" return self.title class Meta: - ordering = ['-article_order', '-pub_time'] - verbose_name = _('article') - verbose_name_plural = verbose_name - get_latest_by = 'id' + ordering = ['-article_order', '-pub_time'] # 排序规则:先按排序权重降序,再按发布时间降序 + verbose_name = _('article') # 模型显示名称(单数) + verbose_name_plural = verbose_name # 模型显示名称(复数) + get_latest_by = 'id' # 按id获取最新记录 def get_absolute_url(self): + """生成文章详情页的相对URL""" return reverse('blog:detailbyid', kwargs={ 'article_id': self.id, 'year': self.creation_time.year, @@ -125,160 +163,174 @@ class Article(BaseModel): 'day': self.creation_time.day }) - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_category_tree(self): + """获取当前文章所属分类的层级树(含父级分类)""" tree = self.category.get_category_tree() - names = list(map(lambda c: (c.name, c.get_absolute_url()), tree)) - - return names + # 转换为 (分类名称, 分类URL) 的列表 + return [(c.name, c.get_absolute_url()) for c in tree] def save(self, *args, **kwargs): + """重写保存方法(可扩展,此处直接调用父类方法)""" super().save(*args, **kwargs) def viewed(self): + """增加阅读量并保存(仅更新views字段)""" self.views += 1 - self.save(update_fields=['views']) + self.save(update_fields=['views']) # 只更新views字段,优化性能 def comment_list(self): - cache_key = 'article_comments_{id}'.format(id=self.id) - value = cache.get(cache_key) - if value: - logger.info('get article comments:{id}'.format(id=self.id)) - return value - else: - comments = self.comment_set.filter(is_enable=True).order_by('-id') - cache.set(cache_key, comments, 60 * 100) - logger.info('set article comments:{id}'.format(id=self.id)) - return comments + """获取当前文章的有效评论列表(带缓存)""" + cache_key = f'article_comments_{self.id}' + # 尝试从缓存获取 + cached_comments = cache.get(cache_key) + if cached_comments: + logger.info(f'从缓存获取文章评论: {self.id}') + return cached_comments + # 缓存未命中,从数据库查询 + comments = self.comment_set.filter(is_enable=True).order_by('-id') # 按ID降序(最新在前) + cache.set(cache_key, comments, 60 * 100) # 缓存100分钟 + logger.info(f'缓存文章评论: {self.id}') + return comments def get_admin_url(self): + """获取后台管理编辑页面的URL""" + # 自动获取模型的app标签和模型名称 info = (self._meta.app_label, self._meta.model_name) - return reverse('admin:%s_%s_change' % info, args=(self.pk,)) + return reverse(f'admin:{info[0]}_{info[1]}_change', args=(self.pk,)) - @cache_decorator(expiration=60 * 100) + @cache_decorator(expiration=60 * 100) # 缓存100分钟 def next_article(self): - # 下一篇 - return Article.objects.filter( - id__gt=self.id, status='p').order_by('id').first() + """获取下一篇文章(ID更大、已发布的第一篇)""" + return Article.objects.filter(id__gt=self.id, status='p').order_by('id').first() - @cache_decorator(expiration=60 * 100) + @cache_decorator(expiration=60 * 100) # 缓存100分钟 def prev_article(self): - # 前一篇 + """获取上一篇文章(ID更小、已发布的最后一篇)""" return Article.objects.filter(id__lt=self.id, status='p').first() def get_first_image_url(self): - """ - Get the first image url from article.body. - :return: - """ + """从文章内容中提取第一张图片的URL(用于封面图等场景)""" + # 正则匹配markdown图片格式: ![描述](URL) match = re.search(r'!\[.*?\]\((.+?)\)', self.body) if match: - return match.group(1) - return "" + return match.group(1) # 返回匹配到的URL + return "" # 无图片时返回空 class Category(BaseModel): - """文章分类""" - name = models.CharField(_('category name'), max_length=30, unique=True) + """ + 分类模型 + 用于文章的分类管理,支持层级结构(父分类) + """ + name = models.CharField(_('category name'), max_length=30, unique=True) # 分类名称(唯一) parent_category = models.ForeignKey( 'self', verbose_name=_('parent category'), blank=True, null=True, - on_delete=models.CASCADE) - slug = models.SlugField(default='no-slug', max_length=60, blank=True) - index = models.IntegerField(default=0, verbose_name=_('index')) + on_delete=models.CASCADE # 父分类删除时,子分类也删除 + ) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) # 分类的URL标识符 + index = models.IntegerField(default=0, verbose_name=_('index')) # 排序权重(值越大越靠前) class Meta: - ordering = ['-index'] + ordering = ['-index'] # 按排序权重降序排列 verbose_name = _('category') verbose_name_plural = verbose_name def get_absolute_url(self): - return reverse( - 'blog:category_detail', kwargs={ - 'category_name': self.slug}) + """生成分类详情页的相对URL""" + return reverse('blog:category_detail', kwargs={'category_name': self.slug}) def __str__(self): return self.name - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_category_tree(self): """ - 递归获得分类目录的父级 - :return: + 递归获取当前分类的层级树(含所有父级分类) + 例如:子分类 -> 父分类 -> 顶级分类 """ categorys = [] def parse(category): categorys.append(category) - if category.parent_category: + if category.parent_category: # 若存在父分类,继续递归 parse(category.parent_category) parse(self) return categorys - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_sub_categorys(self): """ - 获得当前分类目录所有子集 - :return: + 递归获取当前分类的所有子分类(含多级子分类) """ categorys = [] - all_categorys = Category.objects.all() + all_categorys = Category.objects.all() # 获取所有分类 def parse(category): if category not in categorys: categorys.append(category) + # 获取当前分类的直接子分类 childs = all_categorys.filter(parent_category=category) for child in childs: - if category not in categorys: + if child not in categorys: categorys.append(child) - parse(child) + parse(child) # 递归处理子分类 parse(self) return categorys class Tag(BaseModel): - """文章标签""" - name = models.CharField(_('tag name'), max_length=30, unique=True) - slug = models.SlugField(default='no-slug', max_length=60, blank=True) + """ + 标签模型 + 用于文章的标签管理(多对多关系) + """ + name = models.CharField(_('tag name'), max_length=30, unique=True) # 标签名称(唯一) + slug = models.SlugField(default='no-slug', max_length=60, blank=True) # 标签的URL标识符 def __str__(self): return self.name def get_absolute_url(self): + """生成标签详情页的相对URL""" return reverse('blog:tag_detail', kwargs={'tag_name': self.slug}) - @cache_decorator(60 * 60 * 10) + @cache_decorator(60 * 60 * 10) # 缓存10小时 def get_article_count(self): + """获取该标签关联的文章数量(去重)""" return Article.objects.filter(tags__name=self.name).distinct().count() class Meta: - ordering = ['name'] + ordering = ['name'] # 按名称升序排列 verbose_name = _('tag') verbose_name_plural = verbose_name class Links(models.Model): - """友情链接""" - - name = models.CharField(_('link name'), max_length=30, unique=True) - link = models.URLField(_('link')) - sequence = models.IntegerField(_('order'), unique=True) + """ + 友情链接模型 + 存储网站的友情链接信息 + """ + name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称(唯一) + link = models.URLField(_('link')) # 链接URL + sequence = models.IntegerField(_('order'), unique=True) # 排序序号(唯一,用于控制显示顺序) is_enable = models.BooleanField( - _('is show'), default=True, blank=False, null=False) + _('is show'), default=True, blank=False, null=False) # 是否启用(显示) show_type = models.CharField( _('show type'), max_length=1, choices=LinkShowType.choices, - default=LinkShowType.I) - creation_time = models.DateTimeField(_('creation time'), default=now) - last_mod_time = models.DateTimeField(_('modify time'), default=now) + default=LinkShowType.I # 默认为首页展示 + ) + creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间 + last_mod_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间 class Meta: - ordering = ['sequence'] + ordering = ['sequence'] # 按排序序号升序排列 verbose_name = _('link') verbose_name_plural = verbose_name @@ -287,16 +339,19 @@ class Links(models.Model): class SideBar(models.Model): - """侧边栏,可以展示一些html内容""" - name = models.CharField(_('title'), max_length=100) - content = models.TextField(_('content')) - sequence = models.IntegerField(_('order'), unique=True) - is_enable = models.BooleanField(_('is enable'), default=True) + """ + 侧边栏模型 + 用于展示网站侧边栏内容(支持HTML) + """ + name = models.CharField(_('title'), max_length=100) # 侧边栏标题 + content = models.TextField(_('content')) # 侧边栏内容(HTML格式) + sequence = models.IntegerField(_('order'), unique=True) # 排序序号(控制显示顺序) + is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用 creation_time = models.DateTimeField(_('creation time'), default=now) last_mod_time = models.DateTimeField(_('modify time'), default=now) class Meta: - ordering = ['sequence'] + ordering = ['sequence'] # 按排序序号升序排列 verbose_name = _('sidebar') verbose_name_plural = verbose_name @@ -305,59 +360,38 @@ class SideBar(models.Model): class BlogSettings(models.Model): - """blog的配置""" + """ + 博客配置模型 + 存储网站的全局设置(单例模式,仅允许一条记录) + """ site_name = models.CharField( - _('site name'), - max_length=200, - null=False, - blank=False, - default='') + _('site name'), max_length=200, null=False, blank=False, default='') # 网站名称 site_description = models.TextField( - _('site description'), - max_length=1000, - null=False, - blank=False, - default='') + _('site description'), max_length=1000, null=False, blank=False, default='') # 网站描述 site_seo_description = models.TextField( - _('site seo description'), max_length=1000, null=False, blank=False, default='') + _('site seo description'), max_length=1000, null=False, blank=False, default='') # SEO描述 site_keywords = models.TextField( - _('site keywords'), - max_length=1000, - null=False, - blank=False, - default='') - article_sub_length = models.IntegerField(_('article sub length'), default=300) - sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) - sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) - article_comment_count = models.IntegerField(_('article comment count'), default=5) - show_google_adsense = models.BooleanField(_('show adsense'), default=False) + _('site keywords'), max_length=1000, null=False, blank=False, default='') # 网站关键词(SEO) + article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度 + sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量 + sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量 + article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章页显示评论数量 + show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示谷歌广告 google_adsense_codes = models.TextField( - _('adsense code'), max_length=2000, null=True, blank=True, default='') - open_site_comment = models.BooleanField(_('open site comment'), default=True) - global_header = models.TextField("公共头部", null=True, blank=True, default='') - global_footer = models.TextField("公共尾部", null=True, blank=True, default='') + _('adsense code'), max_length=2000, null=True, blank=True, default='') # 谷歌广告代码 + open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开启全站评论 + global_header = models.TextField("公共头部", null=True, blank=True, default='') # 全局头部HTML代码 + global_footer = models.TextField("公共尾部", null=True, blank=True, default='') # 全局尾部HTML代码 beian_code = models.CharField( - '备案号', - max_length=2000, - null=True, - blank=True, - default='') + '备案号', max_length=2000, null=True, blank=True, default='') # 网站备案号 analytics_code = models.TextField( - "网站统计代码", - max_length=1000, - null=False, - blank=False, - default='') + "网站统计代码", max_length=1000, null=False, blank=False, default='') # 统计代码(如百度统计) show_gongan_code = models.BooleanField( - '是否显示公安备案号', default=False, null=False) + '是否显示公安备案号', default=False, null=False) # 是否显示公安备案号 gongan_beiancode = models.TextField( - '公安备案号', - max_length=2000, - null=True, - blank=True, - default='') + '公安备案号', max_length=2000, null=True, blank=True, default='') # 公安备案号 comment_need_review = models.BooleanField( - '评论是否需要审核', default=False, null=False) + '评论是否需要审核', default=False, null=False) # 评论是否需要审核后显示 class Meta: verbose_name = _('Website configuration') @@ -367,10 +401,15 @@ class BlogSettings(models.Model): return self.site_name def clean(self): + """ + 数据验证:确保仅存在一条配置记录 + 在保存前调用,用于防止创建多条配置 + """ if BlogSettings.objects.exclude(id=self.id).count(): - raise ValidationError(_('There can only be one configuration')) + raise ValidationError(_('There can only be one configuration')) # 仅允许一个配置记录 def save(self, *args, **kwargs): + """保存配置后清除缓存(确保配置实时生效)""" super().save(*args, **kwargs) from djangoblog.utils import cache - cache.clear() + cache.clear() # 清除所有缓存 \ No newline at end of file