合并分支 #10

Merged
plhw57tbe merged 43 commits from smy_branch into master 6 months ago

@ -1,47 +1,66 @@
from django.contrib import admin from django.contrib import admin # 导入Django管理后台模块
from django.urls import reverse from django.urls import reverse # 导入reverse函数用于生成URL
from django.utils.html import format_html from django.utils.html import format_html # 导入HTML格式化函数用于生成HTML标签
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _ # 导入翻译函数,用于国际化
def disable_commentstatus(modeladmin, request, queryset): def disable_commentstatus(modeladmin, request, queryset):
# 批量禁用选中的评论将is_enable设为False
queryset.update(is_enable=False) queryset.update(is_enable=False)
def enable_commentstatus(modeladmin, request, queryset): def enable_commentstatus(modeladmin, request, queryset):
# 批量启用选中的评论将is_enable设为True
queryset.update(is_enable=True) queryset.update(is_enable=True)
# 为批量操作设置显示名称(支持国际化)
disable_commentstatus.short_description = _('Disable comments') disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments') enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin): class CommentAdmin(admin.ModelAdmin):
# 管理后台列表页每页显示20条记录
list_per_page = 20 list_per_page = 20
# 列表页显示的字段
list_display = ( list_display = (
'id', 'id', # 评论ID
'body', 'body', # 评论内容
'link_to_userinfo', 'link_to_userinfo', # 评论作者(带链接)
'link_to_article', 'link_to_article', # 关联文章(带链接)
'is_enable', 'is_enable', # 是否启用
'creation_time') 'creation_time' # 创建时间
)
# 列表页中可点击跳转详情页的字段
list_display_links = ('id', 'body', 'is_enable') list_display_links = ('id', 'body', 'is_enable')
# 右侧筛选器,按是否启用筛选
list_filter = ('is_enable',) list_filter = ('is_enable',)
# 编辑页排除的字段(不允许手动编辑)
exclude = ('creation_time', 'last_modify_time') exclude = ('creation_time', 'last_modify_time')
# 注册批量操作函数
actions = [disable_commentstatus, enable_commentstatus] actions = [disable_commentstatus, enable_commentstatus]
def link_to_userinfo(self, obj): def link_to_userinfo(self, obj):
# 生成评论作者的管理后台编辑链接
# 获取用户模型的app标签和模型名称
info = (obj.author._meta.app_label, obj.author._meta.model_name) info = (obj.author._meta.app_label, obj.author._meta.model_name)
# 生成用户编辑页的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
# 返回带链接的HTML显示昵称或邮箱
return format_html( return format_html(
u'<a href="%s">%s</a>' % u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email)) (link, obj.author.nickname if obj.author.nickname else obj.author.email))
def link_to_article(self, obj): def link_to_article(self, obj):
# 生成关联文章的管理后台编辑链接
# 获取文章模型的app标签和模型名称
info = (obj.article._meta.app_label, obj.article._meta.model_name) info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 生成文章编辑页的URL
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,)) link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
# 返回带链接的HTML显示文章标题
return format_html( return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title)) u'<a href="%s">%s</a>' % (link, obj.article.title))
# 设置自定义字段在列表页的显示名称(支持国际化)
link_to_userinfo.short_description = _('User') link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article') link_to_article.short_description = _('Article')

@ -1,5 +1,4 @@
from django.apps import AppConfig from django.apps import AppConfig # 导入Django的应用配置基类
class CommentsConfig(AppConfig): # 定义评论应用的配置类继承自AppConfig
class CommentsConfig(AppConfig): name = 'comments' # 指定应用的名称为'comments'Django通过此名称识别该应用
name = 'comments'

@ -1,13 +1,15 @@
from django import forms from django import forms # 导入Django表单基础模块
from django.forms import ModelForm 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( parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False) widget=forms.HiddenInput, required=False)
class Meta: class Meta: # 元数据配置
model = Comment model = Comment # 指定表单关联的模型为Comment
fields = ['body'] fields = ['body'] # 表单包含的字段仅包含评论内容字段body

@ -1,38 +1,37 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14 # 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'), # 依赖blog应用的0001_initial迁移确保Article模型存在
dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), # 依赖用户模型的可交换迁移(支持自定义用户模型)
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
operations = [ operations = [ # 迁移操作列表:当前迁移需执行的数据库操作
migrations.CreateModel( migrations.CreateModel( # 创建Comment模型对应数据库表
name='Comment', name='Comment', # 模型名称为Comment评论模型
fields=[ fields=[ # 模型字段定义
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), # 自增主键,自动创建,作为表的唯一标识
('body', models.TextField(max_length=300, verbose_name='正文')), ('body', models.TextField(max_length=300, verbose_name='正文')), # 评论正文字段文本类型最大300字符
('created_time', models.DateTimeField(default=django.utils.timezone.now, 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='修改时间')), ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), # 最后修改时间字段,默认值为当前时间
('is_enable', models.BooleanField(default=True, 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', 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='作者')), ('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='上级评论')), ('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')), # 自关联外键(支持评论回复),允许为空
], ],
options={ options={ # 模型元数据配置
'verbose_name': '评论', 'verbose_name': '评论', # 模型单数显示名称
'verbose_name_plural': '评论', 'verbose_name_plural': '评论', # 模型复数显示名称
'ordering': ['-id'], 'ordering': ['-id'], # 默认排序按id降序最新评论在前
'get_latest_by': 'id', 'get_latest_by': 'id', # 获取最新记录时依据id字段
}, },
), ),
] ]

@ -1,18 +1,17 @@
# Generated by Django 4.1.7 on 2023-04-24 13:48 # 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 = [ # 迁移依赖需先执行comments应用的0001_initial迁移
dependencies = [
('comments', '0001_initial'), ('comments', '0001_initial'),
] ]
operations = [ operations = [ # 迁移操作列表:当前需要执行的数据库变更
migrations.AlterField( migrations.AlterField( # 修改已有字段
model_name='comment', model_name='comment', # 要修改的模型名称为Comment
name='is_enable', name='is_enable', # 要修改的字段名称为is_enable
field=models.BooleanField(default=False, verbose_name='是否显示'), field=models.BooleanField(default=False, verbose_name='是否显示'), # 将字段默认值从True改为False评论默认不显示
), ),
] ]

@ -1,60 +1,59 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13 # 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), # 依赖用户模型的可交换迁移
dependencies = [ ('blog', '0005_alter_article_options_alter_category_options_and_more'), # 依赖blog应用的指定迁移
migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('comments', '0002_alter_comment_is_enable'), # 依赖comments应用的0002迁移
('blog', '0005_alter_article_options_alter_category_options_and_more'),
('comments', '0002_alter_comment_is_enable'),
] ]
operations = [ operations = [ # 迁移操作列表:当前需要执行的数据库变更
migrations.AlterModelOptions( migrations.AlterModelOptions( # 修改模型的元数据配置
name='comment', name='comment', # 目标模型为Comment
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'}, options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'}, # 将显示名称改为英文
), ),
migrations.RemoveField( migrations.RemoveField( # 删除现有字段
model_name='comment', model_name='comment', # 目标模型为Comment
name='created_time', name='created_time', # 要删除的字段为created_time
), ),
migrations.RemoveField( migrations.RemoveField( # 删除现有字段
model_name='comment', model_name='comment', # 目标模型为Comment
name='last_mod_time', name='last_mod_time', # 要删除的字段为last_mod_time
), ),
migrations.AddField( migrations.AddField( # 添加新字段
model_name='comment', model_name='comment', # 目标模型为Comment
name='creation_time', name='creation_time', # 新字段名称为creation_time
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'), # 时间字段,默认当前时间,显示名称为英文
), ),
migrations.AddField( migrations.AddField( # 添加新字段
model_name='comment', model_name='comment', # 目标模型为Comment
name='last_modify_time', name='last_modify_time', # 新字段名称为last_modify_time
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'), # 时间字段,默认当前时间,显示名称为英文
), ),
migrations.AlterField( migrations.AlterField( # 修改现有字段
model_name='comment', model_name='comment', # 目标模型为Comment
name='article', name='article', # 目标字段为article
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'), # 将显示名称改为英文
), ),
migrations.AlterField( migrations.AlterField( # 修改现有字段
model_name='comment', model_name='comment', # 目标模型为Comment
name='author', name='author', # 目标字段为author
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'), # 将显示名称改为英文
), ),
migrations.AlterField( migrations.AlterField( # 修改现有字段
model_name='comment', model_name='comment', # 目标模型为Comment
name='is_enable', name='is_enable', # 目标字段为is_enable
field=models.BooleanField(default=False, verbose_name='enable'), field=models.BooleanField(default=False, verbose_name='enable'), # 将显示名称改为英文"enable"
), ),
migrations.AlterField( migrations.AlterField( # 修改现有字段
model_name='comment', model_name='comment', # 目标模型为Comment
name='parent_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'), field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'), # 将显示名称改为英文
), ),
] ]

@ -1,39 +1,47 @@
from django.conf import settings from django.conf import settings # 导入Django项目设置用于获取用户模型
from django.db import models from django.db import models # 导入Django模型模块用于定义数据模型
from django.utils.timezone import now from django.utils.timezone import now # 导入当前时间工具,用于时间字段默认值
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _ # 导入翻译函数,支持国际化
from blog.models import Article from blog.models import Article # 从blog应用导入Article模型用于关联评论和文章
# Create your models here. # Create your models here.
class Comment(models.Model): class Comment(models.Model):
# 评论内容字段文本类型最大长度300字符显示名称为"正文"
body = models.TextField('正文', max_length=300) body = models.TextField('正文', max_length=300)
# 评论创建时间字段,使用国际化显示名称,默认值为当前时间
creation_time = models.DateTimeField(_('creation time'), default=now) creation_time = models.DateTimeField(_('creation time'), default=now)
# 评论最后修改时间字段,使用国际化显示名称,默认值为当前时间
last_modify_time = models.DateTimeField(_('last modify time'), default=now) last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# 外键关联到用户模型,使用国际化显示名称,级联删除(用户删除则评论删除)
author = models.ForeignKey( author = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
verbose_name=_('author'), verbose_name=_('author'),
on_delete=models.CASCADE) on_delete=models.CASCADE)
# 外键关联到文章模型,使用国际化显示名称,级联删除(文章删除则评论删除)
article = models.ForeignKey( article = models.ForeignKey(
Article, Article,
verbose_name=_('article'), verbose_name=_('article'),
on_delete=models.CASCADE) on_delete=models.CASCADE)
# 自关联外键,用于实现评论回复功能,允许为空,级联删除
parent_comment = models.ForeignKey( parent_comment = models.ForeignKey(
'self', 'self',
verbose_name=_('parent comment'), verbose_name=_('parent comment'),
blank=True, blank=True,
null=True, null=True,
on_delete=models.CASCADE) on_delete=models.CASCADE)
# 评论是否启用的开关,布尔类型,默认不启用,不允许为空
is_enable = models.BooleanField(_('enable'), is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False) default=False, blank=False, null=False)
class Meta: class Meta:
ordering = ['-id'] ordering = ['-id'] # 默认排序方式按ID降序最新评论在前
verbose_name = _('comment') verbose_name = _('comment') # 模型单数显示名称(国际化)
verbose_name_plural = verbose_name verbose_name_plural = verbose_name # 模型复数显示名称(与单数相同)
get_latest_by = 'id' get_latest_by = 'id' # 获取最新记录时依据ID字段
def __str__(self): def __str__(self):
return self.body # 模型实例的字符串表示,返回评论内容
return self.body

@ -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): def parse_commenttree(commentlist, comment):
"""获得当前评论子评论的列表 """获得当前评论子评论的列表
用法: {% parse_commenttree article_comments comment as childcomments %} 用法: {% parse_commenttree article_comments comment as childcomments %}
""" """
datas = [] datas = [] # 用于存储子评论的列表
def parse(c): def parse(c): # 定义递归函数,用于递归获取所有子评论
# 筛选出当前评论的直接子评论(已启用状态)
childs = commentlist.filter(parent_comment=c, is_enable=True) childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs: for child in childs: # 遍历直接子评论
datas.append(child) datas.append(child) # 将子评论添加到列表
parse(child) parse(child) # 递归处理子评论的子评论(嵌套评论)
parse(comment) parse(comment) # 从当前评论开始递归获取所有子评论
return datas return datas # 返回所有子评论列表
@register.inclusion_tag('comments/tags/comment_item.html') @register.inclusion_tag('comments/tags/comment_item.html') # 将函数注册为包含标签,指定模板文件
def show_comment_item(comment, ischild): def show_comment_item(comment, ischild):
"""评论""" """评论展示标签"""
# 根据是否为子评论设置深度(用于前端样式区分,如缩进)
depth = 1 if ischild else 2 depth = 1 if ischild else 2
# 返回上下文数据供模板comment_item.html使用
return { return {
'comment_item': comment, 'comment_item': comment, # 当前评论对象
'depth': depth 'depth': depth # 评论深度(用于样式控制)
} }

@ -1,109 +1,61 @@
from django.test import Client, RequestFactory, TransactionTestCase from django.test import Client, RequestFactory, TransactionTestCase # 导入Django测试相关类
from django.urls import reverse from django.urls import reverse # 导入reverse函数用于生成URL
from accounts.models import BlogUser from accounts.models import BlogUser # 从accounts应用导入BlogUser模型用户模型
from blog.models import Category, Article from blog.models import Category, Article # 从blog应用导入分类和文章模型
from comments.models import Comment from comments.models import Comment # 导入评论模型
from comments.templatetags.comments_tags import * from comments.templatetags.comment_tags import * # 导入评论相关的模板标签
from djangoblog.utils import get_max_articleid_commentid from djangoblog.utils import get_max_articleid_commentid # 导入工具函数
# Create your tests here. # Create your tests here.
class CommentsTest(TransactionTestCase): class CommentsTest(TransactionTestCase): # 定义评论测试类,继承事务测试类(支持数据库事务回滚)
def setUp(self): def setUp(self): # 测试前的初始化方法,每个测试方法执行前都会调用
self.client = Client() self.client = Client() # 创建测试客户端,用于模拟用户请求
self.factory = RequestFactory() self.factory = RequestFactory() # 创建请求工厂,用于构造请求对象
# 配置博客评论设置
from blog.models import BlogSettings from blog.models import BlogSettings
value = BlogSettings() value = BlogSettings()
value.comment_need_review = True value.comment_need_review = True # 设置评论需要审核
value.save() value.save()
# 创建超级用户(测试用)
self.user = BlogUser.objects.create_superuser( self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com", email="liangliangyy1@gmail.com",
username="liangliangyy1", username="liangliangyy1",
password="liangliangyy1") password="liangliangyy1")
def update_article_comment_status(self, article): def update_article_comment_status(self, article): # 辅助方法:更新文章所有评论为启用状态
comments = article.comment_set.all() comments = article.comment_set.all() # 获取文章的所有评论
for comment in comments: for comment in comments: # 遍历评论
comment.is_enable = True comment.is_enable = True # 设置为启用
comment.save() comment.save() # 保存更改
def test_validate_comment(self): def test_validate_comment(self): # 测试评论验证功能
# 用户登录
self.client.login(username='liangliangyy1', password='liangliangyy1') self.client.login(username='liangliangyy1', password='liangliangyy1')
# 创建测试分类
category = Category() category = Category()
category.name = "categoryccc" category.name = "categoryccc"
category.save() category.save()
# 创建测试文章
article = Article() article = Article()
article.title = "nicetitleccc" article.title = "nicetitleccc"
article.body = "nicecontentccc" article.body = "nicecontentccc"
article.author = self.user article.author = self.user # 设置作者为测试用户
article.category = category article.category = category # 设置分类
article.type = 'a' article.type = 'a' # 文章类型(假设'a'表示普通文章)
article.status = 'p' article.status = 'p' # 发布状态(假设'p'表示已发布)
article.save() article.save()
# 生成评论提交的URL
comment_url = reverse( comment_url = reverse(
'comments:postcomment', kwargs={ 'comments:postcomment', kwargs={
'article_id': article.id}) 'article_id': article.id}) # 传入文章ID参数
response = self.client.post(comment_url, # 发送评论提交请求代码不完整后续应补充POST数据和断言
{ 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)

@ -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" app_name = "comments" # 定义应用的命名空间用于模板中URL反向解析
urlpatterns = [ urlpatterns = [ # URL模式列表定义URL与视图的映射关系
path( path(
'article/<int:article_id>/postcomment', 'article/<int:article_id>/postcomment', # URL路径包含文章ID参数整数类型
views.CommentPostView.as_view(), views.CommentPostView.as_view(), # 关联的视图类使用as_view()方法转换为可调用视图
name='postcomment'), name='postcomment' # 该URL的名称用于反向解析
] ),
]

@ -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 get_current_site # 从自定义工具模块导入获取当前站点域名的函数
from djangoblog.utils import send_email from djangoblog.utils import send_email # 从自定义工具模块导入发送邮件的函数
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__) # 创建当前模块的日志记录器,用于记录该模块的日志
def send_comment_email(comment): def send_comment_email(comment):
"""
发送评论相关邮件
1. 向评论作者发送评论成功的感谢邮件
2. 若当前评论是回复有父评论向父评论作者发送回复通知邮件
"""
# 获取当前网站的域名(用于拼接文章链接)
site = get_current_site().domain site = get_current_site().domain
# 邮件主题:评论感谢(支持国际化)
subject = _('Thanks for your comment') subject = _('Thanks for your comment')
# 拼接评论对应的文章访问链接HTTPS协议
article_url = f"https://{site}{comment.article.get_absolute_url()}" article_url = f"https://{site}{comment.article.get_absolute_url()}"
# 构建给评论作者的HTML格式邮件内容支持国际化通过占位符注入动态数据
html_content = _("""<p>Thank you very much for your comments on this site</p> html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a> You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments, to review your comments,
Thank you again! Thank you again!
<br /> <br />
If the link above cannot be opened, please copy this link to your browser. 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 tomail = comment.author.email
# 调用发送邮件函数,向评论作者发送感谢邮件
send_email([tomail], subject, html_content) send_email([tomail], subject, html_content)
try: try:
# 判断当前评论是否有父评论(即是否是回复评论)
if comment.parent_comment: if comment.parent_comment:
# 构建给父评论作者的HTML格式邮件内容回复通知支持国际化
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s received a reply. <br/> %(comment_body)s
<br/> <br/>
@ -30,9 +47,16 @@ def send_comment_email(comment):
<br/> <br/>
If the link above cannot be opened, please copy this link to your browser. If the link above cannot be opened, please copy this link to your browser.
%(article_url)s %(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 tomail = comment.parent_comment.author.email
# 调用发送邮件函数,向父评论作者发送回复通知邮件
send_email([tomail], subject, html_content) send_email([tomail], subject, html_content)
# 捕获发送回复邮件过程中的异常(避免单个邮件发送失败影响整体流程)
except Exception as e: except Exception as e:
logger.error(e) # 记录异常日志(便于问题排查)
logger.error(e)

@ -1,63 +1,76 @@
# Create your views here. # Create your views here.
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError # 导入验证异常类,用于处理验证错误
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect # 导入HTTP重定向类用于页面跳转
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404 # 导入获取对象或返回404的工具函数
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator # 导入方法装饰器工具,用于为类视图方法添加装饰器
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect # 导入CSRF保护装饰器防止跨站请求伪造
from django.views.generic.edit import FormView from django.views.generic.edit import FormView # 导入表单视图基类,用于处理表单提交逻辑
from accounts.models import BlogUser from accounts.models import BlogUser # 从accounts应用导入用户模型
from blog.models import Article from blog.models import Article # 从blog应用导入文章模型
from .forms import CommentForm from .forms import CommentForm # 从当前应用导入评论表单
from .models import Comment from .models import Comment # 从当前应用导入评论模型
class CommentPostView(FormView): 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): def dispatch(self, *args, **kwargs):
# 调用父类的dispatch方法处理请求分发
return super(CommentPostView, self).dispatch(*args, **kwargs) return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id'] """处理GET请求重定向到文章详情页的评论区"""
article = get_object_or_404(Article, pk=article_id) article_id = self.kwargs['article_id'] # 从URL参数中获取文章ID
url = article.get_absolute_url() article = get_object_or_404(Article, pk=article_id) # 获取对应的文章对象不存在则返回404
return HttpResponseRedirect(url + "#comments") url = article.get_absolute_url() # 获取文章的绝对URL
return HttpResponseRedirect(url + "#comments") # 重定向到文章详情页的评论区锚点
def form_invalid(self, form): 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({ return self.render_to_response({
'form': form, 'form': form, # 验证失败的表单(包含错误信息)
'article': article 'article': article # 文章对象
}) })
def form_valid(self, form): def form_valid(self, form):
"""提交的数据验证合法后的逻辑""" """处理表单验证通过后的逻辑:保存评论并跳转"""
user = self.request.user user = self.request.user # 获取当前登录用户
author = BlogUser.objects.get(pk=user.pk) author = BlogUser.objects.get(pk=user.pk) # 获取用户对应的BlogUser对象
article_id = self.kwargs['article_id'] article_id = self.kwargs['article_id'] # 获取文章ID
article = get_object_or_404(Article, pk=article_id) article = get_object_or_404(Article, pk=article_id) # 获取文章对象
# 检查文章是否允许评论(评论状态为关闭或文章状态为草稿则不允许评论)
if article.comment_status == 'c' or article.status == 'c': if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.") raise ValidationError("该文章评论已关闭.") # 抛出验证异常
comment = form.save(False)
comment.article = article comment = form.save(False) # 不立即保存表单数据,返回评论对象
comment.article = article # 设置评论关联的文章
# 获取博客设置,判断评论是否需要审核
from djangoblog.utils import get_blog_setting from djangoblog.utils import get_blog_setting
settings = get_blog_setting() settings = get_blog_setting()
if not settings.comment_need_review: if not settings.comment_need_review: # 如果不需要审核
comment.is_enable = True comment.is_enable = True # 直接设置评论为启用状态
comment.author = author
comment.author = author # 设置评论的作者
# 处理回复功能如果存在父评论ID则设置父评论
if form.cleaned_data['parent_comment_id']: if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get( parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id']) pk=form.cleaned_data['parent_comment_id']) # 获取父评论对象
comment.parent_comment = parent_comment comment.parent_comment = parent_comment # 设置当前评论的父评论
comment.save(True) # 保存评论到数据库
comment.save(True) # 重定向到文章详情页的当前评论位置(带锚点)
return HttpResponseRedirect( return HttpResponseRedirect(
"%s#div-comment-%d" % "%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk)) (article.get_absolute_url(), comment.pk)) # 拼接URL包含评论ID锚点

@ -1,48 +1,52 @@
# Docker Compose配置文件版本为3指定兼容的Compose语法版本
version: '3' version: '3'
# 定义所有服务(容器)
services: services:
# 1. Elasticsearch服务用于全文搜索功能集成IK中文分词器
es: es:
image: liangliangyy/elasticsearch-analysis-ik:8.6.1 image: liangliangyy/elasticsearch-analysis-ik:8.6.1 # 使用带IK分词器的ES镜像版本8.6.1
container_name: es container_name: es # 容器名称固定为"es",便于管理
restart: always restart: always # 容器退出后自动重启(确保服务持续运行)
environment: environment: # 环境变量配置
- discovery.type=single-node - discovery.type=single-node # 单节点模式(无需集群,适合测试/小型部署)
- "ES_JAVA_OPTS=-Xms512m -Xmx512m" - "ES_JAVA_OPTS=-Xms512m -Xmx512m" # 设置JVM内存大小初始/最大均为512M避免内存溢出
ports: ports: # 端口映射主机9200端口 → 容器9200端口ES默认API端口
- 9200:9200 - 9200:9200
volumes: volumes: # 数据卷挂载持久化ES数据
- ./bin/datas/es/:/usr/share/elasticsearch/data/ - ./bin/datas/es/:/usr/share/elasticsearch/data/ # 主机目录 → 容器内ES数据存储目录
# 2. Kibana服务ES的可视化管理工具用于操作/监控ES
kibana: kibana:
image: kibana:8.6.1 image: kibana:8.6.1 # Kibana镜像版本需与ES一致8.6.1
restart: always restart: always # 容器退出后自动重启
container_name: kibana container_name: kibana # 容器名称固定为"kibana"
ports: ports: # 端口映射主机5601端口 → 容器5601端口Kibana默认Web端口
- 5601:5601 - 5601:5601
environment: environment: # 环境变量配置指定关联的ES地址
- ELASTICSEARCH_HOSTS=http://es:9200 - ELASTICSEARCH_HOSTS=http://es:9200 # 指向同网络内的"es"服务(容器间通过服务名通信)
# 3. Django博客服务核心应用服务
djangoblog: djangoblog:
build: . build: . # 基于当前目录的Dockerfile构建镜像不使用现成镜像需本地有Dockerfile
restart: always restart: always # 容器退出后自动重启
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' # 容器启动后执行的命令:运行启动脚本
ports: ports: # 端口映射主机8000端口 → 容器8000端口Django默认开发服务器端口
- "8000:8000" - "8000:8000"
volumes: volumes: # 数据卷挂载:持久化应用数据/静态资源
- ./collectedstatic:/code/djangoblog/collectedstatic - ./collectedstatic:/code/djangoblog/collectedstatic # 主机静态资源目录 → 容器内静态资源目录Nginx可直接访问
- ./uploads:/code/djangoblog/uploads - ./uploads:/code/djangoblog/uploads # 主机上传文件目录 → 容器内上传文件目录(如博客图片)
environment: environment: # 环境变量配置Django应用的关键参数数据库、缓存、ES等
- DJANGO_MYSQL_DATABASE=djangoblog - DJANGO_MYSQL_DATABASE=djangoblog # Django连接的MySQL数据库名
- DJANGO_MYSQL_USER=root - DJANGO_MYSQL_USER=root # MySQL用户名
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E # MySQL密码
- DJANGO_MYSQL_HOST=db - DJANGO_MYSQL_HOST=db # MySQL服务地址指向同网络内的"db"服务需额外配置db服务
- DJANGO_MYSQL_PORT=3306 - DJANGO_MYSQL_PORT=3306 # MySQL端口
- DJANGO_MEMCACHED_LOCATION=memcached:11211 - DJANGO_MEMCACHED_LOCATION=memcached:11211 # Memcached缓存地址指向同网络内的"memcached"服务,需额外配置)
- DJANGO_ELASTICSEARCH_HOST=es:9200 - DJANGO_ELASTICSEARCH_HOST=es:9200 # ES服务地址指向同网络内的"es"服务)
links: links: # 显式链接到其他服务已逐步被depends_on替代此处用于兼容
- db # 链接到MySQL服务
- memcached # 链接到Memcached服务
depends_on: # 服务依赖启动djangoblog前先启动db服务确保数据库就绪
- db - db
- memcached container_name: djangoblog # 容器名称固定为"djangoblog"
depends_on:
- db
container_name: djangoblog

@ -1,60 +1,67 @@
# Docker Compose配置文件版本为3指定Compose语法版本
version: '3' version: '3'
# 定义所有服务(容器)
services: services:
# 1. MySQL数据库服务存储应用数据
db: db:
image: mysql:latest image: mysql:latest # 使用最新版MySQL镜像
restart: always restart: always # 容器退出后自动重启(确保服务持续运行)
environment: environment: # 环境变量配置(数据库初始化参数)
- MYSQL_DATABASE=djangoblog - MYSQL_DATABASE=djangoblog # 自动创建的数据库名称
- MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E - MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E # MySQL root用户密码
ports: ports: # 端口映射主机3306端口 → 容器3306端口MySQL默认端口
- 3306:3306 - 3306:3306
volumes: volumes: # 数据卷挂载持久化MySQL数据
- ./bin/datas/mysql/:/var/lib/mysql - ./bin/datas/mysql/:/var/lib/mysql # 主机目录 → 容器内MySQL数据存储目录
depends_on: depends_on: # 服务依赖启动db前先启动redis可能用于数据库缓存等场景
- redis - redis
container_name: db container_name: db # 容器名称固定为"db"
# 2. Django博客应用服务核心应用
djangoblog: djangoblog:
build: build: # 构建配置
context: ../../ context: ../../ # 指定Dockerfile所在的上下文目录上级目录的上级目录
restart: always restart: always # 容器退出后自动重启
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' command: bash -c 'sh /code/djangoblog/bin/docker_start.sh' # 启动命令:执行应用启动脚本
ports: ports: # 端口映射主机8000端口 → 容器8000端口Django应用端口
- "8000:8000" - "8000:8000"
volumes: volumes: # 数据卷挂载:持久化应用数据和配置
- ./collectedstatic:/code/djangoblog/collectedstatic - ./collectedstatic:/code/djangoblog/collectedstatic # 静态资源目录供Nginx访问
- ./logs:/code/djangoblog/logs - ./logs:/code/djangoblog/logs # 应用日志目录
- ./uploads:/code/djangoblog/uploads - ./uploads:/code/djangoblog/uploads # 用户上传文件目录(如图片)
environment: environment: # 环境变量配置(应用连接参数)
- DJANGO_MYSQL_DATABASE=djangoblog - DJANGO_MYSQL_DATABASE=djangoblog # 数据库名称与db服务对应
- DJANGO_MYSQL_USER=root - DJANGO_MYSQL_USER=root # 数据库用户名
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E - DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E # 数据库密码与db服务对应
- DJANGO_MYSQL_HOST=db - DJANGO_MYSQL_HOST=db # 数据库服务地址(指向同网络内的"db"服务)
- DJANGO_MYSQL_PORT=3306 - DJANGO_MYSQL_PORT=3306 # 数据库端口
- DJANGO_REDIS_URL=redis:6379 - DJANGO_REDIS_URL=redis:6379 # Redis服务地址指向同网络内的"redis"服务)
links: links: # 显式链接到其他服务(用于容器间通信)
- db # 链接到MySQL服务
- redis # 链接到Redis服务
depends_on: # 服务依赖启动djangoblog前先启动db服务确保数据库就绪
- db - db
- redis container_name: djangoblog # 容器名称固定为"djangoblog"
depends_on:
- db # 3. Nginx服务反向代理和静态资源服务
container_name: djangoblog
nginx: nginx:
restart: always restart: always # 容器退出后自动重启
image: nginx:latest image: nginx:latest # 使用最新版Nginx镜像
ports: ports: # 端口映射HTTP(80)和HTTPS(443)端口
- "80:80" - "80:80"
- "443:443" - "443:443"
volumes: volumes: # 数据卷挂载Nginx配置和静态资源
- ./bin/nginx.conf:/etc/nginx/nginx.conf - ./bin/nginx.conf:/etc/nginx/nginx.conf # 主机Nginx配置文件 → 容器内Nginx配置文件
- ./collectedstatic:/code/djangoblog/collectedstatic - ./collectedstatic:/code/djangoblog/collectedstatic # 静态资源目录与djangoblog服务共享
links: links: # 链接到djangoblog服务实现反向代理
- djangoblog:djangoblog - djangoblog:djangoblog # 将djangoblog服务映射为"djangoblog"主机名
container_name: nginx container_name: nginx # 容器名称固定为"nginx"
# 4. Redis服务缓存服务用于提升应用性能
redis: redis:
restart: always restart: always # 容器退出后自动重启
image: redis:latest image: redis:latest # 使用最新版Redis镜像
container_name: redis container_name: redis # 容器名称固定为"redis"
ports: ports: # 端口映射主机6379端口 → 容器6379端口Redis默认端口
- "6379:6379" - "6379:6379"

@ -1,119 +1,124 @@
apiVersion: v1 apiVersion: v1 # Kubernetes API版本v1为稳定版本
kind: ConfigMap kind: ConfigMap # 资源类型为ConfigMap用于存储非敏感配置数据
metadata: metadata:
name: web-nginx-config name: web-nginx-config # ConfigMap名称标识该Nginx配置资源
namespace: djangoblog namespace: djangoblog # 所属命名空间用于资源隔离对应djangoblog应用
data: data: # 配置数据,键为文件名,值为文件内容
nginx.conf: | nginx.conf: | # Nginx主配置文件
user nginx; user nginx; # Nginx进程运行的用户
worker_processes auto; worker_processes auto; # 工作进程数auto表示按CPU核心数自动分配
error_log /var/log/nginx/error.log notice; error_log /var/log/nginx/error.log notice; # 错误日志路径及级别notice级别
pid /var/run/nginx.pid; pid /var/run/nginx.pid; # Nginx进程PID文件路径
events { events { # 事件处理配置块
worker_connections 1024; worker_connections 1024; # 每个工作进程最大连接数
multi_accept on; multi_accept on; # 允许工作进程同时接受多个新连接
use epoll; use epoll; # 使用epoll I/O模型Linux下高效事件驱动模型
} }
http { http { # HTTP核心配置块
include /etc/nginx/mime.types; include /etc/nginx/mime.types; # 引入MIME类型映射文件识别文件类型
default_type application/octet-stream; default_type application/octet-stream; # 默认MIME类型未知类型时使用
# 定义日志格式命名为main
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'; '"$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; # gzip压缩补充配置
keepalive_timeout 65; gzip_vary on; # 启用Vary: Accept-Encoding响应头告知代理缓存压缩/非压缩版本)
gzip on; gzip_proxied any; # 对所有代理请求启用压缩
gzip_disable "msie6"; gzip_comp_level 8; # 压缩级别1-98为较高压缩率
gzip_buffers 16 8k; # 压缩缓冲区大小16个8k缓冲区
gzip_vary on; gzip_http_version 1.1; # 仅对HTTP/1.1及以上版本启用压缩
gzip_proxied any; # 需压缩的文件类型文本、JS、CSS、图片等
gzip_comp_level 8;
gzip_buffers 16 8k;
gzip_http_version 1.1;
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; 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: | djangoblog.conf: | # lylinux.net域名的Nginx站点配置
server { server { # 处理lylinux.net域名的服务配置
server_name lylinux.net; server_name lylinux.net; # 绑定的主域名
root /code/djangoblog/collectedstatic/; root /code/djangoblog/collectedstatic/; # 网站根目录(静态文件目录)
listen 80; listen 80; # 监听80端口HTTP
keepalive_timeout 70; keepalive_timeout 70; # 该站点长连接超时时间
location /static/ {
expires max; location /static/ { # 处理静态文件请求
alias /code/djangoblog/collectedstatic/; 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)$ { location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ {
root /resource/djangopub; root /resource/djangopub; # 这些文件的根目录
expires 1d; expires 1d; # 缓存1天
access_log off; access_log off; # 关闭访问日志
error_log off; error_log off; # 关闭错误日志
} }
location / { location / { # 处理其他所有请求反向代理到Django
proxy_set_header X-Real-IP $remote_addr; # 设置代理请求头(传递客户端信息给后端)
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; # 客户端真实IP
proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 代理链IP列表
proxy_set_header X-NginX-Proxy true; proxy_set_header Host $http_host; # 原始请求Host
proxy_redirect off; proxy_set_header X-NginX-Proxy true; # 标识经Nginx代理
proxy_redirect off; # 禁用代理重定向
# 若请求文件不存在反向代理到Django服务djangoblog为K8s内部服务名
if (!-f $request_filename) { if (!-f $request_filename) {
proxy_pass http://djangoblog:8000; proxy_pass http://djangoblog:8000;
break; break;
} }
} }
} }
server { server { # 处理www.lylinux.net域名重定向配置
server_name www.lylinux.net; server_name www.lylinux.net; # 绑定的www子域名
listen 80; listen 80; # 监听80端口
return 301 https://lylinux.net$request_uri; return 301 https://lylinux.net$request_uri; # 永久重定向到主域名HTTPS地址
} }
resource.lylinux.net.conf: | resource.lylinux.net.conf: | # resource.lylinux.net子域名的配置资源服务器
server { server {
index index.html index.htm; index index.html index.htm; # 默认索引文件
server_name resource.lylinux.net; server_name resource.lylinux.net; # 绑定的资源子域名
root /resource/; root /resource/; # 资源文件根目录
location /djangoblog/ { location /djangoblog/ { # 映射Django静态文件路径
alias /code/djangoblog/collectedstatic/; alias /code/djangoblog/collectedstatic/; # 实际静态文件路径
} }
access_log off; access_log off; # 关闭访问日志
error_log off; error_log off; # 关闭错误日志
include lylinux/resource.conf; include lylinux/resource.conf; # 引入通用资源配置
} }
lylinux.resource.conf: | lylinux.resource.conf: | # 通用资源配置(被资源服务器引用)
expires max; expires max; # 资源缓存有效期设为最大
access_log off; access_log off; # 关闭访问日志
log_not_found off; log_not_found off; # 关闭文件未找到的错误日志
add_header Pragma public; add_header Pragma public; # 缓存控制头(告知客户端可缓存)
add_header Cache-Control "public"; add_header Cache-Control "public"; # 缓存控制头(公开可缓存)
add_header "Access-Control-Allow-Origin" "*"; add_header "Access-Control-Allow-Origin" "*"; # 允许跨域访问(所有域名)
--- ---
apiVersion: v1 apiVersion: v1 # Kubernetes API版本
kind: ConfigMap kind: ConfigMap # 资源类型为ConfigMap存储环境变量
metadata: metadata:
name: djangoblog-env name: djangoblog-env # ConfigMap名称标识Django环境变量配置
namespace: djangoblog namespace: djangoblog # 所属命名空间(与应用一致)
data: data: # 环境变量键值对
DJANGO_MYSQL_DATABASE: djangoblog DJANGO_MYSQL_DATABASE: djangoblog # Django连接的MySQL数据库名
DJANGO_MYSQL_USER: db_user DJANGO_MYSQL_USER: db_user # MySQL登录用户名
DJANGO_MYSQL_PASSWORD: db_password DJANGO_MYSQL_PASSWORD: db_password # MySQL登录密码
DJANGO_MYSQL_HOST: db_host DJANGO_MYSQL_HOST: db_host # MySQL服务地址K8s内部服务名或IP
DJANGO_MYSQL_PORT: db_port DJANGO_MYSQL_PORT: db_port # MySQL服务端口
DJANGO_REDIS_URL: "redis:6379" DJANGO_REDIS_URL: "redis:6379" # Redis服务地址及端口
DJANGO_DEBUG: "False" DJANGO_DEBUG: "False" # Django调试模式生产环境关闭
MYSQL_ROOT_PASSWORD: db_password MYSQL_ROOT_PASSWORD: db_password # MySQL root用户密码用于初始化
MYSQL_DATABASE: djangoblog MYSQL_DATABASE: djangoblog # 初始化的MySQL数据库名
MYSQL_PASSWORD: db_password MYSQL_PASSWORD: db_password # MySQL普通用户密码与Django配置一致
DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Django加密密钥用于会话、CSRF等

@ -1,132 +1,161 @@
# 第一部分Django 博客应用部署配置
# apiVersion 指定 Kubernetes API 版本apps/v1 是 Deployment 资源的稳定版本
apiVersion: apps/v1 apiVersion: apps/v1
# kind 定义资源类型为 Deployment用于管理Pod的创建和扩展
kind: Deployment kind: Deployment
metadata: metadata:
# Deployment 的名称
name: djangoblog name: djangoblog
# 部署所在的命名空间(用于资源隔离)
namespace: djangoblog namespace: djangoblog
# 为 Deployment 添加标签(用于筛选和关联资源)
labels: labels:
app: djangoblog app: djangoblog
spec: spec:
# 副本数:指定运行的 Pod 数量为 3 个(实现高可用)
replicas: 3 replicas: 3
# 选择器:用于匹配要管理的 Pod 标签(必须与下面 template.metadata.labels 一致)
selector: selector:
matchLabels: matchLabels:
app: djangoblog app: djangoblog
# Pod 模板:定义要创建的 Pod 的规格
template: template:
metadata: metadata:
# Pod 的标签(与上面的 selector.matchLabels 对应)
labels: labels:
app: djangoblog app: djangoblog
spec: spec:
# 容器列表:一个 Pod 可以包含多个容器,这里定义应用容器
containers: containers:
- name: djangoblog - name: djangoblog # 容器名称
# 容器使用的镜像Django 博客应用镜像)
image: liangliangyy/djangoblog:latest image: liangliangyy/djangoblog:latest
# 镜像拉取策略Always 表示每次都从仓库拉取最新镜像
imagePullPolicy: Always imagePullPolicy: Always
# 容器暴露的端口Django 应用默认运行在 8000 端口)
ports: ports:
- containerPort: 8000 - containerPort: 8000
# 从配置映射ConfigMap中注入环境变量
envFrom: envFrom:
- configMapRef: - configMapRef:
name: djangoblog-env name: djangoblog-env # 引用的 ConfigMap 名称
# 就绪探针:判断容器是否已准备好接收请求(服务发现会依赖此状态)
readinessProbe: readinessProbe:
httpGet: httpGet: # 通过 HTTP 请求检查就绪状态
path: / path: / # 检查的路径(应用根目录)
port: 8000 port: 8000 # 检查的端口
initialDelaySeconds: 10 initialDelaySeconds: 10 # 容器启动后延迟 10 秒开始首次检查
periodSeconds: 30 periodSeconds: 30 # 每隔 30 秒检查一次
# 存活探针:判断容器是否存活,若失败会重启容器
livenessProbe: livenessProbe:
httpGet: httpGet: # 通过 HTTP 请求检查存活状态
path: / path: / # 检查的路径
port: 8000 port: 8000 # 检查的端口
initialDelaySeconds: 10 initialDelaySeconds: 10 # 容器启动后延迟 10 秒开始首次检查
periodSeconds: 30 periodSeconds: 30 # 每隔 30 秒检查一次
# 资源限制:控制容器对 CPU 和内存的使用
resources: resources:
requests: requests: # 资源请求(调度时的最小需求)
cpu: 10m cpu: 10m # 10 毫核 CPU1核=1000m
memory: 100Mi memory: 100Mi # 100 兆内存
limits: limits: # 资源限制(容器最大可使用的资源)
cpu: "2" cpu: "2" # 2 核 CPU
memory: 2Gi memory: 2Gi # 2 吉内存
# 卷挂载:将持久卷挂载到容器内的指定路径
volumeMounts: volumeMounts:
- name: djangoblog - name: djangoblog # 引用下面 volumes 中定义的卷名称
mountPath: /code/djangoblog/collectedstatic mountPath: /code/djangoblog/collectedstatic # 容器内的挂载路径Django 静态文件目录)
- name: resource - name: resource # 引用资源卷
mountPath: /resource mountPath: /resource # 容器内的资源文件目录
# 卷定义:声明需要挂载的持久卷
volumes: volumes:
- name: djangoblog - name: djangoblog # 卷名称(与上面 volumeMounts.name 对应)
persistentVolumeClaim: persistentVolumeClaim: # 使用持久卷声明PVC
claimName: djangoblog-pvc claimName: djangoblog-pvc # 引用的 PVC 名称(需提前创建)
- name: resource - name: resource # 资源卷名称
persistentVolumeClaim: persistentVolumeClaim:
claimName: resource-pvc claimName: resource-pvc # 资源对应的 PVC 名称
---
# 第二部分Redis 缓存服务部署配置
--- # 分隔符:用于在一个文件中定义多个 Kubernetes 资源
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: redis name: redis # Redis 部署名称
namespace: djangoblog namespace: djangoblog # 同属 djangoblog 命名空间
labels: labels:
app: redis app: redis # Redis 标签
spec: spec:
replicas: 1 replicas: 1 # Redis 单副本(简单部署,生产环境可能需要集群)
selector: selector:
matchLabels: matchLabels:
app: redis app: redis # 匹配 Redis Pod 标签
template: template:
metadata: metadata:
labels: labels:
app: redis app: redis # Pod 标签
spec: spec:
containers: containers:
- name: redis - name: redis # 容器名称
image: redis:latest image: redis:latest # Redis 官方最新镜像
# 镜像拉取策略IfNotPresent 表示本地有则使用本地镜像,否则拉取
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
ports: ports:
- containerPort: 6379 - containerPort: 6379 # Redis 默认端口
# 资源限制Redis 对资源需求较低)
resources: resources:
requests: requests:
cpu: 10m cpu: 10m
memory: 100Mi memory: 100Mi
limits: limits:
cpu: 200m cpu: 200m # 限制最大 200 毫核 CPU
memory: 2Gi memory: 2Gi
# 第三部分MySQL 数据库部署配置
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: db name: db # 数据库部署名称
namespace: djangoblog namespace: djangoblog
labels: labels:
app: db app: db # 数据库标签
spec: spec:
replicas: 1 replicas: 1 # 数据库单副本(生产环境需考虑主从或集群)
selector: selector:
matchLabels: matchLabels:
app: db app: db # 匹配数据库 Pod 标签
template: template:
metadata: metadata:
labels: labels:
app: db app: db # Pod 标签
spec: spec:
containers: containers:
- name: db - name: db # 容器名称
image: mysql:latest image: mysql:latest # MySQL 官方最新镜像
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
ports: ports:
- containerPort: 3306 - containerPort: 3306 # MySQL 默认端口
# 从 ConfigMap 注入环境变量(如数据库密码、用户名等)
envFrom: envFrom:
- configMapRef: - configMapRef:
name: djangoblog-env name: djangoblog-env # 复用 Django 应用的环境变量配置
# 就绪探针:通过执行 mysqladmin ping 检查数据库是否就绪
readinessProbe: readinessProbe:
exec: exec: # 执行命令检查
command: command:
- mysqladmin - mysqladmin
- ping - ping
- "-h" - "-h"
- "127.0.0.1" - "127.0.0.1" # 数据库主机(容器内本地)
- "-u" - "-u"
- "root" - "root" # 用户名
- "-p$MYSQL_ROOT_PASSWORD" - "-p$MYSQL_ROOT_PASSWORD" # 密码(从环境变量获取)
initialDelaySeconds: 10 initialDelaySeconds: 10 # 延迟 10 秒检查
periodSeconds: 10 periodSeconds: 10 # 每 10 秒检查一次
# 存活探针:同就绪探针,确保数据库存活
livenessProbe: livenessProbe:
exec: exec:
command: command:
@ -139,6 +168,7 @@ spec:
- "-p$MYSQL_ROOT_PASSWORD" - "-p$MYSQL_ROOT_PASSWORD"
initialDelaySeconds: 10 initialDelaySeconds: 10
periodSeconds: 10 periodSeconds: 10
# 资源限制(数据库对资源需求较高)
resources: resources:
requests: requests:
cpu: 10m cpu: 10m
@ -146,38 +176,42 @@ spec:
limits: limits:
cpu: "2" cpu: "2"
memory: 2Gi memory: 2Gi
# 挂载数据库数据目录(持久化存储,避免数据丢失)
volumeMounts: volumeMounts:
- name: db-data - name: db-data # 引用数据卷
mountPath: /var/lib/mysql mountPath: /var/lib/mysql # MySQL 数据存储路径
volumes: volumes:
- name: db-data - name: db-data # 数据卷名称
persistentVolumeClaim: persistentVolumeClaim:
claimName: db-pvc claimName: db-pvc # 数据库对应的 PVC 名称
# 第四部分Nginx 反向代理部署配置
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: nginx name: nginx # Nginx 部署名称
namespace: djangoblog namespace: djangoblog
labels: labels:
app: nginx app: nginx # Nginx 标签
spec: spec:
replicas: 1 replicas: 1 # Nginx 单副本
selector: selector:
matchLabels: matchLabels:
app: nginx app: nginx # 匹配 Nginx Pod 标签
template: template:
metadata: metadata:
labels: labels:
app: nginx app: nginx # Pod 标签
spec: spec:
containers: containers:
- name: nginx - name: nginx # 容器名称
image: nginx:latest image: nginx:latest # Nginx 官方最新镜像
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
ports: ports:
- containerPort: 80 - containerPort: 80 # Nginx 默认端口
# 资源限制
resources: resources:
requests: requests:
cpu: 10m cpu: 10m
@ -185,67 +219,82 @@ spec:
limits: limits:
cpu: "2" cpu: "2"
memory: 2Gi memory: 2Gi
# 卷挂载:挂载配置文件和静态资源
volumeMounts: volumeMounts:
# 挂载 Nginx 主配置文件subPath 表示只挂载单个文件,而非目录)
- name: nginx-config - name: nginx-config
mountPath: /etc/nginx/nginx.conf mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf subPath: nginx.conf
# 挂载默认站点配置
- name: nginx-config - name: nginx-config
mountPath: /etc/nginx/conf.d/default.conf mountPath: /etc/nginx/conf.d/default.conf
subPath: djangoblog.conf subPath: djangoblog.conf
# 挂载资源站点配置
- name: nginx-config - name: nginx-config
mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf
subPath: resource.lylinux.net.conf subPath: resource.lylinux.net.conf
# 挂载额外的资源配置
- name: nginx-config - name: nginx-config
mountPath: /etc/nginx/lylinux/resource.conf mountPath: /etc/nginx/lylinux/resource.conf
subPath: lylinux.resource.conf subPath: lylinux.resource.conf
# 挂载 Django 静态文件目录(与 Django 应用共享存储)
- name: djangoblog-pvc - name: djangoblog-pvc
mountPath: /code/djangoblog/collectedstatic mountPath: /code/djangoblog/collectedstatic
# 挂载资源文件目录
- name: resource-pvc - name: resource-pvc
mountPath: /resource mountPath: /resource
volumes: volumes:
# Nginx 配置卷:通过 ConfigMap 挂载配置文件(避免在镜像中硬编码配置)
- name: nginx-config - name: nginx-config
configMap: configMap:
name: web-nginx-config name: web-nginx-config # 引用的 ConfigMap 名称
# 挂载 Django 静态文件对应的 PVC
- name: djangoblog-pvc - name: djangoblog-pvc
persistentVolumeClaim: persistentVolumeClaim:
claimName: djangoblog-pvc claimName: djangoblog-pvc
# 挂载资源文件对应的 PVC
- name: resource-pvc - name: resource-pvc
persistentVolumeClaim: persistentVolumeClaim:
claimName: resource-pvc claimName: resource-pvc
# 第五部分Elasticsearch 搜索引擎部署配置
--- ---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: elasticsearch name: elasticsearch # ES 部署名称
namespace: djangoblog namespace: djangoblog
labels: labels:
app: elasticsearch app: elasticsearch # ES 标签
spec: spec:
replicas: 1 replicas: 1 # ES 单节点(生产环境需集群)
selector: selector:
matchLabels: matchLabels:
app: elasticsearch app: elasticsearch # 匹配 ES Pod 标签
template: template:
metadata: metadata:
labels: labels:
app: elasticsearch app: elasticsearch # Pod 标签
spec: spec:
containers: containers:
- name: elasticsearch - name: elasticsearch # 容器名称
# 带 IK 分词器的 ES 镜像(适用于中文搜索)
image: liangliangyy/elasticsearch-analysis-ik:8.6.1 image: liangliangyy/elasticsearch-analysis-ik:8.6.1
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
# ES 环境变量配置
env: env:
- name: discovery.type - name: discovery.type # 单节点模式(无需集群发现)
value: single-node value: single-node
- name: ES_JAVA_OPTS - name: ES_JAVA_OPTS # JVM 内存配置(根据需求调整)
value: "-Xms256m -Xmx256m" value: "-Xms256m -Xmx256m"
- name: xpack.security.enabled - name: xpack.security.enabled # 关闭安全验证(简化部署)
value: "false" value: "false"
- name: xpack.monitoring.templates.enabled - name: xpack.monitoring.templates.enabled # 关闭监控模板
value: "false" value: "false"
ports: ports:
- containerPort: 9200 - containerPort: 9200 # ES HTTP 接口端口
# 资源限制ES 对内存需求较高)
resources: resources:
requests: requests:
cpu: 10m cpu: 10m
@ -253,22 +302,25 @@ spec:
limits: limits:
cpu: "2" cpu: "2"
memory: 2Gi memory: 2Gi
# 就绪探针:检查 ES 是否就绪
readinessProbe: readinessProbe:
httpGet: httpGet:
path: / path: / # ES 健康检查路径
port: 9200 port: 9200
initialDelaySeconds: 15 initialDelaySeconds: 15 # 延迟 15 秒ES 启动较慢)
periodSeconds: 30 periodSeconds: 30
# 存活探针:检查 ES 是否存活
livenessProbe: livenessProbe:
httpGet: httpGet:
path: / path: /
port: 9200 port: 9200
initialDelaySeconds: 15 initialDelaySeconds: 15
periodSeconds: 30 periodSeconds: 30
# 挂载 ES 数据目录(持久化存储索引数据)
volumeMounts: volumeMounts:
- name: elasticsearch-data - name: elasticsearch-data
mountPath: /usr/share/elasticsearch/data/ mountPath: /usr/share/elasticsearch/data/ # ES 数据存储路径
volumes: volumes:
- name: elasticsearch-data - name: elasticsearch-data
persistentVolumeClaim: persistentVolumeClaim:
claimName: elasticsearch-pvc claimName: elasticsearch-pvc # ES 对应的 PVC 名称

@ -1,17 +1,31 @@
# Ingress 资源配置(用于管理外部访问集群内服务的规则)
# apiVersion 指定 Kubernetes API 版本networking.k8s.io/v1 是 Ingress 的稳定版本
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
# kind 定义资源类型为 Ingress用于配置外部访问规则
kind: Ingress kind: Ingress
metadata: metadata:
# Ingress 资源的名称
name: nginx name: nginx
# 所属命名空间(与前面的部署资源保持一致,确保资源在同一命名空间内可访问)
namespace: djangoblog namespace: djangoblog
spec: spec:
# 指定 Ingress 控制器的类别(需提前部署对应类别的 Ingress Controller这里使用 nginx 类型)
ingressClassName: nginx ingressClassName: nginx
# 访问规则定义(外部请求如何路由到集群内的服务)
rules: rules:
# 未指定 host 表示匹配所有未被其他规则匹配的主机(可理解为默认规则)
- http: - http:
# HTTP 协议的路由规则
paths: paths:
# 路径规则:匹配以 / 开头的所有请求(即所有路径)
- path: / - path: /
# 路径匹配类型Prefix 表示前缀匹配(/ 会匹配所有路径)
pathType: Prefix pathType: Prefix
# 后端服务配置:请求转发到哪个服务
backend: backend:
service: service:
# 目标服务的名称(需提前创建名为 nginx 的 Service关联到 nginx 部署的 Pod
name: nginx name: nginx
# 目标服务的端口号(对应 nginx 服务暴露的 80 端口)
port: port:
number: 80 number: 80

@ -1,40 +1,44 @@
apiVersion: v1 # 第一部分数据库MySQL专用持久卷配置
kind: PersistentVolume apiVersion: v1 # PV 资源使用的 Kubernetes API 版本
kind: PersistentVolume # 资源类型为持久卷PV用于提供集群级别的存储资源
metadata: metadata:
name: local-pv-db name: local-pv-db # PV 的名称需唯一这里明确关联数据库db
spec: spec:
capacity: capacity: # 定义 PV 的存储容量
storage: 10Gi storage: 10Gi # 分配 10GiB 存储空间(数据库通常需要较大空间)
volumeMode: Filesystem volumeMode: Filesystem # 卷模式Filesystem 表示以文件系统形式挂载(另一种是 Block 块设备)
accessModes: accessModes: # 访问模式:定义 PV 可被如何访问
- ReadWriteOnce - ReadWriteOnce # 仅允许单个节点以读写方式挂载(适合数据库等需独占写入的场景)
persistentVolumeReclaimPolicy: Retain persistentVolumeReclaimPolicy: Retain # 回收策略Retain 表示 PV 被释放后保留数据,需手动清理
storageClassName: local-storage storageClassName: local-storage # 存储类名称,用于与 PersistentVolumeClaimPVC匹配
local: local: # 声明为本地存储(使用节点上的本地磁盘,非分布式存储)
path: /mnt/local-storage-db path: /mnt/local-storage-db # 本地存储的实际路径(需在对应节点上提前创建该目录)
nodeAffinity: nodeAffinity: # 节点亲和性:限制 PV 只能被特定节点使用(本地存储必须配置)
required: required: # 强制要求:必须满足以下条件才能使用该 PV
nodeSelectorTerms: nodeSelectorTerms:
- matchExpressions: - matchExpressions: # 匹配规则
- key: kubernetes.io/hostname - key: kubernetes.io/hostname # 匹配节点的主机名标签
operator: In operator: In # 操作符In 表示值在指定列表中
values: values:
- master - master # 仅允许主机名为 "master" 的节点使用该 PV
---
# 第二部分Django 应用静态文件专用持久卷配置
--- # 分隔符:用于在一个文件中定义多个资源
apiVersion: v1 apiVersion: v1
kind: PersistentVolume kind: PersistentVolume
metadata: metadata:
name: local-pv-djangoblog name: local-pv-djangoblog # PV 名称,关联 Django 应用
spec: spec:
capacity: capacity:
storage: 5Gi storage: 5Gi # 分配 5GiB 存储空间(静态文件需求较小)
volumeMode: Filesystem volumeMode: Filesystem
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce # 单节点读写(静态文件通常由单节点写入,多节点读取可考虑 ReadOnlyMany
persistentVolumeReclaimPolicy: Retain persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage storageClassName: local-storage # 与前面的 PV 共用同一存储类
local: local:
path: /mnt/local-storage-djangoblog path: /mnt/local-storage-djangoblog # Django 静态文件的本地存储路径
nodeAffinity: nodeAffinity:
required: required:
nodeSelectorTerms: nodeSelectorTerms:
@ -42,24 +46,25 @@ spec:
- key: kubernetes.io/hostname - key: kubernetes.io/hostname
operator: In operator: In
values: values:
- master - master # 同样限制在 "master" 节点
# 第三部分:资源文件专用持久卷配置
--- ---
apiVersion: v1 apiVersion: v1
kind: PersistentVolume kind: PersistentVolume
metadata: metadata:
name: local-pv-resource name: local-pv-resource # PV 名称,关联通用资源文件
spec: spec:
capacity: capacity:
storage: 5Gi storage: 5Gi # 分配 5GiB 存储空间
volumeMode: Filesystem volumeMode: Filesystem
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
persistentVolumeReclaimPolicy: Retain persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage storageClassName: local-storage
local: local:
path: /mnt/resource/ path: /mnt/resource/ # 资源文件(如上传的图片、附件等)的本地存储路径
nodeAffinity: nodeAffinity:
required: required:
nodeSelectorTerms: nodeSelectorTerms:
@ -67,23 +72,25 @@ spec:
- key: kubernetes.io/hostname - key: kubernetes.io/hostname
operator: In operator: In
values: values:
- master - master # 限制在 "master" 节点
# 第四部分Elasticsearch 搜索引擎专用持久卷配置
--- ---
apiVersion: v1 apiVersion: v1
kind: PersistentVolume kind: PersistentVolume
metadata: metadata:
name: local-pv-elasticsearch name: local-pv-elasticsearch # PV 名称,关联 Elasticsearch
spec: spec:
capacity: capacity:
storage: 5Gi storage: 5Gi # 分配 5GiB 存储空间(用于存储 ES 索引数据)
volumeMode: Filesystem volumeMode: Filesystem
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
persistentVolumeReclaimPolicy: Retain persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage storageClassName: local-storage
local: local:
path: /mnt/local-storage-elasticsearch path: /mnt/local-storage-elasticsearch # ES 数据的本地存储路径
nodeAffinity: nodeAffinity:
required: required:
nodeSelectorTerms: nodeSelectorTerms:
@ -91,4 +98,4 @@ spec:
- key: kubernetes.io/hostname - key: kubernetes.io/hostname
operator: In operator: In
values: values:
- master - master # 限制在 "master" 节点

@ -1,60 +1,66 @@
apiVersion: v1 # 第一部分数据库MySQL持久卷声明PVC
kind: PersistentVolumeClaim # PVC 用于向 Kubernetes 请求存储资源,需与 PV 匹配后才能供 Pod 使用
apiVersion: v1 # PVC 资源对应的 Kubernetes API 版本
kind: PersistentVolumeClaim # 资源类型为持久卷声明PVC
metadata: metadata:
name: db-pvc name: db-pvc # PVC 名称,需与数据库 Deployment 中引用的 PVC 名称一致
namespace: djangoblog namespace: djangoblog # 所属命名空间,与数据库 Deployment、对应 PV 保持一致(资源隔离)
spec: spec:
storageClassName: local-storage storageClassName: local-storage # 存储类名称,必须与目标 PV 的 storageClassName 完全匹配(用于筛选 PV
volumeName: local-pv-db volumeName: local-pv-db # 显式指定绑定的 PV 名称(强制绑定,非必填;不指定则按条件自动匹配)
accessModes: accessModes: # 访问模式,需与目标 PV 的 accessModes 兼容(否则无法绑定)
- ReadWriteOnce - ReadWriteOnce # 单节点读写模式,与数据库 PV 的访问模式一致(满足数据库独占写入需求)
resources: resources: # 存储资源请求,定义需要的存储容量
requests: requests:
storage: 10Gi storage: 10Gi # 请求 10GiB 存储空间,需小于或等于目标 PV 的 capacity此处与 db PV 容量完全匹配)
--- # 第二部分Django 应用静态文件持久卷声明PVC
--- # 资源分隔符,用于在单个文件中定义多个 Kubernetes 资源
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: djangoblog-pvc name: djangoblog-pvc # PVC 名称,需与 Django Deployment 中 volumeMounts 引用的 PVC 名称一致
namespace: djangoblog namespace: djangoblog
spec: spec:
volumeName: local-pv-djangoblog volumeName: local-pv-djangoblog # 显式绑定 Django 应用专用 PV
storageClassName: local-storage storageClassName: local-storage # 与 Django 应用 PV 的存储类一致
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce # 单节点读写,与 Django 应用 PV 访问模式匹配
resources: resources:
requests: requests:
storage: 5Gi storage: 5Gi # 请求 5GiB 存储空间,与 Django 应用 PV 容量一致(用于存储静态文件)
# 第三部分资源文件如上传附件、图片持久卷声明PVC
--- ---
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: resource-pvc name: resource-pvc # PVC 名称,需与 Django、Nginx Deployment 中引用的资源卷 PVC 名称一致
namespace: djangoblog namespace: djangoblog
spec: spec:
volumeName: local-pv-resource volumeName: local-pv-resource # 显式绑定资源文件专用 PV
storageClassName: local-storage storageClassName: local-storage # 与资源文件 PV 的存储类一致
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce # 单节点读写,与资源文件 PV 访问模式匹配
resources: resources:
requests: requests:
storage: 5Gi storage: 5Gi # 请求 5GiB 存储空间,与资源文件 PV 容量一致
# 第四部分Elasticsearch搜索引擎持久卷声明PVC
--- ---
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: elasticsearch-pvc name: elasticsearch-pvc # PVC 名称,需与 Elasticsearch Deployment 中引用的 PVC 名称一致
namespace: djangoblog namespace: djangoblog
spec: spec:
volumeName: local-pv-elasticsearch volumeName: local-pv-elasticsearch # 显式绑定 Elasticsearch 专用 PV
storageClassName: local-storage storageClassName: local-storage # 与 Elasticsearch PV 的存储类一致
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce # 单节点读写,与 Elasticsearch PV 访问模式匹配
resources: resources:
requests: requests:
storage: 5Gi storage: 5Gi # 请求 5GiB 存储空间,与 Elasticsearch PV 容量一致(用于存储索引数据)

@ -1,80 +1,93 @@
apiVersion: v1 # 第一部分Django 应用服务Service
kind: Service # Service 用于为集群内的 Pod 提供稳定网络访问地址,实现 Pod 访问的负载均衡和服务发现
apiVersion: v1 # Service 资源对应的 Kubernetes API 版本
kind: Service # 资源类型为 Service
metadata: metadata:
name: djangoblog name: djangoblog # Service 名称,需与其他组件(如 Nginx 配置)中引用的服务名一致
namespace: djangoblog namespace: djangoblog # 所属命名空间,与 Django Deployment、其他组件保持一致资源隔离
labels: labels:
app: djangoblog app: djangoblog # 服务标签,用于筛选和管理服务资源
spec: spec:
selector: selector: # 标签选择器:通过标签匹配要管理的 Pod必须与 Django Pod 的标签一致)
app: djangoblog app: djangoblog
ports: ports: # 端口配置:定义服务暴露的端口与 Pod 端口的映射关系
- protocol: TCP - protocol: TCP # 网络协议,默认 TCP常用还有 UDP
port: 8000 port: 8000 # 服务暴露给集群内部的端口(其他组件通过此端口访问该服务)
targetPort: 8000 targetPort: 8000 # 服务转发请求到 Pod 的目标端口(需与 Django 容器暴露的端口一致)
type: ClusterIP type: ClusterIP # 服务类型ClusterIP 表示仅在集群内部暴露服务,外部无法直接访问(适合内部组件通信)
---
# 第二部分Nginx 服务Service
--- # 资源分隔符,用于在单个文件中定义多个 Kubernetes 资源
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: nginx name: nginx # Service 名称,需与 Ingress 配置中引用的服务名一致
namespace: djangoblog namespace: djangoblog
labels: labels:
app: nginx app: nginx
spec: spec:
selector: selector:
app: nginx app: nginx # 匹配 Nginx Pod 的标签
ports: ports:
- protocol: TCP - protocol: TCP
port: 80 port: 80 # 服务暴露的端口Ingress 转发请求到该端口)
targetPort: 80 targetPort: 80 # 转发到 Nginx 容器暴露的 80 端口
type: ClusterIP type: ClusterIP # 集群内部访问(外部通过 Ingress 间接访问 Nginx 服务)
# 第三部分Redis 缓存服务Service
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: redis name: redis # Service 名称,需与 Django 应用配置中访问 Redis 的服务名一致
namespace: djangoblog namespace: djangoblog
labels: labels:
app: redis app: redis
spec: spec:
selector: selector:
app: redis app: redis # 匹配 Redis Pod 的标签
ports: ports:
- protocol: TCP - protocol: TCP
port: 6379 port: 6379 # 服务暴露的端口Redis 默认端口)
targetPort: 6379 targetPort: 6379 # 转发到 Redis 容器暴露的 6379 端口
type: ClusterIP type: ClusterIP # 仅集群内部访问(缓存服务无需外部暴露)
# 第四部分MySQL 数据库服务Service
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: db name: db # Service 名称,需与 Django 应用配置中访问数据库的服务名一致
namespace: djangoblog namespace: djangoblog
labels: labels:
app: db app: db
spec: spec:
selector: selector:
app: db app: db # 匹配 MySQL Pod 的标签
ports: ports:
- protocol: TCP - protocol: TCP
port: 3306 port: 3306 # 服务暴露的端口MySQL 默认端口)
targetPort: 3306 targetPort: 3306 # 转发到 MySQL 容器暴露的 3306 端口
type: ClusterIP type: ClusterIP # 仅集群内部访问(数据库服务禁止外部直接访问,保障安全)
# 第五部分Elasticsearch 搜索引擎服务Service
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: elasticsearch name: elasticsearch # Service 名称,需与 Django 应用配置中访问 ES 的服务名一致
namespace: djangoblog namespace: djangoblog
labels: labels:
app: elasticsearch app: elasticsearch
spec: spec:
selector: selector:
app: elasticsearch app: elasticsearch # 匹配 Elasticsearch Pod 的标签
ports: ports:
- protocol: TCP - protocol: TCP
port: 9200 port: 9200 # 服务暴露的端口ES HTTP 接口默认端口)
targetPort: 9200 targetPort: 9200 # 转发到 ES 容器暴露的 9200 端口
type: ClusterIP type: ClusterIP # 仅集群内部访问(搜索引擎无需外部直接暴露)

@ -1,10 +1,20 @@
# StorageClass 资源配置(用于定义存储资源的类型和动态供应策略)
# apiVersion 指定 Kubernetes API 版本storage.k8s.io/v1 是 StorageClass 的稳定版本
apiVersion: storage.k8s.io/v1 apiVersion: storage.k8s.io/v1
# kind 定义资源类型为 StorageClass用于统一管理存储资源的属性
kind: StorageClass kind: StorageClass
metadata: metadata:
# StorageClass 的名称,需与前面 PV 和 PVC 中指定的 storageClassName 一致
name: local-storage name: local-storage
# 注解:设置为默认存储类(当 PVC 未指定 storageClassName 时,自动使用此存储类)
annotations: annotations:
storageclass.kubernetes.io/is-default-class: "true" storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/no-provisioner spec:
volumeBindingMode: Immediate # 存储供应器:指定用于动态创建 PV 的插件(此处使用 no-provisioner 表示不支持动态供应)
# 因为前面的 PV 是手动创建的本地存储,无需动态生成,所以使用此供应器
provisioner: kubernetes.io/no-provisioner
# 卷绑定模式Immediate 表示 PVC 创建后立即尝试绑定可用的 PV不等待 Pod 调度)
# 对于本地存储,若使用 WaitForFirstConsumer 模式会更合适(等待 Pod 调度后再绑定对应节点的 PV
# 此处配置为 Immediate需确保 PV 已提前创建且满足 PVC 条件
volumeBindingMode: Immediate

@ -1,50 +1,82 @@
user nginx; # Nginx 核心配置文件,用于处理静态资源和反向代理请求到 Django 应用
# 全局配置段:设置 Nginx 整体运行参数
nginx; # 标识该文件为 Nginx 配置文件(固定起始标识)
# 工作进程数auto 表示自动根据服务器 CPU 核心数分配(优化并发性能)
worker_processes auto; worker_processes auto;
# 错误日志配置指定日志路径和日志级别notice 级别记录重要信息,不冗余)
error_log /var/log/nginx/error.log notice; error_log /var/log/nginx/error.log notice;
# PID 文件路径:存储 Nginx 主进程 ID用于管理 Nginx 进程(如重启、停止)
pid /var/run/nginx.pid; pid /var/run/nginx.pid;
# 事件模块配置:控制 Nginx 网络连接相关参数
events { events {
# 单个工作进程允许的最大并发连接数1024 为基础值,可根据服务器性能调整)
worker_connections 1024; worker_connections 1024;
} }
# HTTP 模块配置:处理 HTTP 请求的核心配置,包含全局规则和虚拟主机
http { http {
# 引入 MIME 类型映射文件:定义不同文件后缀对应的 Content-Type .html 对应 text/html
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
# 默认 MIME 类型:当文件类型未匹配时,默认使用二进制流类型(避免浏览器直接解析未知文件)
default_type application/octet-stream; default_type application/octet-stream;
# 日志格式定义:自定义访问日志的记录字段,命名为 main
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'; '"$http_user_agent" "$http_x_forwarded_for"';
# 访问日志配置:使用上面定义的 main 格式,指定日志存储路径
access_log /var/log/nginx/access.log main; access_log /var/log/nginx/access.log main;
# 开启高效文件传输模式:使用内核零拷贝技术,提升静态文件传输效率
sendfile on; sendfile on;
#tcp_nopush on; # tcp_nopush on; # 可选配置:开启后会累积数据包再发送,适合大文件传输,默认关闭
# 长连接超时时间:客户端与 Nginx 保持连接的最长时间65 秒,超时后自动断开)
keepalive_timeout 65; keepalive_timeout 65;
#gzip on; # gzip on; # 可选配置:开启 Gzip 压缩,减少传输带宽,默认关闭
# 虚拟主机配置:定义一个具体的站点规则(处理 80 端口的 HTTP 请求)
server { server {
# 站点根目录:默认请求的文件查找路径(此处指向 Django 静态文件目录)
root /code/djangoblog/collectedstatic/; root /code/djangoblog/collectedstatic/;
# 监听端口:该虚拟主机处理 80 端口的请求Nginx 默认 HTTP 端口)
listen 80; listen 80;
# 长连接超时时间:覆盖 HTTP 模块的全局配置,仅作用于当前虚拟主机
keepalive_timeout 70; keepalive_timeout 70;
# 静态资源路径规则:匹配以 /static/ 开头的请求(处理 Django 静态文件)
location /static/ { location /static/ {
# 缓存控制:设置静态文件缓存时间为最大(浏览器会长期缓存,减少重复请求)
expires max; expires max;
# 路径别名:将 /static/ 请求映射到实际的静态文件目录(与 root 配合确保路径正确)
alias /code/djangoblog/collectedstatic/; alias /code/djangoblog/collectedstatic/;
} }
# 默认路径规则:匹配所有未被上面规则匹配的请求(转发到 Django 应用)
location / { location / {
# 转发请求头:传递客户端真实 IP Django否则 Django 会认为请求来自 Nginx
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
# 转发请求头:传递客户端 IP 列表(适用于多层代理场景)
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 转发请求头:传递原始请求的 Host 头(确保 Django 正确识别请求域名)
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
# 转发标识:告诉 Django 请求来自 Nginx 代理
proxy_set_header X-NginX-Proxy true; proxy_set_header X-NginX-Proxy true;
# 关闭重定向处理:禁止 Nginx 自动修改 Django 返回的重定向地址
proxy_redirect off; proxy_redirect off;
# 条件判断:如果请求的文件在 Nginx 本地不存在(非静态文件)
if (!-f $request_filename) { if (!-f $request_filename) {
# 反向代理:将请求转发到 Django 服务(通过 Kubernetes Service 名称 djangoblog 8000 端口)
proxy_pass http://djangoblog:8000; proxy_pass http://djangoblog:8000;
break; break; # 跳出条件判断,不再执行后续规则
} }
} }
} }
} }

@ -1 +1,3 @@
# Django 应用的默认配置指定
# 作用:告诉 Django 当该应用被加载时,应使用哪个配置类进行初始化
default_app_config = 'djangoblog.apps.DjangoblogAppConfig' default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -1,64 +1,90 @@
# 导入 Django 内置的 AdminSite 基础类(后台管理站点核心类)
from django.contrib.admin import AdminSite from django.contrib.admin import AdminSite
# 导入日志记录模型(用于记录后台操作日志)
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
# 导入站点管理相关的默认 admin 类和模型Django 内置的站点管理功能)
from django.contrib.sites.admin import SiteAdmin from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from accounts.admin import * # 导入各应用自定义的 admin 配置和模型(将各模块的后台管理逻辑聚合到此处)
from blog.admin import * from accounts.admin import * # 用户账户相关的 admin 配置
from blog.models import * from blog.admin import * # 博客核心(文章、分类等)的 admin 配置
from comments.admin import * from blog.models import * # 博客核心模型
from comments.models import * from comments.admin import * # 评论相关的 admin 配置
from comments.models import *# 评论模型
# 导入自定义的日志条目 admin 配置(扩展日志展示功能)
from djangoblog.logentryadmin import LogEntryAdmin from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import * from oauth.admin import * # 第三方登录OAuth相关的 admin 配置
from oauth.models import * from oauth.models import * # OAuth 相关模型
from owntracks.admin import * from owntracks.admin import *# 位置追踪OwnTracks相关的 admin 配置
from owntracks.models import * from owntracks.models import *# 位置追踪模型
from servermanager.admin import * from servermanager.admin import *# 服务器管理相关的 admin 配置
from servermanager.models import * from servermanager.models import *# 服务器管理模型
# 自定义后台管理站点类(继承自 Django 内置的 AdminSite
class DjangoBlogAdminSite(AdminSite): class DjangoBlogAdminSite(AdminSite):
# 后台站点头部显示的标题(登录后顶部导航栏的文字)
site_header = 'djangoblog administration' site_header = 'djangoblog administration'
# 浏览器标签页显示的标题(页面标题)
site_title = 'djangoblog site admin' site_title = 'djangoblog site admin'
# 初始化方法(调用父类构造方法,确保基础功能正常)
def __init__(self, name='admin'): def __init__(self, name='admin'):
super().__init__(name) super().__init__(name)
# 权限控制方法:判断用户是否有权限访问后台
def has_permission(self, request): def has_permission(self, request):
# 仅允许超级用户is_superuser=True访问后台
return request.user.is_superuser return request.user.is_superuser
# 以下为注释掉的自定义 URL 示例(可扩展后台功能)
# def get_urls(self): # def get_urls(self):
# # 先获取父类默认的 URL 配置
# urls = super().get_urls() # urls = super().get_urls()
# from django.urls import path # from django.urls import path
# # 导入自定义视图(例如刷新缓存的视图)
# from blog.views import refresh_memcache # from blog.views import refresh_memcache
# #
# # 定义自定义 URL 规则(如添加一个 /admin/refresh/ 路径)
# my_urls = [ # my_urls = [
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"), # path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ] # ]
# # 合并默认 URL 和自定义 URL自定义 URL 优先级更高)
# return urls + my_urls # return urls + my_urls
# 实例化自定义的后台管理站点(名称为 'admin',与默认后台路径保持一致)
admin_site = DjangoBlogAdminSite(name='admin') admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticlelAdmin) # 注册模型与对应的 admin 配置到自定义后台站点(实现各模型在后台的管理界面)
admin_site.register(Category, CategoryAdmin) # 博客核心模型注册
admin_site.register(Tag, TagAdmin) admin_site.register(Article, ArticlelAdmin) # 文章模型 + 其admin配置
admin_site.register(Links, LinksAdmin) admin_site.register(Category, CategoryAdmin) # 分类模型 + 其admin配置
admin_site.register(SideBar, SideBarAdmin) admin_site.register(Tag, TagAdmin) # 标签模型 + 其admin配置
admin_site.register(BlogSettings, BlogSettingsAdmin) 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) # OAuth 相关模型注册
admin_site.register(OAuthConfig, OAuthConfigAdmin) 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(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin) # 后台操作日志模型注册(使用自定义的 LogEntryAdmin 配置,增强日志展示)
admin_site.register(LogEntry, LogEntryAdmin)

@ -1,11 +1,20 @@
# 导入 Django 的应用配置基类(所有应用配置类需继承此类)
from django.apps import AppConfig from django.apps import AppConfig
# 自定义应用配置类(用于 djangoblog 应用的初始化设置)
class DjangoblogAppConfig(AppConfig): class DjangoblogAppConfig(AppConfig):
# 定义模型主键的默认类型:使用 BigAutoField自增 BigInteger 类型)
# 替代旧版的 AutoField自增 Integer支持更大范围的主键值
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
# 应用的名称(必须与项目中 INSTALLED_APPS 配置的名称一致)
name = 'djangoblog' name = 'djangoblog'
# 应用就绪方法:当 Django 加载完所有应用后自动调用(用于初始化操作)
def ready(self): def ready(self):
# 调用父类的 ready 方法,确保基础初始化逻辑执行
super().ready() super().ready()
# Import and load plugins here # 导入并加载插件(应用启动时加载所有注册的插件)
# 注意:避免在模块顶部导入,防止 Django 初始化时循环导入问题
from .plugin_manage.loader import load_plugins from .plugin_manage.loader import load_plugins
load_plugins() # 执行插件加载函数(例如注册钩子、初始化插件功能等)
load_plugins()

@ -1,7 +1,11 @@
# 导入线程模块:用于异步执行耗时操作(如发送邮件,避免阻塞主流程)
import _thread import _thread
# 导入日志模块:记录操作日志和错误信息
import logging import logging
# 导入 Django 信号核心类:用于定义和处理自定义信号
import django.dispatch import django.dispatch
# 导入 Django 配置、模型、工具类:支撑信号处理中的业务逻辑
from django.conf import settings from django.conf import settings
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out 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.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
# 导入项目自定义模型和工具函数:适配博客业务场景
from comments.models import Comment from comments.models import Comment
from comments.utils import send_comment_email from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify 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 djangoblog.utils import get_current_site
from oauth.models import OAuthUser from oauth.models import OAuthUser
# 初始化日志对象:用于记录当前模块的日志(如邮件发送失败、爬虫通知错误)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 定义自定义信号第三方登录OAuth成功后触发的信号携带用户ID参数
oauth_user_login_signal = django.dispatch.Signal(['id']) oauth_user_login_signal = django.dispatch.Signal(['id'])
# 定义自定义信号:发送邮件的信号,携带收件人、标题、内容参数
send_email_signal = django.dispatch.Signal( send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content']) ['emailto', 'title', 'content'])
# 信号接收器:监听 send_email_signal 信号,触发邮件发送逻辑
@receiver(send_email_signal) @receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs): def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto'] # 从信号参数中提取邮件相关信息
title = kwargs['title'] emailto = kwargs['emailto'] # 收件人列表
content = kwargs['content'] title = kwargs['title'] # 邮件标题
content = kwargs['content'] # 邮件内容HTML格式
# 构建 HTML 格式邮件:支持富文本内容
msg = EmailMultiAlternatives( msg = EmailMultiAlternatives(
title, title,
content, content,
from_email=settings.DEFAULT_FROM_EMAIL, from_email=settings.DEFAULT_FROM_EMAIL, # 发件人(从项目配置中获取)
to=emailto) to=emailto)
msg.content_subtype = "html" msg.content_subtype = "html" # 声明邮件内容为 HTML 类型
# 记录邮件发送日志到数据库
from servermanager.models import EmailSendLog from servermanager.models import EmailSendLog
log = EmailSendLog() log = EmailSendLog()
log.title = title log.title = title
log.content = content log.content = content
log.emailto = ','.join(emailto) log.emailto = ','.join(emailto) # 收件人列表转字符串存储
try: try:
# 发送邮件:返回成功发送的邮件数量
result = msg.send() result = msg.send()
log.send_result = result > 0 log.send_result = result > 0 # 发送成功标记(数量>0即为成功
except Exception as e: except Exception as e:
# 捕获发送异常,记录错误日志
logger.error(f"失败邮箱号: {emailto}, {e}") logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False log.send_result = False # 标记发送失败
log.save() finally:
# 保存日志记录到数据库
log.save()
# 信号接收器:监听 oauth_user_login_signal 信号,处理 OAuth 登录后的逻辑
@receiver(oauth_user_login_signal) @receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs): def oauth_user_login_signal_handler(sender, **kwargs):
# 从信号参数中提取 OAuth 用户ID
id = kwargs['id'] id = kwargs['id']
# 获取对应的 OAuth 用户对象
oauthuser = OAuthUser.objects.get(id=id) oauthuser = OAuthUser.objects.get(id=id)
# 获取当前站点域名(用于判断头像是否为本站地址)
site = get_current_site().domain site = get_current_site().domain
# 若用户头像不是本站地址(如第三方平台的远程图片),则下载并保存到本地
if oauthuser.picture and not oauthuser.picture.find(site) >= 0: if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture) oauthuser.picture = save_user_avatar(oauthuser.picture) # 下载并更新头像路径
oauthuser.save() oauthuser.save() # 保存更新后的用户信息
# 删除侧边栏缓存:用户登录状态变化可能影响侧边栏内容(如显示登录用户信息)
delete_sidebar_cache() delete_sidebar_cache()
# 信号接收器:监听所有模型的 post_save 信号(模型保存后触发)
@receiver(post_save) @receiver(post_save)
def model_post_save_callback( def model_post_save_callback(
sender, sender,
@ -73,50 +97,74 @@ def model_post_save_callback(
using, using,
update_fields, update_fields,
**kwargs): **kwargs):
# 标记是否需要清理缓存
clearcache = False clearcache = False
# 跳过 Admin 操作日志LogEntry的处理避免日志保存时触发不必要的逻辑
if isinstance(instance, LogEntry): if isinstance(instance, LogEntry):
return return
# 处理有 "get_full_url" 方法的模型(如 Article 文章模型)
if 'get_full_url' in dir(instance): if 'get_full_url' in dir(instance):
# 判断是否仅更新了 "views" 字段(文章阅读量)
is_update_views = update_fields == {'views'} is_update_views = update_fields == {'views'}
# 非测试环境且非阅读量更新:通知搜索引擎(如百度)收录新页面
if not settings.TESTING and not is_update_views: if not settings.TESTING and not is_update_views:
try: try:
notify_url = instance.get_full_url() notify_url = instance.get_full_url() # 获取模型的完整访问链接
SpiderNotify.baidu_notify([notify_url]) SpiderNotify.baidu_notify([notify_url]) # 调用百度爬虫通知接口
except Exception as ex: except Exception as ex:
# 捕获通知异常,记录错误日志
logger.error("notify sipder", ex) logger.error("notify sipder", ex)
# 非阅读量更新:标记需要清理缓存(如文章内容、标题修改)
if not is_update_views: if not is_update_views:
clearcache = True clearcache = True
# 处理 Comment 评论模型的保存逻辑
if isinstance(instance, Comment): if isinstance(instance, Comment):
# 仅处理已启用的评论is_enable=True
if instance.is_enable: if instance.is_enable:
# 获取评论所属文章的访问路径
path = instance.article.get_absolute_url() path = instance.article.get_absolute_url()
# 获取当前站点域名(处理端口号,仅保留域名部分)
site = get_current_site().domain site = get_current_site().domain
if site.find(':') > 0: if site.find(':') > 0:
site = site[0:site.find(':')] site = site[0:site.find(':')]
# 清理文章详情页的视图缓存:避免显示旧评论
expire_view_cache( expire_view_cache(
path, path,
servername=site, servername=site,
serverport=80, serverport=80,
key_prefix='blogdetail') key_prefix='blogdetail')
# 清理 SEO 处理器缓存:评论变化可能影响页面 SEO 信息
if cache.get('seo_processor'): if cache.get('seo_processor'):
cache.delete('seo_processor') cache.delete('seo_processor')
# 清理该文章的评论列表缓存
comment_cache_key = 'article_comments_{id}'.format( comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id) id=instance.article.id)
cache.delete(comment_cache_key) cache.delete(comment_cache_key)
# 清理侧边栏缓存:侧边栏可能显示最新评论
delete_sidebar_cache() delete_sidebar_cache()
# 清理评论分页视图的缓存
delete_view_cache('article_comments', [str(instance.article.pk)]) delete_view_cache('article_comments', [str(instance.article.pk)])
# 异步发送评论通知邮件:用线程避免阻塞评论保存流程
_thread.start_new_thread(send_comment_email, (instance,)) _thread.start_new_thread(send_comment_email, (instance,))
# 若标记需要清理缓存,则清空全局缓存(确保最新数据生效)
if clearcache: if clearcache:
cache.clear() cache.clear()
# 信号接收器同时监听用户登录user_logged_in和登出user_logged_out信号
@receiver(user_logged_in) @receiver(user_logged_in)
@receiver(user_logged_out) @receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs): def user_auth_callback(sender, request, user, **kwargs):
# 若用户存在且用户名有效(排除异常情况)
if user and user.username: if user and user.username:
# 记录用户登录/登出日志
logger.info(user) logger.info(user)
# 清理侧边栏缓存:登录状态变化可能影响侧边栏(如显示/隐藏用户菜单)
delete_sidebar_cache() delete_sidebar_cache()
# cache.clear() # cache.clear() # 注释:若需全局清缓存可启用,当前仅清理侧边栏缓存

@ -1,150 +1,184 @@
# 导入 Django 字符串处理工具:确保字符串编码兼容
from django.utils.encoding import force_str from django.utils.encoding import force_str
# 导入 Elasticsearch DSL 工具:构建 Elasticsearch 查询语句
from elasticsearch_dsl import Q from elasticsearch_dsl import Q
# 导入 Haystack 核心类:实现自定义搜索后端、查询和引擎
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm from haystack.forms import ModelSearchForm # Haystack 基础搜索表单
from haystack.models import SearchResult from haystack.models import SearchResult # Haystack 搜索结果封装类
from haystack.utils import log as logging from haystack.utils import log as logging # Haystack 日志工具
# 导入项目自定义的 Elasticsearch 文档和管理器:关联博客文章模型
from blog.documents import ArticleDocument, ArticleDocumentManager from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article from blog.models import Article # 博客核心文章模型
# 初始化日志对象:记录搜索相关日志(如查询语句、错误信息)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 自定义 Elasticsearch 搜索后端:实现 Haystack 与 Elasticsearch 的底层交互
class ElasticSearchBackend(BaseSearchBackend): class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options): def __init__(self, connection_alias, **connection_options):
# 调用父类构造方法,初始化 Haystack 基础搜索后端
super( super(
ElasticSearchBackend, ElasticSearchBackend,
self).__init__( self).__init__(
connection_alias, connection_alias,
**connection_options) **connection_options)
# 初始化文章文档管理器:负责 Elasticsearch 索引的创建、更新、删除
self.manager = ArticleDocumentManager() self.manager = ArticleDocumentManager()
# 启用拼写建议功能:用于返回搜索关键词的推荐词
self.include_spelling = True self.include_spelling = True
# 辅助方法:将模型实例转换为 Elasticsearch 文档Document
def _get_models(self, iterable): def _get_models(self, iterable):
# 若传入空列表,默认获取所有文章;否则使用传入的模型实例
models = iterable if iterable and iterable[0] else Article.objects.all() models = iterable if iterable and iterable[0] else Article.objects.all()
# 通过文档管理器将模型转换为 Elasticsearch 可识别的文档
docs = self.manager.convert_to_doc(models) docs = self.manager.convert_to_doc(models)
return docs return docs
# 初始化索引:创建 Elasticsearch 索引并批量添加文档
def _create(self, models): def _create(self, models):
self.manager.create_index() self.manager.create_index() # 创建 Elasticsearch 索引结构
docs = self._get_models(models) docs = self._get_models(models) # 转换模型为文档
self.manager.rebuild(docs) self.manager.rebuild(docs) # 批量写入文档到索引
# 删除索引中的文档:根据模型实例删除对应 Elasticsearch 记录
def _delete(self, models): def _delete(self, models):
for m in models: for m in models:
m.delete() m.delete() # 调用文档的 delete 方法,删除 Elasticsearch 中的对应记录
return True return True
# 重建索引:全量更新 Elasticsearch 中的文档(覆盖旧数据)
def _rebuild(self, models): def _rebuild(self, models):
# 若未指定模型,默认获取所有文章
models = models if models else Article.objects.all() models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models) docs = self._get_models(models) # 转换模型为文档
self.manager.update_docs(docs) self.manager.update_docs(docs) # 批量更新文档到索引
# Haystack 标准方法:增量更新索引(更新指定模型对应的文档)
def update(self, index, iterable, commit=True): def update(self, index, iterable, commit=True):
models = self._get_models(iterable) # 转换模型为文档
self.manager.update_docs(models) # 增量更新文档
models = self._get_models(iterable) # Haystack 标准方法:移除单个模型对应的索引记录
self.manager.update_docs(models)
def remove(self, obj_or_string): def remove(self, obj_or_string):
models = self._get_models([obj_or_string]) models = self._get_models([obj_or_string]) # 转换为文档
self._delete(models) self._delete(models) # 删除文档
# Haystack 标准方法:清空索引(删除所有相关记录)
def clear(self, models=None, commit=True): def clear(self, models=None, commit=True):
self.remove(None) self.remove(None) # 调用 remove 方法清空索引
@staticmethod @staticmethod
def get_suggestion(query: str) -> str: def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词""" """
生成搜索关键词的推荐词基于 Elasticsearch 拼写建议功能
若未找到推荐词返回原查询词
"""
# 构建 Elasticsearch 查询:匹配文章内容,并启用拼写建议
search = ArticleDocument.search() \ search = ArticleDocument.search() \
.query("match", body=query) \ .query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \ .suggest('suggest_search', query, term={'field': 'body'}) \
.execute() .execute() # 执行查询
keywords = [] keywords = []
# 提取 Elasticsearch 返回的建议词
for suggest in search.suggest.suggest_search: for suggest in search.suggest.suggest_search:
if suggest["options"]: if suggest["options"]: # 若有推荐词,取第一个
keywords.append(suggest["options"][0]["text"]) keywords.append(suggest["options"][0]["text"])
else: else: # 若无推荐词,保留原查询词
keywords.append(suggest["text"]) keywords.append(suggest["text"])
return ' '.join(keywords) return ' '.join(keywords) # 拼接推荐词为字符串返回
# Haystack 核心搜索方法:执行搜索并返回结果(带日志记录装饰器)
@log_query @log_query
def search(self, query_string, **kwargs): 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') # 若为 NoneElasticsearch 会返回默认数量结果
# 推荐词搜索 # 生成推荐词:根据 is_suggest 标识判断是否需要拼写建议
if getattr(self, "is_suggest", None): if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string) suggestion = self.get_suggestion(query_string)
else: else:
suggestion = query_string suggestion = query_string # 不需要建议则使用原查询词
# 构建 Elasticsearch 查询条件(布尔查询)
# 1. 匹配条件:标题或内容包含推荐词,匹配度最低 70%
q = Q('bool', q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)], should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%") minimum_should_match="70%")
# 构建完整搜索请求:
# - 过滤条件:使用上面的 q 匹配结果且文章状态为“已发布”status='p'、类型为“文章”type='a'
# - 不返回文档源数据source=False仅获取 ID 和得分,减少数据传输
# - 分页:按 start_offset 和 end_offset 截取结果
search = ArticleDocument.search() \ search = ArticleDocument.search() \
.query('bool', filter=[q]) \ .query('bool', filter=[q]) \
.filter('term', status='p') \ .filter('term', status='p') \
.filter('term', type='a') \ .filter('term', type='a') \
.source(False)[start_offset: end_offset] .source(False)[start_offset: end_offset]
# 执行搜索,获取 Elasticsearch 返回结果
results = search.execute() results = search.execute()
hits = results['hits'].total hits = results['hits'].total # 匹配到的总结果数
raw_results = [] raw_results = [] # 存储 Haystack 标准格式的搜索结果
# 解析 Elasticsearch 原始结果,封装为 Haystack 的 SearchResult 格式
for raw_result in results['hits']['hits']: for raw_result in results['hits']['hits']:
app_label = 'blog' app_label = 'blog' # 模型所属应用
model_name = 'Article' model_name = 'Article' # 模型名称
additional_fields = {} additional_fields = {} # 额外字段(此处无额外信息,留空)
# 实例化 SearchResult封装应用名、模型名、文档ID、匹配得分等信息
result_class = SearchResult result_class = SearchResult
result = result_class( result = result_class(
app_label, app_label,
model_name, model_name,
raw_result['_id'], raw_result['_id'], # Elasticsearch 中文档的 ID
raw_result['_score'], raw_result['_score'], # 匹配得分(用于排序)
**additional_fields) **additional_fields)
raw_results.append(result) raw_results.append(result)
# 搜索结果元数据:分面(无分面需求,留空)、拼写建议
facets = {} facets = {}
# 若推荐词与原查询词不同,返回推荐词;否则为 None
spelling_suggestion = None if query_string == suggestion else suggestion spelling_suggestion = None if query_string == suggestion else suggestion
# 返回 Haystack 标准格式的搜索结果
return { return {
'results': raw_results, 'results': raw_results, # 封装后的搜索结果列表
'hits': hits, 'hits': hits, # 总匹配数
'facets': facets, 'facets': facets, # 分面数据(空)
'spelling_suggestion': spelling_suggestion, 'spelling_suggestion': spelling_suggestion, # 拼写建议
} }
# 自定义 Elasticsearch 查询类:处理查询参数解析、格式清洗等
class ElasticSearchQuery(BaseSearchQuery): class ElasticSearchQuery(BaseSearchQuery):
# 转换日期格式:适配 Elasticsearch 的日期查询需求
def _convert_datetime(self, date): 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')) return force_str(date.strftime('%Y%m%d%H%M%S'))
else: else: # 若为date仅年月日补全时分秒为000000
return force_str(date.strftime('%Y%m%d000000')) return force_str(date.strftime('%Y%m%d000000'))
# 清洗查询词:处理 Haystack 保留词和特殊字符,避免查询语法错误
def clean(self, query_fragment): def clean(self, query_fragment):
""" words = query_fragment.split() # 拆分查询词为单词列表
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()
cleaned_words = [] cleaned_words = []
for word in words: for word in words:
# 处理 Haystack 保留词(如 AND、OR转为小写避免语法冲突
if word in self.backend.RESERVED_WORDS: if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower()) word = word.replace(word, word.lower())
# 处理特殊字符(如 +、-、*):包含特殊字符的单词用引号包裹
for char in self.backend.RESERVED_CHARACTERS: for char in self.backend.RESERVED_CHARACTERS:
if char in word: if char in word:
word = "'%s'" % word word = "'%s'" % word
@ -152,32 +186,39 @@ class ElasticSearchQuery(BaseSearchQuery):
cleaned_words.append(word) cleaned_words.append(word)
return ' '.join(cleaned_words) return ' '.join(cleaned_words) # 拼接清洗后的查询词
# 构建查询片段:适配自定义查询逻辑(此处直接返回查询字符串)
def build_query_fragment(self, field, filter_type, value): def build_query_fragment(self, field, filter_type, value):
return value.query_string return value.query_string
# 获取搜索结果总数:通过 get_results 结果长度计算
def get_count(self): def get_count(self):
results = self.get_results() results = self.get_results()
return len(results) if results else 0 return len(results) if results else 0
# 获取拼写建议:返回后端生成的推荐词
def get_spelling_suggestion(self, preferred_query=None): def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion return self._spelling_suggestion
# 构建搜索参数:继承父类逻辑,可自定义扩展参数
def build_params(self, spelling_query=None): def build_params(self, spelling_query=None):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query) kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs return kwargs
# 自定义搜索表单:扩展 Haystack 基础表单,支持“是否启用拼写建议”的控制
class ElasticSearchModelSearchForm(ModelSearchForm): class ElasticSearchModelSearchForm(ModelSearchForm):
def search(self): def search(self):
# 是否建议搜索 # 根据请求参数is_suggest设置后端是否启用拼写建议
# 若 is_suggest = "no",则不启用;否则启用
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no" self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
# 调用父类 search 方法,执行搜索并返回结果
sqs = super().search() sqs = super().search()
return sqs return sqs
# 自定义 Elasticsearch 搜索引擎:关联后端和查询类,供 Haystack 调用
class ElasticSearchEngine(BaseEngine): class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend backend = ElasticSearchBackend # 绑定自定义搜索后端
query = ElasticSearchQuery query = ElasticSearchQuery # 绑定自定义查询类

@ -1,40 +1,61 @@
# 导入 Django 用户模型工具:获取当前项目的用户模型(支持自定义用户模型)
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
# 导入 Django 内置的 Feed 基类:用于快速实现 RSS/Atom 订阅功能
from django.contrib.syndication.views import Feed from django.contrib.syndication.views import Feed
# 导入 Django 时间工具:处理时区和当前时间
from django.utils import timezone from django.utils import timezone
# 导入 RSS 2.0 格式生成器:指定 Feed 输出格式为 RSS 2.0 标准
from django.utils.feedgenerator import Rss201rev2Feed from django.utils.feedgenerator import Rss201rev2Feed
# 导入博客核心模型和工具:关联文章数据及 Markdown 解析
from blog.models import Article from blog.models import Article
from djangoblog.utils import CommonMarkdown from djangoblog.utils import CommonMarkdown # 自定义 Markdown 解析工具(将文章内容转为 HTML
# 自定义 RSS Feed 类:继承 Django 内置 Feed 类,实现博客文章的订阅功能
class DjangoBlogFeed(Feed): class DjangoBlogFeed(Feed):
# 指定 Feed 生成器类型:使用 RSS 2.0 标准格式(最常用的 RSS 版本)
feed_type = Rss201rev2Feed feed_type = Rss201rev2Feed
# Feed 描述信息:显示在订阅源的说明中
description = '大巧无工,重剑无锋.' description = '大巧无工,重剑无锋.'
# Feed 标题:订阅源的名称(通常为博客名称)
title = "且听风吟 大巧无工,重剑无锋. " title = "且听风吟 大巧无工,重剑无锋. "
# Feed 的链接:订阅源自身的 URL通常指向博客首页或 Feed 专属页面)
link = "/feed/" link = "/feed/"
# 订阅源作者名称:从系统第一个用户的昵称获取(适合个人博客)
def author_name(self): def author_name(self):
return get_user_model().objects.first().nickname return get_user_model().objects.first().nickname
# 订阅源作者链接:指向作者的个人页面(通过用户模型的 get_absolute_url 方法获取)
def author_link(self): def author_link(self):
return get_user_model().objects.first().get_absolute_url() return get_user_model().objects.first().get_absolute_url()
# 订阅源包含的项目(文章):定义要展示在 Feed 中的内容
def items(self): def items(self):
# 筛选条件类型为文章type='a'、状态为已发布status='p'
# 排序规则:按发布时间倒序(最新发布的文章在前)
# 数量限制:只显示最新的 5 篇文章
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5] return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
# 单个项目(文章)的标题:使用文章自身的标题
def item_title(self, item): def item_title(self, item):
return item.title return item.title
# 单个项目(文章)的描述:将 Markdown 格式的文章内容转为 HTML 后展示
def item_description(self, item): def item_description(self, item):
return CommonMarkdown.get_markdown(item.body) return CommonMarkdown.get_markdown(item.body) # 调用工具类解析 Markdown
# 订阅源的版权信息:动态生成包含当前年份的版权声明
def feed_copyright(self): def feed_copyright(self):
now = timezone.now() now = timezone.now() # 获取当前时间(带时区)
return "Copyright© {year} 且听风吟".format(year=now.year) return "Copyright© {year} 且听风吟".format(year=now.year) # 格式化版权信息
# 单个项目(文章)的链接:指向文章详情页(通过文章模型的 get_absolute_url 方法获取)
def item_link(self, item): def item_link(self, item):
return item.get_absolute_url() return item.get_absolute_url()
# 单个项目文章的唯一标识GUID此处留空Django 会默认使用 item_link 作为 GUID
def item_guid(self, item): def item_guid(self, item):
return return

@ -1,27 +1,37 @@
# 导入 Django Admin 核心模块:用于自定义后台管理界面
from django.contrib import admin from django.contrib import admin
# 导入 Admin 日志相关常量和模型:处理日志操作类型(如删除)
from django.contrib.admin.models import DELETION from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
# 导入 Django URL 和字符串处理工具:生成反向链接、处理编码和转义
from django.urls import reverse, NoReverseMatch from django.urls import reverse, NoReverseMatch
from django.utils.encoding import force_str from django.utils.encoding import force_str
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
# 导入国际化工具:支持后台文字的多语言翻译
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# 自定义 LogEntry Admin 类:用于在 Django 后台管理 Admin 操作日志(记录用户对模型的增删改操作)
class LogEntryAdmin(admin.ModelAdmin): class LogEntryAdmin(admin.ModelAdmin):
# 列表页筛选器:按“内容类型”(即操作的模型,如 Article、Comment筛选日志
list_filter = [ list_filter = [
'content_type' 'content_type'
] ]
# 列表页搜索框:支持按“对象名称”(如文章标题)和“操作描述”(如“修改了标题”)搜索
search_fields = [ search_fields = [
'object_repr', 'object_repr',
'change_message' 'change_message'
] ]
# 列表页可点击的链接:点击“操作时间”或“操作描述”可进入日志详情页
list_display_links = [ list_display_links = [
'action_time', 'action_time',
'get_change_message', 'get_change_message',
] ]
# 列表页展示的字段:操作时间、操作用户(带链接)、操作模型、操作对象(带链接)、操作描述
list_display = [ list_display = [
'action_time', 'action_time',
'user_link', 'user_link',
@ -30,62 +40,81 @@ class LogEntryAdmin(admin.ModelAdmin):
'get_change_message', 'get_change_message',
] ]
# 权限控制:禁止添加日志(日志由系统自动生成,不允许手动添加)
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False
# 权限控制:仅允许超级用户或拥有“修改日志”权限的用户查看/修改日志,且禁止 POST 请求(避免提交修改)
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
return ( return (
request.user.is_superuser or request.user.is_superuser or
request.user.has_perm('admin.change_logentry') request.user.has_perm('admin.change_logentry')
) and request.method != 'POST' ) and request.method != 'POST'
# 权限控制:禁止删除日志(日志需留存,不允许手动删除)
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
return False return False
# 自定义列表字段:操作对象(生成带链接的对象名称,点击可跳转到对象的编辑页)
def object_link(self, obj): def object_link(self, obj):
# 转义对象名称(避免 XSS 攻击)
object_link = escape(obj.object_repr) object_link = escape(obj.object_repr)
# 获取操作对象的内容类型(即所属模型)
content_type = obj.content_type content_type = obj.content_type
# 若操作不是“删除”DELETION且内容类型存在排除异常情况
if obj.action_flag != DELETION and content_type is not None: if obj.action_flag != DELETION and content_type is not None:
# try returning an actual link instead of object repr string
try: try:
# 生成对象编辑页的 URL格式admin/应用名/模型名/change/对象ID/
url = reverse( url = reverse(
'admin:{}_{}_change'.format(content_type.app_label, 'admin:{}_{}_change'.format(content_type.app_label,
content_type.model), content_type.model),
args=[obj.object_id] args=[obj.object_id]
) )
# 将对象名称转为链接(点击跳转到编辑页)
object_link = '<a href="{}">{}</a>'.format(url, object_link) object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch: except NoReverseMatch:
# 若无法生成链接(如模型未注册到 Admin则保留纯文本名称
pass pass
# 标记为安全 HTML告诉 Django 无需转义,避免链接被当作文本显示)
return mark_safe(object_link) 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): def user_link(self, obj):
# 获取用户模型的内容类型
content_type = ContentType.objects.get_for_model(type(obj.user)) content_type = ContentType.objects.get_for_model(type(obj.user))
# 转义用户名(避免 XSS 攻击)
user_link = escape(force_str(obj.user)) user_link = escape(force_str(obj.user))
try: try:
# try returning an actual link instead of object repr string # 生成用户编辑页的 URL
url = reverse( url = reverse(
'admin:{}_{}_change'.format(content_type.app_label, 'admin:{}_{}_change'.format(content_type.app_label,
content_type.model), content_type.model),
args=[obj.user.pk] args=[obj.user.pk]
) )
# 将用户名转为链接(点击跳转到用户编辑页)
user_link = '<a href="{}">{}</a>'.format(url, user_link) user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch: except NoReverseMatch:
# 若无法生成链接(如用户模型未注册到 Admin则保留纯文本用户名
pass pass
return mark_safe(user_link) 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): def get_queryset(self, request):
queryset = super(LogEntryAdmin, self).get_queryset(request) queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type') return queryset.prefetch_related('content_type')
# 自定义批量操作:移除“批量删除”按钮(防止误删日志)
def get_actions(self, request): def get_actions(self, request):
actions = super(LogEntryAdmin, self).get_actions(request) actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions: if 'delete_selected' in actions:
del actions['delete_selected'] del actions['delete_selected'] # 删除“批量删除”操作
return actions return actions

@ -1,7 +1,11 @@
ARTICLE_DETAIL_LOAD = 'article_detail_load' # 文章相关操作的标识常量:用于统一管理操作类型,避免硬编码字符串导致的不一致问题
ARTICLE_CREATE = 'article_create' # 场景:可用于日志记录、统计分析、权限校验等,通过常量标识具体操作
ARTICLE_UPDATE = 'article_update' ARTICLE_DETAIL_LOAD = 'article_detail_load' # 文章详情页加载操作标识(如用户访问某篇文章详情时使用)
ARTICLE_DELETE = 'article_delete' ARTICLE_CREATE = 'article_create' # 文章创建操作标识(如用户发布新文章时使用)
ARTICLE_UPDATE = 'article_update' # 文章更新操作标识(如用户编辑已发布文章时使用)
ARTICLE_CONTENT_HOOK_NAME = "the_content" ARTICLE_DELETE = 'article_delete' # 文章删除操作标识(如用户删除某篇文章时使用)
# 文章内容钩子Hook名称常量用于定义文章内容处理的钩子函数/扩展点名称
# 场景:在 Django 等框架中,可通过钩子机制对文章内容进行自定义处理(如过滤敏感词、添加水印、解析 markdown 等)
# 例如:注册名为 "the_content" 的钩子函数,在文章内容渲染前自动执行处理逻辑
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -13,331 +13,371 @@ import os
import sys import sys
from pathlib import Path from pathlib import Path
# 导入 Django 国际化工具:用于多语言文本翻译
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
# 自定义工具函数:将环境变量转为布尔值(处理配置的灵活性)
def env_to_bool(env, default): 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' 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 BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ # 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( SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6' '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 = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False # DEBUG = False # 生产环境手动关闭调试的示例
# 测试模式标识:判断是否正在执行测试命令(如 python manage.py test
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ALLOWED_HOSTS = [] # 允许访问的主机:生产环境需指定具体域名,开发环境用'*'允许所有主机(存在安全风险,生产禁用)
# ALLOWED_HOSTS = [] # 生产环境初始空配置(需补充具体域名)
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# django 4.0新增配置 # Django 4.0+ 新增配置:信任的 CSRF 来源(避免跨域 CSRF 验证失败,生产环境需指定真实域名)
CSRF_TRUSTED_ORIGINS = ['http://example.com'] CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
# Application definition已安装的应用Django 内置应用 + 第三方应用 + 自定义应用)
INSTALLED_APPS = [ INSTALLED_APPS = [
# 'django.contrib.admin', # Django 内置 Admin 应用使用精简版配置SimpleAdminConfig减少不必要功能
# 'django.contrib.admin', # 完整版 Admin 配置(此处注释,使用精简版)
'django.contrib.admin.apps.SimpleAdminConfig', 'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth', 'django.contrib.auth', # 认证与授权系统
'django.contrib.contenttypes', 'django.contrib.contenttypes', # 内容类型框架(关联模型与权限)
'django.contrib.sessions', 'django.contrib.sessions', # 会话管理(用户登录状态保持)
'django.contrib.messages', 'django.contrib.messages', # 消息提示系统(如登录成功提示)
'django.contrib.staticfiles', 'django.contrib.staticfiles', # 静态文件管理CSS/JS/图片)
'django.contrib.sites', 'django.contrib.sites', # 多站点支持(用于 RSS、OAuth 等功能)
'django.contrib.sitemaps', 'django.contrib.sitemaps', # 站点地图生成(利于 SEO
'mdeditor', 'mdeditor', # 第三方应用Markdown 编辑器(用于文章编写)
'haystack', 'haystack', # 第三方应用:搜索框架(对接 Whoosh/Elasticsearch
'blog', 'blog', # 自定义应用:博客核心功能(文章、分类等)
'accounts', 'accounts', # 自定义应用:用户账户管理(登录、注册等)
'comments', 'comments', # 自定义应用:评论功能
'oauth', 'oauth', # 自定义应用:第三方登录(如 GitHub、微博
'servermanager', 'servermanager', # 自定义应用:服务器管理(命令执行、日志记录)
'owntracks', 'owntracks', # 自定义应用位置追踪OwnTracks 数据管理)
'compressor', 'compressor', # 第三方应用静态文件压缩CSS/JS 压缩,提升加载速度)
'djangoblog' 'djangoblog' # 自定义应用:项目核心配置(如信号、插件)
] ]
# 中间件:处理请求/响应的钩子(按顺序执行,影响请求流程)
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', # 安全相关中间件HTTPS、XSS 防护等)
'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', # 会话管理中间件
'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', # 国际化中间件(处理多语言切换)
'django.middleware.locale.LocaleMiddleware', 'django.middleware.gzip.GZipMiddleware', # GZip 压缩中间件(减少响应体积)
'django.middleware.gzip.GZipMiddleware', # 'django.middleware.cache.UpdateCacheMiddleware', # 缓存更新中间件(注释:按需启用)
# 'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.common.CommonMiddleware', # 通用中间件(处理 URL 重定向、404 等)
'django.middleware.common.CommonMiddleware', # 'django.middleware.cache.FetchFromCacheMiddleware', # 缓存读取中间件(注释:按需启用)
# 'django.middleware.cache.FetchFromCacheMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', # CSRF 防护中间件(防止跨站请求伪造)
'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证中间件(绑定用户到请求)
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', # 消息中间件(传递提示信息)
'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持防护中间件
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.http.ConditionalGetMiddleware', # 条件请求中间件(缓存协商,减少重复请求)
'django.middleware.http.ConditionalGetMiddleware', 'blog.middleware.OnlineMiddleware' # 自定义中间件:用户在线状态管理
'blog.middleware.OnlineMiddleware'
] ]
# 根 URL 配置:指定项目的主 URL 路由文件
ROOT_URLCONF = 'djangoblog.urls' ROOT_URLCONF = 'djangoblog.urls'
# 模板配置:定义模板引擎、路径及上下文处理器
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 'BACKEND': 'django.template.backends.django.DjangoTemplates', # 使用 Django 内置模板引擎
'DIRS': [os.path.join(BASE_DIR, 'templates')], 'DIRS': [os.path.join(BASE_DIR, 'templates')], # 全局模板目录(项目根目录下的 templates
'APP_DIRS': True, 'APP_DIRS': True, # 允许从各应用的 templates 目录加载模板
'OPTIONS': { 'OPTIONS': {
# 上下文处理器:向所有模板注入全局变量(如用户信息、请求对象)
'context_processors': [ 'context_processors': [
'django.template.context_processors.debug', 'django.template.context_processors.debug', # 调试模式变量(如 DEBUG
'django.template.context_processors.request', 'django.template.context_processors.request', # 请求对象request
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth', # 认证相关变量(如 user
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages', # 消息变量(如 messages
'blog.context_processors.seo_processor' 'blog.context_processors.seo_processor' # 自定义上下文处理器:注入 SEO 相关数据
], ],
}, },
}, },
] ]
# WSGI 应用:指定项目的 WSGI 入口文件(用于部署,如 Gunicorn、uWSGI
WSGI_APPLICATION = 'djangoblog.wsgi.application' 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 = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.mysql', # 数据库引擎MySQL
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', 'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', # 数据库名(优先从环境变量获取)
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', 'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', # 数据库用户名
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', 'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or 'root', # 数据库密码
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', 'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', # 数据库主机(默认本地)
'PORT': int( 'PORT': int(os.environ.get('DJANGO_MYSQL_PORT') or 3306), # 数据库端口(默认 3306
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': { '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 = [ AUTH_PASSWORD_VALIDATORS = [
{ {
# 验证密码与用户属性(如用户名、邮箱)的相似度
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
}, },
{ {
# 验证密码最小长度(默认 8 位)
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
}, },
{ {
# 验证密码是否在常见弱密码列表中
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
}, },
{ {
# 验证密码是否纯数字
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
}, },
] ]
# Internationalization国际化配置多语言支持
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGES = ( LANGUAGES = (
('en', _('English')), ('en', _('English')), # 英语
('zh-hans', _('Simplified Chinese')), ('zh-hans', _('Simplified Chinese')), # 简体中文
('zh-hant', _('Traditional Chinese')), ('zh-hant', _('Traditional Chinese')), # 繁体中文
) )
# 翻译文件目录:指定多语言翻译文件(.po/.mo的存放路径
LOCALE_PATHS = ( LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'), os.path.join(BASE_DIR, 'locale'),
) )
LANGUAGE_CODE = 'zh-hans' LANGUAGE_CODE = 'zh-hans' # 默认语言:简体中文
TIME_ZONE = 'Asia/Shanghai' # 时区:上海(中国时区)
TIME_ZONE = 'Asia/Shanghai' USE_I18N = True # 启用国际化(支持多语言)
USE_L10N = True # 启用本地化(支持区域化日期、数字格式)
USE_I18N = True USE_TZ = False # 禁用 UTC 时间(使用本地时区存储时间,避免时区转换问题)
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
# Search搜索框架配置Haystack + Whoosh/Elasticsearch
HAYSTACK_CONNECTIONS = { HAYSTACK_CONNECTIONS = {
'default': { 'default': {
# 搜索引擎:默认使用 Whoosh轻量级全文搜索引擎适合中小项目
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', 'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
# 索引文件路径Whoosh 索引文件存储位置
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), 'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
}, },
} }
# Automatically update searching index # 实时更新索引:当模型数据(如文章)新增/修改/删除时,自动更新搜索索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' 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' AUTH_USER_MODEL = 'accounts.BlogUser'
# 登录 URL未登录用户访问需认证页面时重定向到该 URL
LOGIN_URL = '/login/' LOGIN_URL = '/login/'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
# bootstrap color styles # Custom Settings自定义业务配置
BOOTSTRAP_COLOR_TYPES = [ 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' 'default', 'primary', 'success', 'info', 'warning', 'danger'
] ]
PAGINATE_BY = 10 # 分页大小:每页显示 10 条数据(如文章列表、评论列表)
CACHE_CONTROL_MAX_AGE = 2592000 # HTTP 缓存有效期30 天(单位:秒)
# paginate # Cache缓存配置默认本地内存缓存支持 Redis 扩展)
PAGINATE_BY = 10
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
CACHES = { CACHES = {
'default': { 'default': {
# 本地内存缓存(适合开发环境,生产环境建议用 Redis/Memcached
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800, 'TIMEOUT': 10800, # 缓存有效期3 小时(单位:秒)
'LOCATION': 'unique-snowflake', 'LOCATION': 'unique-snowflake', # 缓存实例标识(避免多实例冲突)
} }
} }
# 使用redis作为缓存 # 若环境变量指定 Redis 地址,则使用 Redis 作为缓存(生产环境推荐)
if os.environ.get("DJANGO_REDIS_URL"): if os.environ.get("DJANGO_REDIS_URL"):
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache', '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') \ BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn' or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # Email邮件配置用于发送验证码、评论通知等
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # SMTP 邮件后端
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) # 是否启用 TLS 加密(与 SSL 二选一)
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) # 是否启用 SSL 加密(默认启用,端口通常为 465
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' # SMTP 服务器地址(默认阿里云)
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) # SMTP 端口SSL 通常为 465TLS 通常为 587
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') # 发送邮件的邮箱账号(优先环境变量)
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') # 邮箱密码/授权码
SERVER_EMAIL = EMAIL_HOST_USER DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # 默认发件人邮箱
# Setting debug=false did NOT handle except email notifications SERVER_EMAIL = EMAIL_HOST_USER # 服务器错误通知发件人邮箱
# 管理员邮箱:生产环境错误(如 500 异常)会发送到此邮箱
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')] ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# WX ADMIN password(Two times md5) # 微信管理员密码:二次 MD5 加密(用于微信后台管理验证,具体业务自定义)
WXADMIN = os.environ.get( WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' '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): if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True) os.makedirs(LOG_PATH, exist_ok=True)
LOGGING = { LOGGING = {
'version': 1, 'version': 1, # 日志配置版本(固定为 1
'disable_existing_loggers': False, 'disable_existing_loggers': False, # 不禁用已存在的日志器
'root': { 'root': { # 根日志器(所有未指定日志器的日志都会走这里)
'level': 'INFO', 'level': 'INFO', # 日志级别INFO记录普通信息及以上级别
'handlers': ['console', 'log_file'], 'handlers': ['console', 'log_file'], # 日志处理器(控制台 + 文件)
}, },
'formatters': { 'formatters': { # 日志格式:定义日志的输出结构
'verbose': { 'verbose': {
# 日志格式:时间 + 级别 + 调用位置 + 消息(便于问题定位)
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s', 'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
} }
}, },
'filters': { 'filters': { # 日志过滤器:按条件过滤日志
'require_debug_false': { 'require_debug_false': { # 仅当 DEBUG=False 时生效(生产环境)
'()': 'django.utils.log.RequireDebugFalse', '()': 'django.utils.log.RequireDebugFalse',
}, },
'require_debug_true': { 'require_debug_true': { # 仅当 DEBUG=True 时生效(开发环境)
'()': 'django.utils.log.RequireDebugTrue', '()': 'django.utils.log.RequireDebugTrue',
}, },
}, },
'handlers': { 'handlers': { # 日志处理器:定义日志的输出方式
'log_file': { 'log_file': { # 文件处理器:按时间轮转切割日志
'level': 'INFO', 'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler', 'class': 'logging.handlers.TimedRotatingFileHandler', # 按时间轮转的处理器
'filename': os.path.join(LOG_PATH, 'djangoblog.log'), 'filename': os.path.join(LOG_PATH, 'djangoblog.log'), # 日志文件路径
'when': 'D', 'when': 'D', # 轮转周期每天Day
'formatter': 'verbose', 'formatter': 'verbose', # 使用 verbose 格式
'interval': 1, 'interval': 1, # 轮转间隔1 个周期1 天)
'delay': True, 'delay': True, # 延迟创建文件(直到有日志才创建)
'backupCount': 5, 'backupCount': 5, # 保留日志备份数5 天
'encoding': 'utf-8' 'encoding': 'utf-8' # 日志文件编码
}, },
'console': { 'console': { # 控制台处理器:开发环境输出到终端
'level': 'DEBUG', 'level': 'DEBUG',
'filters': ['require_debug_true'], 'filters': ['require_debug_true'], # 仅开发环境生效
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'formatter': 'verbose' 'formatter': 'verbose'
}, },
'null': { 'null': { # 空处理器:用于屏蔽不需要的日志
'class': 'logging.NullHandler', 'class': 'logging.NullHandler',
}, },
'mail_admins': { 'mail_admins': { # 邮件处理器:生产环境错误发送到管理员邮箱
'level': 'ERROR', 'level': 'ERROR', # 仅 ERROR 级别日志触发
'filters': ['require_debug_false'], 'filters': ['require_debug_false'], # 仅生产环境生效
'class': 'django.utils.log.AdminEmailHandler' 'class': 'django.utils.log.AdminEmailHandler'
} }
}, },
'loggers': { 'loggers': { # 自定义日志器:针对特定模块配置日志
'djangoblog': { 'djangoblog': { # 项目核心模块日志器
'handlers': ['log_file', 'console'], 'handlers': ['log_file', 'console'], # 输出到文件和控制台
'level': 'INFO', 'level': 'INFO',
'propagate': True, 'propagate': True, # 是否向上传递到根日志器(此处开启)
}, },
'django.request': { 'django.request': { # Django 请求模块日志器(记录请求相关错误)
'handlers': ['mail_admins'], 'handlers': ['mail_admins'], # 错误发送到管理员邮箱
'level': 'ERROR', '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 = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.FileSystemFinder', # 查找全局 STATICFILES 目录
'django.contrib.staticfiles.finders.AppDirectoriesFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 查找各应用的 static 目录
# other 'compressor.finders.CompressorFinder', # 查找压缩后的静态文件(第三方 compressor 应用)
'compressor.finders.CompressorFinder',
) )
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
# 静态文件压缩配置compressor 应用)
COMPRESS_ENABLED = True # 启用压缩(生产环境建议开启,提升加载速度)
# COMPRESS_OFFLINE = True # 离线压缩(预先生成压缩文件,生产环境推荐,此处注释按需启用)
# CSS 压缩过滤器:绝对路径处理 + 代码压缩
COMPRESS_CSS_FILTERS = [ COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter', 'compressor.filters.css_default.CssAbsoluteFilter',
# css minimizer
'compressor.filters.cssmin.CSSMinFilter' 'compressor.filters.cssmin.CSSMinFilter'
] ]
# JS 压缩过滤器:代码压缩
COMPRESS_JS_FILTERS = [ COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter' 'compressor.filters.jsmin.JSMinFilter'
] ]
# Media Files媒体文件配置用户上传的文件如文章图片、头像
# 媒体文件存储目录:项目根目录下的 uploads 目录
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
# 媒体文件 URL 前缀:前端通过 /media/ 访问上传的文件
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
# X-Frame-Options防止点击劫持SAMEORIGIN仅允许同域页面嵌入当前页面
X_FRAME_OPTIONS = 'SAMEORIGIN' X_FRAME_OPTIONS = 'SAMEORIGIN'
# 默认模型主键类型Django 3.2+ 新增,指定模型默认主键为 BigAutoField64 位自增整数)
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Elasticsearch 配置(按需启用:若环境变量指定 Elasticsearch 地址,则使用 Elasticsearch 替代 Whoosh
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'): if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = { ELASTICSEARCH_DSL = {
'default': { 'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') 'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') # Elasticsearch 连接地址
}, },
} }
# 替换 Haystack 搜索引擎为自定义的 Elasticsearch 引擎
HAYSTACK_CONNECTIONS = { HAYSTACK_CONNECTIONS = {
'default': { 'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', 'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
}, },
} }
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins' # Plugin System插件系统配置自定义插件功能
PLUGINS_DIR = BASE_DIR / 'plugins' # 插件目录:项目根目录下的 plugins 目录
# 激活的插件列表:指定需要加载的插件(如文章版权、阅读时长统计等)
ACTIVE_PLUGINS = [ ACTIVE_PLUGINS = [
'article_copyright', 'article_copyright', # 文章版权插件
'reading_time', 'reading_time', # 阅读时长统计插件
'external_links', 'external_links', # 外部链接处理插件
'view_count', 'view_count', # 文章阅读量统计插件
'seo_optimizer' 'seo_optimizer' # SEO 优化插件
] ]

@ -1,59 +1,88 @@
# 导入 Django 内置站点地图基类:用于快速实现标准化 sitemap.xml
from django.contrib.sitemaps import Sitemap from django.contrib.sitemaps import Sitemap
# 导入 URL 反向解析工具:生成页面的绝对 URL适配路由命名
from django.urls import reverse from django.urls import reverse
# 导入博客核心模型:用于生成动态内容(文章、分类、标签)的站点地图
from blog.models import Article, Category, Tag from blog.models import Article, Category, Tag
# 静态页面站点地图类:处理无数据库关联的静态页面(如博客首页)
class StaticViewSitemap(Sitemap): class StaticViewSitemap(Sitemap):
# 优先级0.5(取值 0.0-1.0,值越高搜索引擎越优先抓取,静态页优先级中等)
priority = 0.5 priority = 0.5
# 更新频率daily每天更新适合内容相对稳定但可能微调的静态页
changefreq = 'daily' changefreq = 'daily'
# 定义要包含的静态页面:返回路由名称列表(对应 urls.py 中命名的路由)
def items(self): def items(self):
return ['blog:index', ] return ['blog:index', ] # 此处仅包含博客首页(路由名为 'blog:index'
# 生成静态页面的 URL通过路由名称反向解析为绝对路径
def location(self, item): def location(self, item):
return reverse(item) return reverse(item) # item 为 items() 返回的路由名称,如 'blog:index'
# 文章站点地图类处理博客文章的动态页面核心内容SEO 关键)
class ArticleSiteMap(Sitemap): class ArticleSiteMap(Sitemap):
# 更新频率monthly每月更新适合文章发布后较少修改的场景
changefreq = "monthly" changefreq = "monthly"
# 优先级0.6(高于静态页和分类/标签,文章是站点核心内容,优先抓取)
priority = "0.6" priority = "0.6"
# 定义要包含的文章:仅筛选“已发布”状态的文章(排除草稿、私有文章)
def items(self): def items(self):
return Article.objects.filter(status='p') return Article.objects.filter(status='p')
# 文章最后更新时间:用于搜索引擎判断内容是否更新,提升抓取效率
def lastmod(self, obj): def lastmod(self, obj):
return obj.last_modify_time return obj.last_modify_time # 引用文章模型的“最后修改时间”字段
# 分类站点地图类:处理文章分类页面(聚合类内容,辅助 SEO
class CategorySiteMap(Sitemap): class CategorySiteMap(Sitemap):
# 更新频率Weekly每周更新分类内容更新频率低于文章
changefreq = "Weekly" changefreq = "Weekly"
# 优先级0.6(与文章同级,分类页是重要的内容聚合入口)
priority = "0.6" priority = "0.6"
# 定义要包含的分类:获取所有分类(无论是否有文章,确保分类页被收录)
def items(self): def items(self):
return Category.objects.all() return Category.objects.all()
# 分类最后更新时间:判断分类下内容是否有变化(如新增/修改文章)
def lastmod(self, obj): def lastmod(self, obj):
return obj.last_modify_time return obj.last_modify_time # 引用分类模型的“最后修改时间”字段
# 标签站点地图类:处理文章标签页面(细分内容聚合,补充 SEO 覆盖)
class TagSiteMap(Sitemap): class TagSiteMap(Sitemap):
# 更新频率Weekly每周更新标签内容更新频率与分类一致
changefreq = "Weekly" changefreq = "Weekly"
# 优先级0.3(低于文章和分类,标签页是辅助导航入口,优先级较低)
priority = "0.3" priority = "0.3"
# 定义要包含的标签:获取所有标签(确保所有标签页被搜索引擎收录)
def items(self): def items(self):
return Tag.objects.all() return Tag.objects.all()
# 标签最后更新时间:判断标签下内容是否有变化
def lastmod(self, obj): def lastmod(self, obj):
return obj.last_modify_time return obj.last_modify_time # 引用标签模型的“最后修改时间”字段
# 用户站点地图类:处理文章作者页面(展示用户发布的所有文章,提升作者页曝光)
class UserSiteMap(Sitemap): class UserSiteMap(Sitemap):
# 更新频率Weekly每周更新用户发布文章频率通常较低
changefreq = "Weekly" changefreq = "Weekly"
# 优先级0.3(与标签同级,作者页是辅助内容入口)
priority = "0.3" priority = "0.3"
# 定义要包含的用户:获取所有发布过文章的作者(去重,避免重复收录同一用户)
def items(self): def items(self):
# 1. 从所有文章中提取作者字段 → 2. 转集合去重 → 3. 转列表返回
return list(set(map(lambda x: x.author, Article.objects.all()))) return list(set(map(lambda x: x.author, Article.objects.all())))
# 用户页面最后更新时间:用用户“注册时间”替代(简化逻辑,也可改为用户最新文章发布时间)
def lastmod(self, obj): def lastmod(self, obj):
return obj.date_joined return obj.date_joined # 引用用户模型的“注册时间”字段

@ -1,21 +1,43 @@
# 导入日志模块:记录推送过程的成功信息与错误,便于排查问题
import logging import logging
# 导入 HTTP 请求库:用于向百度搜索引擎接口发送 POST 请求
import requests import requests
# 导入 Django 项目配置:获取百度主动推送的接口 URL从 settings.py 读取)
from django.conf import settings from django.conf import settings
# 初始化日志对象:指定日志归属为当前模块,便于区分不同模块的日志
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# 搜索引擎推送工具类:封装向百度等搜索引擎主动提交页面的逻辑
class SpiderNotify(): class SpiderNotify():
@staticmethod @staticmethod
def baidu_notify(urls): def baidu_notify(urls):
"""
向百度搜索引擎主动推送页面基于百度站长平台的链接提交接口
目的让百度快速发现新页面缩短收录周期提升 SEO 效率
Args:
urls: 待推送的 URL 列表 ['https://xxx.com/article/1/', 'https://xxx.com/article/2/']
"""
try: try:
# 格式化请求数据:百度接口要求 URL 以换行符(\n分隔拼接成字符串
data = '\n'.join(urls) data = '\n'.join(urls)
# 发送 POST 请求:调用 settings 中配置的百度推送接口 URL
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data) result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
# 记录推送结果:将百度返回的响应文本(如成功/失败数量)写入日志
logger.info(result.text) logger.info(result.text)
except Exception as e: except Exception as e:
# 捕获异常(如网络错误、接口超时等),记录错误信息到日志
logger.error(e) logger.error(e)
@staticmethod @staticmethod
def notify(url): def notify(url):
SpiderNotify.baidu_notify(url) """
通用推送方法统一入口当前仅调用百度推送可扩展支持其他搜索引擎
Args:
url: 待推送的 URL支持单个 URL URL 列表此处兼容百度推送的列表格式
"""
# 调用百度推送方法,实现页面提交
SpiderNotify.baidu_notify(url)

@ -1,32 +1,29 @@
# 导入 Django 内置的测试基类:提供单元测试所需的基础功能(如断言、测试环境初始化)
from django.test import TestCase from django.test import TestCase
# 导入项目自定义工具模块测试其中的工具函数如加密、Markdown解析、字典转URL参数
from djangoblog.utils import * from djangoblog.utils import *
# 自定义测试类:继承 TestCase用于测试 djangoblog 项目的工具函数功能
class DjangoBlogTest(TestCase): class DjangoBlogTest(TestCase):
# 测试前置方法:在每个测试方法(以 test_ 开头)执行前自动调用
# 用于初始化测试数据、配置测试环境等(此处暂无需初始化,留空)
def setUp(self): def setUp(self):
pass pass
# 核心测试方法:测试 utils 模块中的多个工具函数(命名以 test_ 开头Django 会自动识别执行)
def test_utils(self): 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) 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 # 一级标题
''') ```python # Python 代码块
self.assertIsNotNone(c) import os
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)

@ -13,52 +13,88 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include 1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
""" """
# 导入项目配置:用于获取静态文件/媒体文件路径、DEBUG 状态等
from django.conf import settings from django.conf import settings
# 导入国际化路由工具:生成带语言前缀的路由(如 /en/admin/、/zh-hans/blog/
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
# 导入静态文件路由工具:开发环境下提供静态文件访问(生产环境需 Nginx 处理)
from django.conf.urls.static import static from django.conf.urls.static import static
# 导入站点地图视图:关联站点地图配置,生成 sitemap.xml
from django.contrib.sitemaps.views import sitemap from django.contrib.sitemaps.views import sitemap
# 导入 URL 路由组件path 用于精确匹配include 用于引入子应用路由
from django.urls import path, include from django.urls import path, include
# 导入 re_path支持正则表达式匹配 URL适配复杂路由场景
from django.urls import re_path from django.urls import re_path
# 导入 Haystack 搜索视图工厂:用于自定义搜索视图和表单
from haystack.views import search_view_factory from haystack.views import search_view_factory
from blog.views import EsSearchView # 导入项目自定义视图和配置:关联核心功能路由
from djangoblog.admin_site import admin_site from blog.views import EsSearchView # 自定义 Elasticsearch 搜索视图
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm from djangoblog.admin_site import admin_site # 自定义后台管理站点(替代默认 admin
from djangoblog.feeds import DjangoBlogFeed from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm # 自定义搜索表单
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap from djangoblog.feeds import DjangoBlogFeed # RSS 订阅 Feed 视图
from djangoblog.sitemap import ( # 站点地图配置类
ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
)
# 站点地图聚合配置:将各类型站点地图归类,用于生成统一的 sitemap.xml
sitemaps = { sitemaps = {
'blog': ArticleSiteMap, # 文章站点地图
'blog': ArticleSiteMap, 'Category': CategorySiteMap, # 分类站点地图
'Category': CategorySiteMap, 'Tag': TagSiteMap, # 标签站点地图
'Tag': TagSiteMap, 'User': UserSiteMap, # 作者站点地图
'User': UserSiteMap, 'static': StaticViewSitemap # 静态页面站点地图
'static': StaticViewSitemap
} }
handler404 = 'blog.views.page_not_found_view' # 自定义错误页面路由:指定 404/500/403 错误时跳转的视图
handler500 = 'blog.views.server_error_view' handler404 = 'blog.views.page_not_found_view' # 404 页面未找到
handle403 = 'blog.views.permission_denied_view' handler500 = 'blog.views.server_error_view' # 500 服务器内部错误
handle403 = 'blog.views.permission_denied_view'# 403 权限拒绝(注意变量名应为 handler403此处可能是笔误
# 基础 URL 路由:不包含语言前缀的公共路由
urlpatterns = [ urlpatterns = [
# 国际化路由:提供语言切换功能(如 /i18n/setlang/ 接口)
path('i18n/', include('django.conf.urls.i18n')), path('i18n/', include('django.conf.urls.i18n')),
] ]
# 带语言前缀的路由:通过 i18n_patterns 自动添加语言前缀(如 /zh-hans/、/en/
# prefix_default_language=False默认语言不显示前缀如中文默认不显示 /zh-hans/,直接用根路径)
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns(
# 1. 后台管理路由:使用自定义的 admin_site替代默认 admin访问路径如 /admin/
re_path(r'^admin/', admin_site.urls), re_path(r'^admin/', admin_site.urls),
# 2. 博客核心路由:引入 blog 应用的子路由,命名空间为 'blog'(路由名如 blog:index
re_path(r'', include('blog.urls', namespace='blog')), re_path(r'', include('blog.urls', namespace='blog')),
# 3. Markdown 编辑器路由:引入 mdeditor 第三方应用的路由,用于文章编辑时的 Markdown 预览
re_path(r'mdeditor/', include('mdeditor.urls')), re_path(r'mdeditor/', include('mdeditor.urls')),
# 4. 评论路由:引入 comments 应用的子路由,命名空间为 'comment'
re_path(r'', include('comments.urls', namespace='comment')), re_path(r'', include('comments.urls', namespace='comment')),
# 5. 用户账户路由:引入 accounts 应用的子路由(登录、注册、个人中心),命名空间为 'account'
re_path(r'', include('accounts.urls', namespace='account')), re_path(r'', include('accounts.urls', namespace='account')),
# 6. 第三方登录路由:引入 oauth 应用的子路由GitHub、微博登录命名空间为 'oauth'
re_path(r'', include('oauth.urls', namespace='oauth')), re_path(r'', include('oauth.urls', namespace='oauth')),
# 7. 站点地图路由:生成 sitemap.xml供搜索引擎抓取访问路径 /sitemap.xml
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps}, re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'), name='django.contrib.sitemaps.views.sitemap'),
# 8. RSS 订阅路由:提供两种访问路径(/feed/ 和 /rss/),均指向 DjangoBlogFeed 视图
re_path(r'^feed/$', DjangoBlogFeed()), re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()), re_path(r'^rss/$', DjangoBlogFeed()),
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm), # 9. 搜索路由使用自定义的搜索视图EsSearchView和表单ElasticSearchModelSearchForm
name='search'), # 访问路径如 /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('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks')) # 11. 位置追踪路由:引入 owntracks 应用的子路由(位置数据查看),命名空间为 'owntracks'
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 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: if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT) document_root=settings.MEDIA_ROOT)

@ -2,105 +2,173 @@
# encoding: utf-8 # encoding: utf-8
import logging import logging # 日志模块:记录操作信息和错误
import os import os # 系统操作模块:处理文件路径、目录创建等
import random import random # 随机数模块:生成验证码等随机内容
import string import string # 字符串模块:提供数字、字母等常量
import uuid import uuid # 唯一模块:生成唯一标识符(用于头像文件名)
from hashlib import sha256 from hashlib import sha256 # 加密模块:提供 SHA256 加密算法
import bleach import bleach # HTML 清理模块:过滤不安全的 HTML 标签(防 XSS 攻击)
import markdown import markdown # Markdown 解析模块:将 Markdown 文本转为 HTML
import requests import requests # HTTP 请求模块:下载网络资源(如用户头像)
from django.conf import settings from django.conf import settings # Django 配置:获取项目设置(如静态文件路径)
from django.contrib.sites.models import Site from django.contrib.sites.models import Site # 站点模型:获取当前站点信息(域名等)
from django.core.cache import cache from django.core.cache import cache # 缓存模块:操作 Django 缓存(获取/设置/删除)
from django.templatetags.static import static from django.templatetags.static import static # 静态文件工具:生成静态文件的 URL
# 初始化日志对象:指定日志归属为当前模块,便于日志分类
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def get_max_articleid_commentid(): def get_max_articleid_commentid():
"""
获取当前最大的文章 ID 和评论 ID用于数据统计或初始化
Returns:
tuple: (最大文章 ID, 最大评论 ID)
"""
# 延迟导入模型:避免循环导入问题(工具模块可能被模型模块引用)
from blog.models import Article from blog.models import Article
from comments.models import Comment from comments.models import Comment
# 返回最新文章和评论的主键ID
return (Article.objects.latest().pk, Comment.objects.latest().pk) return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str): def get_sha256(str):
"""
对字符串进行 SHA256 加密用于密码加密唯一标识生成等
Args:
str: 待加密的字符串
Returns:
str: 加密后的 64 位十六进制字符串
"""
# 创建 SHA256 加密对象,需先将字符串转为字节流(指定编码 utf-8
m = sha256(str.encode('utf-8')) m = sha256(str.encode('utf-8'))
# 返回十六进制加密结果
return m.hexdigest() return m.hexdigest()
def cache_decorator(expiration=3 * 60): def cache_decorator(expiration=3 * 60):
"""
缓存装饰器装饰函数将函数返回值缓存指定时间默认 3 分钟
作用减少重复计算或数据库查询提升性能
Args:
expiration: 缓存有效期默认 3 分钟
Returns:
装饰器函数包装原函数实现缓存逻辑
"""
def wrapper(func): def wrapper(func):
def news(*args, **kwargs): def news(*args, **kwargs):
# 尝试生成缓存键(优先使用视图对象的 get_cache_key 方法)
try: try:
view = args[0] view = args[0] # 若第一个参数是视图对象
key = view.get_cache_key() key = view.get_cache_key() # 使用视图自带的缓存键
except: except:
key = None key = None # 非视图函数,需自定义缓存键
# 若未生成缓存键,则基于函数和参数生成唯一键
if not key: if not key:
# 将函数、参数转为字符串,确保唯一性
unique_str = repr((func, args, kwargs)) unique_str = repr((func, args, kwargs))
# 对字符串进行 SHA256 加密,生成固定长度的缓存键
m = sha256(unique_str.encode('utf-8')) m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest() key = m.hexdigest()
# 尝试从缓存获取数据
value = cache.get(key) value = cache.get(key)
if value is not None: if value is not None:
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key)) # 缓存命中:返回缓存值(处理 None 的特殊标记)
if str(value) == '__default_cache_value__': if str(value) == '__default_cache_value__':
return None return None
else: else:
return value return value
else: else:
# 缓存未命中:执行原函数获取结果
logger.debug( logger.debug(
'cache_decorator set cache:%s key:%s' % 'cache_decorator set cache:%s key:%s' %
(func.__name__, key)) (func.__name__, key))
value = func(*args, **kwargs) value = func(*args, **kwargs)
# 缓存结果(用特殊标记表示 None避免缓存不生效
if value is None: if value is None:
cache.set(key, '__default_cache_value__', expiration) cache.set(key, '__default_cache_value__', expiration)
else: else:
cache.set(key, value, expiration) cache.set(key, value, expiration)
return value return value
return news return news
return wrapper return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None): def expire_view_cache(path, servername, serverport, key_prefix=None):
''' '''
刷新视图缓存 主动刷新指定 URL 路径的视图缓存用于数据更新后清理旧缓存
:param path:url路径
:param servername:host Args:
:param serverport:端口 path: URL 路径 '/article/1/'
:param key_prefix:前缀 servername: 服务器域名 'www.example.com'
:return:是否成功 serverport: 服务器端口 80
key_prefix: 缓存键前缀与视图缓存配置一致
Returns:
bool: 缓存是否成功删除
''' '''
from django.http import HttpRequest from django.http import HttpRequest # 延迟导入:避免启动时依赖冲突
from django.utils.cache import get_cache_key from django.utils.cache import get_cache_key # 获取视图缓存键的工具
# 构造模拟请求对象(用于生成缓存键)
request = HttpRequest() request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport} request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path request.path = path
# 获取该请求对应的缓存键
key = get_cache_key(request, key_prefix=key_prefix, cache=cache) key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key: if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path)) logger.info('expire_view_cache:get key:{path}'.format(path=path))
# 若缓存存在,则删除
if cache.get(key): if cache.get(key):
cache.delete(key) cache.delete(key)
return True return True
return False return False
@cache_decorator() @cache_decorator() # 应用缓存装饰器:缓存当前站点信息(默认 3 分钟)
def get_current_site(): def get_current_site():
"""
获取当前站点信息域名等从缓存获取以减少数据库查询
Returns:
Site: Django Site 模型实例
"""
site = Site.objects.get_current() site = Site.objects.get_current()
return site return site
class CommonMarkdown: class CommonMarkdown:
"""
Markdown 解析工具类 Markdown 文本转为 HTML并支持提取目录TOC
"""
@staticmethod @staticmethod
def _convert_markdown(value): def _convert_markdown(value):
"""
内部方法执行 Markdown 转换返回 HTML 内容和目录
Args:
value: Markdown 格式的文本
Returns:
tuple: (转换后的 HTML 内容, 目录 HTML)
"""
# 初始化 Markdown 解析器,启用扩展:
# - extra: 支持表格、脚注等扩展语法
# - codehilite: 代码高亮
# - toc: 生成目录
# - tables: 表格支持extra 已包含,此处冗余可能为兼容)
md = markdown.Markdown( md = markdown.Markdown(
extensions=[ extensions=[
'extra', 'extra',
@ -109,124 +177,227 @@ class CommonMarkdown:
'tables', 'tables',
] ]
) )
body = md.convert(value) body = md.convert(value) # 转换文本为 HTML
toc = md.toc toc = md.toc # 提取目录 HTML
return body, toc return body, toc
@staticmethod @staticmethod
def get_markdown_with_toc(value): def get_markdown_with_toc(value):
"""
获取带目录的 Markdown 转换结果
Args:
value: Markdown 文本
Returns:
tuple: (HTML 内容, 目录 HTML)
"""
body, toc = CommonMarkdown._convert_markdown(value) body, toc = CommonMarkdown._convert_markdown(value)
return body, toc return body, toc
@staticmethod @staticmethod
def get_markdown(value): def get_markdown(value):
"""
获取仅包含 HTML 内容的转换结果忽略目录
Args:
value: Markdown 文本
Returns:
str: 转换后的 HTML 内容
"""
body, toc = CommonMarkdown._convert_markdown(value) body, toc = CommonMarkdown._convert_markdown(value)
return body return body
def send_email(emailto, title, content): def send_email(emailto, title, content):
"""
发送邮件通过信号机制触发解耦发送逻辑
Args:
emailto: 收件人列表 ['user@example.com']
title: 邮件标题
content: 邮件内容HTML 格式
"""
# 延迟导入信号:避免循环导入
from djangoblog.blog_signals import send_email_signal from djangoblog.blog_signals import send_email_signal
# 发送信号,由信号接收器(如 send_email_signal_handler处理实际发送
send_email_signal.send( send_email_signal.send(
send_email.__class__, send_email.__class__, # 信号发送者(此处用当前函数的类)
emailto=emailto, emailto=emailto,
title=title, title=title,
content=content) content=content)
def generate_code() -> str: def generate_code() -> str:
"""生成随机数验证码""" """
生成 6 位数字验证码用于邮箱验证登录验证码等
Returns:
str: 6 位数字字符串
"""
# 从数字字符集中随机选择 6 个,拼接为字符串
return ''.join(random.sample(string.digits, 6)) return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict): 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='/')) url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()]) for k, v in dict.items()])
return url return url
def get_blog_setting(): def get_blog_setting():
"""
获取博客系统设置如站点名称描述等优先从缓存获取
Returns:
BlogSettings: 博客设置模型实例
"""
# 尝试从缓存获取
value = cache.get('get_blog_setting') value = cache.get('get_blog_setting')
if value: if value:
return value return value
else: else:
# 延迟导入模型:避免循环导入
from blog.models import BlogSettings from blog.models import BlogSettings
# 若数据库中无设置记录,初始化默认设置
if not BlogSettings.objects.count(): if not BlogSettings.objects.count():
setting = BlogSettings() setting = BlogSettings()
setting.site_name = 'djangoblog' setting.site_name = 'djangoblog' # 站点名称
setting.site_description = '基于Django的博客系统' setting.site_description = '基于Django的博客系统' # 站点描述
setting.site_seo_description = '基于Django的博客系统' setting.site_seo_description = '基于Django的博客系统' # SEO 描述
setting.site_keywords = 'Django,Python' setting.site_keywords = 'Django,Python' # SEO 关键词
setting.article_sub_length = 300 setting.article_sub_length = 300 # 文章摘要长度
setting.sidebar_article_count = 10 setting.sidebar_article_count = 10 # 侧边栏显示文章数
setting.sidebar_comment_count = 5 setting.sidebar_comment_count = 5 # 侧边栏显示评论数
setting.show_google_adsense = False setting.show_google_adsense = False # 是否显示谷歌广告
setting.open_site_comment = True setting.open_site_comment = True # 是否开启评论功能
setting.analytics_code = '' setting.analytics_code = '' # 统计代码(如百度统计)
setting.beian_code = '' setting.beian_code = '' # 备案号
setting.show_gongan_code = False setting.show_gongan_code = False # 是否显示公安备案
setting.comment_need_review = False setting.comment_need_review = False # 评论是否需要审核
setting.save() setting.save() # 保存默认设置
# 从数据库获取设置
value = BlogSettings.objects.first() value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting') logger.info('set cache get_blog_setting')
# 缓存设置(默认使用 cache_decorator 的有效期,或依赖全局缓存配置)
cache.set('get_blog_setting', value) cache.set('get_blog_setting', value)
return value return value
def save_user_avatar(url): 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: try:
# 本地头像存储目录(静态文件目录下的 avatar 文件夹)
basedir = os.path.join(settings.STATICFILES, 'avatar') basedir = os.path.join(settings.STATICFILES, 'avatar')
# 发送 HTTP 请求下载头像(超时 2 秒)
rsp = requests.get(url, timeout=2) rsp = requests.get(url, timeout=2)
if rsp.status_code == 200: if rsp.status_code == 200: # 下载成功
# 若目录不存在则创建
if not os.path.exists(basedir): if not os.path.exists(basedir):
os.makedirs(basedir) os.makedirs(basedir)
# 验证 URL 是否为图片格式(通过文件扩展名判断)
image_extensions = ['.jpg', '.png', 'jpeg', '.gif'] image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0 isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
# 提取文件扩展名,默认为 .jpg
ext = os.path.splitext(url)[1] if isimage else '.jpg' ext = os.path.splitext(url)[1] if isimage else '.jpg'
# 生成唯一文件名UUID 避免冲突)
save_filename = str(uuid.uuid4().hex) + ext save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像:' + basedir + save_filename) logger.info('保存用户头像:' + basedir + save_filename)
# 写入文件到本地目录
with open(os.path.join(basedir, save_filename), 'wb+') as file: with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content) file.write(rsp.content)
# 返回本地头像的静态 URL
return static('avatar/' + save_filename) return static('avatar/' + save_filename)
except Exception as e: except Exception as e:
# 下载失败(如网络错误、超时),记录错误并返回默认头像
logger.error(e) logger.error(e)
return static('blog/img/avatar.png') return static('blog/img/avatar.png')
def delete_sidebar_cache(): 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] keys = ["sidebar" + x for x in LinkShowType.values]
# 遍历删除所有侧边栏缓存键
for k in keys: for k in keys:
logger.info('delete sidebar key:' + k) logger.info('delete sidebar key:' + k)
cache.delete(k) cache.delete(k)
def delete_view_cache(prefix, keys): 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) key = make_template_fragment_key(prefix, keys)
# 删除缓存
cache.delete(key) cache.delete(key)
def get_resource_url(): def get_resource_url():
"""
获取静态资源的基础 URL用于动态生成资源路径
Returns:
str: 静态资源 URL 前缀 'http://example.com/static/'
"""
if settings.STATIC_URL: if settings.STATIC_URL:
return settings.STATIC_URL return settings.STATIC_URL
else: else:
# 若未配置 STATIC_URL从当前站点域名生成
site = get_current_site() site = get_current_site()
return 'http://' + site.domain + '/static/' return 'http://' + site.domain + '/static/'
# HTML 清理配置:允许的标签和属性(防止 XSS 攻击)
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p'] 'h2', 'p'] # 允许的 HTML 标签
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} ALLOWED_ATTRIBUTES = { # 允许的标签属性(键为标签,值为属性列表)
'a': ['href', 'title'],
'abbr': ['title'],
'acronym': ['title']
}
def sanitize_html(html): 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)

@ -6,11 +6,18 @@ It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
""" """
# 导入系统模块:用于设置环境变量
import os import os
# 导入 Django WSGI 核心函数:生成符合 WSGI 标准的应用对象
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
# 设置 Django 项目的配置模块环境变量
# 作用:告诉 Django 启动时加载哪个配置文件(此处为项目根目录下的 djangoblog.settings
# 部署时可通过修改该值切换配置(如 djangoblog.settings.production 对应生产环境配置)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
application = get_wsgi_application() # 生成 WSGI 应用对象
# 作用:将 Django 项目包装为 WSGI 兼容的应用,供 WSGI 服务器(如 Gunicorn、uWSGI调用
# 该对象是 Django 与 Web 服务器交互的核心入口
application = get_wsgi_application()
Loading…
Cancel
Save