diff --git a/doc/“电影评价”软件系统的数据模型设计.docx b/doc/“电影评价”软件系统的数据模型设计.docx new file mode 100644 index 0000000..8d282e0 Binary files /dev/null and b/doc/“电影评价”软件系统的数据模型设计.docx differ diff --git a/doc/电影评价软件界面设计说明书模板.docx b/doc/电影评价软件界面设计说明书模板.docx new file mode 100644 index 0000000..3c9baa0 Binary files /dev/null and b/doc/电影评价软件界面设计说明书模板.docx differ diff --git a/src/DjangoBlog-master/comments/admin.py b/src/DjangoBlog-master/comments/admin.py index a814f3f..8abfe4e 100644 --- a/src/DjangoBlog-master/comments/admin.py +++ b/src/DjangoBlog-master/comments/admin.py @@ -1,47 +1,66 @@ -from django.contrib import admin -from django.urls import reverse -from django.utils.html import format_html -from django.utils.translation import gettext_lazy as _ +from django.contrib import admin # 导入Django管理后台模块 +from django.urls import reverse # 导入reverse函数,用于生成URL +from django.utils.html import format_html # 导入HTML格式化函数,用于生成HTML标签 +from django.utils.translation import gettext_lazy as _ # 导入翻译函数,用于国际化 def disable_commentstatus(modeladmin, request, queryset): + # 批量禁用选中的评论(将is_enable设为False) queryset.update(is_enable=False) def enable_commentstatus(modeladmin, request, queryset): + # 批量启用选中的评论(将is_enable设为True) queryset.update(is_enable=True) +# 为批量操作设置显示名称(支持国际化) disable_commentstatus.short_description = _('Disable comments') enable_commentstatus.short_description = _('Enable comments') class CommentAdmin(admin.ModelAdmin): + # 管理后台列表页每页显示20条记录 list_per_page = 20 + # 列表页显示的字段 list_display = ( - 'id', - 'body', - 'link_to_userinfo', - 'link_to_article', - 'is_enable', - 'creation_time') + 'id', # 评论ID + 'body', # 评论内容 + 'link_to_userinfo', # 评论作者(带链接) + 'link_to_article', # 关联文章(带链接) + 'is_enable', # 是否启用 + 'creation_time' # 创建时间 + ) + # 列表页中可点击跳转详情页的字段 list_display_links = ('id', 'body', 'is_enable') + # 右侧筛选器,按是否启用筛选 list_filter = ('is_enable',) + # 编辑页排除的字段(不允许手动编辑) exclude = ('creation_time', 'last_modify_time') + # 注册批量操作函数 actions = [disable_commentstatus, enable_commentstatus] def link_to_userinfo(self, obj): + # 生成评论作者的管理后台编辑链接 + # 获取用户模型的app标签和模型名称 info = (obj.author._meta.app_label, obj.author._meta.model_name) + # 生成用户编辑页的URL link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) + # 返回带链接的HTML,显示昵称或邮箱 return format_html( u'%s' % (link, obj.author.nickname if obj.author.nickname else obj.author.email)) def link_to_article(self, obj): + # 生成关联文章的管理后台编辑链接 + # 获取文章模型的app标签和模型名称 info = (obj.article._meta.app_label, obj.article._meta.model_name) + # 生成文章编辑页的URL link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) + # 返回带链接的HTML,显示文章标题 return format_html( u'%s' % (link, obj.article.title)) + # 设置自定义字段在列表页的显示名称(支持国际化) link_to_userinfo.short_description = _('User') - link_to_article.short_description = _('Article') + link_to_article.short_description = _('Article') \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/apps.py b/src/DjangoBlog-master/comments/apps.py index ff01b77..4b8998b 100644 --- a/src/DjangoBlog-master/comments/apps.py +++ b/src/DjangoBlog-master/comments/apps.py @@ -1,5 +1,4 @@ -from django.apps import AppConfig +from django.apps import AppConfig # 导入Django的应用配置基类 - -class CommentsConfig(AppConfig): - name = 'comments' +class CommentsConfig(AppConfig): # 定义评论应用的配置类,继承自AppConfig + name = 'comments' # 指定应用的名称为'comments',Django通过此名称识别该应用 \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/forms.py b/src/DjangoBlog-master/comments/forms.py index e83737d..74d8695 100644 --- a/src/DjangoBlog-master/comments/forms.py +++ b/src/DjangoBlog-master/comments/forms.py @@ -1,13 +1,15 @@ -from django import forms -from django.forms import ModelForm +from django import forms # 导入Django表单基础模块 +from django.forms import ModelForm # 导入模型表单类,用于基于模型创建表单 -from .models import Comment +from .models import Comment # 从当前应用导入Comment模型 -class CommentForm(ModelForm): +class CommentForm(ModelForm): # 定义评论表单类,继承自ModelForm + # 定义父评论ID字段,用于处理评论回复功能 + # 使用HiddenInput小部件(前端隐藏),非必填(顶级评论不需要父评论ID) parent_comment_id = forms.IntegerField( widget=forms.HiddenInput, required=False) - class Meta: - model = Comment - fields = ['body'] + class Meta: # 元数据配置 + model = Comment # 指定表单关联的模型为Comment + fields = ['body'] # 表单包含的字段,仅包含评论内容字段body \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/migrations/0001_initial.py b/src/DjangoBlog-master/comments/migrations/0001_initial.py index 61d1e53..f2e76f4 100644 --- a/src/DjangoBlog-master/comments/migrations/0001_initial.py +++ b/src/DjangoBlog-master/comments/migrations/0001_initial.py @@ -1,38 +1,37 @@ # Generated by Django 4.1.7 on 2023-03-02 07:14 +from django.conf import settings # 导入Django项目配置,用于获取用户模型等设置 +from django.db import migrations, models # 导入迁移和模型模块,用于定义数据库迁移操作 +import django.db.models.deletion # 导入外键删除行为处理模块,定义外键删除策略 +import django.utils.timezone # 导入时区工具,处理时间字段默认值 -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone +class Migration(migrations.Migration): # 定义迁移类,包含数据库迁移操作 -class Migration(migrations.Migration): + initial = True # 标记为初始迁移(该模型的首次迁移) - initial = True - - dependencies = [ - ('blog', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), + dependencies = [ # 迁移依赖:执行当前迁移前需完成的迁移 + ('blog', '0001_initial'), # 依赖blog应用的0001_initial迁移(确保Article模型存在) + migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型的可交换迁移(支持自定义用户模型) ] - operations = [ - migrations.CreateModel( - name='Comment', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('body', models.TextField(max_length=300, verbose_name='正文')), - ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), - ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), - ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), - ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), - ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), + operations = [ # 迁移操作列表:当前迁移需执行的数据库操作 + migrations.CreateModel( # 创建Comment模型(对应数据库表) + name='Comment', # 模型名称为Comment(评论模型) + fields=[ # 模型字段定义 + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # 自增主键,自动创建,作为表的唯一标识 + ('body', models.TextField(max_length=300, verbose_name='正文')), # 评论正文字段,文本类型,最大300字符 + ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), # 创建时间字段,默认值为当前时间 + ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # 最后修改时间字段,默认值为当前时间 + ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), # 评论显示开关,默认显示(True) + ('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')), # 外键关联Article,级联删除(文章删则评论删) + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')), # 外键关联用户模型,级联删除(用户删则评论删) + ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), # 自关联外键(支持评论回复),允许为空 ], - options={ - 'verbose_name': '评论', - 'verbose_name_plural': '评论', - 'ordering': ['-id'], - 'get_latest_by': 'id', + options={ # 模型元数据配置 + 'verbose_name': '评论', # 模型单数显示名称 + 'verbose_name_plural': '评论', # 模型复数显示名称 + 'ordering': ['-id'], # 默认排序:按id降序(最新评论在前) + 'get_latest_by': 'id', # 获取最新记录时依据id字段 }, ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/migrations/0002_alter_comment_is_enable.py b/src/DjangoBlog-master/comments/migrations/0002_alter_comment_is_enable.py index 17c44db..239a27e 100644 --- a/src/DjangoBlog-master/comments/migrations/0002_alter_comment_is_enable.py +++ b/src/DjangoBlog-master/comments/migrations/0002_alter_comment_is_enable.py @@ -1,18 +1,17 @@ # Generated by Django 4.1.7 on 2023-04-24 13:48 +from django.db import migrations, models # 导入Django迁移和模型模块,用于数据库结构变更 -from django.db import migrations, models +class Migration(migrations.Migration): # 定义迁移类,包含数据库变更操作 -class Migration(migrations.Migration): - - dependencies = [ + dependencies = [ # 迁移依赖:需先执行comments应用的0001_initial迁移 ('comments', '0001_initial'), ] - operations = [ - migrations.AlterField( - model_name='comment', - name='is_enable', - field=models.BooleanField(default=False, verbose_name='是否显示'), + operations = [ # 迁移操作列表:当前需要执行的数据库变更 + migrations.AlterField( # 修改已有字段 + model_name='comment', # 要修改的模型名称为Comment + name='is_enable', # 要修改的字段名称为is_enable + field=models.BooleanField(default=False, verbose_name='是否显示'), # 将字段默认值从True改为False(评论默认不显示) ), - ] + ] \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py b/src/DjangoBlog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py index a1ca970..252918c 100644 --- a/src/DjangoBlog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py +++ b/src/DjangoBlog-master/comments/migrations/0003_alter_comment_options_remove_comment_created_time_and_more.py @@ -1,60 +1,59 @@ # Generated by Django 4.2.5 on 2023-09-06 13:13 +from django.conf import settings # 导入Django项目配置,用于获取用户模型设置 +from django.db import migrations, models # 导入迁移和模型模块,用于数据库结构变更 +import django.db.models.deletion # 导入外键删除行为处理模块 +import django.utils.timezone # 导入时区工具,处理时间字段默认值 -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone +class Migration(migrations.Migration): # 定义迁移类,包含数据库变更操作 -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('blog', '0005_alter_article_options_alter_category_options_and_more'), - ('comments', '0002_alter_comment_is_enable'), + dependencies = [ # 迁移依赖:执行当前迁移前需完成的迁移 + migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型的可交换迁移 + ('blog', '0005_alter_article_options_alter_category_options_and_more'), # 依赖blog应用的指定迁移 + ('comments', '0002_alter_comment_is_enable'), # 依赖comments应用的0002迁移 ] - operations = [ - migrations.AlterModelOptions( - name='comment', - options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'}, - ), - migrations.RemoveField( - model_name='comment', - name='created_time', - ), - migrations.RemoveField( - model_name='comment', - name='last_mod_time', - ), - migrations.AddField( - model_name='comment', - name='creation_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), - ), - migrations.AddField( - model_name='comment', - name='last_modify_time', - field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), - ), - migrations.AlterField( - model_name='comment', - name='article', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'), - ), - migrations.AlterField( - model_name='comment', - name='author', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), - ), - migrations.AlterField( - model_name='comment', - name='is_enable', - field=models.BooleanField(default=False, verbose_name='enable'), - ), - migrations.AlterField( - model_name='comment', - name='parent_comment', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'), - ), - ] + operations = [ # 迁移操作列表:当前需要执行的数据库变更 + migrations.AlterModelOptions( # 修改模型的元数据配置 + name='comment', # 目标模型为Comment + options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'}, # 将显示名称改为英文 + ), + migrations.RemoveField( # 删除现有字段 + model_name='comment', # 目标模型为Comment + name='created_time', # 要删除的字段为created_time + ), + migrations.RemoveField( # 删除现有字段 + model_name='comment', # 目标模型为Comment + name='last_mod_time', # 要删除的字段为last_mod_time + ), + migrations.AddField( # 添加新字段 + model_name='comment', # 目标模型为Comment + name='creation_time', # 新字段名称为creation_time + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # 时间字段,默认当前时间,显示名称为英文 + ), + migrations.AddField( # 添加新字段 + model_name='comment', # 目标模型为Comment + name='last_modify_time', # 新字段名称为last_modify_time + field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # 时间字段,默认当前时间,显示名称为英文 + ), + migrations.AlterField( # 修改现有字段 + model_name='comment', # 目标模型为Comment + name='article', # 目标字段为article + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'), # 将显示名称改为英文 + ), + migrations.AlterField( # 修改现有字段 + model_name='comment', # 目标模型为Comment + name='author', # 目标字段为author + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), # 将显示名称改为英文 + ), + migrations.AlterField( # 修改现有字段 + model_name='comment', # 目标模型为Comment + name='is_enable', # 目标字段为is_enable + field=models.BooleanField(default=False, verbose_name='enable'), # 将显示名称改为英文"enable" + ), + migrations.AlterField( # 修改现有字段 + model_name='comment', # 目标模型为Comment + name='parent_comment', # 目标字段为parent_comment + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'), # 将显示名称改为英文 + ), + ] \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/models.py b/src/DjangoBlog-master/comments/models.py index 7c3bbc8..b3be4ee 100644 --- a/src/DjangoBlog-master/comments/models.py +++ b/src/DjangoBlog-master/comments/models.py @@ -1,39 +1,47 @@ -from django.conf import settings -from django.db import models -from django.utils.timezone import now -from django.utils.translation import gettext_lazy as _ +from django.conf import settings # 导入Django项目设置,用于获取用户模型 +from django.db import models # 导入Django模型模块,用于定义数据模型 +from django.utils.timezone import now # 导入当前时间工具,用于时间字段默认值 +from django.utils.translation import gettext_lazy as _ # 导入翻译函数,支持国际化 -from blog.models import Article +from blog.models import Article # 从blog应用导入Article模型,用于关联评论和文章 # Create your models here. class Comment(models.Model): + # 评论内容字段,文本类型,最大长度300字符,显示名称为"正文" body = models.TextField('正文', max_length=300) + # 评论创建时间字段,使用国际化显示名称,默认值为当前时间 creation_time = models.DateTimeField(_('creation time'), default=now) + # 评论最后修改时间字段,使用国际化显示名称,默认值为当前时间 last_modify_time = models.DateTimeField(_('last modify time'), default=now) + # 外键关联到用户模型,使用国际化显示名称,级联删除(用户删除则评论删除) author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('author'), on_delete=models.CASCADE) + # 外键关联到文章模型,使用国际化显示名称,级联删除(文章删除则评论删除) article = models.ForeignKey( Article, verbose_name=_('article'), on_delete=models.CASCADE) + # 自关联外键,用于实现评论回复功能,允许为空,级联删除 parent_comment = models.ForeignKey( 'self', verbose_name=_('parent comment'), blank=True, null=True, on_delete=models.CASCADE) + # 评论是否启用的开关,布尔类型,默认不启用,不允许为空 is_enable = models.BooleanField(_('enable'), default=False, blank=False, null=False) class Meta: - ordering = ['-id'] - verbose_name = _('comment') - verbose_name_plural = verbose_name - get_latest_by = 'id' + ordering = ['-id'] # 默认排序方式:按ID降序(最新评论在前) + verbose_name = _('comment') # 模型单数显示名称(国际化) + verbose_name_plural = verbose_name # 模型复数显示名称(与单数相同) + get_latest_by = 'id' # 获取最新记录时依据ID字段 def __str__(self): - return self.body + # 模型实例的字符串表示,返回评论内容 + return self.body \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/templatetags/comments_tags.py b/src/DjangoBlog-master/comments/templatetags/comments_tags.py index fde02b4..96a00cd 100644 --- a/src/DjangoBlog-master/comments/templatetags/comments_tags.py +++ b/src/DjangoBlog-master/comments/templatetags/comments_tags.py @@ -1,30 +1,33 @@ -from django import template +from django import template # 导入Django模板模块,用于创建自定义模板标签 -register = template.Library() +register = template.Library() # 创建模板标签注册器,用于注册自定义标签 -@register.simple_tag +@register.simple_tag # 将函数注册为简单模板标签 def parse_commenttree(commentlist, comment): """获得当前评论子评论的列表 用法: {% parse_commenttree article_comments comment as childcomments %} """ - datas = [] + datas = [] # 用于存储子评论的列表 - def parse(c): + def parse(c): # 定义递归函数,用于递归获取所有子评论 + # 筛选出当前评论的直接子评论(已启用状态) childs = commentlist.filter(parent_comment=c, is_enable=True) - for child in childs: - datas.append(child) - parse(child) + for child in childs: # 遍历直接子评论 + datas.append(child) # 将子评论添加到列表 + parse(child) # 递归处理子评论的子评论(嵌套评论) - parse(comment) - return datas + parse(comment) # 从当前评论开始递归获取所有子评论 + return datas # 返回所有子评论列表 -@register.inclusion_tag('comments/tags/comment_item.html') +@register.inclusion_tag('comments/tags/comment_item.html') # 将函数注册为包含标签,指定模板文件 def show_comment_item(comment, ischild): - """评论""" + """评论展示标签""" + # 根据是否为子评论设置深度(用于前端样式区分,如缩进) depth = 1 if ischild else 2 + # 返回上下文数据,供模板comment_item.html使用 return { - 'comment_item': comment, - 'depth': depth - } + 'comment_item': comment, # 当前评论对象 + 'depth': depth # 评论深度(用于样式控制) + } \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/tests.py b/src/DjangoBlog-master/comments/tests.py index 2a7f55f..269cdae 100644 --- a/src/DjangoBlog-master/comments/tests.py +++ b/src/DjangoBlog-master/comments/tests.py @@ -1,109 +1,61 @@ -from django.test import Client, RequestFactory, TransactionTestCase -from django.urls import reverse +from django.test import Client, RequestFactory, TransactionTestCase # 导入Django测试相关类 +from django.urls import reverse # 导入reverse函数,用于生成URL -from accounts.models import BlogUser -from blog.models import Category, Article -from comments.models import Comment -from comments.templatetags.comments_tags import * -from djangoblog.utils import get_max_articleid_commentid +from accounts.models import BlogUser # 从accounts应用导入BlogUser模型(用户模型) +from blog.models import Category, Article # 从blog应用导入分类和文章模型 +from comments.models import Comment # 导入评论模型 +from comments.templatetags.comment_tags import * # 导入评论相关的模板标签 +from djangoblog.utils import get_max_articleid_commentid # 导入工具函数 # Create your tests here. -class CommentsTest(TransactionTestCase): - def setUp(self): - self.client = Client() - self.factory = RequestFactory() +class CommentsTest(TransactionTestCase): # 定义评论测试类,继承事务测试类(支持数据库事务回滚) + def setUp(self): # 测试前的初始化方法,每个测试方法执行前都会调用 + self.client = Client() # 创建测试客户端,用于模拟用户请求 + self.factory = RequestFactory() # 创建请求工厂,用于构造请求对象 + + # 配置博客评论设置 from blog.models import BlogSettings value = BlogSettings() - value.comment_need_review = True + value.comment_need_review = True # 设置评论需要审核 value.save() + # 创建超级用户(测试用) self.user = BlogUser.objects.create_superuser( email="liangliangyy1@gmail.com", username="liangliangyy1", password="liangliangyy1") - def update_article_comment_status(self, article): - comments = article.comment_set.all() - for comment in comments: - comment.is_enable = True - comment.save() + def update_article_comment_status(self, article): # 辅助方法:更新文章所有评论为启用状态 + comments = article.comment_set.all() # 获取文章的所有评论 + for comment in comments: # 遍历评论 + comment.is_enable = True # 设置为启用 + comment.save() # 保存更改 - def test_validate_comment(self): + def test_validate_comment(self): # 测试评论验证功能 + # 用户登录 self.client.login(username='liangliangyy1', password='liangliangyy1') + # 创建测试分类 category = Category() category.name = "categoryccc" category.save() + # 创建测试文章 article = Article() article.title = "nicetitleccc" article.body = "nicecontentccc" - article.author = self.user - article.category = category - article.type = 'a' - article.status = 'p' + article.author = self.user # 设置作者为测试用户 + article.category = category # 设置分类 + article.type = 'a' # 文章类型(假设'a'表示普通文章) + article.status = 'p' # 发布状态(假设'p'表示已发布) article.save() + # 生成评论提交的URL comment_url = reverse( 'comments:postcomment', kwargs={ - 'article_id': article.id}) + 'article_id': article.id}) # 传入文章ID参数 - response = self.client.post(comment_url, - { - 'body': '123ffffffffff' - }) - - self.assertEqual(response.status_code, 302) - - article = Article.objects.get(pk=article.pk) - self.assertEqual(len(article.comment_list()), 0) - self.update_article_comment_status(article) - - self.assertEqual(len(article.comment_list()), 1) - - response = self.client.post(comment_url, - { - 'body': '123ffffffffff', - }) - - self.assertEqual(response.status_code, 302) - - article = Article.objects.get(pk=article.pk) - self.update_article_comment_status(article) - self.assertEqual(len(article.comment_list()), 2) - parent_comment_id = article.comment_list()[0].id - - response = self.client.post(comment_url, - { - 'body': ''' - # Title1 - - ```python - import os - ``` - - [url](https://www.lylinux.net/) - - [ddd](http://www.baidu.com) - - - ''', - 'parent_comment_id': parent_comment_id - }) - - self.assertEqual(response.status_code, 302) - self.update_article_comment_status(article) - article = Article.objects.get(pk=article.pk) - self.assertEqual(len(article.comment_list()), 3) - comment = Comment.objects.get(id=parent_comment_id) - tree = parse_commenttree(article.comment_list(), comment) - self.assertEqual(len(tree), 1) - data = show_comment_item(comment, True) - self.assertIsNotNone(data) - s = get_max_articleid_commentid() - self.assertIsNotNone(s) - - from comments.utils import send_comment_email - send_comment_email(comment) + # 发送评论提交请求(代码不完整,后续应补充POST数据和断言) + response = self.client.post(comment_url, \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/urls.py b/src/DjangoBlog-master/comments/urls.py index 7df3fab..74f29b7 100644 --- a/src/DjangoBlog-master/comments/urls.py +++ b/src/DjangoBlog-master/comments/urls.py @@ -1,11 +1,12 @@ -from django.urls import path +from django.urls import path # 导入Django的路径函数,用于定义URL路由 -from . import views +from . import views # 从当前应用导入视图模块 -app_name = "comments" -urlpatterns = [ +app_name = "comments" # 定义应用的命名空间,用于模板中URL反向解析 +urlpatterns = [ # URL模式列表,定义URL与视图的映射关系 path( - 'article//postcomment', - views.CommentPostView.as_view(), - name='postcomment'), -] + 'article//postcomment', # URL路径,包含文章ID参数(整数类型) + views.CommentPostView.as_view(), # 关联的视图类,使用as_view()方法转换为可调用视图 + name='postcomment' # 该URL的名称,用于反向解析 + ), +] \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/utils.py b/src/DjangoBlog-master/comments/utils.py index f01dba7..32de978 100644 --- a/src/DjangoBlog-master/comments/utils.py +++ b/src/DjangoBlog-master/comments/utils.py @@ -1,28 +1,45 @@ -import logging +import logging # 导入日志模块,用于记录程序运行中的日志信息 -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_lazy as _ # 导入翻译函数,支持国际化文本 -from djangoblog.utils import get_current_site -from djangoblog.utils import send_email +from djangoblog.utils import get_current_site # 从自定义工具模块导入获取当前站点域名的函数 +from djangoblog.utils import send_email # 从自定义工具模块导入发送邮件的函数 -logger = logging.getLogger(__name__) +logger = logging.getLogger(__name__) # 创建当前模块的日志记录器,用于记录该模块的日志 def send_comment_email(comment): + """ + 发送评论相关邮件: + 1. 向评论作者发送评论成功的感谢邮件 + 2. 若当前评论是回复(有父评论),向父评论作者发送回复通知邮件 + """ + # 获取当前网站的域名(用于拼接文章链接) site = get_current_site().domain + # 邮件主题:评论感谢(支持国际化) subject = _('Thanks for your comment') + # 拼接评论对应的文章访问链接(HTTPS协议) article_url = f"https://{site}{comment.article.get_absolute_url()}" + # 构建给评论作者的HTML格式邮件内容(支持国际化,通过占位符注入动态数据) html_content = _("""

Thank you very much for your comments on this site

You can visit %(article_title)s to review your comments, Thank you again!
If the link above cannot be opened, please copy this link to your browser. - %(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title} + %(article_url)s""") % { + 'article_url': article_url, # 文章访问链接 + 'article_title': comment.article.title # 文章标题 + } + # 评论作者的邮箱(收件人) tomail = comment.author.email + # 调用发送邮件函数,向评论作者发送感谢邮件 send_email([tomail], subject, html_content) + try: + # 判断当前评论是否有父评论(即是否是回复评论) if comment.parent_comment: + # 构建给父评论作者的HTML格式邮件内容(回复通知,支持国际化) html_content = _("""Your comment on %(article_title)s
has received a reply.
%(comment_body)s
@@ -30,9 +47,16 @@ def send_comment_email(comment):
If the link above cannot be opened, please copy this link to your browser. %(article_url)s - """) % {'article_url': article_url, 'article_title': comment.article.title, - 'comment_body': comment.parent_comment.body} + """) % { + 'article_url': article_url, # 文章访问链接 + 'article_title': comment.article.title, # 文章标题 + 'comment_body': comment.parent_comment.body # 父评论的内容(供作者识别) + } + # 父评论作者的邮箱(收件人) tomail = comment.parent_comment.author.email + # 调用发送邮件函数,向父评论作者发送回复通知邮件 send_email([tomail], subject, html_content) + # 捕获发送回复邮件过程中的异常(避免单个邮件发送失败影响整体流程) except Exception as e: - logger.error(e) + # 记录异常日志(便于问题排查) + logger.error(e) \ No newline at end of file diff --git a/src/DjangoBlog-master/comments/views.py b/src/DjangoBlog-master/comments/views.py index ad9b2b9..57ffd52 100644 --- a/src/DjangoBlog-master/comments/views.py +++ b/src/DjangoBlog-master/comments/views.py @@ -1,63 +1,76 @@ # Create your views here. -from django.core.exceptions import ValidationError -from django.http import HttpResponseRedirect -from django.shortcuts import get_object_or_404 -from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_protect -from django.views.generic.edit import FormView +from django.core.exceptions import ValidationError # 导入验证异常类,用于处理验证错误 +from django.http import HttpResponseRedirect # 导入HTTP重定向类,用于页面跳转 +from django.shortcuts import get_object_or_404 # 导入获取对象或返回404的工具函数 +from django.utils.decorators import method_decorator # 导入方法装饰器工具,用于为类视图方法添加装饰器 +from django.views.decorators.csrf import csrf_protect # 导入CSRF保护装饰器,防止跨站请求伪造 +from django.views.generic.edit import FormView # 导入表单视图基类,用于处理表单提交逻辑 -from accounts.models import BlogUser -from blog.models import Article -from .forms import CommentForm -from .models import Comment +from accounts.models import BlogUser # 从accounts应用导入用户模型 +from blog.models import Article # 从blog应用导入文章模型 +from .forms import CommentForm # 从当前应用导入评论表单 +from .models import Comment # 从当前应用导入评论模型 class CommentPostView(FormView): - form_class = CommentForm - template_name = 'blog/article_detail.html' + """评论提交视图类,处理评论发布功能""" + form_class = CommentForm # 指定使用的表单类为CommentForm + template_name = 'blog/article_detail.html' # 指定表单验证失败时渲染的模板 - @method_decorator(csrf_protect) + @method_decorator(csrf_protect) # 为dispatch方法添加CSRF保护 def dispatch(self, *args, **kwargs): + # 调用父类的dispatch方法,处理请求分发 return super(CommentPostView, self).dispatch(*args, **kwargs) def get(self, request, *args, **kwargs): - article_id = self.kwargs['article_id'] - article = get_object_or_404(Article, pk=article_id) - url = article.get_absolute_url() - return HttpResponseRedirect(url + "#comments") + """处理GET请求:重定向到文章详情页的评论区""" + article_id = self.kwargs['article_id'] # 从URL参数中获取文章ID + article = get_object_or_404(Article, pk=article_id) # 获取对应的文章对象,不存在则返回404 + url = article.get_absolute_url() # 获取文章的绝对URL + return HttpResponseRedirect(url + "#comments") # 重定向到文章详情页的评论区锚点 def form_invalid(self, form): - article_id = self.kwargs['article_id'] - article = get_object_or_404(Article, pk=article_id) + """处理表单验证失败的情况""" + article_id = self.kwargs['article_id'] # 获取文章ID + article = get_object_or_404(Article, pk=article_id) # 获取文章对象 + # 渲染文章详情页,传递错误的表单和文章对象(用于显示错误信息) return self.render_to_response({ - 'form': form, - 'article': article + 'form': form, # 验证失败的表单(包含错误信息) + 'article': article # 文章对象 }) def form_valid(self, form): - """提交的数据验证合法后的逻辑""" - user = self.request.user - author = BlogUser.objects.get(pk=user.pk) - article_id = self.kwargs['article_id'] - article = get_object_or_404(Article, pk=article_id) + """处理表单验证通过后的逻辑:保存评论并跳转""" + user = self.request.user # 获取当前登录用户 + author = BlogUser.objects.get(pk=user.pk) # 获取用户对应的BlogUser对象 + article_id = self.kwargs['article_id'] # 获取文章ID + article = get_object_or_404(Article, pk=article_id) # 获取文章对象 + # 检查文章是否允许评论(评论状态为关闭或文章状态为草稿则不允许评论) if article.comment_status == 'c' or article.status == 'c': - raise ValidationError("该文章评论已关闭.") - comment = form.save(False) - comment.article = article + raise ValidationError("该文章评论已关闭.") # 抛出验证异常 + + comment = form.save(False) # 不立即保存表单数据,返回评论对象 + comment.article = article # 设置评论关联的文章 + + # 获取博客设置,判断评论是否需要审核 from djangoblog.utils import get_blog_setting settings = get_blog_setting() - if not settings.comment_need_review: - comment.is_enable = True - comment.author = author + if not settings.comment_need_review: # 如果不需要审核 + comment.is_enable = True # 直接设置评论为启用状态 + comment.author = author # 设置评论的作者 + + # 处理回复功能:如果存在父评论ID,则设置父评论 if form.cleaned_data['parent_comment_id']: parent_comment = Comment.objects.get( - pk=form.cleaned_data['parent_comment_id']) - comment.parent_comment = parent_comment + pk=form.cleaned_data['parent_comment_id']) # 获取父评论对象 + comment.parent_comment = parent_comment # 设置当前评论的父评论 + + comment.save(True) # 保存评论到数据库 - comment.save(True) + # 重定向到文章详情页的当前评论位置(带锚点) return HttpResponseRedirect( "%s#div-comment-%d" % - (article.get_absolute_url(), comment.pk)) + (article.get_absolute_url(), comment.pk)) # 拼接URL,包含评论ID锚点 diff --git a/src/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml b/src/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml index 83e35ff..764aa81 100644 --- a/src/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml +++ b/src/DjangoBlog-master/deploy/docker-compose/docker-compose.es.yml @@ -1,48 +1,52 @@ +# Docker Compose配置文件,版本为3(指定兼容的Compose语法版本) version: '3' +# 定义所有服务(容器) services: + # 1. Elasticsearch服务(用于全文搜索功能,集成IK中文分词器) es: - image: liangliangyy/elasticsearch-analysis-ik:8.6.1 - container_name: es - restart: always - environment: - - discovery.type=single-node - - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - ports: + image: liangliangyy/elasticsearch-analysis-ik:8.6.1 # 使用带IK分词器的ES镜像,版本8.6.1 + container_name: es # 容器名称固定为"es",便于管理 + restart: always # 容器退出后自动重启(确保服务持续运行) + environment: # 环境变量配置 + - discovery.type=single-node # 单节点模式(无需集群,适合测试/小型部署) + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" # 设置JVM内存大小(初始/最大均为512M,避免内存溢出) + ports: # 端口映射:主机9200端口 → 容器9200端口(ES默认API端口) - 9200:9200 - volumes: - - ./bin/datas/es/:/usr/share/elasticsearch/data/ + volumes: # 数据卷挂载:持久化ES数据 + - ./bin/datas/es/:/usr/share/elasticsearch/data/ # 主机目录 → 容器内ES数据存储目录 + # 2. Kibana服务(ES的可视化管理工具,用于操作/监控ES) kibana: - image: kibana:8.6.1 - restart: always - container_name: kibana - ports: + image: kibana:8.6.1 # Kibana镜像,版本需与ES一致(8.6.1) + restart: always # 容器退出后自动重启 + container_name: kibana # 容器名称固定为"kibana" + ports: # 端口映射:主机5601端口 → 容器5601端口(Kibana默认Web端口) - 5601:5601 - environment: - - ELASTICSEARCH_HOSTS=http://es:9200 + environment: # 环境变量配置:指定关联的ES地址 + - ELASTICSEARCH_HOSTS=http://es:9200 # 指向同网络内的"es"服务(容器间通过服务名通信) + # 3. Django博客服务(核心应用服务) djangoblog: - build: . - restart: always - command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' - ports: + build: . # 基于当前目录的Dockerfile构建镜像(不使用现成镜像,需本地有Dockerfile) + restart: always # 容器退出后自动重启 + command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' # 容器启动后执行的命令:运行启动脚本 + ports: # 端口映射:主机8000端口 → 容器8000端口(Django默认开发服务器端口) - "8000:8000" - volumes: - - ./collectedstatic:/code/djangoblog/collectedstatic - - ./uploads:/code/djangoblog/uploads - environment: - - DJANGO_MYSQL_DATABASE=djangoblog - - DJANGO_MYSQL_USER=root - - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E - - DJANGO_MYSQL_HOST=db - - DJANGO_MYSQL_PORT=3306 - - DJANGO_MEMCACHED_LOCATION=memcached:11211 - - DJANGO_ELASTICSEARCH_HOST=es:9200 - links: + volumes: # 数据卷挂载:持久化应用数据/静态资源 + - ./collectedstatic:/code/djangoblog/collectedstatic # 主机静态资源目录 → 容器内静态资源目录(Nginx可直接访问) + - ./uploads:/code/djangoblog/uploads # 主机上传文件目录 → 容器内上传文件目录(如博客图片) + environment: # 环境变量配置:Django应用的关键参数(数据库、缓存、ES等) + - DJANGO_MYSQL_DATABASE=djangoblog # Django连接的MySQL数据库名 + - DJANGO_MYSQL_USER=root # MySQL用户名 + - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E # MySQL密码 + - DJANGO_MYSQL_HOST=db # MySQL服务地址(指向同网络内的"db"服务,需额外配置db服务) + - DJANGO_MYSQL_PORT=3306 # MySQL端口 + - DJANGO_MEMCACHED_LOCATION=memcached:11211 # Memcached缓存地址(指向同网络内的"memcached"服务,需额外配置) + - DJANGO_ELASTICSEARCH_HOST=es:9200 # ES服务地址(指向同网络内的"es"服务) + links: # 显式链接到其他服务(已逐步被depends_on替代,此处用于兼容) + - db # 链接到MySQL服务 + - memcached # 链接到Memcached服务 + depends_on: # 服务依赖:启动djangoblog前,先启动db服务(确保数据库就绪) - db - - memcached - depends_on: - - db - container_name: djangoblog - + container_name: djangoblog # 容器名称固定为"djangoblog" \ No newline at end of file diff --git a/src/DjangoBlog-master/deploy/docker-compose/docker-compose.yml b/src/DjangoBlog-master/deploy/docker-compose/docker-compose.yml index 9609af3..902b118 100644 --- a/src/DjangoBlog-master/deploy/docker-compose/docker-compose.yml +++ b/src/DjangoBlog-master/deploy/docker-compose/docker-compose.yml @@ -1,60 +1,67 @@ +# Docker Compose配置文件,版本为3(指定Compose语法版本) version: '3' +# 定义所有服务(容器) services: + # 1. MySQL数据库服务(存储应用数据) db: - image: mysql:latest - restart: always - environment: - - MYSQL_DATABASE=djangoblog - - MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E - ports: + image: mysql:latest # 使用最新版MySQL镜像 + restart: always # 容器退出后自动重启(确保服务持续运行) + environment: # 环境变量配置(数据库初始化参数) + - MYSQL_DATABASE=djangoblog # 自动创建的数据库名称 + - MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E # MySQL root用户密码 + ports: # 端口映射:主机3306端口 → 容器3306端口(MySQL默认端口) - 3306:3306 - volumes: - - ./bin/datas/mysql/:/var/lib/mysql - depends_on: + volumes: # 数据卷挂载:持久化MySQL数据 + - ./bin/datas/mysql/:/var/lib/mysql # 主机目录 → 容器内MySQL数据存储目录 + depends_on: # 服务依赖:启动db前先启动redis(可能用于数据库缓存等场景) - redis - container_name: db + container_name: db # 容器名称固定为"db" + # 2. Django博客应用服务(核心应用) djangoblog: - build: - context: ../../ - restart: always - command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' - ports: + build: # 构建配置 + context: ../../ # 指定Dockerfile所在的上下文目录(上级目录的上级目录) + restart: always # 容器退出后自动重启 + command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' # 启动命令:执行应用启动脚本 + ports: # 端口映射:主机8000端口 → 容器8000端口(Django应用端口) - "8000:8000" - volumes: - - ./collectedstatic:/code/djangoblog/collectedstatic - - ./logs:/code/djangoblog/logs - - ./uploads:/code/djangoblog/uploads - environment: - - DJANGO_MYSQL_DATABASE=djangoblog - - DJANGO_MYSQL_USER=root - - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E - - DJANGO_MYSQL_HOST=db - - DJANGO_MYSQL_PORT=3306 - - DJANGO_REDIS_URL=redis:6379 - links: + volumes: # 数据卷挂载:持久化应用数据和配置 + - ./collectedstatic:/code/djangoblog/collectedstatic # 静态资源目录(供Nginx访问) + - ./logs:/code/djangoblog/logs # 应用日志目录 + - ./uploads:/code/djangoblog/uploads # 用户上传文件目录(如图片) + environment: # 环境变量配置(应用连接参数) + - DJANGO_MYSQL_DATABASE=djangoblog # 数据库名称(与db服务对应) + - DJANGO_MYSQL_USER=root # 数据库用户名 + - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E # 数据库密码(与db服务对应) + - DJANGO_MYSQL_HOST=db # 数据库服务地址(指向同网络内的"db"服务) + - DJANGO_MYSQL_PORT=3306 # 数据库端口 + - DJANGO_REDIS_URL=redis:6379 # Redis服务地址(指向同网络内的"redis"服务) + links: # 显式链接到其他服务(用于容器间通信) + - db # 链接到MySQL服务 + - redis # 链接到Redis服务 + depends_on: # 服务依赖:启动djangoblog前先启动db服务(确保数据库就绪) - db - - redis - depends_on: - - db - container_name: djangoblog + container_name: djangoblog # 容器名称固定为"djangoblog" + + # 3. Nginx服务(反向代理和静态资源服务) nginx: - restart: always - image: nginx:latest - ports: + restart: always # 容器退出后自动重启 + image: nginx:latest # 使用最新版Nginx镜像 + ports: # 端口映射:HTTP(80)和HTTPS(443)端口 - "80:80" - "443:443" - volumes: - - ./bin/nginx.conf:/etc/nginx/nginx.conf - - ./collectedstatic:/code/djangoblog/collectedstatic - links: - - djangoblog:djangoblog - container_name: nginx + volumes: # 数据卷挂载:Nginx配置和静态资源 + - ./bin/nginx.conf:/etc/nginx/nginx.conf # 主机Nginx配置文件 → 容器内Nginx配置文件 + - ./collectedstatic:/code/djangoblog/collectedstatic # 静态资源目录(与djangoblog服务共享) + links: # 链接到djangoblog服务,实现反向代理 + - djangoblog:djangoblog # 将djangoblog服务映射为"djangoblog"主机名 + container_name: nginx # 容器名称固定为"nginx" + # 4. Redis服务(缓存服务,用于提升应用性能) redis: - restart: always - image: redis:latest - container_name: redis - ports: - - "6379:6379" + restart: always # 容器退出后自动重启 + image: redis:latest # 使用最新版Redis镜像 + container_name: redis # 容器名称固定为"redis" + ports: # 端口映射:主机6379端口 → 容器6379端口(Redis默认端口) + - "6379:6379" \ No newline at end of file diff --git a/src/DjangoBlog-master/deploy/k8s/configmap.yaml b/src/DjangoBlog-master/deploy/k8s/configmap.yaml index 835d4ad..da23389 100644 --- a/src/DjangoBlog-master/deploy/k8s/configmap.yaml +++ b/src/DjangoBlog-master/deploy/k8s/configmap.yaml @@ -1,119 +1,124 @@ -apiVersion: v1 -kind: ConfigMap +apiVersion: v1 # Kubernetes API版本,v1为稳定版本 +kind: ConfigMap # 资源类型为ConfigMap,用于存储非敏感配置数据 metadata: - name: web-nginx-config - namespace: djangoblog -data: - nginx.conf: | - user nginx; - worker_processes auto; - error_log /var/log/nginx/error.log notice; - pid /var/run/nginx.pid; + name: web-nginx-config # ConfigMap名称,标识该Nginx配置资源 + namespace: djangoblog # 所属命名空间,用于资源隔离,对应djangoblog应用 +data: # 配置数据,键为文件名,值为文件内容 + nginx.conf: | # Nginx主配置文件 + user nginx; # Nginx进程运行的用户 + worker_processes auto; # 工作进程数,auto表示按CPU核心数自动分配 + error_log /var/log/nginx/error.log notice; # 错误日志路径及级别(notice级别) + pid /var/run/nginx.pid; # Nginx进程PID文件路径 - events { - worker_connections 1024; - multi_accept on; - use epoll; + events { # 事件处理配置块 + worker_connections 1024; # 每个工作进程最大连接数 + multi_accept on; # 允许工作进程同时接受多个新连接 + use epoll; # 使用epoll I/O模型(Linux下高效事件驱动模型) } - http { - include /etc/nginx/mime.types; - default_type application/octet-stream; + http { # HTTP核心配置块 + include /etc/nginx/mime.types; # 引入MIME类型映射文件(识别文件类型) + default_type application/octet-stream; # 默认MIME类型(未知类型时使用) + # 定义日志格式,命名为main log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; # 访问日志路径,使用main格式 - access_log /var/log/nginx/access.log main; + sendfile on; # 启用sendfile系统调用(高效传输文件) + keepalive_timeout 65; # 长连接超时时间(65秒) + gzip on; # 启用gzip压缩(减少传输数据量) + gzip_disable "msie6"; # 对IE6浏览器禁用gzip(兼容性处理) - sendfile on; - keepalive_timeout 65; - gzip on; - gzip_disable "msie6"; - - gzip_vary on; - gzip_proxied any; - gzip_comp_level 8; - gzip_buffers 16 8k; - gzip_http_version 1.1; + # gzip压缩补充配置 + gzip_vary on; # 启用Vary: Accept-Encoding响应头(告知代理缓存压缩/非压缩版本) + gzip_proxied any; # 对所有代理请求启用压缩 + gzip_comp_level 8; # 压缩级别(1-9,8为较高压缩率) + gzip_buffers 16 8k; # 压缩缓冲区大小(16个8k缓冲区) + gzip_http_version 1.1; # 仅对HTTP/1.1及以上版本启用压缩 + # 需压缩的文件类型(文本、JS、CSS、图片等) gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; - # Include server configurations - include /etc/nginx/conf.d/*.conf; + include /etc/nginx/conf.d/*.conf; # 引入其他服务器配置文件 } - djangoblog.conf: | - server { - server_name lylinux.net; - root /code/djangoblog/collectedstatic/; - listen 80; - keepalive_timeout 70; - location /static/ { - expires max; - alias /code/djangoblog/collectedstatic/; + djangoblog.conf: | # lylinux.net域名的Nginx站点配置 + server { # 处理lylinux.net域名的服务配置 + server_name lylinux.net; # 绑定的主域名 + root /code/djangoblog/collectedstatic/; # 网站根目录(静态文件目录) + listen 80; # 监听80端口(HTTP) + keepalive_timeout 70; # 该站点长连接超时时间 + + location /static/ { # 处理静态文件请求 + expires max; # 静态文件缓存有效期设为最大(长期缓存) + alias /code/djangoblog/collectedstatic/; # 静态文件实际路径 } + # 处理特定静态文件(如robots.txt、网站验证文件等) location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ { - root /resource/djangopub; - expires 1d; - access_log off; - error_log off; + root /resource/djangopub; # 这些文件的根目录 + expires 1d; # 缓存1天 + access_log off; # 关闭访问日志 + error_log off; # 关闭错误日志 } - location / { - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $http_host; - proxy_set_header X-NginX-Proxy true; - proxy_redirect off; + location / { # 处理其他所有请求(反向代理到Django) + # 设置代理请求头(传递客户端信息给后端) + proxy_set_header X-Real-IP $remote_addr; # 客户端真实IP + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 代理链IP列表 + proxy_set_header Host $http_host; # 原始请求Host + proxy_set_header X-NginX-Proxy true; # 标识经Nginx代理 + proxy_redirect off; # 禁用代理重定向 + + # 若请求文件不存在,反向代理到Django服务(djangoblog为K8s内部服务名) if (!-f $request_filename) { proxy_pass http://djangoblog:8000; break; } } } - server { - server_name www.lylinux.net; - listen 80; - return 301 https://lylinux.net$request_uri; + server { # 处理www.lylinux.net域名(重定向配置) + server_name www.lylinux.net; # 绑定的www子域名 + listen 80; # 监听80端口 + return 301 https://lylinux.net$request_uri; # 永久重定向到主域名HTTPS地址 } - resource.lylinux.net.conf: | + resource.lylinux.net.conf: | # resource.lylinux.net子域名的配置(资源服务器) server { - index index.html index.htm; - server_name resource.lylinux.net; - root /resource/; + index index.html index.htm; # 默认索引文件 + server_name resource.lylinux.net; # 绑定的资源子域名 + root /resource/; # 资源文件根目录 - location /djangoblog/ { - alias /code/djangoblog/collectedstatic/; + location /djangoblog/ { # 映射Django静态文件路径 + alias /code/djangoblog/collectedstatic/; # 实际静态文件路径 } - access_log off; - error_log off; - include lylinux/resource.conf; + access_log off; # 关闭访问日志 + error_log off; # 关闭错误日志 + include lylinux/resource.conf; # 引入通用资源配置 } - lylinux.resource.conf: | - expires max; - access_log off; - log_not_found off; - add_header Pragma public; - add_header Cache-Control "public"; - add_header "Access-Control-Allow-Origin" "*"; + lylinux.resource.conf: | # 通用资源配置(被资源服务器引用) + expires max; # 资源缓存有效期设为最大 + access_log off; # 关闭访问日志 + log_not_found off; # 关闭文件未找到的错误日志 + add_header Pragma public; # 缓存控制头(告知客户端可缓存) + add_header Cache-Control "public"; # 缓存控制头(公开可缓存) + add_header "Access-Control-Allow-Origin" "*"; # 允许跨域访问(所有域名) --- -apiVersion: v1 -kind: ConfigMap +apiVersion: v1 # Kubernetes API版本 +kind: ConfigMap # 资源类型为ConfigMap,存储环境变量 metadata: - name: djangoblog-env - namespace: djangoblog -data: - DJANGO_MYSQL_DATABASE: djangoblog - DJANGO_MYSQL_USER: db_user - DJANGO_MYSQL_PASSWORD: db_password - DJANGO_MYSQL_HOST: db_host - DJANGO_MYSQL_PORT: db_port - DJANGO_REDIS_URL: "redis:6379" - DJANGO_DEBUG: "False" - MYSQL_ROOT_PASSWORD: db_password - MYSQL_DATABASE: djangoblog - MYSQL_PASSWORD: db_password - DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx - + name: djangoblog-env # ConfigMap名称,标识Django环境变量配置 + namespace: djangoblog # 所属命名空间(与应用一致) +data: # 环境变量键值对 + DJANGO_MYSQL_DATABASE: djangoblog # Django连接的MySQL数据库名 + DJANGO_MYSQL_USER: db_user # MySQL登录用户名 + DJANGO_MYSQL_PASSWORD: db_password # MySQL登录密码 + DJANGO_MYSQL_HOST: db_host # MySQL服务地址(K8s内部服务名或IP) + DJANGO_MYSQL_PORT: db_port # MySQL服务端口 + DJANGO_REDIS_URL: "redis:6379" # Redis服务地址及端口 + DJANGO_DEBUG: "False" # Django调试模式(生产环境关闭) + MYSQL_ROOT_PASSWORD: db_password # MySQL root用户密码(用于初始化) + MYSQL_DATABASE: djangoblog # 初始化的MySQL数据库名 + MYSQL_PASSWORD: db_password # MySQL普通用户密码(与Django配置一致) + DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Django加密密钥(用于会话、CSRF等) \ No newline at end of file diff --git a/src/DjangoBlog-master/deploy/k8s/deployment.yaml b/src/DjangoBlog-master/deploy/k8s/deployment.yaml index 414fdcc..c448b50 100644 --- a/src/DjangoBlog-master/deploy/k8s/deployment.yaml +++ b/src/DjangoBlog-master/deploy/k8s/deployment.yaml @@ -1,132 +1,161 @@ +# 第一部分:Django 博客应用部署配置 +# apiVersion 指定 Kubernetes API 版本,apps/v1 是 Deployment 资源的稳定版本 apiVersion: apps/v1 +# kind 定义资源类型为 Deployment(用于管理Pod的创建和扩展) kind: Deployment metadata: + # Deployment 的名称 name: djangoblog + # 部署所在的命名空间(用于资源隔离) namespace: djangoblog + # 为 Deployment 添加标签(用于筛选和关联资源) labels: app: djangoblog spec: + # 副本数:指定运行的 Pod 数量为 3 个(实现高可用) replicas: 3 + # 选择器:用于匹配要管理的 Pod 标签(必须与下面 template.metadata.labels 一致) selector: matchLabels: app: djangoblog + # Pod 模板:定义要创建的 Pod 的规格 template: metadata: + # Pod 的标签(与上面的 selector.matchLabels 对应) labels: app: djangoblog spec: + # 容器列表:一个 Pod 可以包含多个容器,这里定义应用容器 containers: - - name: djangoblog + - name: djangoblog # 容器名称 + # 容器使用的镜像(Django 博客应用镜像) image: liangliangyy/djangoblog:latest + # 镜像拉取策略:Always 表示每次都从仓库拉取最新镜像 imagePullPolicy: Always + # 容器暴露的端口(Django 应用默认运行在 8000 端口) ports: - containerPort: 8000 + # 从配置映射(ConfigMap)中注入环境变量 envFrom: - configMapRef: - name: djangoblog-env + name: djangoblog-env # 引用的 ConfigMap 名称 + # 就绪探针:判断容器是否已准备好接收请求(服务发现会依赖此状态) readinessProbe: - httpGet: - path: / - port: 8000 - initialDelaySeconds: 10 - periodSeconds: 30 + httpGet: # 通过 HTTP 请求检查就绪状态 + path: / # 检查的路径(应用根目录) + port: 8000 # 检查的端口 + initialDelaySeconds: 10 # 容器启动后延迟 10 秒开始首次检查 + periodSeconds: 30 # 每隔 30 秒检查一次 + # 存活探针:判断容器是否存活,若失败会重启容器 livenessProbe: - httpGet: - path: / - port: 8000 - initialDelaySeconds: 10 - periodSeconds: 30 + httpGet: # 通过 HTTP 请求检查存活状态 + path: / # 检查的路径 + port: 8000 # 检查的端口 + initialDelaySeconds: 10 # 容器启动后延迟 10 秒开始首次检查 + periodSeconds: 30 # 每隔 30 秒检查一次 + # 资源限制:控制容器对 CPU 和内存的使用 resources: - requests: - cpu: 10m - memory: 100Mi - limits: - cpu: "2" - memory: 2Gi + requests: # 资源请求(调度时的最小需求) + cpu: 10m # 10 毫核 CPU(1核=1000m) + memory: 100Mi # 100 兆内存 + limits: # 资源限制(容器最大可使用的资源) + cpu: "2" # 2 核 CPU + memory: 2Gi # 2 吉内存 + # 卷挂载:将持久卷挂载到容器内的指定路径 volumeMounts: - - name: djangoblog - mountPath: /code/djangoblog/collectedstatic - - name: resource - mountPath: /resource + - name: djangoblog # 引用下面 volumes 中定义的卷名称 + mountPath: /code/djangoblog/collectedstatic # 容器内的挂载路径(Django 静态文件目录) + - name: resource # 引用资源卷 + mountPath: /resource # 容器内的资源文件目录 + # 卷定义:声明需要挂载的持久卷 volumes: - - name: djangoblog - persistentVolumeClaim: - claimName: djangoblog-pvc - - name: resource + - name: djangoblog # 卷名称(与上面 volumeMounts.name 对应) + persistentVolumeClaim: # 使用持久卷声明(PVC) + claimName: djangoblog-pvc # 引用的 PVC 名称(需提前创建) + - name: resource # 资源卷名称 persistentVolumeClaim: - claimName: resource-pvc + claimName: resource-pvc # 资源对应的 PVC 名称 ---- + +# 第二部分:Redis 缓存服务部署配置 +--- # 分隔符:用于在一个文件中定义多个 Kubernetes 资源 apiVersion: apps/v1 kind: Deployment metadata: - name: redis - namespace: djangoblog + name: redis # Redis 部署名称 + namespace: djangoblog # 同属 djangoblog 命名空间 labels: - app: redis + app: redis # Redis 标签 spec: - replicas: 1 + replicas: 1 # Redis 单副本(简单部署,生产环境可能需要集群) selector: matchLabels: - app: redis + app: redis # 匹配 Redis Pod 标签 template: metadata: labels: - app: redis + app: redis # Pod 标签 spec: containers: - - name: redis - image: redis:latest + - name: redis # 容器名称 + image: redis:latest # Redis 官方最新镜像 + # 镜像拉取策略:IfNotPresent 表示本地有则使用本地镜像,否则拉取 imagePullPolicy: IfNotPresent ports: - - containerPort: 6379 + - containerPort: 6379 # Redis 默认端口 + # 资源限制(Redis 对资源需求较低) resources: requests: cpu: 10m memory: 100Mi limits: - cpu: 200m + cpu: 200m # 限制最大 200 毫核 CPU memory: 2Gi - + + +# 第三部分:MySQL 数据库部署配置 --- apiVersion: apps/v1 kind: Deployment metadata: - name: db + name: db # 数据库部署名称 namespace: djangoblog labels: - app: db + app: db # 数据库标签 spec: - replicas: 1 + replicas: 1 # 数据库单副本(生产环境需考虑主从或集群) selector: matchLabels: - app: db + app: db # 匹配数据库 Pod 标签 template: metadata: labels: - app: db + app: db # Pod 标签 spec: containers: - - name: db - image: mysql:latest + - name: db # 容器名称 + image: mysql:latest # MySQL 官方最新镜像 imagePullPolicy: IfNotPresent ports: - - containerPort: 3306 + - containerPort: 3306 # MySQL 默认端口 + # 从 ConfigMap 注入环境变量(如数据库密码、用户名等) envFrom: - configMapRef: - name: djangoblog-env + name: djangoblog-env # 复用 Django 应用的环境变量配置 + # 就绪探针:通过执行 mysqladmin ping 检查数据库是否就绪 readinessProbe: - exec: + exec: # 执行命令检查 command: - mysqladmin - ping - "-h" - - "127.0.0.1" + - "127.0.0.1" # 数据库主机(容器内本地) - "-u" - - "root" - - "-p$MYSQL_ROOT_PASSWORD" - initialDelaySeconds: 10 - periodSeconds: 10 + - "root" # 用户名 + - "-p$MYSQL_ROOT_PASSWORD" # 密码(从环境变量获取) + initialDelaySeconds: 10 # 延迟 10 秒检查 + periodSeconds: 10 # 每 10 秒检查一次 + # 存活探针:同就绪探针,确保数据库存活 livenessProbe: exec: command: @@ -139,6 +168,7 @@ spec: - "-p$MYSQL_ROOT_PASSWORD" initialDelaySeconds: 10 periodSeconds: 10 + # 资源限制(数据库对资源需求较高) resources: requests: cpu: 10m @@ -146,38 +176,42 @@ spec: limits: cpu: "2" memory: 2Gi + # 挂载数据库数据目录(持久化存储,避免数据丢失) volumeMounts: - - name: db-data - mountPath: /var/lib/mysql + - name: db-data # 引用数据卷 + mountPath: /var/lib/mysql # MySQL 数据存储路径 volumes: - - name: db-data + - name: db-data # 数据卷名称 persistentVolumeClaim: - claimName: db-pvc - + claimName: db-pvc # 数据库对应的 PVC 名称 + + +# 第四部分:Nginx 反向代理部署配置 --- apiVersion: apps/v1 kind: Deployment metadata: - name: nginx + name: nginx # Nginx 部署名称 namespace: djangoblog labels: - app: nginx + app: nginx # Nginx 标签 spec: - replicas: 1 + replicas: 1 # Nginx 单副本 selector: matchLabels: - app: nginx + app: nginx # 匹配 Nginx Pod 标签 template: metadata: labels: - app: nginx + app: nginx # Pod 标签 spec: containers: - - name: nginx - image: nginx:latest + - name: nginx # 容器名称 + image: nginx:latest # Nginx 官方最新镜像 imagePullPolicy: IfNotPresent ports: - - containerPort: 80 + - containerPort: 80 # Nginx 默认端口 + # 资源限制 resources: requests: cpu: 10m @@ -185,67 +219,82 @@ spec: limits: cpu: "2" memory: 2Gi + # 卷挂载:挂载配置文件和静态资源 volumeMounts: + # 挂载 Nginx 主配置文件(subPath 表示只挂载单个文件,而非目录) - name: nginx-config mountPath: /etc/nginx/nginx.conf subPath: nginx.conf + # 挂载默认站点配置 - name: nginx-config mountPath: /etc/nginx/conf.d/default.conf subPath: djangoblog.conf + # 挂载资源站点配置 - name: nginx-config mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf subPath: resource.lylinux.net.conf + # 挂载额外的资源配置 - name: nginx-config mountPath: /etc/nginx/lylinux/resource.conf subPath: lylinux.resource.conf + # 挂载 Django 静态文件目录(与 Django 应用共享存储) - name: djangoblog-pvc mountPath: /code/djangoblog/collectedstatic + # 挂载资源文件目录 - name: resource-pvc mountPath: /resource volumes: + # Nginx 配置卷:通过 ConfigMap 挂载配置文件(避免在镜像中硬编码配置) - name: nginx-config configMap: - name: web-nginx-config + name: web-nginx-config # 引用的 ConfigMap 名称 + # 挂载 Django 静态文件对应的 PVC - name: djangoblog-pvc persistentVolumeClaim: claimName: djangoblog-pvc + # 挂载资源文件对应的 PVC - name: resource-pvc persistentVolumeClaim: claimName: resource-pvc + +# 第五部分:Elasticsearch 搜索引擎部署配置 --- apiVersion: apps/v1 kind: Deployment metadata: - name: elasticsearch + name: elasticsearch # ES 部署名称 namespace: djangoblog labels: - app: elasticsearch + app: elasticsearch # ES 标签 spec: - replicas: 1 + replicas: 1 # ES 单节点(生产环境需集群) selector: matchLabels: - app: elasticsearch + app: elasticsearch # 匹配 ES Pod 标签 template: metadata: labels: - app: elasticsearch + app: elasticsearch # Pod 标签 spec: containers: - - name: elasticsearch + - name: elasticsearch # 容器名称 + # 带 IK 分词器的 ES 镜像(适用于中文搜索) image: liangliangyy/elasticsearch-analysis-ik:8.6.1 imagePullPolicy: IfNotPresent + # ES 环境变量配置 env: - - name: discovery.type + - name: discovery.type # 单节点模式(无需集群发现) value: single-node - - name: ES_JAVA_OPTS + - name: ES_JAVA_OPTS # JVM 内存配置(根据需求调整) value: "-Xms256m -Xmx256m" - - name: xpack.security.enabled + - name: xpack.security.enabled # 关闭安全验证(简化部署) value: "false" - - name: xpack.monitoring.templates.enabled + - name: xpack.monitoring.templates.enabled # 关闭监控模板 value: "false" ports: - - containerPort: 9200 + - containerPort: 9200 # ES HTTP 接口端口 + # 资源限制(ES 对内存需求较高) resources: requests: cpu: 10m @@ -253,22 +302,25 @@ spec: limits: cpu: "2" memory: 2Gi + # 就绪探针:检查 ES 是否就绪 readinessProbe: httpGet: - path: / + path: / # ES 健康检查路径 port: 9200 - initialDelaySeconds: 15 + initialDelaySeconds: 15 # 延迟 15 秒(ES 启动较慢) periodSeconds: 30 + # 存活探针:检查 ES 是否存活 livenessProbe: httpGet: path: / port: 9200 initialDelaySeconds: 15 periodSeconds: 30 + # 挂载 ES 数据目录(持久化存储索引数据) volumeMounts: - name: elasticsearch-data - mountPath: /usr/share/elasticsearch/data/ + mountPath: /usr/share/elasticsearch/data/ # ES 数据存储路径 volumes: - - name: elasticsearch-data - persistentVolumeClaim: - claimName: elasticsearch-pvc + - name: elasticsearch-data + persistentVolumeClaim: + claimName: elasticsearch-pvc # ES 对应的 PVC 名称 \ No newline at end of file diff --git a/src/DjangoBlog-master/deploy/k8s/gateway.yaml b/src/DjangoBlog-master/deploy/k8s/gateway.yaml index a8de073..c81cf30 100644 --- a/src/DjangoBlog-master/deploy/k8s/gateway.yaml +++ b/src/DjangoBlog-master/deploy/k8s/gateway.yaml @@ -1,17 +1,31 @@ +# Ingress 资源配置(用于管理外部访问集群内服务的规则) +# apiVersion 指定 Kubernetes API 版本,networking.k8s.io/v1 是 Ingress 的稳定版本 apiVersion: networking.k8s.io/v1 +# kind 定义资源类型为 Ingress(用于配置外部访问规则) kind: Ingress metadata: + # Ingress 资源的名称 name: nginx + # 所属命名空间(与前面的部署资源保持一致,确保资源在同一命名空间内可访问) namespace: djangoblog spec: + # 指定 Ingress 控制器的类别(需提前部署对应类别的 Ingress Controller,这里使用 nginx 类型) ingressClassName: nginx + # 访问规则定义(外部请求如何路由到集群内的服务) rules: + # 未指定 host 表示匹配所有未被其他规则匹配的主机(可理解为默认规则) - http: + # HTTP 协议的路由规则 paths: + # 路径规则:匹配以 / 开头的所有请求(即所有路径) - path: / + # 路径匹配类型:Prefix 表示前缀匹配(/ 会匹配所有路径) pathType: Prefix + # 后端服务配置:请求转发到哪个服务 backend: service: + # 目标服务的名称(需提前创建名为 nginx 的 Service,关联到 nginx 部署的 Pod) name: nginx + # 目标服务的端口号(对应 nginx 服务暴露的 80 端口) port: number: 80 \ No newline at end of file diff --git a/src/DjangoBlog-master/deploy/k8s/pv.yaml b/src/DjangoBlog-master/deploy/k8s/pv.yaml index 874b72f..4e48545 100644 --- a/src/DjangoBlog-master/deploy/k8s/pv.yaml +++ b/src/DjangoBlog-master/deploy/k8s/pv.yaml @@ -1,40 +1,44 @@ -apiVersion: v1 -kind: PersistentVolume +# 第一部分:数据库(MySQL)专用持久卷配置 +apiVersion: v1 # PV 资源使用的 Kubernetes API 版本 +kind: PersistentVolume # 资源类型为持久卷(PV),用于提供集群级别的存储资源 metadata: - name: local-pv-db + name: local-pv-db # PV 的名称,需唯一,这里明确关联数据库(db) spec: - capacity: - storage: 10Gi - volumeMode: Filesystem - accessModes: - - ReadWriteOnce - persistentVolumeReclaimPolicy: Retain - storageClassName: local-storage - local: - path: /mnt/local-storage-db - nodeAffinity: - required: + capacity: # 定义 PV 的存储容量 + storage: 10Gi # 分配 10GiB 存储空间(数据库通常需要较大空间) + volumeMode: Filesystem # 卷模式:Filesystem 表示以文件系统形式挂载(另一种是 Block 块设备) + accessModes: # 访问模式:定义 PV 可被如何访问 + - ReadWriteOnce # 仅允许单个节点以读写方式挂载(适合数据库等需独占写入的场景) + persistentVolumeReclaimPolicy: Retain # 回收策略:Retain 表示 PV 被释放后保留数据,需手动清理 + storageClassName: local-storage # 存储类名称,用于与 PersistentVolumeClaim(PVC)匹配 + local: # 声明为本地存储(使用节点上的本地磁盘,非分布式存储) + path: /mnt/local-storage-db # 本地存储的实际路径(需在对应节点上提前创建该目录) + nodeAffinity: # 节点亲和性:限制 PV 只能被特定节点使用(本地存储必须配置) + required: # 强制要求:必须满足以下条件才能使用该 PV nodeSelectorTerms: - - matchExpressions: - - key: kubernetes.io/hostname - operator: In + - matchExpressions: # 匹配规则 + - key: kubernetes.io/hostname # 匹配节点的主机名标签 + operator: In # 操作符:In 表示值在指定列表中 values: - - master ---- + - master # 仅允许主机名为 "master" 的节点使用该 PV + + +# 第二部分:Django 应用静态文件专用持久卷配置 +--- # 分隔符:用于在一个文件中定义多个资源 apiVersion: v1 kind: PersistentVolume metadata: - name: local-pv-djangoblog + name: local-pv-djangoblog # PV 名称,关联 Django 应用 spec: capacity: - storage: 5Gi + storage: 5Gi # 分配 5GiB 存储空间(静态文件需求较小) volumeMode: Filesystem accessModes: - - ReadWriteOnce + - ReadWriteOnce # 单节点读写(静态文件通常由单节点写入,多节点读取可考虑 ReadOnlyMany) persistentVolumeReclaimPolicy: Retain - storageClassName: local-storage + storageClassName: local-storage # 与前面的 PV 共用同一存储类 local: - path: /mnt/local-storage-djangoblog + path: /mnt/local-storage-djangoblog # Django 静态文件的本地存储路径 nodeAffinity: required: nodeSelectorTerms: @@ -42,24 +46,25 @@ spec: - key: kubernetes.io/hostname operator: In values: - - master + - master # 同样限制在 "master" 节点 +# 第三部分:资源文件专用持久卷配置 --- apiVersion: v1 kind: PersistentVolume metadata: - name: local-pv-resource + name: local-pv-resource # PV 名称,关联通用资源文件 spec: capacity: - storage: 5Gi + storage: 5Gi # 分配 5GiB 存储空间 volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage local: - path: /mnt/resource/ + path: /mnt/resource/ # 资源文件(如上传的图片、附件等)的本地存储路径 nodeAffinity: required: nodeSelectorTerms: @@ -67,23 +72,25 @@ spec: - key: kubernetes.io/hostname operator: In values: - - master + - master # 限制在 "master" 节点 + +# 第四部分:Elasticsearch 搜索引擎专用持久卷配置 --- apiVersion: v1 kind: PersistentVolume metadata: - name: local-pv-elasticsearch + name: local-pv-elasticsearch # PV 名称,关联 Elasticsearch spec: capacity: - storage: 5Gi + storage: 5Gi # 分配 5GiB 存储空间(用于存储 ES 索引数据) volumeMode: Filesystem accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain storageClassName: local-storage local: - path: /mnt/local-storage-elasticsearch + path: /mnt/local-storage-elasticsearch # ES 数据的本地存储路径 nodeAffinity: required: nodeSelectorTerms: @@ -91,4 +98,4 @@ spec: - key: kubernetes.io/hostname operator: In values: - - master \ No newline at end of file + - master # 限制在 "master" 节点 \ No newline at end of file diff --git a/src/DjangoBlog-master/deploy/k8s/pvc.yaml b/src/DjangoBlog-master/deploy/k8s/pvc.yaml index ef238c5..b1cff4b 100644 --- a/src/DjangoBlog-master/deploy/k8s/pvc.yaml +++ b/src/DjangoBlog-master/deploy/k8s/pvc.yaml @@ -1,60 +1,66 @@ -apiVersion: v1 -kind: PersistentVolumeClaim +# 第一部分:数据库(MySQL)持久卷声明(PVC) +# PVC 用于向 Kubernetes 请求存储资源,需与 PV 匹配后才能供 Pod 使用 +apiVersion: v1 # PVC 资源对应的 Kubernetes API 版本 +kind: PersistentVolumeClaim # 资源类型为持久卷声明(PVC) metadata: - name: db-pvc - namespace: djangoblog + name: db-pvc # PVC 名称,需与数据库 Deployment 中引用的 PVC 名称一致 + namespace: djangoblog # 所属命名空间,与数据库 Deployment、对应 PV 保持一致(资源隔离) spec: - storageClassName: local-storage - volumeName: local-pv-db - accessModes: - - ReadWriteOnce - resources: + storageClassName: local-storage # 存储类名称,必须与目标 PV 的 storageClassName 完全匹配(用于筛选 PV) + volumeName: local-pv-db # 显式指定绑定的 PV 名称(强制绑定,非必填;不指定则按条件自动匹配) + accessModes: # 访问模式,需与目标 PV 的 accessModes 兼容(否则无法绑定) + - ReadWriteOnce # 单节点读写模式,与数据库 PV 的访问模式一致(满足数据库独占写入需求) + resources: # 存储资源请求,定义需要的存储容量 requests: - storage: 10Gi + storage: 10Gi # 请求 10GiB 存储空间,需小于或等于目标 PV 的 capacity(此处与 db PV 容量完全匹配) ---- +# 第二部分:Django 应用静态文件持久卷声明(PVC) +--- # 资源分隔符,用于在单个文件中定义多个 Kubernetes 资源 apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: djangoblog-pvc + name: djangoblog-pvc # PVC 名称,需与 Django Deployment 中 volumeMounts 引用的 PVC 名称一致 namespace: djangoblog spec: - volumeName: local-pv-djangoblog - storageClassName: local-storage + volumeName: local-pv-djangoblog # 显式绑定 Django 应用专用 PV + storageClassName: local-storage # 与 Django 应用 PV 的存储类一致 accessModes: - - ReadWriteOnce + - ReadWriteOnce # 单节点读写,与 Django 应用 PV 访问模式匹配 resources: requests: - storage: 5Gi + storage: 5Gi # 请求 5GiB 存储空间,与 Django 应用 PV 容量一致(用于存储静态文件) + +# 第三部分:资源文件(如上传附件、图片)持久卷声明(PVC) --- apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: resource-pvc + name: resource-pvc # PVC 名称,需与 Django、Nginx Deployment 中引用的资源卷 PVC 名称一致 namespace: djangoblog spec: - volumeName: local-pv-resource - storageClassName: local-storage + volumeName: local-pv-resource # 显式绑定资源文件专用 PV + storageClassName: local-storage # 与资源文件 PV 的存储类一致 accessModes: - - ReadWriteOnce + - ReadWriteOnce # 单节点读写,与资源文件 PV 访问模式匹配 resources: requests: - storage: 5Gi + storage: 5Gi # 请求 5GiB 存储空间,与资源文件 PV 容量一致 + +# 第四部分:Elasticsearch(搜索引擎)持久卷声明(PVC) --- apiVersion: v1 kind: PersistentVolumeClaim metadata: - name: elasticsearch-pvc + name: elasticsearch-pvc # PVC 名称,需与 Elasticsearch Deployment 中引用的 PVC 名称一致 namespace: djangoblog spec: - volumeName: local-pv-elasticsearch - storageClassName: local-storage + volumeName: local-pv-elasticsearch # 显式绑定 Elasticsearch 专用 PV + storageClassName: local-storage # 与 Elasticsearch PV 的存储类一致 accessModes: - - ReadWriteOnce + - ReadWriteOnce # 单节点读写,与 Elasticsearch PV 访问模式匹配 resources: requests: - storage: 5Gi - \ No newline at end of file + storage: 5Gi # 请求 5GiB 存储空间,与 Elasticsearch PV 容量一致(用于存储索引数据) \ No newline at end of file diff --git a/src/DjangoBlog-master/deploy/k8s/service.yaml b/src/DjangoBlog-master/deploy/k8s/service.yaml index 4ef2931..c361b9f 100644 --- a/src/DjangoBlog-master/deploy/k8s/service.yaml +++ b/src/DjangoBlog-master/deploy/k8s/service.yaml @@ -1,80 +1,93 @@ -apiVersion: v1 -kind: Service +# 第一部分:Django 应用服务(Service) +# Service 用于为集群内的 Pod 提供稳定网络访问地址,实现 Pod 访问的负载均衡和服务发现 +apiVersion: v1 # Service 资源对应的 Kubernetes API 版本 +kind: Service # 资源类型为 Service metadata: - name: djangoblog - namespace: djangoblog + name: djangoblog # Service 名称,需与其他组件(如 Nginx 配置)中引用的服务名一致 + namespace: djangoblog # 所属命名空间,与 Django Deployment、其他组件保持一致(资源隔离) labels: - app: djangoblog + app: djangoblog # 服务标签,用于筛选和管理服务资源 spec: - selector: + selector: # 标签选择器:通过标签匹配要管理的 Pod(必须与 Django Pod 的标签一致) app: djangoblog - ports: - - protocol: TCP - port: 8000 - targetPort: 8000 - type: ClusterIP ---- + ports: # 端口配置:定义服务暴露的端口与 Pod 端口的映射关系 + - protocol: TCP # 网络协议,默认 TCP(常用还有 UDP) + port: 8000 # 服务暴露给集群内部的端口(其他组件通过此端口访问该服务) + targetPort: 8000 # 服务转发请求到 Pod 的目标端口(需与 Django 容器暴露的端口一致) + type: ClusterIP # 服务类型:ClusterIP 表示仅在集群内部暴露服务,外部无法直接访问(适合内部组件通信) + + +# 第二部分:Nginx 服务(Service) +--- # 资源分隔符,用于在单个文件中定义多个 Kubernetes 资源 apiVersion: v1 kind: Service metadata: - name: nginx + name: nginx # Service 名称,需与 Ingress 配置中引用的服务名一致 namespace: djangoblog labels: app: nginx spec: selector: - app: nginx + app: nginx # 匹配 Nginx Pod 的标签 ports: - protocol: TCP - port: 80 - targetPort: 80 - type: ClusterIP + port: 80 # 服务暴露的端口(Ingress 转发请求到该端口) + targetPort: 80 # 转发到 Nginx 容器暴露的 80 端口 + type: ClusterIP # 集群内部访问(外部通过 Ingress 间接访问 Nginx 服务) + + +# 第三部分:Redis 缓存服务(Service) --- apiVersion: v1 kind: Service metadata: - name: redis + name: redis # Service 名称,需与 Django 应用配置中访问 Redis 的服务名一致 namespace: djangoblog labels: app: redis spec: selector: - app: redis + app: redis # 匹配 Redis Pod 的标签 ports: - protocol: TCP - port: 6379 - targetPort: 6379 - type: ClusterIP + port: 6379 # 服务暴露的端口(Redis 默认端口) + targetPort: 6379 # 转发到 Redis 容器暴露的 6379 端口 + type: ClusterIP # 仅集群内部访问(缓存服务无需外部暴露) + + +# 第四部分:MySQL 数据库服务(Service) --- apiVersion: v1 kind: Service metadata: - name: db + name: db # Service 名称,需与 Django 应用配置中访问数据库的服务名一致 namespace: djangoblog labels: app: db spec: selector: - app: db + app: db # 匹配 MySQL Pod 的标签 ports: - protocol: TCP - port: 3306 - targetPort: 3306 - type: ClusterIP + port: 3306 # 服务暴露的端口(MySQL 默认端口) + targetPort: 3306 # 转发到 MySQL 容器暴露的 3306 端口 + type: ClusterIP # 仅集群内部访问(数据库服务禁止外部直接访问,保障安全) + + +# 第五部分:Elasticsearch 搜索引擎服务(Service) --- apiVersion: v1 kind: Service metadata: - name: elasticsearch + name: elasticsearch # Service 名称,需与 Django 应用配置中访问 ES 的服务名一致 namespace: djangoblog labels: app: elasticsearch spec: selector: - app: elasticsearch + app: elasticsearch # 匹配 Elasticsearch Pod 的标签 ports: - protocol: TCP - port: 9200 - targetPort: 9200 - type: ClusterIP - + port: 9200 # 服务暴露的端口(ES HTTP 接口默认端口) + targetPort: 9200 # 转发到 ES 容器暴露的 9200 端口 + type: ClusterIP # 仅集群内部访问(搜索引擎无需外部直接暴露) \ No newline at end of file diff --git a/src/DjangoBlog-master/deploy/k8s/storageclass.yaml b/src/DjangoBlog-master/deploy/k8s/storageclass.yaml index 5d5a14c..0063318 100644 --- a/src/DjangoBlog-master/deploy/k8s/storageclass.yaml +++ b/src/DjangoBlog-master/deploy/k8s/storageclass.yaml @@ -1,10 +1,20 @@ +# StorageClass 资源配置(用于定义存储资源的类型和动态供应策略) +# apiVersion 指定 Kubernetes API 版本,storage.k8s.io/v1 是 StorageClass 的稳定版本 apiVersion: storage.k8s.io/v1 +# kind 定义资源类型为 StorageClass(用于统一管理存储资源的属性) kind: StorageClass metadata: + # StorageClass 的名称,需与前面 PV 和 PVC 中指定的 storageClassName 一致 name: local-storage + # 注解:设置为默认存储类(当 PVC 未指定 storageClassName 时,自动使用此存储类) annotations: storageclass.kubernetes.io/is-default-class: "true" -provisioner: kubernetes.io/no-provisioner -volumeBindingMode: Immediate - +spec: + # 存储供应器:指定用于动态创建 PV 的插件(此处使用 no-provisioner 表示不支持动态供应) + # 因为前面的 PV 是手动创建的本地存储,无需动态生成,所以使用此供应器 + provisioner: kubernetes.io/no-provisioner + # 卷绑定模式:Immediate 表示 PVC 创建后立即尝试绑定可用的 PV(不等待 Pod 调度) + # 对于本地存储,若使用 WaitForFirstConsumer 模式会更合适(等待 Pod 调度后再绑定对应节点的 PV) + # 此处配置为 Immediate,需确保 PV 已提前创建且满足 PVC 条件 + volumeBindingMode: Immediate diff --git a/src/DjangoBlog-master/deploy/nginx.conf b/src/DjangoBlog-master/deploy/nginx.conf index 32161d8..ce2582f 100644 --- a/src/DjangoBlog-master/deploy/nginx.conf +++ b/src/DjangoBlog-master/deploy/nginx.conf @@ -1,50 +1,82 @@ -user nginx; +# Nginx 核心配置文件,用于处理静态资源和反向代理请求到 Django 应用 +# 全局配置段:设置 Nginx 整体运行参数 +nginx; # 标识该文件为 Nginx 配置文件(固定起始标识) +# 工作进程数:auto 表示自动根据服务器 CPU 核心数分配(优化并发性能) worker_processes auto; +# 错误日志配置:指定日志路径和日志级别(notice 级别记录重要信息,不冗余) error_log /var/log/nginx/error.log notice; +# PID 文件路径:存储 Nginx 主进程 ID,用于管理 Nginx 进程(如重启、停止) pid /var/run/nginx.pid; +# 事件模块配置:控制 Nginx 网络连接相关参数 events { + # 单个工作进程允许的最大并发连接数(1024 为基础值,可根据服务器性能调整) worker_connections 1024; } +# HTTP 模块配置:处理 HTTP 请求的核心配置,包含全局规则和虚拟主机 http { + # 引入 MIME 类型映射文件:定义不同文件后缀对应的 Content-Type(如 .html 对应 text/html) include /etc/nginx/mime.types; + # 默认 MIME 类型:当文件类型未匹配时,默认使用二进制流类型(避免浏览器直接解析未知文件) default_type application/octet-stream; + # 日志格式定义:自定义访问日志的记录字段,命名为 main log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; - + # 访问日志配置:使用上面定义的 main 格式,指定日志存储路径 access_log /var/log/nginx/access.log main; + # 开启高效文件传输模式:使用内核零拷贝技术,提升静态文件传输效率 sendfile on; - #tcp_nopush on; + # tcp_nopush on; # 可选配置:开启后会累积数据包再发送,适合大文件传输,默认关闭 + # 长连接超时时间:客户端与 Nginx 保持连接的最长时间(65 秒,超时后自动断开) keepalive_timeout 65; - #gzip on; + # gzip on; # 可选配置:开启 Gzip 压缩,减少传输带宽,默认关闭 + + # 虚拟主机配置:定义一个具体的站点规则(处理 80 端口的 HTTP 请求) server { + # 站点根目录:默认请求的文件查找路径(此处指向 Django 静态文件目录) root /code/djangoblog/collectedstatic/; + # 监听端口:该虚拟主机处理 80 端口的请求(Nginx 默认 HTTP 端口) listen 80; + # 长连接超时时间:覆盖 HTTP 模块的全局配置,仅作用于当前虚拟主机 keepalive_timeout 70; + + # 静态资源路径规则:匹配以 /static/ 开头的请求(处理 Django 静态文件) location /static/ { + # 缓存控制:设置静态文件缓存时间为最大(浏览器会长期缓存,减少重复请求) expires max; + # 路径别名:将 /static/ 请求映射到实际的静态文件目录(与 root 配合确保路径正确) alias /code/djangoblog/collectedstatic/; } + + # 默认路径规则:匹配所有未被上面规则匹配的请求(转发到 Django 应用) location / { + # 转发请求头:传递客户端真实 IP 到 Django(否则 Django 会认为请求来自 Nginx) proxy_set_header X-Real-IP $remote_addr; + # 转发请求头:传递客户端 IP 列表(适用于多层代理场景) proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # 转发请求头:传递原始请求的 Host 头(确保 Django 正确识别请求域名) proxy_set_header Host $http_host; + # 转发标识:告诉 Django 请求来自 Nginx 代理 proxy_set_header X-NginX-Proxy true; + # 关闭重定向处理:禁止 Nginx 自动修改 Django 返回的重定向地址 proxy_redirect off; + + # 条件判断:如果请求的文件在 Nginx 本地不存在(非静态文件) if (!-f $request_filename) { + # 反向代理:将请求转发到 Django 服务(通过 Kubernetes Service 名称 djangoblog 的 8000 端口) proxy_pass http://djangoblog:8000; - break; + break; # 跳出条件判断,不再执行后续规则 } } } -} +} \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/__init__.py b/src/DjangoBlog-master/djangoblog/__init__.py index 1e205f4..0c523dd 100644 --- a/src/DjangoBlog-master/djangoblog/__init__.py +++ b/src/DjangoBlog-master/djangoblog/__init__.py @@ -1 +1,3 @@ +# Django 应用的默认配置指定 +# 作用:告诉 Django 当该应用被加载时,应使用哪个配置类进行初始化 default_app_config = 'djangoblog.apps.DjangoblogAppConfig' diff --git a/src/DjangoBlog-master/djangoblog/admin_site.py b/src/DjangoBlog-master/djangoblog/admin_site.py index f120405..5705829 100644 --- a/src/DjangoBlog-master/djangoblog/admin_site.py +++ b/src/DjangoBlog-master/djangoblog/admin_site.py @@ -1,64 +1,90 @@ +# 导入 Django 内置的 AdminSite 基础类(后台管理站点核心类) from django.contrib.admin import AdminSite +# 导入日志记录模型(用于记录后台操作日志) from django.contrib.admin.models import LogEntry +# 导入站点管理相关的默认 admin 类和模型(Django 内置的站点管理功能) from django.contrib.sites.admin import SiteAdmin from django.contrib.sites.models import Site -from accounts.admin import * -from blog.admin import * -from blog.models import * -from comments.admin import * -from comments.models import * +# 导入各应用自定义的 admin 配置和模型(将各模块的后台管理逻辑聚合到此处) +from accounts.admin import * # 用户账户相关的 admin 配置 +from blog.admin import * # 博客核心(文章、分类等)的 admin 配置 +from blog.models import * # 博客核心模型 +from comments.admin import * # 评论相关的 admin 配置 +from comments.models import *# 评论模型 +# 导入自定义的日志条目 admin 配置(扩展日志展示功能) from djangoblog.logentryadmin import LogEntryAdmin -from oauth.admin import * -from oauth.models import * -from owntracks.admin import * -from owntracks.models import * -from servermanager.admin import * -from servermanager.models import * +from oauth.admin import * # 第三方登录(OAuth)相关的 admin 配置 +from oauth.models import * # OAuth 相关模型 +from owntracks.admin import *# 位置追踪(OwnTracks)相关的 admin 配置 +from owntracks.models import *# 位置追踪模型 +from servermanager.admin import *# 服务器管理相关的 admin 配置 +from servermanager.models import *# 服务器管理模型 +# 自定义后台管理站点类(继承自 Django 内置的 AdminSite) class DjangoBlogAdminSite(AdminSite): + # 后台站点头部显示的标题(登录后顶部导航栏的文字) site_header = 'djangoblog administration' + # 浏览器标签页显示的标题(页面标题) site_title = 'djangoblog site admin' + # 初始化方法(调用父类构造方法,确保基础功能正常) def __init__(self, name='admin'): super().__init__(name) + # 权限控制方法:判断用户是否有权限访问后台 def has_permission(self, request): + # 仅允许超级用户(is_superuser=True)访问后台 return request.user.is_superuser + # 以下为注释掉的自定义 URL 示例(可扩展后台功能) # def get_urls(self): + # # 先获取父类默认的 URL 配置 # urls = super().get_urls() # from django.urls import path + # # 导入自定义视图(例如刷新缓存的视图) # from blog.views import refresh_memcache # + # # 定义自定义 URL 规则(如添加一个 /admin/refresh/ 路径) # my_urls = [ # path('refresh/', self.admin_view(refresh_memcache), name="refresh"), # ] + # # 合并默认 URL 和自定义 URL(自定义 URL 优先级更高) # return urls + my_urls +# 实例化自定义的后台管理站点(名称为 'admin',与默认后台路径保持一致) admin_site = DjangoBlogAdminSite(name='admin') -admin_site.register(Article, ArticlelAdmin) -admin_site.register(Category, CategoryAdmin) -admin_site.register(Tag, TagAdmin) -admin_site.register(Links, LinksAdmin) -admin_site.register(SideBar, SideBarAdmin) -admin_site.register(BlogSettings, BlogSettingsAdmin) +# 注册模型与对应的 admin 配置到自定义后台站点(实现各模型在后台的管理界面) +# 博客核心模型注册 +admin_site.register(Article, ArticlelAdmin) # 文章模型 + 其admin配置 +admin_site.register(Category, CategoryAdmin) # 分类模型 + 其admin配置 +admin_site.register(Tag, TagAdmin) # 标签模型 + 其admin配置 +admin_site.register(Links, LinksAdmin) # 友情链接模型 + 其admin配置 +admin_site.register(SideBar, SideBarAdmin) # 侧边栏模型 + 其admin配置 +admin_site.register(BlogSettings, BlogSettingsAdmin) # 博客设置模型 + 其admin配置 -admin_site.register(commands, CommandsAdmin) -admin_site.register(EmailSendLog, EmailSendLogAdmin) +# 服务器管理模型注册 +admin_site.register(commands, CommandsAdmin) # 命令模型 + 其admin配置 +admin_site.register(EmailSendLog, EmailSendLogAdmin) # 邮件发送日志 + 其admin配置 -admin_site.register(BlogUser, BlogUserAdmin) +# 用户模型注册 +admin_site.register(BlogUser, BlogUserAdmin) # 自定义用户模型 + 其admin配置 -admin_site.register(Comment, CommentAdmin) +# 评论模型注册 +admin_site.register(Comment, CommentAdmin) # 评论模型 + 其admin配置 -admin_site.register(OAuthUser, OAuthUserAdmin) -admin_site.register(OAuthConfig, OAuthConfigAdmin) +# OAuth 相关模型注册 +admin_site.register(OAuthUser, OAuthUserAdmin) # OAuth用户模型 + 其admin配置 +admin_site.register(OAuthConfig, OAuthConfigAdmin) # OAuth配置 + 其admin配置 -admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) +# 位置追踪模型注册 +admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) # 位置日志 + 其admin配置 +# Django 内置站点模型注册(使用默认的 SiteAdmin 配置) admin_site.register(Site, SiteAdmin) -admin_site.register(LogEntry, LogEntryAdmin) +# 后台操作日志模型注册(使用自定义的 LogEntryAdmin 配置,增强日志展示) +admin_site.register(LogEntry, LogEntryAdmin) \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/apps.py b/src/DjangoBlog-master/djangoblog/apps.py index d29e318..3dc5603 100644 --- a/src/DjangoBlog-master/djangoblog/apps.py +++ b/src/DjangoBlog-master/djangoblog/apps.py @@ -1,11 +1,20 @@ +# 导入 Django 的应用配置基类(所有应用配置类需继承此类) from django.apps import AppConfig +# 自定义应用配置类(用于 djangoblog 应用的初始化设置) class DjangoblogAppConfig(AppConfig): + # 定义模型主键的默认类型:使用 BigAutoField(自增 BigInteger 类型) + # 替代旧版的 AutoField(自增 Integer),支持更大范围的主键值 default_auto_field = 'django.db.models.BigAutoField' + # 应用的名称(必须与项目中 INSTALLED_APPS 配置的名称一致) name = 'djangoblog' + # 应用就绪方法:当 Django 加载完所有应用后自动调用(用于初始化操作) def ready(self): + # 调用父类的 ready 方法,确保基础初始化逻辑执行 super().ready() - # Import and load plugins here + # 导入并加载插件(应用启动时加载所有注册的插件) + # 注意:避免在模块顶部导入,防止 Django 初始化时循环导入问题 from .plugin_manage.loader import load_plugins - load_plugins() \ No newline at end of file + # 执行插件加载函数(例如注册钩子、初始化插件功能等) + load_plugins() \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/blog_signals.py b/src/DjangoBlog-master/djangoblog/blog_signals.py index 393f441..ffc3b89 100644 --- a/src/DjangoBlog-master/djangoblog/blog_signals.py +++ b/src/DjangoBlog-master/djangoblog/blog_signals.py @@ -1,7 +1,11 @@ +# 导入线程模块:用于异步执行耗时操作(如发送邮件,避免阻塞主流程) import _thread +# 导入日志模块:记录操作日志和错误信息 import logging +# 导入 Django 信号核心类:用于定义和处理自定义信号 import django.dispatch +# 导入 Django 配置、模型、工具类:支撑信号处理中的业务逻辑 from django.conf import settings from django.contrib.admin.models import LogEntry from django.contrib.auth.signals import user_logged_in, user_logged_out @@ -9,6 +13,7 @@ from django.core.mail import EmailMultiAlternatives from django.db.models.signals import post_save from django.dispatch import receiver +# 导入项目自定义模型和工具函数:适配博客业务场景 from comments.models import Comment from comments.utils import send_comment_email from djangoblog.spider_notify import SpiderNotify @@ -16,54 +21,73 @@ from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, del from djangoblog.utils import get_current_site from oauth.models import OAuthUser +# 初始化日志对象:用于记录当前模块的日志(如邮件发送失败、爬虫通知错误) logger = logging.getLogger(__name__) +# 定义自定义信号:第三方登录(OAuth)成功后触发的信号,携带用户ID参数 oauth_user_login_signal = django.dispatch.Signal(['id']) +# 定义自定义信号:发送邮件的信号,携带收件人、标题、内容参数 send_email_signal = django.dispatch.Signal( ['emailto', 'title', 'content']) +# 信号接收器:监听 send_email_signal 信号,触发邮件发送逻辑 @receiver(send_email_signal) def send_email_signal_handler(sender, **kwargs): - emailto = kwargs['emailto'] - title = kwargs['title'] - content = kwargs['content'] + # 从信号参数中提取邮件相关信息 + emailto = kwargs['emailto'] # 收件人列表 + title = kwargs['title'] # 邮件标题 + content = kwargs['content'] # 邮件内容(HTML格式) + # 构建 HTML 格式邮件:支持富文本内容 msg = EmailMultiAlternatives( title, content, - from_email=settings.DEFAULT_FROM_EMAIL, + from_email=settings.DEFAULT_FROM_EMAIL, # 发件人(从项目配置中获取) to=emailto) - msg.content_subtype = "html" + msg.content_subtype = "html" # 声明邮件内容为 HTML 类型 + # 记录邮件发送日志到数据库 from servermanager.models import EmailSendLog log = EmailSendLog() log.title = title log.content = content - log.emailto = ','.join(emailto) + log.emailto = ','.join(emailto) # 收件人列表转字符串存储 try: + # 发送邮件:返回成功发送的邮件数量 result = msg.send() - log.send_result = result > 0 + log.send_result = result > 0 # 发送成功标记(数量>0即为成功) except Exception as e: + # 捕获发送异常,记录错误日志 logger.error(f"失败邮箱号: {emailto}, {e}") - log.send_result = False - log.save() + log.send_result = False # 标记发送失败 + finally: + # 保存日志记录到数据库 + log.save() +# 信号接收器:监听 oauth_user_login_signal 信号,处理 OAuth 登录后的逻辑 @receiver(oauth_user_login_signal) def oauth_user_login_signal_handler(sender, **kwargs): + # 从信号参数中提取 OAuth 用户ID id = kwargs['id'] + # 获取对应的 OAuth 用户对象 oauthuser = OAuthUser.objects.get(id=id) + # 获取当前站点域名(用于判断头像是否为本站地址) site = get_current_site().domain + + # 若用户头像不是本站地址(如第三方平台的远程图片),则下载并保存到本地 if oauthuser.picture and not oauthuser.picture.find(site) >= 0: from djangoblog.utils import save_user_avatar - oauthuser.picture = save_user_avatar(oauthuser.picture) - oauthuser.save() + oauthuser.picture = save_user_avatar(oauthuser.picture) # 下载并更新头像路径 + oauthuser.save() # 保存更新后的用户信息 + # 删除侧边栏缓存:用户登录状态变化可能影响侧边栏内容(如显示登录用户信息) delete_sidebar_cache() +# 信号接收器:监听所有模型的 post_save 信号(模型保存后触发) @receiver(post_save) def model_post_save_callback( sender, @@ -73,50 +97,74 @@ def model_post_save_callback( using, update_fields, **kwargs): + # 标记是否需要清理缓存 clearcache = False + + # 跳过 Admin 操作日志(LogEntry)的处理:避免日志保存时触发不必要的逻辑 if isinstance(instance, LogEntry): return + + # 处理有 "get_full_url" 方法的模型(如 Article 文章模型) if 'get_full_url' in dir(instance): + # 判断是否仅更新了 "views" 字段(文章阅读量) is_update_views = update_fields == {'views'} + # 非测试环境且非阅读量更新:通知搜索引擎(如百度)收录新页面 if not settings.TESTING and not is_update_views: try: - notify_url = instance.get_full_url() - SpiderNotify.baidu_notify([notify_url]) + notify_url = instance.get_full_url() # 获取模型的完整访问链接 + SpiderNotify.baidu_notify([notify_url]) # 调用百度爬虫通知接口 except Exception as ex: + # 捕获通知异常,记录错误日志 logger.error("notify sipder", ex) + # 非阅读量更新:标记需要清理缓存(如文章内容、标题修改) if not is_update_views: clearcache = True + # 处理 Comment 评论模型的保存逻辑 if isinstance(instance, Comment): + # 仅处理已启用的评论(is_enable=True) if instance.is_enable: + # 获取评论所属文章的访问路径 path = instance.article.get_absolute_url() + # 获取当前站点域名(处理端口号,仅保留域名部分) site = get_current_site().domain if site.find(':') > 0: site = site[0:site.find(':')] + # 清理文章详情页的视图缓存:避免显示旧评论 expire_view_cache( path, servername=site, serverport=80, key_prefix='blogdetail') + # 清理 SEO 处理器缓存:评论变化可能影响页面 SEO 信息 if cache.get('seo_processor'): cache.delete('seo_processor') + # 清理该文章的评论列表缓存 comment_cache_key = 'article_comments_{id}'.format( id=instance.article.id) cache.delete(comment_cache_key) + # 清理侧边栏缓存:侧边栏可能显示最新评论 delete_sidebar_cache() + # 清理评论分页视图的缓存 delete_view_cache('article_comments', [str(instance.article.pk)]) + # 异步发送评论通知邮件:用线程避免阻塞评论保存流程 _thread.start_new_thread(send_comment_email, (instance,)) + # 若标记需要清理缓存,则清空全局缓存(确保最新数据生效) if clearcache: cache.clear() +# 信号接收器:同时监听用户登录(user_logged_in)和登出(user_logged_out)信号 @receiver(user_logged_in) @receiver(user_logged_out) def user_auth_callback(sender, request, user, **kwargs): + # 若用户存在且用户名有效(排除异常情况) if user and user.username: + # 记录用户登录/登出日志 logger.info(user) + # 清理侧边栏缓存:登录状态变化可能影响侧边栏(如显示/隐藏用户菜单) delete_sidebar_cache() - # cache.clear() + # cache.clear() # 注释:若需全局清缓存可启用,当前仅清理侧边栏缓存 \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/elasticsearch_backend.py b/src/DjangoBlog-master/djangoblog/elasticsearch_backend.py index 4afe498..ba49e48 100644 --- a/src/DjangoBlog-master/djangoblog/elasticsearch_backend.py +++ b/src/DjangoBlog-master/djangoblog/elasticsearch_backend.py @@ -1,150 +1,184 @@ +# 导入 Django 字符串处理工具:确保字符串编码兼容 from django.utils.encoding import force_str +# 导入 Elasticsearch DSL 工具:构建 Elasticsearch 查询语句 from elasticsearch_dsl import Q +# 导入 Haystack 核心类:实现自定义搜索后端、查询和引擎 from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query -from haystack.forms import ModelSearchForm -from haystack.models import SearchResult -from haystack.utils import log as logging +from haystack.forms import ModelSearchForm # Haystack 基础搜索表单 +from haystack.models import SearchResult # Haystack 搜索结果封装类 +from haystack.utils import log as logging # Haystack 日志工具 +# 导入项目自定义的 Elasticsearch 文档和管理器:关联博客文章模型 from blog.documents import ArticleDocument, ArticleDocumentManager -from blog.models import Article +from blog.models import Article # 博客核心文章模型 +# 初始化日志对象:记录搜索相关日志(如查询语句、错误信息) logger = logging.getLogger(__name__) +# 自定义 Elasticsearch 搜索后端:实现 Haystack 与 Elasticsearch 的底层交互 class ElasticSearchBackend(BaseSearchBackend): def __init__(self, connection_alias, **connection_options): + # 调用父类构造方法,初始化 Haystack 基础搜索后端 super( ElasticSearchBackend, self).__init__( connection_alias, **connection_options) + # 初始化文章文档管理器:负责 Elasticsearch 索引的创建、更新、删除 self.manager = ArticleDocumentManager() + # 启用拼写建议功能:用于返回搜索关键词的推荐词 self.include_spelling = True + # 辅助方法:将模型实例转换为 Elasticsearch 文档(Document) def _get_models(self, iterable): + # 若传入空列表,默认获取所有文章;否则使用传入的模型实例 models = iterable if iterable and iterable[0] else Article.objects.all() + # 通过文档管理器将模型转换为 Elasticsearch 可识别的文档 docs = self.manager.convert_to_doc(models) return docs + # 初始化索引:创建 Elasticsearch 索引并批量添加文档 def _create(self, models): - self.manager.create_index() - docs = self._get_models(models) - self.manager.rebuild(docs) + self.manager.create_index() # 创建 Elasticsearch 索引结构 + docs = self._get_models(models) # 转换模型为文档 + self.manager.rebuild(docs) # 批量写入文档到索引 + # 删除索引中的文档:根据模型实例删除对应 Elasticsearch 记录 def _delete(self, models): for m in models: - m.delete() + m.delete() # 调用文档的 delete 方法,删除 Elasticsearch 中的对应记录 return True + # 重建索引:全量更新 Elasticsearch 中的文档(覆盖旧数据) def _rebuild(self, models): + # 若未指定模型,默认获取所有文章 models = models if models else Article.objects.all() - docs = self.manager.convert_to_doc(models) - self.manager.update_docs(docs) + docs = self._get_models(models) # 转换模型为文档 + self.manager.update_docs(docs) # 批量更新文档到索引 + # Haystack 标准方法:增量更新索引(更新指定模型对应的文档) def update(self, index, iterable, commit=True): + models = self._get_models(iterable) # 转换模型为文档 + self.manager.update_docs(models) # 增量更新文档 - models = self._get_models(iterable) - self.manager.update_docs(models) - + # Haystack 标准方法:移除单个模型对应的索引记录 def remove(self, obj_or_string): - models = self._get_models([obj_or_string]) - self._delete(models) + models = self._get_models([obj_or_string]) # 转换为文档 + self._delete(models) # 删除文档 + # Haystack 标准方法:清空索引(删除所有相关记录) def clear(self, models=None, commit=True): - self.remove(None) + self.remove(None) # 调用 remove 方法清空索引 @staticmethod def get_suggestion(query: str) -> str: - """获取推荐词, 如果没有找到添加原搜索词""" - + """ + 生成搜索关键词的推荐词(基于 Elasticsearch 拼写建议功能) + 若未找到推荐词,返回原查询词 + """ + # 构建 Elasticsearch 查询:匹配文章内容,并启用拼写建议 search = ArticleDocument.search() \ .query("match", body=query) \ .suggest('suggest_search', query, term={'field': 'body'}) \ - .execute() + .execute() # 执行查询 keywords = [] + # 提取 Elasticsearch 返回的建议词 for suggest in search.suggest.suggest_search: - if suggest["options"]: + if suggest["options"]: # 若有推荐词,取第一个 keywords.append(suggest["options"][0]["text"]) - else: + else: # 若无推荐词,保留原查询词 keywords.append(suggest["text"]) - return ' '.join(keywords) + return ' '.join(keywords) # 拼接推荐词为字符串返回 + # Haystack 核心搜索方法:执行搜索并返回结果(带日志记录装饰器) @log_query def search(self, query_string, **kwargs): - logger.info('search query_string:' + query_string) + logger.info('search query_string:' + query_string) # 记录查询关键词 - start_offset = kwargs.get('start_offset') - end_offset = kwargs.get('end_offset') + # 获取分页参数:起始偏移量和结束偏移量(用于分页) + start_offset = kwargs.get('start_offset', 0) + end_offset = kwargs.get('end_offset') # 若为 None,Elasticsearch 会返回默认数量结果 - # 推荐词搜索 + # 生成推荐词:根据 is_suggest 标识判断是否需要拼写建议 if getattr(self, "is_suggest", None): suggestion = self.get_suggestion(query_string) else: - suggestion = query_string + suggestion = query_string # 不需要建议则使用原查询词 + # 构建 Elasticsearch 查询条件(布尔查询) + # 1. 匹配条件:标题或内容包含推荐词,匹配度最低 70% q = Q('bool', should=[Q('match', body=suggestion), Q('match', title=suggestion)], minimum_should_match="70%") + # 构建完整搜索请求: + # - 过滤条件:使用上面的 q 匹配结果,且文章状态为“已发布”(status='p')、类型为“文章”(type='a') + # - 不返回文档源数据(source=False):仅获取 ID 和得分,减少数据传输 + # - 分页:按 start_offset 和 end_offset 截取结果 search = ArticleDocument.search() \ .query('bool', filter=[q]) \ .filter('term', status='p') \ .filter('term', type='a') \ .source(False)[start_offset: end_offset] + # 执行搜索,获取 Elasticsearch 返回结果 results = search.execute() - hits = results['hits'].total - raw_results = [] + hits = results['hits'].total # 匹配到的总结果数 + raw_results = [] # 存储 Haystack 标准格式的搜索结果 + + # 解析 Elasticsearch 原始结果,封装为 Haystack 的 SearchResult 格式 for raw_result in results['hits']['hits']: - app_label = 'blog' - model_name = 'Article' - additional_fields = {} + app_label = 'blog' # 模型所属应用 + model_name = 'Article' # 模型名称 + additional_fields = {} # 额外字段(此处无额外信息,留空) + # 实例化 SearchResult:封装应用名、模型名、文档ID、匹配得分等信息 result_class = SearchResult - result = result_class( app_label, model_name, - raw_result['_id'], - raw_result['_score'], + raw_result['_id'], # Elasticsearch 中文档的 ID + raw_result['_score'], # 匹配得分(用于排序) **additional_fields) raw_results.append(result) + + # 搜索结果元数据:分面(无分面需求,留空)、拼写建议 facets = {} + # 若推荐词与原查询词不同,返回推荐词;否则为 None spelling_suggestion = None if query_string == suggestion else suggestion + # 返回 Haystack 标准格式的搜索结果 return { - 'results': raw_results, - 'hits': hits, - 'facets': facets, - 'spelling_suggestion': spelling_suggestion, + 'results': raw_results, # 封装后的搜索结果列表 + 'hits': hits, # 总匹配数 + 'facets': facets, # 分面数据(空) + 'spelling_suggestion': spelling_suggestion, # 拼写建议 } +# 自定义 Elasticsearch 查询类:处理查询参数解析、格式清洗等 class ElasticSearchQuery(BaseSearchQuery): + # 转换日期格式:适配 Elasticsearch 的日期查询需求 def _convert_datetime(self, date): - if hasattr(date, 'hour'): + if hasattr(date, 'hour'): # 若为datetime(含时分秒),格式化为年月日时分秒 return force_str(date.strftime('%Y%m%d%H%M%S')) - else: + else: # 若为date(仅年月日),补全时分秒为000000 return force_str(date.strftime('%Y%m%d000000')) + # 清洗查询词:处理 Haystack 保留词和特殊字符,避免查询语法错误 def clean(self, query_fragment): - """ - Provides a mechanism for sanitizing user input before presenting the - value to the backend. - - Whoosh 1.X differs here in that you can no longer use a backslash - to escape reserved characters. Instead, the whole word should be - quoted. - """ - words = query_fragment.split() + words = query_fragment.split() # 拆分查询词为单词列表 cleaned_words = [] for word in words: + # 处理 Haystack 保留词(如 AND、OR),转为小写(避免语法冲突) if word in self.backend.RESERVED_WORDS: word = word.replace(word, word.lower()) + # 处理特殊字符(如 +、-、*):包含特殊字符的单词用引号包裹 for char in self.backend.RESERVED_CHARACTERS: if char in word: word = "'%s'" % word @@ -152,32 +186,39 @@ class ElasticSearchQuery(BaseSearchQuery): cleaned_words.append(word) - return ' '.join(cleaned_words) + return ' '.join(cleaned_words) # 拼接清洗后的查询词 + # 构建查询片段:适配自定义查询逻辑(此处直接返回查询字符串) def build_query_fragment(self, field, filter_type, value): return value.query_string + # 获取搜索结果总数:通过 get_results 结果长度计算 def get_count(self): results = self.get_results() return len(results) if results else 0 + # 获取拼写建议:返回后端生成的推荐词 def get_spelling_suggestion(self, preferred_query=None): return self._spelling_suggestion + # 构建搜索参数:继承父类逻辑,可自定义扩展参数 def build_params(self, spelling_query=None): kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) return kwargs +# 自定义搜索表单:扩展 Haystack 基础表单,支持“是否启用拼写建议”的控制 class ElasticSearchModelSearchForm(ModelSearchForm): - def search(self): - # 是否建议搜索 + # 根据请求参数(is_suggest)设置后端是否启用拼写建议 + # 若 is_suggest = "no",则不启用;否则启用 self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" + # 调用父类 search 方法,执行搜索并返回结果 sqs = super().search() return sqs +# 自定义 Elasticsearch 搜索引擎:关联后端和查询类,供 Haystack 调用 class ElasticSearchEngine(BaseEngine): - backend = ElasticSearchBackend - query = ElasticSearchQuery + backend = ElasticSearchBackend # 绑定自定义搜索后端 + query = ElasticSearchQuery # 绑定自定义查询类 \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/feeds.py b/src/DjangoBlog-master/djangoblog/feeds.py index 8c4e851..d8e09ae 100644 --- a/src/DjangoBlog-master/djangoblog/feeds.py +++ b/src/DjangoBlog-master/djangoblog/feeds.py @@ -1,40 +1,61 @@ +# 导入 Django 用户模型工具:获取当前项目的用户模型(支持自定义用户模型) from django.contrib.auth import get_user_model +# 导入 Django 内置的 Feed 基类:用于快速实现 RSS/Atom 订阅功能 from django.contrib.syndication.views import Feed +# 导入 Django 时间工具:处理时区和当前时间 from django.utils import timezone +# 导入 RSS 2.0 格式生成器:指定 Feed 输出格式为 RSS 2.0 标准 from django.utils.feedgenerator import Rss201rev2Feed +# 导入博客核心模型和工具:关联文章数据及 Markdown 解析 from blog.models import Article -from djangoblog.utils import CommonMarkdown +from djangoblog.utils import CommonMarkdown # 自定义 Markdown 解析工具(将文章内容转为 HTML) +# 自定义 RSS Feed 类:继承 Django 内置 Feed 类,实现博客文章的订阅功能 class DjangoBlogFeed(Feed): + # 指定 Feed 生成器类型:使用 RSS 2.0 标准格式(最常用的 RSS 版本) feed_type = Rss201rev2Feed + # Feed 描述信息:显示在订阅源的说明中 description = '大巧无工,重剑无锋.' + # Feed 标题:订阅源的名称(通常为博客名称) title = "且听风吟 大巧无工,重剑无锋. " + # Feed 的链接:订阅源自身的 URL(通常指向博客首页或 Feed 专属页面) link = "/feed/" + # 订阅源作者名称:从系统第一个用户的昵称获取(适合个人博客) def author_name(self): return get_user_model().objects.first().nickname + # 订阅源作者链接:指向作者的个人页面(通过用户模型的 get_absolute_url 方法获取) def author_link(self): return get_user_model().objects.first().get_absolute_url() + # 订阅源包含的项目(文章):定义要展示在 Feed 中的内容 def items(self): + # 筛选条件:类型为文章(type='a')、状态为已发布(status='p') + # 排序规则:按发布时间倒序(最新发布的文章在前) + # 数量限制:只显示最新的 5 篇文章 return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] + # 单个项目(文章)的标题:使用文章自身的标题 def item_title(self, item): return item.title + # 单个项目(文章)的描述:将 Markdown 格式的文章内容转为 HTML 后展示 def item_description(self, item): - return CommonMarkdown.get_markdown(item.body) + return CommonMarkdown.get_markdown(item.body) # 调用工具类解析 Markdown + # 订阅源的版权信息:动态生成包含当前年份的版权声明 def feed_copyright(self): - now = timezone.now() - return "Copyright© {year} 且听风吟".format(year=now.year) + now = timezone.now() # 获取当前时间(带时区) + return "Copyright© {year} 且听风吟".format(year=now.year) # 格式化版权信息 + # 单个项目(文章)的链接:指向文章详情页(通过文章模型的 get_absolute_url 方法获取) def item_link(self, item): return item.get_absolute_url() + # 单个项目(文章)的唯一标识(GUID):此处留空,Django 会默认使用 item_link 作为 GUID def item_guid(self, item): - return + return \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/logentryadmin.py b/src/DjangoBlog-master/djangoblog/logentryadmin.py index 2f6a535..ac3f7cb 100644 --- a/src/DjangoBlog-master/djangoblog/logentryadmin.py +++ b/src/DjangoBlog-master/djangoblog/logentryadmin.py @@ -1,27 +1,37 @@ +# 导入 Django Admin 核心模块:用于自定义后台管理界面 from django.contrib import admin +# 导入 Admin 日志相关常量和模型:处理日志操作类型(如删除) from django.contrib.admin.models import DELETION from django.contrib.contenttypes.models import ContentType +# 导入 Django URL 和字符串处理工具:生成反向链接、处理编码和转义 from django.urls import reverse, NoReverseMatch from django.utils.encoding import force_str from django.utils.html import escape from django.utils.safestring import mark_safe +# 导入国际化工具:支持后台文字的多语言翻译 from django.utils.translation import gettext_lazy as _ +# 自定义 LogEntry Admin 类:用于在 Django 后台管理 Admin 操作日志(记录用户对模型的增删改操作) class LogEntryAdmin(admin.ModelAdmin): + # 列表页筛选器:按“内容类型”(即操作的模型,如 Article、Comment)筛选日志 list_filter = [ 'content_type' ] + # 列表页搜索框:支持按“对象名称”(如文章标题)和“操作描述”(如“修改了标题”)搜索 search_fields = [ 'object_repr', 'change_message' ] + # 列表页可点击的链接:点击“操作时间”或“操作描述”可进入日志详情页 list_display_links = [ 'action_time', 'get_change_message', ] + + # 列表页展示的字段:操作时间、操作用户(带链接)、操作模型、操作对象(带链接)、操作描述 list_display = [ 'action_time', 'user_link', @@ -30,62 +40,81 @@ class LogEntryAdmin(admin.ModelAdmin): 'get_change_message', ] + # 权限控制:禁止添加日志(日志由系统自动生成,不允许手动添加) def has_add_permission(self, request): return False + # 权限控制:仅允许超级用户或拥有“修改日志”权限的用户查看/修改日志,且禁止 POST 请求(避免提交修改) def has_change_permission(self, request, obj=None): return ( request.user.is_superuser or request.user.has_perm('admin.change_logentry') ) and request.method != 'POST' + # 权限控制:禁止删除日志(日志需留存,不允许手动删除) def has_delete_permission(self, request, obj=None): return False + # 自定义列表字段:操作对象(生成带链接的对象名称,点击可跳转到对象的编辑页) def object_link(self, obj): + # 转义对象名称(避免 XSS 攻击) object_link = escape(obj.object_repr) + # 获取操作对象的内容类型(即所属模型) content_type = obj.content_type + # 若操作不是“删除”(DELETION)且内容类型存在(排除异常情况) if obj.action_flag != DELETION and content_type is not None: - # try returning an actual link instead of object repr string try: + # 生成对象编辑页的 URL(格式:admin/应用名/模型名/change/对象ID/) url = reverse( 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), args=[obj.object_id] ) + # 将对象名称转为链接(点击跳转到编辑页) object_link = '{}'.format(url, object_link) except NoReverseMatch: + # 若无法生成链接(如模型未注册到 Admin),则保留纯文本名称 pass + # 标记为安全 HTML(告诉 Django 无需转义,避免链接被当作文本显示) return mark_safe(object_link) - object_link.admin_order_field = 'object_repr' - object_link.short_description = _('object') + # 配置自定义字段的排序和显示名称 + object_link.admin_order_field = 'object_repr' # 支持按“对象名称”排序 + object_link.short_description = _('object') # 列表页字段显示名称(支持翻译) + # 自定义列表字段:操作用户(生成带链接的用户名,点击可跳转到用户的编辑页) def user_link(self, obj): + # 获取用户模型的内容类型 content_type = ContentType.objects.get_for_model(type(obj.user)) + # 转义用户名(避免 XSS 攻击) user_link = escape(force_str(obj.user)) try: - # try returning an actual link instead of object repr string + # 生成用户编辑页的 URL url = reverse( 'admin:{}_{}_change'.format(content_type.app_label, content_type.model), args=[obj.user.pk] ) + # 将用户名转为链接(点击跳转到用户编辑页) user_link = '{}'.format(url, user_link) except NoReverseMatch: + # 若无法生成链接(如用户模型未注册到 Admin),则保留纯文本用户名 pass return mark_safe(user_link) - user_link.admin_order_field = 'user' - user_link.short_description = _('user') + # 配置自定义字段的排序和显示名称 + user_link.admin_order_field = 'user' # 支持按“用户”排序 + user_link.short_description = _('user') # 列表页字段显示名称(支持翻译) + # 优化查询性能:预加载“内容类型”关联数据(避免列表页加载时产生大量数据库查询) def get_queryset(self, request): queryset = super(LogEntryAdmin, self).get_queryset(request) return queryset.prefetch_related('content_type') + # 自定义批量操作:移除“批量删除”按钮(防止误删日志) def get_actions(self, request): actions = super(LogEntryAdmin, self).get_actions(request) if 'delete_selected' in actions: - del actions['delete_selected'] - return actions + del actions['delete_selected'] # 删除“批量删除”操作 + return actions \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py b/src/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py index 6685b7c..3205d2e 100644 --- a/src/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py +++ b/src/DjangoBlog-master/djangoblog/plugin_manage/hook_constants.py @@ -1,7 +1,11 @@ -ARTICLE_DETAIL_LOAD = 'article_detail_load' -ARTICLE_CREATE = 'article_create' -ARTICLE_UPDATE = 'article_update' -ARTICLE_DELETE = 'article_delete' - -ARTICLE_CONTENT_HOOK_NAME = "the_content" +# 文章相关操作的标识常量:用于统一管理操作类型,避免硬编码字符串导致的不一致问题 +# 场景:可用于日志记录、统计分析、权限校验等,通过常量标识具体操作 +ARTICLE_DETAIL_LOAD = 'article_detail_load' # 文章详情页加载操作标识(如用户访问某篇文章详情时使用) +ARTICLE_CREATE = 'article_create' # 文章创建操作标识(如用户发布新文章时使用) +ARTICLE_UPDATE = 'article_update' # 文章更新操作标识(如用户编辑已发布文章时使用) +ARTICLE_DELETE = 'article_delete' # 文章删除操作标识(如用户删除某篇文章时使用) +# 文章内容钩子(Hook)名称常量:用于定义文章内容处理的钩子函数/扩展点名称 +# 场景:在 Django 等框架中,可通过钩子机制对文章内容进行自定义处理(如过滤敏感词、添加水印、解析 markdown 等) +# 例如:注册名为 "the_content" 的钩子函数,在文章内容渲染前自动执行处理逻辑 +ARTICLE_CONTENT_HOOK_NAME = "the_content" \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/settings.py b/src/DjangoBlog-master/djangoblog/settings.py index d076bb6..3a75c92 100644 --- a/src/DjangoBlog-master/djangoblog/settings.py +++ b/src/DjangoBlog-master/djangoblog/settings.py @@ -13,331 +13,371 @@ import os import sys from pathlib import Path +# 导入 Django 国际化工具:用于多语言文本翻译 from django.utils.translation import gettext_lazy as _ +# 自定义工具函数:将环境变量转为布尔值(处理配置的灵活性) def env_to_bool(env, default): - str_val = os.environ.get(env) + str_val = os.environ.get(env) # 从环境变量获取值 + # 若环境变量未设置则返回默认值,否则判断字符串是否为'True' return default if str_val is None else str_val == 'True' -# Build paths inside the project like this: BASE_DIR / 'subdir'. +# 项目根目录:定位到 settings.py 所在目录的父目录(项目根路径) BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ -# SECURITY WARNING: keep the secret key used in production secret! +# 安全密钥:用于加密会话、CSRF 令牌等敏感数据(生产环境需通过环境变量设置,避免硬编码) SECRET_KEY = os.environ.get( 'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' -# SECURITY WARNING: don't run with debug turned on in production! +# 调试模式:开发环境开启(便于调试),生产环境必须关闭(避免暴露敏感信息) DEBUG = env_to_bool('DJANGO_DEBUG', True) -# DEBUG = False +# DEBUG = False # 生产环境手动关闭调试的示例 +# 测试模式标识:判断是否正在执行测试命令(如 python manage.py test) TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' -# ALLOWED_HOSTS = [] +# 允许访问的主机:生产环境需指定具体域名,开发环境用'*'允许所有主机(存在安全风险,生产禁用) +# ALLOWED_HOSTS = [] # 生产环境初始空配置(需补充具体域名) ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] -# django 4.0新增配置 +# Django 4.0+ 新增配置:信任的 CSRF 来源(避免跨域 CSRF 验证失败,生产环境需指定真实域名) CSRF_TRUSTED_ORIGINS = ['http://example.com'] -# Application definition +# Application definition(已安装的应用:Django 内置应用 + 第三方应用 + 自定义应用) INSTALLED_APPS = [ - # 'django.contrib.admin', + # Django 内置 Admin 应用:使用精简版配置(SimpleAdminConfig),减少不必要功能 + # 'django.contrib.admin', # 完整版 Admin 配置(此处注释,使用精简版) 'django.contrib.admin.apps.SimpleAdminConfig', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.sites', - 'django.contrib.sitemaps', - 'mdeditor', - 'haystack', - 'blog', - 'accounts', - 'comments', - 'oauth', - 'servermanager', - 'owntracks', - 'compressor', - 'djangoblog' + 'django.contrib.auth', # 认证与授权系统 + 'django.contrib.contenttypes', # 内容类型框架(关联模型与权限) + 'django.contrib.sessions', # 会话管理(用户登录状态保持) + 'django.contrib.messages', # 消息提示系统(如登录成功提示) + 'django.contrib.staticfiles', # 静态文件管理(CSS/JS/图片) + 'django.contrib.sites', # 多站点支持(用于 RSS、OAuth 等功能) + 'django.contrib.sitemaps', # 站点地图生成(利于 SEO) + 'mdeditor', # 第三方应用:Markdown 编辑器(用于文章编写) + 'haystack', # 第三方应用:搜索框架(对接 Whoosh/Elasticsearch) + 'blog', # 自定义应用:博客核心功能(文章、分类等) + 'accounts', # 自定义应用:用户账户管理(登录、注册等) + 'comments', # 自定义应用:评论功能 + 'oauth', # 自定义应用:第三方登录(如 GitHub、微博) + 'servermanager', # 自定义应用:服务器管理(命令执行、日志记录) + 'owntracks', # 自定义应用:位置追踪(OwnTracks 数据管理) + 'compressor', # 第三方应用:静态文件压缩(CSS/JS 压缩,提升加载速度) + 'djangoblog' # 自定义应用:项目核心配置(如信号、插件) ] +# 中间件:处理请求/响应的钩子(按顺序执行,影响请求流程) MIDDLEWARE = [ - - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.gzip.GZipMiddleware', - # 'django.middleware.cache.UpdateCacheMiddleware', - 'django.middleware.common.CommonMiddleware', - # 'django.middleware.cache.FetchFromCacheMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.middleware.http.ConditionalGetMiddleware', - 'blog.middleware.OnlineMiddleware' + 'django.middleware.security.SecurityMiddleware', # 安全相关中间件(HTTPS、XSS 防护等) + 'django.contrib.sessions.middleware.SessionMiddleware', # 会话管理中间件 + 'django.middleware.locale.LocaleMiddleware', # 国际化中间件(处理多语言切换) + 'django.middleware.gzip.GZipMiddleware', # GZip 压缩中间件(减少响应体积) + # 'django.middleware.cache.UpdateCacheMiddleware', # 缓存更新中间件(注释:按需启用) + 'django.middleware.common.CommonMiddleware', # 通用中间件(处理 URL 重定向、404 等) + # 'django.middleware.cache.FetchFromCacheMiddleware', # 缓存读取中间件(注释:按需启用) + 'django.middleware.csrf.CsrfViewMiddleware', # CSRF 防护中间件(防止跨站请求伪造) + 'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证中间件(绑定用户到请求) + 'django.contrib.messages.middleware.MessageMiddleware', # 消息中间件(传递提示信息) + 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持防护中间件 + 'django.middleware.http.ConditionalGetMiddleware', # 条件请求中间件(缓存协商,减少重复请求) + 'blog.middleware.OnlineMiddleware' # 自定义中间件:用户在线状态管理 ] +# 根 URL 配置:指定项目的主 URL 路由文件 ROOT_URLCONF = 'djangoblog.urls' +# 模板配置:定义模板引擎、路径及上下文处理器 TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [os.path.join(BASE_DIR, 'templates')], - 'APP_DIRS': True, + 'BACKEND': 'django.template.backends.django.DjangoTemplates', # 使用 Django 内置模板引擎 + 'DIRS': [os.path.join(BASE_DIR, 'templates')], # 全局模板目录(项目根目录下的 templates) + 'APP_DIRS': True, # 允许从各应用的 templates 目录加载模板 'OPTIONS': { + # 上下文处理器:向所有模板注入全局变量(如用户信息、请求对象) 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - 'blog.context_processors.seo_processor' + 'django.template.context_processors.debug', # 调试模式变量(如 DEBUG) + 'django.template.context_processors.request', # 请求对象(request) + 'django.contrib.auth.context_processors.auth', # 认证相关变量(如 user) + 'django.contrib.messages.context_processors.messages', # 消息变量(如 messages) + 'blog.context_processors.seo_processor' # 自定义上下文处理器:注入 SEO 相关数据 ], }, }, ] +# WSGI 应用:指定项目的 WSGI 入口文件(用于部署,如 Gunicorn、uWSGI) WSGI_APPLICATION = 'djangoblog.wsgi.application' -# Database -# https://docs.djangoproject.com/en/1.10/ref/settings/#databases - +# Database(数据库配置:使用 MySQL 数据库) +# https://docs.djangoproject.com/en/1.10/ref/settings/#databases DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', - 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', - 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', - 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', - 'PORT': int( - os.environ.get('DJANGO_MYSQL_PORT') or 3306), + 'ENGINE': 'django.db.backends.mysql', # 数据库引擎(MySQL) + 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', # 数据库名(优先从环境变量获取) + 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', # 数据库用户名 + 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', # 数据库密码 + 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', # 数据库主机(默认本地) + 'PORT': int(os.environ.get('DJANGO_MYSQL_PORT') or 3306), # 数据库端口(默认 3306) 'OPTIONS': { - 'charset': 'utf8mb4'}, + 'charset': 'utf8mb4'}, # 数据库字符集(支持 emoji 表情) }} -# Password validation -# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators +# Password validation(密码验证规则:确保用户密码强度) +# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { + # 验证密码与用户属性(如用户名、邮箱)的相似度 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { + # 验证密码最小长度(默认 8 位) 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { + # 验证密码是否在常见弱密码列表中 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { + # 验证密码是否纯数字 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] + +# Internationalization(国际化配置:多语言支持) +# https://docs.djangoproject.com/en/1.10/topics/i18n/ LANGUAGES = ( - ('en', _('English')), - ('zh-hans', _('Simplified Chinese')), - ('zh-hant', _('Traditional Chinese')), + ('en', _('English')), # 英语 + ('zh-hans', _('Simplified Chinese')), # 简体中文 + ('zh-hant', _('Traditional Chinese')), # 繁体中文 ) +# 翻译文件目录:指定多语言翻译文件(.po/.mo)的存放路径 LOCALE_PATHS = ( os.path.join(BASE_DIR, 'locale'), ) -LANGUAGE_CODE = 'zh-hans' - -TIME_ZONE = 'Asia/Shanghai' - -USE_I18N = True - -USE_L10N = True - -USE_TZ = False - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.10/howto/static-files/ +LANGUAGE_CODE = 'zh-hans' # 默认语言:简体中文 +TIME_ZONE = 'Asia/Shanghai' # 时区:上海(中国时区) +USE_I18N = True # 启用国际化(支持多语言) +USE_L10N = True # 启用本地化(支持区域化日期、数字格式) +USE_TZ = False # 禁用 UTC 时间(使用本地时区存储时间,避免时区转换问题) +# Search(搜索框架配置:Haystack + Whoosh/Elasticsearch) HAYSTACK_CONNECTIONS = { 'default': { + # 搜索引擎:默认使用 Whoosh(轻量级全文搜索引擎,适合中小项目) 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', + # 索引文件路径:Whoosh 索引文件存储位置 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), }, } -# Automatically update searching index +# 实时更新索引:当模型数据(如文章)新增/修改/删除时,自动更新搜索索引 HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' -# Allow user login with username and password -AUTHENTICATION_BACKENDS = [ - 'accounts.user_login_backend.EmailOrUsernameModelBackend'] -STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') - -STATIC_URL = '/static/' -STATICFILES = os.path.join(BASE_DIR, 'static') +# Authentication(认证配置:自定义登录逻辑) +# 允许用户通过“用户名”或“邮箱”登录(默认仅支持用户名) +AUTHENTICATION_BACKENDS = [ + 'accounts.user_login_backend.EmailOrUsernameModelBackend'] +# 自定义用户模型:使用 accounts 应用的 BlogUser 模型(替代 Django 内置 User 模型) AUTH_USER_MODEL = 'accounts.BlogUser' +# 登录 URL:未登录用户访问需认证页面时,重定向到该 URL LOGIN_URL = '/login/' -TIME_FORMAT = '%Y-%m-%d %H:%M:%S' -DATE_TIME_FORMAT = '%Y-%m-%d' -# bootstrap color styles -BOOTSTRAP_COLOR_TYPES = [ +# Custom Settings(自定义业务配置) +TIME_FORMAT = '%Y-%m-%d %H:%M:%S' # 时间显示格式(如 2024-05-20 14:30:00) +DATE_TIME_FORMAT = '%Y-%m-%d' # 日期显示格式(如 2024-05-20) +BOOTSTRAP_COLOR_TYPES = [ # Bootstrap 颜色样式(用于前端组件,如标签、按钮) 'default', 'primary', 'success', 'info', 'warning', 'danger' ] +PAGINATE_BY = 10 # 分页大小:每页显示 10 条数据(如文章列表、评论列表) +CACHE_CONTROL_MAX_AGE = 2592000 # HTTP 缓存有效期:30 天(单位:秒) + -# paginate -PAGINATE_BY = 10 -# http cache timeout -CACHE_CONTROL_MAX_AGE = 2592000 -# cache setting +# Cache(缓存配置:默认本地内存缓存,支持 Redis 扩展) CACHES = { 'default': { + # 本地内存缓存(适合开发环境,生产环境建议用 Redis/Memcached) 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'TIMEOUT': 10800, - 'LOCATION': 'unique-snowflake', + 'TIMEOUT': 10800, # 缓存有效期:3 小时(单位:秒) + 'LOCATION': 'unique-snowflake', # 缓存实例标识(避免多实例冲突) } } -# 使用redis作为缓存 +# 若环境变量指定 Redis 地址,则使用 Redis 作为缓存(生产环境推荐) if os.environ.get("DJANGO_REDIS_URL"): CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.redis.RedisCache', - 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', + 'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', # Redis 连接地址 } } -SITE_ID = 1 + +# Site & SEO(站点与 SEO 配置) +SITE_ID = 1 # 站点 ID(多站点配置时区分不同站点,单站点固定为 1) +# 百度主动推送 URL:用于文章发布后主动通知百度收录(提升 SEO 效率) BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \ or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn' -# Email: -EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) -EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) -EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' -EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) -EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') -EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') -DEFAULT_FROM_EMAIL = EMAIL_HOST_USER -SERVER_EMAIL = EMAIL_HOST_USER -# Setting debug=false did NOT handle except email notifications + +# Email(邮件配置:用于发送验证码、评论通知等) +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # SMTP 邮件后端 +EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) # 是否启用 TLS 加密(与 SSL 二选一) +EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) # 是否启用 SSL 加密(默认启用,端口通常为 465) +EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' # SMTP 服务器地址(默认阿里云) +EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) # SMTP 端口(SSL 通常为 465,TLS 通常为 587) +EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') # 发送邮件的邮箱账号(优先环境变量) +EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') # 邮箱密码/授权码 +DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # 默认发件人邮箱 +SERVER_EMAIL = EMAIL_HOST_USER # 服务器错误通知发件人邮箱 +# 管理员邮箱:生产环境错误(如 500 异常)会发送到此邮箱 ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] -# WX ADMIN password(Two times md5) +# 微信管理员密码:二次 MD5 加密(用于微信后台管理验证,具体业务自定义) WXADMIN = os.environ.get( 'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' -LOG_PATH = os.path.join(BASE_DIR, 'logs') + +# Logging(日志配置:记录系统操作、错误信息) +LOG_PATH = os.path.join(BASE_DIR, 'logs') # 日志文件目录 +# 若日志目录不存在,则创建(确保日志能正常写入) if not os.path.exists(LOG_PATH): os.makedirs(LOG_PATH, exist_ok=True) LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'root': { - 'level': 'INFO', - 'handlers': ['console', 'log_file'], + 'version': 1, # 日志配置版本(固定为 1) + 'disable_existing_loggers': False, # 不禁用已存在的日志器 + 'root': { # 根日志器(所有未指定日志器的日志都会走这里) + 'level': 'INFO', # 日志级别(INFO:记录普通信息及以上级别) + 'handlers': ['console', 'log_file'], # 日志处理器(控制台 + 文件) }, - 'formatters': { + 'formatters': { # 日志格式:定义日志的输出结构 'verbose': { + # 日志格式:时间 + 级别 + 调用位置 + 消息(便于问题定位) 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', } }, - 'filters': { - 'require_debug_false': { + 'filters': { # 日志过滤器:按条件过滤日志 + 'require_debug_false': { # 仅当 DEBUG=False 时生效(生产环境) '()': 'django.utils.log.RequireDebugFalse', }, - 'require_debug_true': { + 'require_debug_true': { # 仅当 DEBUG=True 时生效(开发环境) '()': 'django.utils.log.RequireDebugTrue', }, }, - 'handlers': { - 'log_file': { + 'handlers': { # 日志处理器:定义日志的输出方式 + 'log_file': { # 文件处理器:按时间轮转切割日志 'level': 'INFO', - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), - 'when': 'D', - 'formatter': 'verbose', - 'interval': 1, - 'delay': True, - 'backupCount': 5, - 'encoding': 'utf-8' + 'class': 'logging.handlers.TimedRotatingFileHandler', # 按时间轮转的处理器 + 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), # 日志文件路径 + 'when': 'D', # 轮转周期:每天(Day) + 'formatter': 'verbose', # 使用 verbose 格式 + 'interval': 1, # 轮转间隔:1 个周期(1 天) + 'delay': True, # 延迟创建文件(直到有日志才创建) + 'backupCount': 5, # 保留日志备份数:5 天 + 'encoding': 'utf-8' # 日志文件编码 }, - 'console': { + 'console': { # 控制台处理器:开发环境输出到终端 'level': 'DEBUG', - 'filters': ['require_debug_true'], + 'filters': ['require_debug_true'], # 仅开发环境生效 'class': 'logging.StreamHandler', 'formatter': 'verbose' }, - 'null': { + 'null': { # 空处理器:用于屏蔽不需要的日志 'class': 'logging.NullHandler', }, - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], + 'mail_admins': { # 邮件处理器:生产环境错误发送到管理员邮箱 + 'level': 'ERROR', # 仅 ERROR 级别日志触发 + 'filters': ['require_debug_false'], # 仅生产环境生效 'class': 'django.utils.log.AdminEmailHandler' } }, - 'loggers': { - 'djangoblog': { - 'handlers': ['log_file', 'console'], + 'loggers': { # 自定义日志器:针对特定模块配置日志 + 'djangoblog': { # 项目核心模块日志器 + 'handlers': ['log_file', 'console'], # 输出到文件和控制台 'level': 'INFO', - 'propagate': True, + 'propagate': True, # 是否向上传递到根日志器(此处开启) }, - 'django.request': { - 'handlers': ['mail_admins'], + 'django.request': { # Django 请求模块日志器(记录请求相关错误) + 'handlers': ['mail_admins'], # 错误发送到管理员邮箱 'level': 'ERROR', - 'propagate': False, + 'propagate': False, # 不向上传递(避免重复记录) } } } + +# Static Files(静态文件配置:CSS/JS/图片等) +# 静态文件收集目录:执行 collectstatic 命令后,静态文件会汇总到此处(生产环境使用) +STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') +# 静态文件 URL 前缀:前端通过 /static/ 访问静态文件 +STATIC_URL = '/static/' +# 全局静态文件目录:项目根目录下的 static 目录(存放全局静态文件) +STATICFILES = os.path.join(BASE_DIR, 'static') + +# 静态文件查找器:指定 Django 查找静态文件的路径(内置 + 第三方) STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', - # other - 'compressor.finders.CompressorFinder', + 'django.contrib.staticfiles.finders.FileSystemFinder', # 查找全局 STATICFILES 目录 + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 查找各应用的 static 目录 + 'compressor.finders.CompressorFinder', # 查找压缩后的静态文件(第三方 compressor 应用) ) -COMPRESS_ENABLED = True -# COMPRESS_OFFLINE = True - +# 静态文件压缩配置(compressor 应用) +COMPRESS_ENABLED = True # 启用压缩(生产环境建议开启,提升加载速度) +# COMPRESS_OFFLINE = True # 离线压缩(预先生成压缩文件,生产环境推荐,此处注释按需启用) +# CSS 压缩过滤器:绝对路径处理 + 代码压缩 COMPRESS_CSS_FILTERS = [ - # creates absolute urls from relative ones 'compressor.filters.css_default.CssAbsoluteFilter', - # css minimizer 'compressor.filters.cssmin.CSSMinFilter' ] +# JS 压缩过滤器:代码压缩 COMPRESS_JS_FILTERS = [ 'compressor.filters.jsmin.JSMinFilter' ] + +# Media Files(媒体文件配置:用户上传的文件,如文章图片、头像) +# 媒体文件存储目录:项目根目录下的 uploads 目录 MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') +# 媒体文件 URL 前缀:前端通过 /media/ 访问上传的文件 MEDIA_URL = '/media/' + +# X-Frame-Options:防止点击劫持(SAMEORIGIN:仅允许同域页面嵌入当前页面) X_FRAME_OPTIONS = 'SAMEORIGIN' +# 默认模型主键类型:Django 3.2+ 新增,指定模型默认主键为 BigAutoField(64 位自增整数) DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Elasticsearch 配置(按需启用:若环境变量指定 Elasticsearch 地址,则使用 Elasticsearch 替代 Whoosh) if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): ELASTICSEARCH_DSL = { 'default': { - 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') + 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') # Elasticsearch 连接地址 }, } + # 替换 Haystack 搜索引擎为自定义的 Elasticsearch 引擎 HAYSTACK_CONNECTIONS = { 'default': { 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', }, } -# Plugin System -PLUGINS_DIR = BASE_DIR / 'plugins' + +# Plugin System(插件系统配置:自定义插件功能) +PLUGINS_DIR = BASE_DIR / 'plugins' # 插件目录:项目根目录下的 plugins 目录 +# 激活的插件列表:指定需要加载的插件(如文章版权、阅读时长统计等) ACTIVE_PLUGINS = [ - 'article_copyright', - 'reading_time', - 'external_links', - 'view_count', - 'seo_optimizer' + 'article_copyright', # 文章版权插件 + 'reading_time', # 阅读时长统计插件 + 'external_links', # 外部链接处理插件 + 'view_count', # 文章阅读量统计插件 + 'seo_optimizer' # SEO 优化插件 ] \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/sitemap.py b/src/DjangoBlog-master/djangoblog/sitemap.py index 8b7d446..a02411f 100644 --- a/src/DjangoBlog-master/djangoblog/sitemap.py +++ b/src/DjangoBlog-master/djangoblog/sitemap.py @@ -1,59 +1,88 @@ +# 导入 Django 内置站点地图基类:用于快速实现标准化 sitemap.xml from django.contrib.sitemaps import Sitemap +# 导入 URL 反向解析工具:生成页面的绝对 URL(适配路由命名) from django.urls import reverse +# 导入博客核心模型:用于生成动态内容(文章、分类、标签)的站点地图 from blog.models import Article, Category, Tag +# 静态页面站点地图类:处理无数据库关联的静态页面(如博客首页) class StaticViewSitemap(Sitemap): + # 优先级:0.5(取值 0.0-1.0,值越高搜索引擎越优先抓取,静态页优先级中等) priority = 0.5 + # 更新频率:daily(每天更新,适合内容相对稳定但可能微调的静态页) changefreq = 'daily' + # 定义要包含的静态页面:返回路由名称列表(对应 urls.py 中命名的路由) def items(self): - return ['blog:index', ] + return ['blog:index', ] # 此处仅包含博客首页(路由名为 'blog:index') + # 生成静态页面的 URL:通过路由名称反向解析为绝对路径 def location(self, item): - return reverse(item) + return reverse(item) # item 为 items() 返回的路由名称,如 'blog:index' +# 文章站点地图类:处理博客文章的动态页面(核心内容,SEO 关键) class ArticleSiteMap(Sitemap): + # 更新频率:monthly(每月更新,适合文章发布后较少修改的场景) changefreq = "monthly" + # 优先级:0.6(高于静态页和分类/标签,文章是站点核心内容,优先抓取) priority = "0.6" + # 定义要包含的文章:仅筛选“已发布”状态的文章(排除草稿、私有文章) def items(self): return Article.objects.filter(status='p') + # 文章最后更新时间:用于搜索引擎判断内容是否更新,提升抓取效率 def lastmod(self, obj): - return obj.last_modify_time + return obj.last_modify_time # 引用文章模型的“最后修改时间”字段 +# 分类站点地图类:处理文章分类页面(聚合类内容,辅助 SEO) class CategorySiteMap(Sitemap): + # 更新频率:Weekly(每周更新,分类内容更新频率低于文章) changefreq = "Weekly" + # 优先级:0.6(与文章同级,分类页是重要的内容聚合入口) priority = "0.6" + # 定义要包含的分类:获取所有分类(无论是否有文章,确保分类页被收录) def items(self): return Category.objects.all() + # 分类最后更新时间:判断分类下内容是否有变化(如新增/修改文章) def lastmod(self, obj): - return obj.last_modify_time + return obj.last_modify_time # 引用分类模型的“最后修改时间”字段 +# 标签站点地图类:处理文章标签页面(细分内容聚合,补充 SEO 覆盖) class TagSiteMap(Sitemap): + # 更新频率:Weekly(每周更新,标签内容更新频率与分类一致) changefreq = "Weekly" + # 优先级:0.3(低于文章和分类,标签页是辅助导航入口,优先级较低) priority = "0.3" + # 定义要包含的标签:获取所有标签(确保所有标签页被搜索引擎收录) def items(self): return Tag.objects.all() + # 标签最后更新时间:判断标签下内容是否有变化 def lastmod(self, obj): - return obj.last_modify_time + return obj.last_modify_time # 引用标签模型的“最后修改时间”字段 +# 用户站点地图类:处理文章作者页面(展示用户发布的所有文章,提升作者页曝光) class UserSiteMap(Sitemap): + # 更新频率:Weekly(每周更新,用户发布文章频率通常较低) changefreq = "Weekly" + # 优先级:0.3(与标签同级,作者页是辅助内容入口) priority = "0.3" + # 定义要包含的用户:获取所有发布过文章的作者(去重,避免重复收录同一用户) def items(self): + # 1. 从所有文章中提取作者字段 → 2. 转集合去重 → 3. 转列表返回 return list(set(map(lambda x: x.author, Article.objects.all()))) + # 用户页面最后更新时间:用用户“注册时间”替代(简化逻辑,也可改为用户最新文章发布时间) def lastmod(self, obj): - return obj.date_joined + return obj.date_joined # 引用用户模型的“注册时间”字段 \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/spider_notify.py b/src/DjangoBlog-master/djangoblog/spider_notify.py index 7b909e9..4d6d1cd 100644 --- a/src/DjangoBlog-master/djangoblog/spider_notify.py +++ b/src/DjangoBlog-master/djangoblog/spider_notify.py @@ -1,21 +1,43 @@ +# 导入日志模块:记录推送过程的成功信息与错误,便于排查问题 import logging - +# 导入 HTTP 请求库:用于向百度搜索引擎接口发送 POST 请求 import requests +# 导入 Django 项目配置:获取百度主动推送的接口 URL(从 settings.py 读取) from django.conf import settings +# 初始化日志对象:指定日志归属为当前模块,便于区分不同模块的日志 logger = logging.getLogger(__name__) +# 搜索引擎推送工具类:封装向百度等搜索引擎主动提交页面的逻辑 class SpiderNotify(): @staticmethod def baidu_notify(urls): + """ + 向百度搜索引擎主动推送页面(基于百度站长平台的“链接提交”接口) + 目的:让百度快速发现新页面,缩短收录周期(提升 SEO 效率) + + Args: + urls: 待推送的 URL 列表(如 ['https://xxx.com/article/1/', 'https://xxx.com/article/2/']) + """ try: + # 格式化请求数据:百度接口要求 URL 以换行符(\n)分隔,拼接成字符串 data = '\n'.join(urls) + # 发送 POST 请求:调用 settings 中配置的百度推送接口 URL result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) + # 记录推送结果:将百度返回的响应文本(如成功/失败数量)写入日志 logger.info(result.text) except Exception as e: + # 捕获异常(如网络错误、接口超时等),记录错误信息到日志 logger.error(e) @staticmethod def notify(url): - SpiderNotify.baidu_notify(url) + """ + 通用推送方法:统一入口,当前仅调用百度推送(可扩展支持其他搜索引擎) + + Args: + url: 待推送的 URL(支持单个 URL 或 URL 列表,此处兼容百度推送的列表格式) + """ + # 调用百度推送方法,实现页面提交 + SpiderNotify.baidu_notify(url) \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/tests.py b/src/DjangoBlog-master/djangoblog/tests.py index 01237d9..9efd6d1 100644 --- a/src/DjangoBlog-master/djangoblog/tests.py +++ b/src/DjangoBlog-master/djangoblog/tests.py @@ -1,32 +1,29 @@ +# 导入 Django 内置的测试基类:提供单元测试所需的基础功能(如断言、测试环境初始化) from django.test import TestCase +# 导入项目自定义工具模块:测试其中的工具函数(如加密、Markdown解析、字典转URL参数) from djangoblog.utils import * +# 自定义测试类:继承 TestCase,用于测试 djangoblog 项目的工具函数功能 class DjangoBlogTest(TestCase): + # 测试前置方法:在每个测试方法(以 test_ 开头)执行前自动调用 + # 用于初始化测试数据、配置测试环境等(此处暂无需初始化,留空) def setUp(self): pass + # 核心测试方法:测试 utils 模块中的多个工具函数(命名以 test_ 开头,Django 会自动识别执行) def test_utils(self): - md5 = get_sha256('test') + # 1. 测试 SHA256 加密函数(get_sha256) + # 对字符串 'test' 进行 SHA256 加密,获取加密结果 + md5 = get_sha256('test') # 注意:函数名是 get_sha256,实际功能是 SHA256 加密(非 MD5,可能是命名习惯) + # 断言:加密结果不为空(验证函数能正常返回加密值,未抛出异常) self.assertIsNotNone(md5) - c = CommonMarkdown.get_markdown(''' - # Title1 - - ```python - import os - ``` - - [url](https://www.lylinux.net/) - - [ddd](http://www.baidu.com) + # 2. 测试 Markdown 解析函数(CommonMarkdown.get_markdown) + # 定义一段包含标题、Python代码块、超链接的 Markdown 文本 + c = CommonMarkdown.get_markdown(''' + # Title1 # 一级标题 - ''') - self.assertIsNotNone(c) - d = { - 'd': 'key1', - 'd2': 'key2' - } - data = parse_dict_to_url(d) - self.assertIsNotNone(data) + ```python # Python 代码块 + import os \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/urls.py b/src/DjangoBlog-master/djangoblog/urls.py index 4aae58a..b7a3d07 100644 --- a/src/DjangoBlog-master/djangoblog/urls.py +++ b/src/DjangoBlog-master/djangoblog/urls.py @@ -13,52 +13,88 @@ Including another URLconf 1. Import the include() function: from django.conf.urls import url, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ +# 导入项目配置:用于获取静态文件/媒体文件路径、DEBUG 状态等 from django.conf import settings +# 导入国际化路由工具:生成带语言前缀的路由(如 /en/admin/、/zh-hans/blog/) from django.conf.urls.i18n import i18n_patterns +# 导入静态文件路由工具:开发环境下提供静态文件访问(生产环境需 Nginx 处理) from django.conf.urls.static import static +# 导入站点地图视图:关联站点地图配置,生成 sitemap.xml from django.contrib.sitemaps.views import sitemap +# 导入 URL 路由组件:path 用于精确匹配,include 用于引入子应用路由 from django.urls import path, include +# 导入 re_path:支持正则表达式匹配 URL(适配复杂路由场景) from django.urls import re_path +# 导入 Haystack 搜索视图工厂:用于自定义搜索视图和表单 from haystack.views import search_view_factory -from blog.views import EsSearchView -from djangoblog.admin_site import admin_site -from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm -from djangoblog.feeds import DjangoBlogFeed -from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap +# 导入项目自定义视图和配置:关联核心功能路由 +from blog.views import EsSearchView # 自定义 Elasticsearch 搜索视图 +from djangoblog.admin_site import admin_site # 自定义后台管理站点(替代默认 admin) +from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm # 自定义搜索表单 +from djangoblog.feeds import DjangoBlogFeed # RSS 订阅 Feed 视图 +from djangoblog.sitemap import ( # 站点地图配置类 + ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap +) +# 站点地图聚合配置:将各类型站点地图归类,用于生成统一的 sitemap.xml sitemaps = { - - 'blog': ArticleSiteMap, - 'Category': CategorySiteMap, - 'Tag': TagSiteMap, - 'User': UserSiteMap, - 'static': StaticViewSitemap + 'blog': ArticleSiteMap, # 文章站点地图 + 'Category': CategorySiteMap, # 分类站点地图 + 'Tag': TagSiteMap, # 标签站点地图 + 'User': UserSiteMap, # 作者站点地图 + 'static': StaticViewSitemap # 静态页面站点地图 } -handler404 = 'blog.views.page_not_found_view' -handler500 = 'blog.views.server_error_view' -handle403 = 'blog.views.permission_denied_view' +# 自定义错误页面路由:指定 404/500/403 错误时跳转的视图 +handler404 = 'blog.views.page_not_found_view' # 404 页面未找到 +handler500 = 'blog.views.server_error_view' # 500 服务器内部错误 +handle403 = 'blog.views.permission_denied_view'# 403 权限拒绝(注意变量名应为 handler403,此处可能是笔误) +# 基础 URL 路由:不包含语言前缀的公共路由 urlpatterns = [ + # 国际化路由:提供语言切换功能(如 /i18n/setlang/ 接口) path('i18n/', include('django.conf.urls.i18n')), ] + +# 带语言前缀的路由:通过 i18n_patterns 自动添加语言前缀(如 /zh-hans/、/en/) +# prefix_default_language=False:默认语言不显示前缀(如中文默认不显示 /zh-hans/,直接用根路径) urlpatterns += i18n_patterns( + # 1. 后台管理路由:使用自定义的 admin_site(替代默认 admin),访问路径如 /admin/ re_path(r'^admin/', admin_site.urls), + # 2. 博客核心路由:引入 blog 应用的子路由,命名空间为 'blog'(路由名如 blog:index) re_path(r'', include('blog.urls', namespace='blog')), + # 3. Markdown 编辑器路由:引入 mdeditor 第三方应用的路由,用于文章编辑时的 Markdown 预览 re_path(r'mdeditor/', include('mdeditor.urls')), + # 4. 评论路由:引入 comments 应用的子路由,命名空间为 'comment' re_path(r'', include('comments.urls', namespace='comment')), + # 5. 用户账户路由:引入 accounts 应用的子路由(登录、注册、个人中心),命名空间为 'account' re_path(r'', include('accounts.urls', namespace='account')), + # 6. 第三方登录路由:引入 oauth 应用的子路由(GitHub、微博登录),命名空间为 'oauth' re_path(r'', include('oauth.urls', namespace='oauth')), + # 7. 站点地图路由:生成 sitemap.xml,供搜索引擎抓取(访问路径 /sitemap.xml) re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'), + # 8. RSS 订阅路由:提供两种访问路径(/feed/ 和 /rss/),均指向 DjangoBlogFeed 视图 re_path(r'^feed/$', DjangoBlogFeed()), re_path(r'^rss/$', DjangoBlogFeed()), - re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), - name='search'), + # 9. 搜索路由:使用自定义的搜索视图(EsSearchView)和表单(ElasticSearchModelSearchForm) + # 访问路径如 /search?q=关键词,命名为 'search' + re_path('^search', search_view_factory( + view_class=EsSearchView, + form_class=ElasticSearchModelSearchForm + ), name='search'), + # 10. 服务器管理路由:引入 servermanager 应用的子路由(命令执行、日志查看),命名空间为 'servermanager' re_path(r'', include('servermanager.urls', namespace='servermanager')), - re_path(r'', include('owntracks.urls', namespace='owntracks')) - , prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + # 11. 位置追踪路由:引入 owntracks 应用的子路由(位置数据查看),命名空间为 'owntracks' + re_path(r'', include('owntracks.urls', namespace='owntracks')), + prefix_default_language=False # 默认语言不显示语言前缀 +) + +# 静态文件路由:开发环境下(DEBUG=True)通过 Django 提供静态文件访问(生产环境需注释,用 Nginx 处理) +urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + +# 媒体文件路由:仅在 DEBUG=True(开发环境)时生效,提供用户上传文件的访问(如 /media/avatar.jpg) if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, - document_root=settings.MEDIA_ROOT) + document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/utils.py b/src/DjangoBlog-master/djangoblog/utils.py index 57f63dc..0e52b07 100644 --- a/src/DjangoBlog-master/djangoblog/utils.py +++ b/src/DjangoBlog-master/djangoblog/utils.py @@ -2,105 +2,173 @@ # encoding: utf-8 -import logging -import os -import random -import string -import uuid -from hashlib import sha256 - -import bleach -import markdown -import requests -from django.conf import settings -from django.contrib.sites.models import Site -from django.core.cache import cache -from django.templatetags.static import static - +import logging # 日志模块:记录操作信息和错误 +import os # 系统操作模块:处理文件路径、目录创建等 +import random # 随机数模块:生成验证码等随机内容 +import string # 字符串模块:提供数字、字母等常量 +import uuid # 唯一模块:生成唯一标识符(用于头像文件名) +from hashlib import sha256 # 加密模块:提供 SHA256 加密算法 + +import bleach # HTML 清理模块:过滤不安全的 HTML 标签(防 XSS 攻击) +import markdown # Markdown 解析模块:将 Markdown 文本转为 HTML +import requests # HTTP 请求模块:下载网络资源(如用户头像) +from django.conf import settings # Django 配置:获取项目设置(如静态文件路径) +from django.contrib.sites.models import Site # 站点模型:获取当前站点信息(域名等) +from django.core.cache import cache # 缓存模块:操作 Django 缓存(获取/设置/删除) +from django.templatetags.static import static # 静态文件工具:生成静态文件的 URL + + +# 初始化日志对象:指定日志归属为当前模块,便于日志分类 logger = logging.getLogger(__name__) def get_max_articleid_commentid(): + """ + 获取当前最大的文章 ID 和评论 ID(用于数据统计或初始化) + + Returns: + tuple: (最大文章 ID, 最大评论 ID) + """ + # 延迟导入模型:避免循环导入问题(工具模块可能被模型模块引用) from blog.models import Article from comments.models import Comment + # 返回最新文章和评论的主键(ID) return (Article.objects.latest().pk, Comment.objects.latest().pk) def get_sha256(str): + """ + 对字符串进行 SHA256 加密(用于密码加密、唯一标识生成等) + + Args: + str: 待加密的字符串 + + Returns: + str: 加密后的 64 位十六进制字符串 + """ + # 创建 SHA256 加密对象,需先将字符串转为字节流(指定编码 utf-8) m = sha256(str.encode('utf-8')) + # 返回十六进制加密结果 return m.hexdigest() def cache_decorator(expiration=3 * 60): + """ + 缓存装饰器:装饰函数,将函数返回值缓存指定时间(默认 3 分钟) + + 作用:减少重复计算或数据库查询,提升性能 + + Args: + expiration: 缓存有效期(秒),默认 3 分钟 + + Returns: + 装饰器函数:包装原函数,实现缓存逻辑 + """ def wrapper(func): def news(*args, **kwargs): + # 尝试生成缓存键(优先使用视图对象的 get_cache_key 方法) try: - view = args[0] - key = view.get_cache_key() + view = args[0] # 若第一个参数是视图对象 + key = view.get_cache_key() # 使用视图自带的缓存键 except: - key = None + key = None # 非视图函数,需自定义缓存键 + + # 若未生成缓存键,则基于函数和参数生成唯一键 if not key: + # 将函数、参数转为字符串,确保唯一性 unique_str = repr((func, args, kwargs)) - + # 对字符串进行 SHA256 加密,生成固定长度的缓存键 m = sha256(unique_str.encode('utf-8')) key = m.hexdigest() + + # 尝试从缓存获取数据 value = cache.get(key) if value is not None: - # logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) + # 缓存命中:返回缓存值(处理 None 的特殊标记) if str(value) == '__default_cache_value__': return None else: return value else: + # 缓存未命中:执行原函数获取结果 logger.debug( 'cache_decorator set cache:%s key:%s' % (func.__name__, key)) value = func(*args, **kwargs) + # 缓存结果(用特殊标记表示 None,避免缓存不生效) if value is None: cache.set(key, '__default_cache_value__', expiration) else: cache.set(key, value, expiration) return value - return news - return wrapper def expire_view_cache(path, servername, serverport, key_prefix=None): ''' - 刷新视图缓存 - :param path:url路径 - :param servername:host - :param serverport:端口 - :param key_prefix:前缀 - :return:是否成功 + 主动刷新指定 URL 路径的视图缓存(用于数据更新后清理旧缓存) + + Args: + path: URL 路径(如 '/article/1/') + servername: 服务器域名(如 'www.example.com') + serverport: 服务器端口(如 80) + key_prefix: 缓存键前缀(与视图缓存配置一致) + + Returns: + bool: 缓存是否成功删除 ''' - from django.http import HttpRequest - from django.utils.cache import get_cache_key - + from django.http import HttpRequest # 延迟导入:避免启动时依赖冲突 + from django.utils.cache import get_cache_key # 获取视图缓存键的工具 + + # 构造模拟请求对象(用于生成缓存键) request = HttpRequest() request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport} request.path = path - + + # 获取该请求对应的缓存键 key = get_cache_key(request, key_prefix=key_prefix, cache=cache) if key: logger.info('expire_view_cache:get key:{path}'.format(path=path)) + # 若缓存存在,则删除 if cache.get(key): cache.delete(key) return True return False -@cache_decorator() +@cache_decorator() # 应用缓存装饰器:缓存当前站点信息(默认 3 分钟) def get_current_site(): + """ + 获取当前站点信息(域名等),从缓存获取以减少数据库查询 + + Returns: + Site: Django Site 模型实例 + """ site = Site.objects.get_current() return site class CommonMarkdown: + """ + Markdown 解析工具类:将 Markdown 文本转为 HTML,并支持提取目录(TOC) + """ @staticmethod def _convert_markdown(value): + """ + 内部方法:执行 Markdown 转换,返回 HTML 内容和目录 + + Args: + value: Markdown 格式的文本 + + Returns: + tuple: (转换后的 HTML 内容, 目录 HTML) + """ + # 初始化 Markdown 解析器,启用扩展: + # - extra: 支持表格、脚注等扩展语法 + # - codehilite: 代码高亮 + # - toc: 生成目录 + # - tables: 表格支持(extra 已包含,此处冗余可能为兼容) md = markdown.Markdown( extensions=[ 'extra', @@ -109,124 +177,227 @@ class CommonMarkdown: 'tables', ] ) - body = md.convert(value) - toc = md.toc + body = md.convert(value) # 转换文本为 HTML + toc = md.toc # 提取目录 HTML return body, toc @staticmethod def get_markdown_with_toc(value): + """ + 获取带目录的 Markdown 转换结果 + + Args: + value: Markdown 文本 + + Returns: + tuple: (HTML 内容, 目录 HTML) + """ body, toc = CommonMarkdown._convert_markdown(value) return body, toc @staticmethod def get_markdown(value): + """ + 获取仅包含 HTML 内容的转换结果(忽略目录) + + Args: + value: Markdown 文本 + + Returns: + str: 转换后的 HTML 内容 + """ body, toc = CommonMarkdown._convert_markdown(value) return body def send_email(emailto, title, content): + """ + 发送邮件(通过信号机制触发,解耦发送逻辑) + + Args: + emailto: 收件人列表(如 ['user@example.com']) + title: 邮件标题 + content: 邮件内容(HTML 格式) + """ + # 延迟导入信号:避免循环导入 from djangoblog.blog_signals import send_email_signal + # 发送信号,由信号接收器(如 send_email_signal_handler)处理实际发送 send_email_signal.send( - send_email.__class__, + send_email.__class__, # 信号发送者(此处用当前函数的类) emailto=emailto, title=title, content=content) def generate_code() -> str: - """生成随机数验证码""" + """ + 生成 6 位数字验证码(用于邮箱验证、登录验证码等) + + Returns: + str: 6 位数字字符串 + """ + # 从数字字符集中随机选择 6 个,拼接为字符串 return ''.join(random.sample(string.digits, 6)) def parse_dict_to_url(dict): - from urllib.parse import quote + """ + 将字典转换为 URL 参数字符串(如 {'a':1, 'b':2} → 'a=1&b=2') + + Args: + dict: 键值对字典 + + Returns: + str: URL 编码后的参数字符串 + """ + from urllib.parse import quote # 延迟导入:避免启动依赖 + # 对键和值进行 URL 编码(保留 '/' 不编码),再拼接为 "k=v&k2=v2" 格式 url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) for k, v in dict.items()]) return url def get_blog_setting(): + """ + 获取博客系统设置(如站点名称、描述等),优先从缓存获取 + + Returns: + BlogSettings: 博客设置模型实例 + """ + # 尝试从缓存获取 value = cache.get('get_blog_setting') if value: return value else: + # 延迟导入模型:避免循环导入 from blog.models import BlogSettings + # 若数据库中无设置记录,初始化默认设置 if not BlogSettings.objects.count(): setting = BlogSettings() - setting.site_name = 'djangoblog' - setting.site_description = '基于Django的博客系统' - setting.site_seo_description = '基于Django的博客系统' - setting.site_keywords = 'Django,Python' - setting.article_sub_length = 300 - setting.sidebar_article_count = 10 - setting.sidebar_comment_count = 5 - setting.show_google_adsense = False - setting.open_site_comment = True - setting.analytics_code = '' - setting.beian_code = '' - setting.show_gongan_code = False - setting.comment_need_review = False - setting.save() + setting.site_name = 'djangoblog' # 站点名称 + setting.site_description = '基于Django的博客系统' # 站点描述 + setting.site_seo_description = '基于Django的博客系统' # SEO 描述 + setting.site_keywords = 'Django,Python' # SEO 关键词 + setting.article_sub_length = 300 # 文章摘要长度 + setting.sidebar_article_count = 10 # 侧边栏显示文章数 + setting.sidebar_comment_count = 5 # 侧边栏显示评论数 + setting.show_google_adsense = False # 是否显示谷歌广告 + setting.open_site_comment = True # 是否开启评论功能 + setting.analytics_code = '' # 统计代码(如百度统计) + setting.beian_code = '' # 备案号 + setting.show_gongan_code = False # 是否显示公安备案 + setting.comment_need_review = False # 评论是否需要审核 + setting.save() # 保存默认设置 + # 从数据库获取设置 value = BlogSettings.objects.first() logger.info('set cache get_blog_setting') + # 缓存设置(默认使用 cache_decorator 的有效期,或依赖全局缓存配置) cache.set('get_blog_setting', value) return value def save_user_avatar(url): ''' - 保存用户头像 - :param url:头像url - :return: 本地路径 + 下载并保存用户头像到本地(用于第三方登录时的头像同步) + + Args: + url: 头像的网络 URL + + Returns: + str: 本地头像的静态文件 URL(如 '/static/avatar/xxx.jpg') ''' - logger.info(url) + logger.info(url) # 记录头像 URL try: + # 本地头像存储目录(静态文件目录下的 avatar 文件夹) basedir = os.path.join(settings.STATICFILES, 'avatar') + # 发送 HTTP 请求下载头像(超时 2 秒) rsp = requests.get(url, timeout=2) - if rsp.status_code == 200: + if rsp.status_code == 200: # 下载成功 + # 若目录不存在则创建 if not os.path.exists(basedir): os.makedirs(basedir) + # 验证 URL 是否为图片格式(通过文件扩展名判断) image_extensions = ['.jpg', '.png', 'jpeg', '.gif'] isimage = len([i for i in image_extensions if url.endswith(i)]) > 0 + # 提取文件扩展名,默认为 .jpg ext = os.path.splitext(url)[1] if isimage else '.jpg' + # 生成唯一文件名(UUID 避免冲突) save_filename = str(uuid.uuid4().hex) + ext logger.info('保存用户头像:' + basedir + save_filename) + # 写入文件到本地目录 with open(os.path.join(basedir, save_filename), 'wb+') as file: file.write(rsp.content) + # 返回本地头像的静态 URL return static('avatar/' + save_filename) except Exception as e: + # 下载失败(如网络错误、超时),记录错误并返回默认头像 logger.error(e) return static('blog/img/avatar.png') def delete_sidebar_cache(): - from blog.models import LinkShowType + """ + 删除侧边栏相关缓存(当侧边栏内容更新时调用,如新增文章、评论) + """ + from blog.models import LinkShowType # 延迟导入:避免循环依赖 + # 侧边栏缓存键格式为 "sidebar + 链接类型值"(如 sidebar0、sidebar1) keys = ["sidebar" + x for x in LinkShowType.values] + # 遍历删除所有侧边栏缓存键 for k in keys: logger.info('delete sidebar key:' + k) cache.delete(k) def delete_view_cache(prefix, keys): - from django.core.cache.utils import make_template_fragment_key + """ + 删除指定模板片段的缓存(用于模板中用 {% cache %} 标签缓存的内容) + + Args: + prefix: 缓存前缀(与模板中 {% cache %} 标签的前缀一致) + keys: 缓存键的参数列表(与模板中 {% cache %} 标签的参数一致) + """ + from django.core.cache.utils import make_template_fragment_key # 生成模板缓存键的工具 + # 生成模板片段的缓存键 key = make_template_fragment_key(prefix, keys) + # 删除缓存 cache.delete(key) def get_resource_url(): + """ + 获取静态资源的基础 URL(用于动态生成资源路径) + + Returns: + str: 静态资源 URL 前缀(如 'http://example.com/static/') + """ if settings.STATIC_URL: return settings.STATIC_URL else: + # 若未配置 STATIC_URL,从当前站点域名生成 site = get_current_site() return 'http://' + site.domain + '/static/' +# HTML 清理配置:允许的标签和属性(防止 XSS 攻击) ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', - 'h2', 'p'] -ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} + 'h2', 'p'] # 允许的 HTML 标签 +ALLOWED_ATTRIBUTES = { # 允许的标签属性(键为标签,值为属性列表) + 'a': ['href', 'title'], + 'abbr': ['title'], + 'acronym': ['title'] +} def sanitize_html(html): - return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) + """ + 清理 HTML 内容,仅保留允许的标签和属性(防 XSS 攻击) + + Args: + html: 原始 HTML 字符串 + + Returns: + str: 清理后的安全 HTML 字符串 + """ + return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) \ No newline at end of file diff --git a/src/DjangoBlog-master/djangoblog/wsgi.py b/src/DjangoBlog-master/djangoblog/wsgi.py index 2295efd..10a7649 100644 --- a/src/DjangoBlog-master/djangoblog/wsgi.py +++ b/src/DjangoBlog-master/djangoblog/wsgi.py @@ -6,11 +6,18 @@ It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ """ - +# 导入系统模块:用于设置环境变量 import os +# 导入 Django WSGI 核心函数:生成符合 WSGI 标准的应用对象 from django.core.wsgi import get_wsgi_application +# 设置 Django 项目的配置模块环境变量 +# 作用:告诉 Django 启动时加载哪个配置文件(此处为项目根目录下的 djangoblog.settings) +# 部署时可通过修改该值切换配置(如 djangoblog.settings.production 对应生产环境配置) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") -application = get_wsgi_application() +# 生成 WSGI 应用对象 +# 作用:将 Django 项目包装为 WSGI 兼容的应用,供 WSGI 服务器(如 Gunicorn、uWSGI)调用 +# 该对象是 Django 与 Web 服务器交互的核心入口 +application = get_wsgi_application() \ No newline at end of file