ls #6

Closed
pqnvcz97o wants to merge 2 commits from ls_branch into develop

8
.idea/.gitignore vendored

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13" project-jdk-type="Python SDK" />
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/zyd2025.iml" filepath="$PROJECT_DIR$/.idea/zyd2025.iml" />
</modules>
</component>
</project>

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="FacetManager">
<facet type="django" name="Django">
<configuration>
<option name="rootFolder" value="$MODULE_DIR$/src" />
<option name="settingsModule" value="settings.py" />
<option name="manageScript" value="$MODULE_DIR$/src/manage.py" />
<option name="environment" value="&lt;map/&gt;" />
<option name="doNotUseTestRunner" value="false" />
<option name="trackFilePattern" value="migrations" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TemplatesService">
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
<option name="TEMPLATE_FOLDERS">
<list>
<option value="$MODULE_DIR$/src/templates" />
</list>
</option>
</component>
</module>

@ -146,7 +146,11 @@ python manage.py runserver
## 🙏 鸣谢
<<<<<<< HEAD
特别感谢 **JetBrains** 为本项目提供的免费开源许可证。gs ls136 syj zyd164
=======
特别感谢 **JetBrains** 为本项目提供的免费开源许可证。gs ls syj143 zyd164
>>>>>>> f2461baa4bb00608551433ffbaeb7a5d4a615925
<p align="center">
<a href="https://www.jetbrains.com/?from=DjangoBlog">

@ -1,379 +1,109 @@
import logging
import os
import uuid
# hooks.py - 插件钩子管理系统
# 提供插件钩子的注册和执行功能,是插件系统的核心组件
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.templatetags.static import static
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
import logging # 导入日志模块,用于记录钩子执行过程中的信息
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__) # 创建当前模块的日志记录器
logger = logging.getLogger(__name__)
# 全局钩子注册表,存储所有已注册的钩子回调函数
# 数据结构为字典:{钩子名称: [回调函数列表]}
_hooks = {}
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
context_object_name = 'article_list'
# 页面类型,分类目录或标签列表等
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
def get_view_cache_key(self):
return self.request.get['pages']
@property
def page_number(self):
page_kwarg = self.page_kwarg
page = self.kwargs.get(
page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
def get_queryset_cache_key(self):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
value = cache.get(cache_key)
if value:
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
def get_queryset(self):
'''
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
class IndexView(ArticleListView):
'''
首页
'''
# 友情链接类型
link_type = LinkShowType.I
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
class ArticleDetailView(DetailView):
'''
文章详情页面
'''
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
def get_context_data(self, **kwargs):
comment_form = CommentForm()
article_comments = self.object.comment_list()
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
if not page.isnumeric():
page = 1
else:
page = int(page)
if page < 1:
page = 1
if page > paginator.num_pages:
page = paginator.num_pages
p_comments = paginator.page(page)
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
# # Filter Hook, 允许插件修改文章正文
article.body = hooks.apply_filters(ARTICLE_CONTENT_HOOK_NAME, article.body, article=article,
request=self.request)
return context
class CategoryDetailView(ArticleListView):
'''
分类目录列表
'''
page_type = "分类目录归档"
def get_queryset_data(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
return cache_key
def get_context_data(self, **kwargs):
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).get_context_data(**kwargs)
class AuthorDetailView(ArticleListView):
'''
作者详情页
'''
page_type = '作者文章归档'
def get_queryset_cache_key(self):
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
def get_queryset_data(self):
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
return super(AuthorDetailView, self).get_context_data(**kwargs)
class TagDetailView(ArticleListView):
'''
标签列表页面
'''
page_type = '分类标签归档'
def get_queryset_data(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
article_list = Article.objects.filter(
tags__name=tag_name, type='a', status='p')
return article_list
def get_queryset_cache_key(self):
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
tag_name = tag.name
self.name = tag_name
cache_key = 'tag_{tag_name}_{page}'.format(
tag_name=tag_name, page=self.page_number)
return cache_key
def get_context_data(self, **kwargs):
# tag_name = self.kwargs['tag_name']
tag_name = self.name
kwargs['page_type'] = TagDetailView.page_type
kwargs['tag_name'] = tag_name
return super(TagDetailView, self).get_context_data(**kwargs)
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
page_type = '文章归档'
paginate_by = None
page_kwarg = None
template_name = 'blog/article_archives.html'
def get_queryset_data(self):
return Article.objects.filter(status='p').all()
def register(hook_name: str, callback: callable):
"""
注册一个钩子回调函数
def get_queryset_cache_key(self):
cache_key = 'archives'
return cache_key
参数:
hook_name (str): 钩子名称用于标识特定的钩子事件
callback (callable): 回调函数当钩子被触发时执行的函数
功能:
将回调函数添加到指定钩子的回调列表中
如果钩子名称不存在则创建新的钩子条目
"""
# 检查钩子名称是否已存在于钩子注册表中
if hook_name not in _hooks:
# 如果不存在,则创建一个新的空列表用于存储回调函数
_hooks[hook_name] = []
class LinkListView(ListView):
model = Links
template_name = 'blog/links_list.html'
# 将回调函数添加到对应钩子的回调列表中
_hooks[hook_name].append(callback)
def get_queryset(self):
return Links.objects.filter(is_enable=True)
# 记录调试日志,显示已注册的钩子和回调函数名称
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
class EsSearchView(SearchView):
def get_context(self):
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
def run_action(hook_name: str, *args, **kwargs):
"""
执行一个 Action Hook动作钩子
return context
Action Hook 特点
- 不需要返回值
- 按顺序执行所有注册到该钩子上的回调函数
- 通常用于在特定事件发生时执行副作用操作
参数:
hook_name (str): 要执行的钩子名称
*args: 传递给回调函数的位置参数
**kwargs: 传递给回调函数的关键字参数
@csrf_exempt
def fileupload(request):
功能:
依次执行所有注册到指定钩子上的回调函数
每个回调函数独立执行一个回调函数的异常不会影响其他回调函数的执行
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
# 检查指定的钩子是否存在已注册的回调函数
if hook_name in _hooks:
# 记录调试日志,显示正在执行的动作钩子
logger.debug(f"Running action hook '{hook_name}'")
# 遍历并执行所有注册到该钩子的回调函数
for callback in _hooks[hook_name]:
try:
# 执行回调函数,传递参数
callback(*args, **kwargs)
except Exception as e:
# 如果回调函数执行出错,记录错误日志但继续执行其他回调函数
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
if request.method == 'POST':
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
url = static(savepath)
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post")
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
url = request.get_full_path()
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'},
status=404)
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
return render(
request, template_name, {
'message': _('Sorry, you do not have permission to access this page?'),
'statuscode': '403'}, status=403)
def clean_cache_view(request):
cache.clear()
return HttpResponse('ok')
执行一个 Filter Hook过滤器钩子
Filter Hook 特点
- 需要处理并返回一个值
- 将值依次传递给所有注册的回调函数进行处理
- 每个回调函数的返回值作为下一个回调函数的输入
参数:
hook_name (str): 要执行的钩子名称
value: 需要被处理的初始值
*args: 传递给回调函数的位置参数
**kwargs: 传递给回调函数的关键字参数
返回:
处理后的最终值经过所有回调函数处理的结果
功能:
将初始值依次传递给所有注册到指定钩子的回调函数
每个回调函数处理值并返回处理结果作为下一个回调函数的输入
如果某个回调函数执行出错记录错误但继续执行其他回调函数
"""
# 检查指定的钩子是否存在已注册的回调函数
if hook_name in _hooks:
# 记录调试日志,显示正在应用的过滤器钩子
logger.debug(f"Applying filter hook '{hook_name}'")
# 遍历所有注册到该钩子的回调函数
for callback in _hooks[hook_name]:
try:
# 将当前值传递给回调函数处理,并将返回值作为新的值
# 实现链式处理value = callback3(callback2(callback1(value)))
value = callback(value, *args, **kwargs)
except Exception as e:
# 如果回调函数执行出错,记录错误日志但继续执行其他回调函数
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
# 返回经过所有回调函数处理后的最终值
return value

@ -1,48 +1,63 @@
version: '3'
# docker-compose.es.yml - Docker Compose 配置文件,用于定义和运行多容器 Docker 应用程序
# 此配置文件包含了 Elasticsearch、Kibana、DjangoBlog、MySQL 和 Memcached 服务
version: '3' # 指定 Docker Compose 文件格式版本为 3
# 定义服务部分
services:
# Elasticsearch 服务配置
es:
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
container_name: es
restart: always
image: liangliangyy/elasticsearch-analysis-ik:8.6.1 # 使用包含 IK 分词器的 Elasticsearch 镜像
container_name: es # 指定容器名称为 es
restart: always # 设置容器重启策略:总是重启
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- discovery.type=single-node # 设置为单节点发现模式,适用于开发环境
- "ES_JAVA_OPTS=-Xms512m -Xmx512m" # 设置 JVM 堆内存大小,初始和最大堆都为 512MB
ports:
- 9200:9200
- 9200:9200 # 映射端口:主机端口 9200 映射到容器端口 9200
volumes:
# 挂载数据卷:将主机的 ./bin/datas/es/ 目录映射到容器的数据目录
- ./bin/datas/es/:/usr/share/elasticsearch/data/
# Kibana 服务配置Elasticsearch 的可视化工具)
kibana:
image: kibana:8.6.1
restart: always
container_name: kibana
image: kibana:8.6.1 # 使用官方 Kibana 镜像版本 8.6.1
restart: always # 设置容器重启策略:总是重启
container_name: kibana # 指定容器名称为 kibana
ports:
- 5601:5601
- 5601:5601 # 映射端口:主机端口 5601 映射到容器端口 5601Kibana 默认端口)
environment:
# 配置 Kibana 连接的 Elasticsearch 地址
- ELASTICSEARCH_HOSTS=http://es:9200
# DjangoBlog 应用服务配置
djangoblog:
build: .
restart: always
build: . # 从当前目录构建 Docker 镜像
restart: always # 设置容器重启策略:总是重启
# 容器启动时执行的命令:运行 docker_start.sh 脚本
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
ports:
- "8000:8000"
- "8000:8000" # 映射端口:主机端口 8000 映射到容器端口 8000Django 默认端口)
volumes:
# 挂载静态文件目录:将主机的 collectedstatic 目录映射到容器内的静态文件目录
- ./collectedstatic:/code/djangoblog/collectedstatic
# 挂载上传文件目录:将主机的 uploads 目录映射到容器内的上传文件目录
- ./uploads:/code/djangoblog/uploads
environment:
- DJANGO_MYSQL_DATABASE=djangoblog
- DJANGO_MYSQL_USER=root
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
- DJANGO_MYSQL_HOST=db
- DJANGO_MYSQL_PORT=3306
- DJANGO_MEMCACHED_LOCATION=memcached:11211
- DJANGO_ELASTICSEARCH_HOST=es:9200
# 配置 Django 应用连接 MySQL 数据库的环境变量
- DJANGO_MYSQL_DATABASE=djangoblog # 数据库名称
- DJANGO_MYSQL_USER=root # 数据库用户名
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E # 数据库密码
- DJANGO_MYSQL_HOST=db # 数据库主机地址
- DJANGO_MYSQL_PORT=3306 # 数据库端口
# 配置 Django 应用连接 Memcached 的环境变量
- DJANGO_MEMCACHED_LOCATION=memcached:11211 # Memcached 地址和端口
# 配置 Django 应用连接 Elasticsearch 的环境变量
- DJANGO_ELASTICSEARCH_HOST=es:9200 # Elasticsearch 地址和端口
links:
- db
- memcached
# 链接到其他服务容器(已弃用,建议使用 networks
- db # 链接到 db 服务
- memcached # 链接到 memcached 服务
depends_on:
- db
container_name: djangoblog
- db # 指定依赖关系djangoblog 服务依赖于 db 服务db 启动后才会启动 djangoblog
container_name: djangoblog # 指定容器名称为 djangoblog

@ -1,60 +1,82 @@
version: '3'
# docker-compose.yml - Docker Compose 配置文件,用于定义和运行多容器 Docker 应用程序
# 此配置文件包含了 MySQL、Redis、DjangoBlog、Nginx 等服务
version: '3' # 指定 Docker Compose 文件格式版本为 3
# 定义服务部分
services:
# MySQL 数据库服务配置
db:
image: mysql:latest
restart: always
image: mysql:latest # 使用最新版本的 MySQL 官方镜像
restart: always # 设置容器重启策略:总是重启
environment:
- MYSQL_DATABASE=djangoblog
- MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E
- MYSQL_DATABASE=djangoblog # 创建数据库时默认创建的数据库名称
- MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E # 设置 MySQL root 用户密码
ports:
- 3306:3306
- 3306:3306 # 映射端口:主机端口 3306 映射到容器端口 3306MySQL 默认端口)
volumes:
# 挂载数据卷:将主机的 ./bin/datas/mysql/ 目录映射到容器的 MySQL 数据目录
# 用于持久化存储数据库数据,避免容器删除后数据丢失
- ./bin/datas/mysql/:/var/lib/mysql
depends_on:
- redis
container_name: db
- redis # 指定依赖关系db 服务依赖于 redis 服务
container_name: db # 指定容器名称为 db
# DjangoBlog 应用服务配置
djangoblog:
build:
context: ../../
restart: always
context: ../../ # 指定构建上下文路径为上级目录的上级目录
restart: always # 设置容器重启策略:总是重启
# 容器启动时执行的命令:运行 docker_start.sh 脚本启动 Django 应用
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
ports:
- "8000:8000"
- "8000:8000" # 映射端口:主机端口 8000 映射到容器端口 8000Django 默认端口)
volumes:
# 挂载静态文件目录:将主机的 collectedstatic 目录映射到容器内的静态文件目录
- ./collectedstatic:/code/djangoblog/collectedstatic
# 挂载日志目录:将主机的 logs 目录映射到容器内的日志目录
- ./logs:/code/djangoblog/logs
# 挂载上传文件目录:将主机的 uploads 目录映射到容器内的上传文件目录
- ./uploads:/code/djangoblog/uploads
environment:
- DJANGO_MYSQL_DATABASE=djangoblog
- DJANGO_MYSQL_USER=root
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
- DJANGO_MYSQL_HOST=db
- DJANGO_MYSQL_PORT=3306
- DJANGO_REDIS_URL=redis:6379
# 配置 Django 应用连接 MySQL 数据库的环境变量
- DJANGO_MYSQL_DATABASE=djangoblog # 数据库名称
- DJANGO_MYSQL_USER=root # 数据库用户名
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E # 数据库密码
- DJANGO_MYSQL_HOST=db # 数据库主机地址(对应 db 服务)
- DJANGO_MYSQL_PORT=3306 # 数据库端口
# 配置 Django 应用连接 Redis 的环境变量
- DJANGO_REDIS_URL=redis:6379 # Redis 地址和端口(对应 redis 服务)
links:
- db
- redis
# 链接到其他服务容器links 已弃用,建议使用 networks 和 depends_on
- db # 链接到 db 服务
- redis # 链接到 redis 服务
depends_on:
- db
container_name: djangoblog
- db # 指定依赖关系djangoblog 服务依赖于 db 服务db 启动后才会启动 djangoblog
container_name: djangoblog # 指定容器名称为 djangoblog
# Nginx Web 服务器服务配置
nginx:
restart: always
image: nginx:latest
restart: always # 设置容器重启策略:总是重启
image: nginx:latest # 使用最新版本的 Nginx 官方镜像
ports:
- "80:80"
- "443:443"
- "80:80" # 映射 HTTP 端口:主机端口 80 映射到容器端口 80
- "443:443" # 映射 HTTPS 端口:主机端口 443 映射到容器端口 443
volumes:
# 挂载 Nginx 配置文件:将主机的 nginx.conf 映射到容器内的配置文件位置
- ./bin/nginx.conf:/etc/nginx/nginx.conf
# 挂载静态文件目录:将主机的 collectedstatic 目录映射到容器内的静态文件目录
# 使 Nginx 可以直接提供静态文件服务
- ./collectedstatic:/code/djangoblog/collectedstatic
links:
- djangoblog:djangoblog
container_name: nginx
# 链接到 djangoblog 服务容器
- djangoblog:djangoblog # 链接到 djangoblog 服务,并设置别名为 djangoblog
container_name: nginx # 指定容器名称为 nginx
# Redis 缓存服务配置
redis:
restart: always
image: redis:latest
container_name: redis
restart: always # 设置容器重启策略:总是重启
image: redis:latest # 使用最新版本的 Redis 官方镜像
container_name: redis # 指定容器名称为 redis
ports:
- "6379:6379"
- "6379:6379" # 映射端口:主机端口 6379 映射到容器端口 6379Redis 默认端口)

@ -1,31 +1,40 @@
#!/usr/bin/env bash
NAME="djangoblog"
DJANGODIR=/code/djangoblog
USER=root
GROUP=root
NUM_WORKERS=1
DJANGO_WSGI_MODULE=djangoblog.wsgi
# 指定脚本使用 bash 解释器执行
# 定义环境变量
NAME="djangoblog" # 应用名称
DJANGODIR=/code/djangoblog # Django 项目目录路径
USER=root # 运行进程的用户
GROUP=root # 运行进程的用户组
NUM_WORKERS=1 # Gunicorn 工作进程数量
DJANGO_WSGI_MODULE=djangoblog.wsgi # Django WSGI 模块路径
# 输出启动信息,显示当前运行用户
echo "Starting $NAME as `whoami`"
# 切换到 Django 项目目录
cd $DJANGODIR
# 设置 Python 路径,将项目目录添加到 PYTHONPATH 环境变量中
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
python manage.py makemigrations && \
python manage.py migrate && \
python manage.py collectstatic --noinput && \
python manage.py compress --force && \
python manage.py build_index && \
python manage.py compilemessages || exit 1
# 执行 Django 管理命令,使用逻辑与运算符 (&&) 确保每一步都成功执行
# 如果任何一步失败,使用 || exit 1 确保脚本退出
python manage.py makemigrations && \ # 创建数据库迁移文件
python manage.py migrate && \ # 执行数据库迁移
python manage.py collectstatic --noinput && \ # 收集静态文件,不提示输入
python manage.py compress --force && \ # 强制压缩静态文件Django Compressor
python manage.py build_index && \ # 构建搜索索引(可能用于 Elasticsearch
python manage.py compilemessages || exit 1 # 编译国际化消息文件
# 使用 Gunicorn 启动 Django 应用
# exec 命令会替换当前 shell 进程,使 Gunicorn 成为容器的主进程
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user=$USER --group=$GROUP \
--bind 0.0.0.0:8000 \
--log-level=debug \
--log-file=- \
--worker-class gevent \
--threads 4
--name $NAME \ # 设置进程名称
--workers $NUM_WORKERS \ # 设置工作进程数量
--user=$USER --group=$GROUP \ # 设置运行进程的用户和组
--bind 0.0.0.0:8000 \ # 绑定地址和端口(监听所有网络接口的 8000 端口)
--log-level=debug \ # 设置日志级别为 debug
--log-file=- \ # 将日志输出到标准输出(控制台)
--worker-class gevent \ # 使用 gevent 作为工作进程类(异步 workers
--threads 4 # 每个工作进程使用的线程数

@ -1,119 +1,196 @@
apiVersion: v1
kind: ConfigMap
# configmap.yaml - Kubernetes ConfigMap 资源配置文件
# 用于存储非机密性的配置数据,以键值对的形式保存配置信息
# 第一个 ConfigMap用于配置 Nginx 服务器
apiVersion: v1 # Kubernetes API 版本
kind: ConfigMap # 资源类型为 ConfigMap
metadata:
name: web-nginx-config
namespace: djangoblog
name: web-nginx-config # ConfigMap 名称
namespace: djangoblog # 所属命名空间
data:
# Nginx 主配置文件内容
nginx.conf: |
# 定义运行 nginx 的用户
user nginx;
# 工作进程数auto 表示自动根据 CPU 核心数设置
worker_processes auto;
# 错误日志文件路径和级别
error_log /var/log/nginx/error.log notice;
# nginx 进程 PID 文件路径
pid /var/run/nginx.pid;
# 事件模块配置
events {
# 单个工作进程的最大并发连接数
worker_connections 1024;
# 允许一次接受所有新连接
multi_accept on;
# 使用 epoll I/O 模型Linux 系统下性能更好)
use epoll;
}
# HTTP 模块配置
http {
# 包含 MIME 类型定义文件
include /etc/nginx/mime.types;
# 默认文件类型
default_type application/octet-stream;
# 定义日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# 访问日志配置
access_log /var/log/nginx/access.log main;
# 启用 sendfile 优化文件传输
sendfile on;
# 保持连接的超时时间
keepalive_timeout 65;
# 启用 gzip 压缩
gzip on;
# 对 IE6 禁用 gzip
gzip_disable "msie6";
# gzip 压缩相关配置
gzip_vary on;
gzip_proxied any;
# gzip 压缩级别1-9数字越大压缩率越高但消耗更多 CPU
gzip_comp_level 8;
# gzip 缓冲区设置
gzip_buffers 16 8k;
# gzip HTTP 版本
gzip_http_version 1.1;
# 启用 gzip 压缩的文件类型
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;
}
# DjangoBlog 网站配置
djangoblog.conf: |
# 主服务器配置块
server {
# 服务器域名
server_name lylinux.net;
# 网站根目录
root /code/djangoblog/collectedstatic/;
# 监听端口
listen 80;
# 保持连接超时时间
keepalive_timeout 70;
# 静态文件处理位置块
location /static/ {
# 设置过期时间最大值(浏览器缓存)
expires max;
# 静态文件别名路径
alias /code/djangoblog/collectedstatic/;
}
# 特殊文件处理位置块(如 robots.txt、favicon.ico 等)
location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ {
root /resource/djangopub;
expires 1d;
access_log off;
error_log off;
# 文件根目录
root /resource/djangopub;
# 过期时间 1 天
expires 1d;
# 关闭访问日志
access_log off;
# 关闭错误日志
error_log off;
}
# 根路径处理位置块
location / {
# 设置代理请求头信息
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
# 关闭代理重定向
proxy_redirect off;
# 如果请求的文件不存在,则代理到 Django 应用
if (!-f $request_filename) {
proxy_pass http://djangoblog:8000;
break;
}
}
}
# 重定向服务器配置块(将 www 重定向到主域名)
server {
server_name www.lylinux.net;
listen 80;
# 301 永久重定向到主域名
return 301 https://lylinux.net$request_uri;
}
# 资源服务器配置
resource.lylinux.net.conf: |
server {
# 默认首页文件
index index.html index.htm;
# 资源服务器域名
server_name resource.lylinux.net;
# 网站根目录
root /resource/;
# DjangoBlog 静态文件路径配置
location /djangoblog/ {
alias /code/djangoblog/collectedstatic/;
}
# 关闭访问日志
access_log off;
# 关闭错误日志
error_log off;
# 包含资源服务器的额外配置
include lylinux/resource.conf;
}
# 资源配置文件内容
lylinux.resource.conf: |
# 设置最大过期时间
expires max;
# 关闭访问日志
access_log off;
# 关闭未找到文件的日志记录
log_not_found off;
# 添加 Pragma 响应头(公共缓存)
add_header Pragma public;
# 添加 Cache-Control 响应头(公共缓存)
add_header Cache-Control "public";
# 添加跨域访问控制响应头(允许所有域访问)
add_header "Access-Control-Allow-Origin" "*";
---
# 第二个 ConfigMap用于配置 DjangoBlog 应用环境变量
apiVersion: v1
kind: ConfigMap
metadata:
name: djangoblog-env
namespace: djangoblog
name: djangoblog-env # ConfigMap 名称
namespace: djangoblog # 所属命名空间
data:
DJANGO_MYSQL_DATABASE: djangoblog
DJANGO_MYSQL_USER: db_user
DJANGO_MYSQL_PASSWORD: db_password
DJANGO_MYSQL_HOST: db_host
DJANGO_MYSQL_PORT: db_port
DJANGO_REDIS_URL: "redis:6379"
DJANGO_DEBUG: "False"
MYSQL_ROOT_PASSWORD: db_password
MYSQL_DATABASE: djangoblog
MYSQL_PASSWORD: db_password
DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Django 数据库配置
DJANGO_MYSQL_DATABASE: djangoblog # 数据库名称
DJANGO_MYSQL_USER: db_user # 数据库用户名
DJANGO_MYSQL_PASSWORD: db_password # 数据库密码
DJANGO_MYSQL_HOST: db_host # 数据库主机地址
DJANGO_MYSQL_PORT: db_port # 数据库端口
# Django Redis 配置
DJANGO_REDIS_URL: "redis:6379" # Redis 服务器地址和端口
# Django 调试模式配置
DJANGO_DEBUG: "False" # 关闭调试模式(生产环境应设为 False
# MySQL 配置
MYSQL_ROOT_PASSWORD: db_password # MySQL root 用户密码
MYSQL_DATABASE: djangoblog # MySQL 数据库名称
MYSQL_PASSWORD: db_password # MySQL 用户密码
# Django 密钥(用于加密签名等安全功能)
DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

@ -1,121 +1,128 @@
apiVersion: apps/v1
kind: Deployment
# deployment.yaml - Kubernetes Deployment 资源配置文件
# 用于定义应用的部署配置,包括副本数量、容器镜像、资源限制等
---
# 第一个 DeploymentDjangoBlog 应用部署配置
apiVersion: apps/v1 # Kubernetes API 版本
kind: Deployment # 资源类型为 Deployment
metadata:
name: djangoblog
namespace: djangoblog
name: djangoblog # Deployment 名称
namespace: djangoblog # 所属命名空间
labels:
app: djangoblog
app: djangoblog # 标签,用于标识该 Deployment
spec:
replicas: 3
replicas: 3 # 副本数量,运行 3 个 Pod 实例
selector:
matchLabels:
app: djangoblog
template:
app: djangoblog # 选择器,匹配标签为 app=djangoblog 的 Pod
template: # Pod 模板定义
metadata:
labels:
app: djangoblog
spec:
app: djangoblog # Pod 标签
spec: # Pod 规格定义
containers:
- name: djangoblog
image: liangliangyy/djangoblog:latest
imagePullPolicy: Always
- name: djangoblog # 容器名称
image: liangliangyy/djangoblog:latest # 使用的镜像
imagePullPolicy: Always # 镜像拉取策略:总是拉取最新镜像
ports:
- containerPort: 8000
- containerPort: 8000 # 容器暴露的端口
envFrom:
- configMapRef:
name: djangoblog-env
readinessProbe:
name: djangoblog-env # 从名为 djangoblog-env 的 ConfigMap 注入环境变量
readinessProbe: # 就绪探针:检查应用是否准备好接收流量
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
livenessProbe:
path: / # 检查路径
port: 8000 # 检查端口
initialDelaySeconds: 10 # 初始延迟时间(秒)
periodSeconds: 30 # 检查间隔(秒)
livenessProbe: # 存活探针:检查应用是否正常运行
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
resources:
path: / # 检查路径
port: 8000 # 检查端口
initialDelaySeconds: 10 # 初始延迟时间(秒)
periodSeconds: 30 # 检查间隔(秒)
resources: # 资源限制和请求
requests:
cpu: 10m
memory: 100Mi
cpu: 10m # CPU 请求10 毫核
memory: 100Mi # 内存请求100 兆字节
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: djangoblog
mountPath: /code/djangoblog/collectedstatic
cpu: "2" # CPU 限制2 核
memory: 2Gi # 内存限制2 吉字节
volumeMounts: # 卷挂载点
- name: djangoblog # 卷名称
mountPath: /code/djangoblog/collectedstatic # 挂载路径
- name: resource
mountPath: /resource
volumes:
volumes: # 卷定义
- name: djangoblog
persistentVolumeClaim:
claimName: djangoblog-pvc
claimName: djangoblog-pvc # 使用名为 djangoblog-pvc 的持久卷声明
- name: resource
persistentVolumeClaim:
claimName: resource-pvc
claimName: resource-pvc # 使用名为 resource-pvc 的持久卷声明
---
# 第二个 DeploymentRedis 缓存服务部署配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: djangoblog
name: redis # Deployment 名称
namespace: djangoblog # 所属命名空间
labels:
app: redis
app: redis # 标签
spec:
replicas: 1
replicas: 1 # 副本数量1 个实例Redis 通常只需要一个实例)
selector:
matchLabels:
app: redis
app: redis # 选择标签为 app=redis 的 Pod
template:
metadata:
labels:
app: redis
app: redis # Pod 标签
spec:
containers:
- name: redis
image: redis:latest
imagePullPolicy: IfNotPresent
- name: redis # 容器名称
image: redis:latest # 使用最新版 Redis 镜像
imagePullPolicy: IfNotPresent # 镜像拉取策略:本地不存在时才拉取
ports:
- containerPort: 6379
- containerPort: 6379 # Redis 默认端口
resources:
requests:
cpu: 10m
memory: 100Mi
cpu: 10m # CPU 请求
memory: 100Mi # 内存请求
limits:
cpu: 200m
memory: 2Gi
cpu: 200m # CPU 限制200 毫核
memory: 2Gi # 内存限制
---
# 第三个 DeploymentMySQL 数据库部署配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
namespace: djangoblog
name: db # Deployment 名称
namespace: djangoblog # 所属命名空间
labels:
app: db
app: db # 标签
spec:
replicas: 1
replicas: 1 # 副本数量1 个实例(数据库通常只需要一个实例)
selector:
matchLabels:
app: db
app: db # 选择标签为 app=db 的 Pod
template:
metadata:
labels:
app: db
app: db # Pod 标签
spec:
containers:
- name: db
image: mysql:latest
imagePullPolicy: IfNotPresent
- name: db # 容器名称
image: mysql:latest # 使用最新版 MySQL 镜像
imagePullPolicy: IfNotPresent # 镜像拉取策略
ports:
- containerPort: 3306
- containerPort: 3306 # MySQL 默认端口
envFrom:
- configMapRef:
name: djangoblog-env
readinessProbe:
name: djangoblog-env # 从 ConfigMap 注入环境变量
readinessProbe: # 就绪探针:使用 mysqladmin 检查数据库是否就绪
exec:
command:
- mysqladmin
@ -124,10 +131,10 @@ spec:
- "127.0.0.1"
- "-u"
- "root"
- "-p$MYSQL_ROOT_PASSWORD"
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
- "-p$MYSQL_ROOT_PASSWORD" # 使用环境变量中的密码
initialDelaySeconds: 10 # 初始延迟时间
periodSeconds: 10 # 检查间隔
livenessProbe: # 存活探针:使用 mysqladmin 检查数据库是否存活
exec:
command:
- mysqladmin
@ -136,139 +143,141 @@ spec:
- "127.0.0.1"
- "-u"
- "root"
- "-p$MYSQL_ROOT_PASSWORD"
initialDelaySeconds: 10
periodSeconds: 10
- "-p$MYSQL_ROOT_PASSWORD" # 使用环境变量中的密码
initialDelaySeconds: 10 # 初始延迟时间
periodSeconds: 10 # 检查间隔
resources:
requests:
cpu: 10m
memory: 100Mi
cpu: 10m # CPU 请求
memory: 100Mi # 内存请求
limits:
cpu: "2"
memory: 2Gi
cpu: "2" # CPU 限制
memory: 2Gi # 内存限制
volumeMounts:
- name: db-data
mountPath: /var/lib/mysql
mountPath: /var/lib/mysql # MySQL 数据存储路径
volumes:
- name: db-data
persistentVolumeClaim:
claimName: db-pvc
claimName: db-pvc # 使用名为 db-pvc 的持久卷声明
---
# 第四个 DeploymentNginx 反向代理部署配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: djangoblog
name: nginx # Deployment 名称
namespace: djangoblog # 所属命名空间
labels:
app: nginx
app: nginx # 标签
spec:
replicas: 1
replicas: 1 # 副本数量1 个实例
selector:
matchLabels:
app: nginx
app: nginx # 选择标签为 app=nginx 的 Pod
template:
metadata:
labels:
app: nginx
app: nginx # Pod 标签
spec:
containers:
- name: nginx
image: nginx:latest
imagePullPolicy: IfNotPresent
- name: nginx # 容器名称
image: nginx:latest # 使用最新版 Nginx 镜像
imagePullPolicy: IfNotPresent # 镜像拉取策略
ports:
- containerPort: 80
- containerPort: 80 # HTTP 端口
resources:
requests:
cpu: 10m
memory: 100Mi
cpu: 10m # CPU 请求
memory: 100Mi # 内存请求
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
cpu: "2" # CPU 限制
memory: 2Gi # 内存限制
volumeMounts: # 挂载多个配置文件和数据卷
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
mountPath: /etc/nginx/nginx.conf # 挂载主配置文件
subPath: nginx.conf # 指定 ConfigMap 中的键名
- name: nginx-config
mountPath: /etc/nginx/conf.d/default.conf
mountPath: /etc/nginx/conf.d/default.conf # 挂载默认站点配置
subPath: djangoblog.conf
- 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
- name: nginx-config
mountPath: /etc/nginx/lylinux/resource.conf
mountPath: /etc/nginx/lylinux/resource.conf # 挂载资源配置
subPath: lylinux.resource.conf
- name: djangoblog-pvc
mountPath: /code/djangoblog/collectedstatic
mountPath: /code/djangoblog/collectedstatic # 挂载 Django 静态文件
- name: resource-pvc
mountPath: /resource
mountPath: /resource # 挂载资源文件
volumes:
- name: nginx-config
configMap:
name: web-nginx-config
name: web-nginx-config # 使用名为 web-nginx-config 的 ConfigMap
- name: djangoblog-pvc
persistentVolumeClaim:
claimName: djangoblog-pvc
claimName: djangoblog-pvc # 使用 DjangoBlog 持久卷声明
- name: resource-pvc
persistentVolumeClaim:
claimName: resource-pvc
claimName: resource-pvc # 使用资源持久卷声明
---
# 第五个 DeploymentElasticsearch 搜索引擎部署配置
apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
namespace: djangoblog
name: elasticsearch # Deployment 名称
namespace: djangoblog # 所属命名空间
labels:
app: elasticsearch
app: elasticsearch # 标签
spec:
replicas: 1
replicas: 1 # 副本数量1 个实例
selector:
matchLabels:
app: elasticsearch
app: elasticsearch # 选择标签为 app=elasticsearch 的 Pod
template:
metadata:
labels:
app: elasticsearch
app: elasticsearch # Pod 标签
spec:
containers:
- name: elasticsearch
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
imagePullPolicy: IfNotPresent
env:
- name: elasticsearch # 容器名称
image: liangliangyy/elasticsearch-analysis-ik:8.6.1 # 使用包含 IK 分词器的 Elasticsearch 镜像
imagePullPolicy: IfNotPresent # 镜像拉取策略
env: # 环境变量配置
- name: discovery.type
value: single-node
value: single-node # 设置为单节点模式
- name: ES_JAVA_OPTS
value: "-Xms256m -Xmx256m"
value: "-Xms256m -Xmx256m" # 设置 JVM 堆内存大小
- name: xpack.security.enabled
value: "false"
value: "false" # 禁用安全功能
- name: xpack.monitoring.templates.enabled
value: "false"
value: "false" # 禁用监控模板
ports:
- containerPort: 9200
- containerPort: 9200 # Elasticsearch REST API 端口
resources:
requests:
cpu: 10m
memory: 100Mi
cpu: 10m # CPU 请求
memory: 100Mi # 内存请求
limits:
cpu: "2"
memory: 2Gi
readinessProbe:
cpu: "2" # CPU 限制
memory: 2Gi # 内存限制
readinessProbe: # 就绪探针
httpGet:
path: /
port: 9200
initialDelaySeconds: 15
periodSeconds: 30
livenessProbe:
path: / # 检查路径
port: 9200 # 检查端口
initialDelaySeconds: 15 # 初始延迟时间
periodSeconds: 30 # 检查间隔
livenessProbe: # 存活探针
httpGet:
path: /
port: 9200
initialDelaySeconds: 15
periodSeconds: 30
path: / # 检查路径
port: 9200 # 检查端口
initialDelaySeconds: 15 # 初始延迟时间
periodSeconds: 30 # 检查间隔
volumeMounts:
- name: elasticsearch-data
mountPath: /usr/share/elasticsearch/data/
mountPath: /usr/share/elasticsearch/data/ # Elasticsearch 数据存储路径
volumes:
- name: elasticsearch-data
persistentVolumeClaim:
claimName: elasticsearch-pvc
claimName: elasticsearch-pvc # 使用名为 elasticsearch-pvc 的持久卷声明

@ -1,17 +1,20 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
# gateway.yaml - Kubernetes Ingress 资源配置文件
# 用于配置外部访问集群内服务的路由规则,是 Kubernetes 中的 API 网关
apiVersion: networking.k8s.io/v1 # Kubernetes API 版本,使用 networking.k8s.io/v1 API 组
kind: Ingress # 资源类型为 Ingress入口网关
metadata:
name: nginx
namespace: djangoblog
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
name: nginx # Ingress 资源名称
namespace: djangoblog # 所属命名空间,将此 Ingress 部署到 djangoblog 命名空间中
spec: # Ingress 规格定义
ingressClassName: nginx # 指定使用的 Ingress 控制器类名为 nginx
rules: # 路由规则定义
- http: # HTTP 路由规则
paths: # 路径匹配规则列表
- path: / # 匹配的路径前缀,这里匹配所有路径
pathType: Prefix # 路径匹配类型为前缀匹配Prefix
backend: # 后端服务配置
service: # 服务配置
name: nginx # 后端服务名称,将流量转发到名为 nginx 的服务
port: # 端口配置
number: 80 # 服务端口号,转发到 nginx 服务的 80 端口

@ -1,94 +1,102 @@
apiVersion: v1
kind: PersistentVolume
# pv.yaml - Kubernetes PersistentVolume 资源配置文件
# 用于定义持久化卷PV为 Pod 提供持久化存储能力
---
# 第一个 PersistentVolume用于数据库存储
apiVersion: v1 # Kubernetes API 版本
kind: PersistentVolume # 资源类型为 PersistentVolume持久化卷
metadata:
name: local-pv-db
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/local-storage-db
nodeAffinity:
name: local-pv-db # PV 名称
spec: # PV 规格定义
capacity: # 容量配置
storage: 10Gi # 存储容量为 10GB
volumeMode: Filesystem # 卷模式为文件系统(与块设备相对)
accessModes: # 访问模式
- ReadWriteOnce # 单节点读写模式(同一时间只能被一个节点以读写方式挂载)
persistentVolumeReclaimPolicy: Retain # PV 回收策略:保留(删除 PVC 时不会删除 PV 和数据)
storageClassName: local-storage # 存储类名称,用于与 PVC 匹配
local: # 本地卷配置
path: /mnt/local-storage-db # 本地存储路径
nodeAffinity: # 节点亲和性配置,指定 PV 只能在特定节点上使用
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
- key: kubernetes.io/hostname # 匹配节点的主机名标签
operator: In # 操作符为 In在指定值中
values:
- master
- master # 指定只能在名为 master 的节点上使用此 PV
---
# 第二个 PersistentVolume用于 DjangoBlog 应用存储
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-djangoblog
name: local-pv-djangoblog # PV 名称
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
storage: 5Gi # 存储容量为 5GB比数据库小
volumeMode: Filesystem # 卷模式为文件系统
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
- ReadWriteOnce # 单节点读写模式
persistentVolumeReclaimPolicy: Retain # PV 回收策略:保留
storageClassName: local-storage # 存储类名称
local:
path: /mnt/local-storage-djangoblog
nodeAffinity:
path: /mnt/local-storage-djangoblog # 本地存储路径
nodeAffinity: # 节点亲和性配置
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
- key: kubernetes.io/hostname # 匹配节点的主机名标签
operator: In # 操作符为 In
values:
- master
- master # 指定只能在名为 master 的节点上使用此 PV
---
# 第三个 PersistentVolume用于资源文件存储
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-resource
name: local-pv-resource # PV 名称
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
storage: 5Gi # 存储容量为 5GB
volumeMode: Filesystem # 卷模式为文件系统
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
- ReadWriteOnce # 单节点读写模式
persistentVolumeReclaimPolicy: Retain # PV 回收策略:保留
storageClassName: local-storage # 存储类名称
local:
path: /mnt/resource/
nodeAffinity:
path: /mnt/resource/ # 本地存储路径
nodeAffinity: # 节点亲和性配置
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
- key: kubernetes.io/hostname # 匹配节点的主机名标签
operator: In # 操作符为 In
values:
- master
- master # 指定只能在名为 master 的节点上使用此 PV
---
# 第四个 PersistentVolume用于 Elasticsearch 存储
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-elasticsearch
name: local-pv-elasticsearch # PV 名称
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
storage: 5Gi # 存储容量为 5GB
volumeMode: Filesystem # 卷模式为文件系统
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
- ReadWriteOnce # 单节点读写模式
persistentVolumeReclaimPolicy: Retain # PV 回收策略:保留
storageClassName: local-storage # 存储类名称
local:
path: /mnt/local-storage-elasticsearch
nodeAffinity:
path: /mnt/local-storage-elasticsearch # 本地存储路径
nodeAffinity: # 节点亲和性配置
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
- key: kubernetes.io/hostname # 匹配节点的主机名标签
operator: In # 操作符为 In
values:
- master
- master # 指定只能在名为 master 的节点上使用此 PV

@ -1,60 +1,66 @@
apiVersion: v1
kind: PersistentVolumeClaim
# pvc.yaml - Kubernetes PersistentVolumeClaim 资源配置文件
# 用于定义持久化卷声明PVC是用户对存储资源的请求
---
# 第一个 PersistentVolumeClaim为数据库服务申请存储空间
apiVersion: v1 # Kubernetes API 版本
kind: PersistentVolumeClaim # 资源类型为 PersistentVolumeClaim持久化卷声明
metadata:
name: db-pvc
namespace: djangoblog
spec:
storageClassName: local-storage
volumeName: local-pv-db
accessModes:
- ReadWriteOnce
resources:
name: db-pvc # PVC 名称
namespace: djangoblog # 所属命名空间
spec: # PVC 规格定义
storageClassName: local-storage # 指定存储类名称,用于匹配相应的 PV
volumeName: local-pv-db # 直接绑定到指定的 PVlocal-pv-db
accessModes: # 访问模式
- ReadWriteOnce # 单节点读写模式(同一时间只能被一个节点以读写方式挂载)
resources: # 资源请求
requests:
storage: 10Gi
storage: 10Gi # 请求存储空间为 10GB
---
# 第二个 PersistentVolumeClaim为 DjangoBlog 应用申请存储空间
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: djangoblog-pvc
namespace: djangoblog
name: djangoblog-pvc # PVC 名称
namespace: djangoblog # 所属命名空间
spec:
volumeName: local-pv-djangoblog
storageClassName: local-storage
volumeName: local-pv-djangoblog # 直接绑定到指定的 PVlocal-pv-djangoblog
storageClassName: local-storage # 存储类名称
accessModes:
- ReadWriteOnce
- ReadWriteOnce # 单节点读写模式
resources:
requests:
storage: 5Gi
storage: 5Gi # 请求存储空间为 5GB
---
# 第三个 PersistentVolumeClaim为资源文件申请存储空间
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: resource-pvc
namespace: djangoblog
name: resource-pvc # PVC 名称
namespace: djangoblog # 所属命名空间
spec:
volumeName: local-pv-resource
storageClassName: local-storage
volumeName: local-pv-resource # 直接绑定到指定的 PVlocal-pv-resource
storageClassName: local-storage # 存储类名称
accessModes:
- ReadWriteOnce
- ReadWriteOnce # 单节点读写模式
resources:
requests:
storage: 5Gi
storage: 5Gi # 请求存储空间为 5GB
---
# 第四个 PersistentVolumeClaim为 Elasticsearch 服务申请存储空间
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: elasticsearch-pvc
namespace: djangoblog
name: elasticsearch-pvc # PVC 名称
namespace: djangoblog # 所属命名空间
spec:
volumeName: local-pv-elasticsearch
storageClassName: local-storage
volumeName: local-pv-elasticsearch # 直接绑定到指定的 PVlocal-pv-elasticsearch
storageClassName: local-storage # 存储类名称
accessModes:
- ReadWriteOnce
- ReadWriteOnce # 单节点读写模式
resources:
requests:
storage: 5Gi
storage: 5Gi # 请求存储空间为 5GB

@ -1,80 +1,94 @@
apiVersion: v1
kind: Service
# service.yaml - Kubernetes Service 资源配置文件
# 用于定义服务,为 Pod 提供稳定的网络访问入口和负载均衡
# 注意:文件开头有一个拼写错误 "tapiVersion",应该是 "apiVersion"
---
# 第一个 ServiceDjangoBlog 应用服务
apiVersion: v1 # Kubernetes API 版本
kind: Service # 资源类型为 Service服务
metadata:
name: djangoblog
namespace: djangoblog
name: djangoblog # 服务名称
namespace: djangoblog # 所属命名空间
labels:
app: djangoblog
spec:
selector:
app: djangoblog
ports:
- protocol: TCP
port: 8000
targetPort: 8000
type: ClusterIP
app: djangoblog # 标签,用于标识该服务
spec: # 服务规格定义
selector: # 选择器,用于匹配后端 Pod
app: djangoblog # 匹配标签为 app=djangoblog 的 Pod
ports: # 端口配置
- protocol: TCP # 协议类型为 TCP
port: 8000 # 服务暴露的端口
targetPort: 8000 # 目标端口,即 Pod 中容器暴露的端口
type: ClusterIP # 服务类型为 ClusterIP仅在集群内部可访问
---
# 第二个 ServiceNginx 服务
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: djangoblog
name: nginx # 服务名称
namespace: djangoblog # 所属命名空间
labels:
app: nginx
app: nginx # 标签
spec:
selector:
app: nginx
app: nginx # 匹配标签为 app=nginx 的 Pod
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
- protocol: TCP # 协议类型为 TCP
port: 80 # 服务暴露的端口HTTP 默认端口)
targetPort: 80 # 目标端口
type: ClusterIP # 服务类型为 ClusterIP
---
# 第三个 ServiceRedis 缓存服务
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: djangoblog
name: redis # 服务名称
namespace: djangoblog # 所属命名空间
labels:
app: redis
app: redis # 标签
spec:
selector:
app: redis
app: redis # 匹配标签为 app=redis 的 Pod
ports:
- protocol: TCP
port: 6379
targetPort: 6379
type: ClusterIP
- protocol: TCP # 协议类型为 TCP
port: 6379 # 服务暴露的端口Redis 默认端口)
targetPort: 6379 # 目标端口
type: ClusterIP # 服务类型为 ClusterIP
---
# 第四个 ServiceMySQL 数据库服务
apiVersion: v1
kind: Service
metadata:
name: db
namespace: djangoblog
name: db # 服务名称
namespace: djangoblog # 所属命名空间
labels:
app: db
app: db # 标签
spec:
selector:
app: db
app: db # 匹配标签为 app=db 的 Pod
ports:
- protocol: TCP
port: 3306
targetPort: 3306
type: ClusterIP
- protocol: TCP # 协议类型为 TCP
port: 3306 # 服务暴露的端口MySQL 默认端口)
targetPort: 3306 # 目标端口
type: ClusterIP # 服务类型为 ClusterIP
---
# 第五个 ServiceElasticsearch 搜索引擎服务
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: djangoblog
name: elasticsearch # 服务名称
namespace: djangoblog # 所属命名空间
labels:
app: elasticsearch
app: elasticsearch # 标签
spec:
selector:
app: elasticsearch
app: elasticsearch # 匹配标签为 app=elasticsearch 的 Pod
ports:
- protocol: TCP
port: 9200
targetPort: 9200
type: ClusterIP
- protocol: TCP # 协议类型为 TCP
port: 9200 # 服务暴露的端口Elasticsearch REST API 默认端口)
targetPort: 9200 # 目标端口
type: ClusterIP # 服务类型为 ClusterIP

@ -1,10 +1,10 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
annotations:
storageclass.kubernetes.io/is-default-class: "true"
# 这个注解将当前存储类设置为集群的默认存储类
# 当用户创建 PVC 时,如果没有指定 storageClassName将自动使用此默认存储类
storageclass.kubernetes.io/is-default-class: "true"
# 指定使用的存储供应器
# kubernetes.io/no-provisioner 表示这是一个静态供应的存储类
# 需要管理员预先创建好 PersistentVolume (PV) 资源,而不是动态创建
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: Immediate

@ -1,50 +0,0 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
server {
root /code/djangoblog/collectedstatic/;
listen 80;
keepalive_timeout 70;
location /static/ {
expires max;
alias /code/djangoblog/collectedstatic/;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://djangoblog:8000;
break;
}
}
}
}

@ -1 +1,6 @@
# __init__.py - Django 应用初始化配置文件
# 该文件用于配置 Django 应用的默认应用配置类
# 指定 Django 应用的默认配置类
# 当 Django 启动时,会自动加载这个配置类来初始化应用
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -1,32 +1,63 @@
from django.contrib.admin import AdminSite
from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
# admin_site.py - 自定义 Django 管理站点配置文件
# 用于创建和配置自定义的 Django 管理后台界面
# 从 Django 内置模块导入相关组件
from django.contrib.admin import AdminSite # Django 管理站点基类
from django.contrib.admin.models import LogEntry # 管理日志模型
from django.contrib.sites.admin import SiteAdmin # 站点管理类
from django.contrib.sites.models import Site # 站点模型
# 从项目各个模块导入管理员类和模型
from accounts.admin import * # 账户相关管理员类
from blog.admin import * # 博客相关管理员类
from blog.models import * # 博客相关模型
from comments.admin import * # 评论相关管理员类
from comments.models import * # 评论相关模型
from djangoblog.logentryadmin import LogEntryAdmin # 自定义日志条目管理员类
from oauth.admin import * # OAuth 相关管理员类
from oauth.models import * # OAuth 相关模型
from owntracks.admin import * # OwnTracks 相关管理员类
from owntracks.models import * # OwnTracks 相关模型
from servermanager.admin import * # 服务器管理相关管理员类
from servermanager.models import * # 服务器管理相关模型
class DjangoBlogAdminSite(AdminSite):
"""
自定义 Django 管理站点类
继承自 Django AdminSite用于提供个性化的管理界面
"""
# 设置管理站点的头部标题
site_header = 'djangoblog administration'
# 设置管理站点的标题
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
"""
初始化自定义管理站点
参数:
name (str): 管理站点的名称默认为 'admin'
"""
# 调用父类的初始化方法
super().__init__(name)
def has_permission(self, request):
"""
检查用户是否有访问管理站点的权限
参数:
request: HTTP 请求对象
返回:
bool: 如果用户是超级用户则返回 True否则返回 False
"""
# 只有超级用户才能访问管理站点
return request.user.is_superuser
# 注释掉的 get_urls 方法示例,展示如何添加自定义管理页面
# def get_urls(self):
# urls = super().get_urls()
# from django.urls import path
@ -38,27 +69,37 @@ class DjangoBlogAdminSite(AdminSite):
# return urls + my_urls
# 创建自定义管理站点的实例
admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
# 在自定义管理站点中注册模型和对应的管理员类
# 博客相关模型注册
admin_site.register(Article, ArticlelAdmin) # 注册文章模型和管理员类
admin_site.register(Category, CategoryAdmin) # 注册分类模型和管理员类
admin_site.register(Tag, TagAdmin) # 注册标签模型和管理员类
admin_site.register(Links, LinksAdmin) # 注册链接模型和管理员类
admin_site.register(SideBar, SideBarAdmin) # 注册侧边栏模型和管理员类
admin_site.register(BlogSettings, BlogSettingsAdmin) # 注册博客设置模型和管理员类
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
# 服务器管理相关模型注册
admin_site.register(commands, CommandsAdmin) # 注册命令模型和管理员类
admin_site.register(EmailSendLog, EmailSendLogAdmin) # 注册邮件发送日志模型和管理员类
admin_site.register(BlogUser, BlogUserAdmin)
# 用户账户相关模型注册
admin_site.register(BlogUser, BlogUserAdmin) # 注册博客用户模型和管理员类
admin_site.register(Comment, CommentAdmin)
# 评论相关模型注册
admin_site.register(Comment, CommentAdmin) # 注册评论模型和管理员类
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
# OAuth 相关模型注册
admin_site.register(OAuthUser, OAuthUserAdmin) # 注册 OAuth 用户模型和管理员类
admin_site.register(OAuthConfig, OAuthConfigAdmin) # 注册 OAuth 配置模型和管理员类
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
# OwnTracks 相关模型注册
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) # 注册 OwnTracks 日志模型和管理员类
admin_site.register(Site, SiteAdmin)
# Django 站点框架相关模型注册
admin_site.register(Site, SiteAdmin) # 注册站点模型和管理员类
admin_site.register(LogEntry, LogEntryAdmin)
# 管理日志相关模型注册
admin_site.register(LogEntry, LogEntryAdmin) # 注册日志条目模型和自定义管理员类

@ -1,11 +1,33 @@
from django.apps import AppConfig
# apps.py - Django 应用配置文件
# 用于定义应用的配置信息和初始化逻辑
from django.apps import AppConfig # 从 Django 导入应用配置基类
class DjangoblogAppConfig(AppConfig):
"""
DjangoBlog 应用的配置类
继承自 Django AppConfig用于配置应用的元数据和初始化逻辑
"""
# 指定模型中默认的自动字段类型为 BigAutoField
# BigAutoField 是一个 64 位整数,比默认的 AutoField (32 位) 能支持更大的数据量
default_auto_field = 'django.db.models.BigAutoField'
# 应用的名称,必须与应用目录名称一致
name = 'djangoblog'
def ready(self):
"""
应用准备就绪时调用的方法
Django 启动过程中当应用注册表完全加载后执行
用于执行应用启动时需要进行的初始化操作
"""
# 调用父类的 ready 方法,确保基础初始化完成
super().ready()
# Import and load plugins here
from .plugin_manage.loader import load_plugins
load_plugins()
# 在此处导入并加载插件
# 将插件加载放在 ready 方法中可以确保 Django 应用完全初始化后再加载插件
# 避免在应用未准备就绪时尝试访问 Django 组件导致的问题
from .plugin_manage.loader import load_plugins # 导入插件加载器
load_plugins() # 调用插件加载函数,动态加载已激活的插件

@ -1,66 +1,109 @@
import _thread
import logging
import django.dispatch
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
from django.dispatch import receiver
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
logger = logging.getLogger(__name__)
# blog_signals.py - Django 信号处理模块
# 用于处理 Django 应用中的各种事件信号,实现解耦的事件驱动架构
import _thread # 导入线程模块,用于启动新线程执行耗时操作
import logging # 导入日志模块,用于记录运行信息
# 导入 Django 相关模块
import django.dispatch # Django 信号分发模块
from django.conf import settings # Django 配置模块
from django.contrib.admin.models import LogEntry # Django 管理日志模型
from django.contrib.auth.signals import user_logged_in, user_logged_out # 用户登录/登出信号
from django.core.mail import EmailMultiAlternatives # Django 邮件发送类
from django.db.models.signals import post_save # 模型保存后触发的信号
from django.dispatch import receiver # 信号接收器装饰器
# 导入项目相关模块
from comments.models import Comment # 评论模型
from comments.utils import send_comment_email # 发送评论邮件工具函数
from djangoblog.spider_notify import SpiderNotify # 搜索引擎通知工具
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache # 缓存工具函数
from djangoblog.utils import get_current_site # 获取当前站点工具函数
from oauth.models import OAuthUser # OAuth 用户模型
logger = logging.getLogger(__name__) # 创建当前模块的日志记录器
# 定义自定义信号
# OAuth 用户登录信号,携带用户 ID 参数
oauth_user_login_signal = django.dispatch.Signal(['id'])
# 发送邮件信号,携带收件人、标题和内容参数
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
"""
发送邮件信号处理函数
当触发 send_email_signal 信号时执行用于异步发送邮件
参数:
sender: 信号发送者
**kwargs: 信号传递的参数字典包含 emailto, title, content
"""
# 从信号参数中提取邮件信息
emailto = kwargs['emailto'] # 收件人列表
title = kwargs['title'] # 邮件标题
content = kwargs['content'] # 邮件内容
# 创建邮件对象
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto)
msg.content_subtype = "html"
title, # 邮件标题
content, # 邮件内容
from_email=settings.DEFAULT_FROM_EMAIL, # 发件人邮箱
to=emailto) # 收件人列表
msg.content_subtype = "html" # 设置邮件内容类型为 HTML
# 创建邮件发送日志记录
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto)
log.title = title # 记录邮件标题
log.content = content # 记录邮件内容
log.emailto = ','.join(emailto) # 记录收件人(转换为逗号分隔的字符串)
try:
# 尝试发送邮件
result = msg.send()
# 记录发送结果result > 0 表示发送成功)
log.send_result = result > 0
except Exception as e:
# 如果发送失败,记录错误日志
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
log.send_result = False # 记录发送失败
# 保存邮件发送日志
log.save()
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
"""
OAuth 用户登录信号处理函数
OAuth 用户登录时触发用于处理用户头像等信息
参数:
sender: 信号发送者
**kwargs: 信号传递的参数字典包含用户 ID
"""
# 从信号参数中获取用户 ID
id = kwargs['id']
# 获取 OAuth 用户对象
oauthuser = OAuthUser.objects.get(id=id)
# 获取当前站点域名
site = get_current_site().domain
# 检查用户头像是否需要处理
# 如果用户有头像且头像 URL 中不包含当前站点域名,则需要保存头像到本地
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
# 保存用户头像到本地并更新头像 URL
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
oauthuser.save() # 保存更新后的用户信息
# 删除侧边栏缓存,确保显示最新信息
delete_sidebar_cache()
@ -73,42 +116,82 @@ def model_post_save_callback(
using,
update_fields,
**kwargs):
clearcache = False
"""
模型保存后回调函数
当任何模型保存时触发用于处理缓存清理搜索引擎通知等操作
参数:
sender: 发送信号的模型类
instance: 保存的模型实例
created: 布尔值表示是否是新创建的实例
raw: 布尔值表示是否是原始加载的数据
using: 使用的数据库别名
update_fields: 更新的字段集合
**kwargs: 其他参数
"""
clearcache = False # 标记是否需要清除缓存
# 如果是管理日志条目,直接返回不处理
if isinstance(instance, LogEntry):
return
# 如果实例有 get_full_url 方法(通常是可公开访问的模型)
if 'get_full_url' in dir(instance):
# 判断是否只是更新了浏览量字段
is_update_views = update_fields == {'views'}
# 如果不是测试环境且不是仅更新浏览量,则通知搜索引擎
if not settings.TESTING and not is_update_views:
try:
# 获取实例的完整 URL 并通知搜索引擎
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
SpiderNotify.baidu_notify([notify_url]) # 通知百度搜索引擎
except Exception as ex:
logger.error("notify sipder", ex)
# 记录通知搜索引擎时的错误
logger.error("notify spider", ex)
# 如果不是仅更新浏览量,则需要清除缓存
if not is_update_views:
clearcache = True
# 如果是评论模型实例
if isinstance(instance, Comment):
# 如果评论是启用状态
if instance.is_enable:
# 获取关联文章的绝对 URL
path = instance.article.get_absolute_url()
# 获取当前站点域名
site = get_current_site().domain
# 如果域名包含端口号,则去掉端口号部分
if site.find(':') > 0:
site = site[0:site.find(':')]
# 过期文章详情页面的缓存
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
# 如果有 SEO 处理器缓存,则删除
if cache.get('seo_processor'):
cache.delete('seo_processor')
# 删除文章评论缓存
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
# 删除侧边栏缓存
delete_sidebar_cache()
# 删除文章评论视图缓存
delete_view_cache('article_comments', [str(instance.article.pk)])
# 启动新线程发送评论邮件通知(异步执行,避免阻塞主流程)
_thread.start_new_thread(send_comment_email, (instance,))
# 如果需要清除缓存,则执行清除操作
if clearcache:
cache.clear()
@ -116,7 +199,22 @@ def model_post_save_callback(
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
"""
用户认证回调函数
当用户登录或登出时触发用于处理缓存清理等操作
参数:
sender: 信号发送者
request: HTTP 请求对象
user: 用户对象
**kwargs: 其他参数
"""
# 如果用户存在且有用户名
if user and user.username:
# 记录用户登录/登出日志
logger.info(user)
# 删除侧边栏缓存,确保显示最新信息
delete_sidebar_cache()
# cache.clear()
# cache.clear() # 注释掉的代码:清除所有缓存

@ -1,183 +1,362 @@
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
# elasticsearch_backend.py - Django Haystack 的 Elasticsearch 搜索后端实现
# 提供基于 Elasticsearch 的全文搜索功能,用于替代默认的搜索后端
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
# 从 Django 和第三方库导入相关模块
from django.utils.encoding import force_str # 强制转换为字符串的工具函数
from elasticsearch_dsl import Q # Elasticsearch DSL 查询构建器
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query # Haystack 搜索后端基类
from haystack.forms import ModelSearchForm # Haystack 搜索表单基类
from haystack.models import SearchResult # Haystack 搜索结果模型
from haystack.utils import log as logging # Haystack 日志工具
logger = logging.getLogger(__name__)
# 从项目模块导入相关组件
from blog.documents import ArticleDocument, ArticleDocumentManager # 文章 Elasticsearch 文档和管理器
from blog.models import Article # 文章模型
logger = logging.getLogger(__name__) # 创建当前模块的日志记录器
class ElasticSearchBackend(BaseSearchBackend):
"""
Elasticsearch 搜索后端实现类
继承自 Haystack BaseSearchBackend提供基于 Elasticsearch 的搜索功能
"""
def __init__(self, connection_alias, **connection_options):
"""
初始化 Elasticsearch 搜索后端
参数:
connection_alias: 连接别名
**connection_options: 连接选项
"""
# 调用父类初始化方法
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager()
self.include_spelling = True
self.manager = ArticleDocumentManager() # 创建文章文档管理器实例
self.include_spelling = True # 启用拼写检查功能
def _get_models(self, iterable):
"""
获取模型实例列表
参数:
iterable: 可迭代的模型实例或查询集
返回:
转换后的文档列表
"""
# 如果提供了模型实例则使用,否则获取所有文章
models = iterable if iterable and iterable[0] else Article.objects.all()
# 将模型实例转换为 Elasticsearch 文档
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
"""
创建索引和文档
参数:
models: 模型实例列表
"""
self.manager.create_index() # 创建索引
docs = self._get_models(models) # 获取文档
self.manager.rebuild(docs) # 重建索引
def _delete(self, models):
"""
删除文档
参数:
models: 要删除的模型实例列表
返回:
True 表示删除成功
"""
for m in models:
m.delete()
m.delete() # 删除每个模型实例
return True
def _rebuild(self, models):
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
"""
重建索引
参数:
models: 模型实例列表
"""
models = models if models else Article.objects.all() # 如果未提供模型则获取所有文章
docs = self.manager.convert_to_doc(models) # 转换为文档
self.manager.update_docs(docs) # 更新文档
def update(self, index, iterable, commit=True):
"""
更新索引中的文档
models = self._get_models(iterable)
self.manager.update_docs(models)
参数:
index: 索引对象
iterable: 可迭代的模型实例
commit: 是否立即提交更改
"""
models = self._get_models(iterable) # 获取模型文档
self.manager.update_docs(models) # 更新文档
def remove(self, obj_or_string):
models = self._get_models([obj_or_string])
self._delete(models)
"""
从索引中移除对象
参数:
obj_or_string: 要移除的对象或字符串
"""
models = self._get_models([obj_or_string]) # 获取要删除的文档
self._delete(models) # 删除文档
def clear(self, models=None, commit=True):
self.remove(None)
"""
清空索引
参数:
models: 要清空的模型列表
commit: 是否立即提交更改
"""
self.remove(None) # 移除所有文档
@staticmethod
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
"""
获取搜索建议词如果没有找到则添加原搜索词
参数:
query: 原始搜索查询
返回:
建议的搜索词字符串
"""
# 构建 Elasticsearch 搜索查询,包含术语建议
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
keywords = [] # 存储建议关键词
# 遍历搜索建议结果
for suggest in search.suggest.suggest_search:
if suggest["options"]:
# 如果有建议选项,使用第一个建议词
keywords.append(suggest["options"][0]["text"])
else:
# 如果没有建议选项,使用原词
keywords.append(suggest["text"])
return ' '.join(keywords)
return ' '.join(keywords) # 返回拼接的建议词
@log_query
def search(self, query_string, **kwargs):
logger.info('search query_string:' + query_string)
"""
执行搜索查询
参数:
query_string: 搜索查询字符串
**kwargs: 其他参数
返回:
包含搜索结果的字典
"""
logger.info('search query_string:' + query_string) # 记录搜索日志
# 获取分页参数
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
# 推荐词搜索
if getattr(self, "is_suggest", None):
# 如果启用建议搜索,获取建议词
suggestion = self.get_suggestion(query_string)
else:
# 否则使用原始查询词
suggestion = query_string
# 构建复合查询:在标题和正文中匹配建议词
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
minimum_should_match="70%") # 至少匹配 70% 的 should 条件
# 构建完整的搜索查询
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
results = search.execute()
hits = results['hits'].total
raw_results = []
.query('bool', filter=[q]) \ # 使用过滤器查询
.filter('term', status='p') \ # 过滤已发布的文章
.filter('term', type='a') \ # 过滤文章类型
.source(False)[start_offset: end_offset] # 不返回源数据,设置分页
results = search.execute() # 执行搜索
hits = results['hits'].total # 获取总命中数
raw_results = [] # 存储原始搜索结果
# 处理每个搜索结果
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
app_label = 'blog' # 应用标签
model_name = 'Article' # 模型名称
additional_fields = {} # 附加字段
result_class = SearchResult
result_class = SearchResult # 搜索结果类
# 创建 SearchResult 对象
result = result_class(
app_label,
model_name,
raw_result['_id'],
raw_result['_score'],
raw_result['_id'], # 文档 ID
raw_result['_score'], # 匹配得分
**additional_fields)
raw_results.append(result)
facets = {}
raw_results.append(result) # 添加到结果列表
facets = {} # 面部结果(此处为空)
# 如果查询词与建议词不同,则提供拼写建议
spelling_suggestion = None if query_string == suggestion else suggestion
# 返回搜索结果字典
return {
'results': raw_results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
'results': raw_results, # 搜索结果列表
'hits': hits, # 总命中数
'facets': facets, # 面部结果
'spelling_suggestion': spelling_suggestion, # 拼写建议
}
class ElasticSearchQuery(BaseSearchQuery):
"""
Elasticsearch 查询类
继承自 Haystack BaseSearchQuery用于构建 Elasticsearch 查询
"""
def _convert_datetime(self, date):
"""
转换日期时间为字符串格式
参数:
date: 日期时间对象
返回:
格式化的日期时间字符串
"""
if hasattr(date, 'hour'):
# 如果有小时属性,格式化为包含时间的字符串
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
# 否则格式化为日期字符串
return force_str(date.strftime('%Y%m%d000000'))
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
value to the backend.
清理和净化用户输入的查询片段
参数:
query_fragment: 查询片段字符串
返回:
清理后的查询字符串
"""
"""
提供一种机制来在将值传递给后端之前清理用户输入
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.
Whoosh 1.X 在这里有所不同你不能再使用反斜杠来转义保留字符
相反应该引用整个单词
"""
words = query_fragment.split()
cleaned_words = []
words = query_fragment.split() # 分割查询词
cleaned_words = [] # 存储清理后的词
for word in words:
if word in self.backend.RESERVED_WORDS:
# 如果词是保留词,转换为小写
word = word.replace(word, word.lower())
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
# 如果包含保留字符,用引号包围整个词
word = "'%s'" % word
break
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):
return value.query_string
"""
构建查询片段
参数:
field: 字段名
filter_type: 过滤器类型
value: 值对象
返回:
查询字符串
"""
return value.query_string # 返回值对象的查询字符串
def get_count(self):
results = self.get_results()
return len(results) if results else 0
"""
获取搜索结果计数
返回:
搜索结果数量
"""
results = self.get_results() # 获取搜索结果
return len(results) if results else 0 # 返回结果数量
def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion
"""
获取拼写建议
参数:
preferred_query: 首选查询
返回:
拼写建议
"""
return self._spelling_suggestion # 返回拼写建议
def build_params(self, spelling_query=None):
"""
构建查询参数
参数:
spelling_query: 拼写查询
返回:
查询参数字典
"""
# 调用父类方法构建参数
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
"""
Elasticsearch 模型搜索表单类
继承自 Haystack ModelSearchForm用于处理搜索表单
"""
def search(self):
"""
执行搜索
返回:
搜索结果查询集
"""
# 是否建议搜索
# 根据表单数据决定是否启用建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
sqs = super().search() # 调用父类搜索方法
return sqs
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery
"""
Elasticsearch 搜索引擎类
继承自 Haystack BaseEngine用于配置 Elasticsearch 搜索引擎
"""
backend = ElasticSearchBackend # 指定后端类
query = ElasticSearchQuery # 指定查询类

@ -1,40 +1,119 @@
from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed
from django.utils import timezone
from django.utils.feedgenerator import Rss201rev2Feed
# feeds.py - Django RSS 订阅功能实现
# 提供网站内容的 RSS 订阅功能,让用户可以通过 RSS 阅读器获取最新文章
from blog.models import Article
from djangoblog.utils import CommonMarkdown
# 从 Django 和第三方库导入相关模块
from django.contrib.auth import get_user_model # 获取用户模型的工具函数
from django.contrib.syndication.views import Feed # Django RSS 订阅视图基类
from django.utils import timezone # Django 时区工具
from django.utils.feedgenerator import Rss201rev2Feed # RSS 2.0 格式生成器
# 从项目模块导入相关组件
from blog.models import Article # 文章模型
from djangoblog.utils import CommonMarkdown # Markdown 处理工具
class DjangoBlogFeed(Feed):
"""
DjangoBlog RSS 订阅类
继承自 Django Feed 用于生成网站内容的 RSS 订阅源
"""
# 指定 RSS 格式为 RSS 2.0 rev2
feed_type = Rss201rev2Feed
description = '大巧无工,重剑无锋.'
title = "且听风吟 大巧无工,重剑无锋. "
link = "/feed/"
# 订阅源描述信息
description = '大巧无工,重剑无锋.' # 订阅源描述,显示在 RSS 阅读器中
# 订阅源标题
title = "且听风吟 大巧无工,重剑无锋. " # 订阅源标题,显示在 RSS 阅读器中
# 订阅源链接
link = "/feed/" # 订阅源的主链接
def author_name(self):
"""
获取订阅源作者名称
返回:
第一个用户通常是管理员的昵称
"""
return get_user_model().objects.first().nickname
def author_link(self):
"""
获取订阅源作者链接
返回:
第一个用户通常是管理员的个人主页链接
"""
return get_user_model().objects.first().get_absolute_url()
def items(self):
"""
获取订阅项目列表
返回:
最新的 5 篇已发布文章按发布时间倒序排列
过滤条件
- type='a': 文章类型为普通文章而非页面等其他类型
- status='p': 文章状态为已发布
"""
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
"""
获取单个订阅项目的标题
参数:
item: 文章对象
返回:
文章标题
"""
return item.title
def item_description(self, item):
"""
获取单个订阅项目的描述内容
参数:
item: 文章对象
返回:
文章正文的 Markdown 渲染后 HTML 内容
"""
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
now = timezone.now()
"""
获取订阅源版权信息
返回:
包含当前年份的版权声明
"""
now = timezone.now() # 获取当前时间
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
"""
获取单个订阅项目的链接
参数:
item: 文章对象
返回:
文章的绝对 URL
"""
return item.get_absolute_url()
def item_guid(self, item):
"""
获取单个订阅项目的全局唯一标识符
参数:
item: 文章对象
返回:
None使用默认的 GUID 生成方式
"""
return
# 注意这个方法没有返回值Django 会自动生成基于 item_link 的 GUID

@ -1,91 +1,183 @@
from django.contrib import admin
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
# logentryadmin.py - Django 管理日志条目自定义管理类
# 用于自定义 Django 管理后台中的操作日志显示和管理功能
# 从 Django 和第三方库导入相关模块
from django.contrib import admin # Django 管理后台模块
from django.contrib.admin.models import DELETION # 管理操作类型常量(删除操作)
from django.contrib.contenttypes.models import ContentType # 内容类型模型
from django.urls import reverse, NoReverseMatch # URL 反向解析和异常处理
from django.utils.encoding import force_str # 强制转换为字符串
from django.utils.html import escape # HTML 转义函数
from django.utils.safestring import mark_safe # 标记安全字符串
from django.utils.translation import gettext_lazy as _ # 国际化翻译函数
class LogEntryAdmin(admin.ModelAdmin):
"""
管理日志条目自定义管理类
继承自 Django ModelAdmin用于自定义管理日志在后台的显示和操作
"""
# 列表过滤器,允许按内容类型过滤日志条目
list_filter = [
'content_type'
'content_type' # 按内容类型(模型类型)过滤
]
# 搜索字段,允许搜索对象表示和变更消息
search_fields = [
'object_repr',
'change_message'
'object_repr', # 对象表示(通常是对象的字符串表示)
'change_message' # 变更消息(描述了具体的操作内容)
]
# 列表显示链接字段,点击这些字段可以进入详情页
list_display_links = [
'action_time',
'get_change_message',
'action_time', # 操作时间
'get_change_message', # 变更消息
]
# 列表显示字段,定义在列表页显示哪些字段
list_display = [
'action_time',
'user_link',
'content_type',
'object_link',
'get_change_message',
'action_time', # 操作时间
'user_link', # 用户链接(自定义方法)
'content_type', # 内容类型
'object_link', # 对象链接(自定义方法)
'get_change_message', # 变更消息
]
def has_add_permission(self, request):
return False
"""
控制是否允许添加日志条目
参数:
request: HTTP 请求对象
返回:
False 表示不允许手动添加日志条目
"""
return False # 禁止添加日志条目,日志应由系统自动生成
def has_change_permission(self, request, obj=None):
"""
控制是否允许修改日志条目
参数:
request: HTTP 请求对象
obj: 要检查权限的对象可选
返回:
布尔值表示是否允许修改
"""
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
request.user.is_superuser or # 超级用户有权限
request.user.has_perm('admin.change_logentry') # 或具有修改日志权限的用户
) and request.method != 'POST' # 且请求方法不是 POST防止表单提交
def has_delete_permission(self, request, obj=None):
return False
"""
控制是否允许删除日志条目
参数:
request: HTTP 请求对象
obj: 要检查权限的对象可选
返回:
False 表示不允许删除日志条目
"""
return False # 禁止删除日志条目,保持审计记录完整性
def object_link(self, obj):
object_link = escape(obj.object_repr)
content_type = obj.content_type
"""
生成对象链接的显示内容
参数:
obj: LogEntry 对象
返回:
对象的 HTML 链接或纯文本表示
"""
object_link = escape(obj.object_repr) # 转义对象表示,防止 XSS 攻击
content_type = obj.content_type # 获取内容类型
# 如果不是删除操作且内容类型存在
if obj.action_flag != DELETION and content_type is not None:
# try returning an actual link instead of object repr string
# 尝试返回实际链接而不是对象表示字符串
try:
# 构建管理后台编辑页面的 URL
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id]
args=[obj.object_id] # 对象 ID
)
# 创建 HTML 链接
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
# 如果无法构建 URL则保持原始文本
pass
return mark_safe(object_link)
return mark_safe(object_link) # 标记为安全 HTML 并返回
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
# 设置管理后台的排序字段和显示名称
object_link.admin_order_field = 'object_repr' # 按 object_repr 字段排序
object_link.short_description = _('object') # 显示名称为"对象"
def user_link(self, obj):
"""
生成用户链接的显示内容
参数:
obj: LogEntry 对象
返回:
用户的 HTML 链接或纯文本表示
"""
# 获取用户对象的内容类型
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user))
user_link = escape(force_str(obj.user)) # 转义用户表示
try:
# try returning an actual link instead of object repr string
# 尝试返回实际链接而不是对象表示字符串
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk]
args=[obj.user.pk] # 用户主键
)
# 创建 HTML 链接
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
# 如果无法构建 URL则保持原始文本
pass
return mark_safe(user_link)
return mark_safe(user_link) # 标记为安全 HTML 并返回
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
# 设置管理后台的排序字段和显示名称
user_link.admin_order_field = 'user' # 按 user 字段排序
user_link.short_description = _('user') # 显示名称为"用户"
def get_queryset(self, request):
"""
获取查询集优化数据库查询
参数:
request: HTTP 请求对象
返回:
优化后的查询集
"""
# 调用父类方法获取基础查询集
queryset = super(LogEntryAdmin, self).get_queryset(request)
# 预加载 content_type 关联对象,减少数据库查询次数
return queryset.prefetch_related('content_type')
def get_actions(self, request):
"""
获取可用的操作列表
参数:
request: HTTP 请求对象
返回:
可用操作字典
"""
# 调用父类方法获取基础操作列表
actions = super(LogEntryAdmin, self).get_actions(request)
# 如果存在删除选中项操作,则移除它(因为我们禁用了删除权限)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions

@ -1,41 +1,57 @@
import logging
# base_plugin.py - 插件管理系统的基类定义
# 提供插件的基本结构和通用功能,所有插件都需要继承此类
logger = logging.getLogger(__name__)
import logging # 导入日志模块,用于记录插件运行信息
logger = logging.getLogger(__name__) # 创建当前模块的日志记录器
class BasePlugin:
# 插件元数据
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
# 插件元数据属性定义(需要在子类中具体实现)
PLUGIN_NAME = None # 插件名称
PLUGIN_DESCRIPTION = None # 插件描述
PLUGIN_VERSION = None # 插件版本
def __init__(self):
"""
插件初始化方法
在创建插件实例时验证必要元数据并执行初始化流程
"""
# 验证插件元数据是否完整定义
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
# 如果任何必需的元数据未定义,则抛出值错误异常
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
self.init_plugin()
self.register_hooks()
self.init_plugin() # 调用插件初始化方法
self.register_hooks() # 调用钩子注册方法
def init_plugin(self):
"""
插件初始化逻辑
子类可以重写此方法来实现特定的初始化操作
例如数据库连接配置加载资源初始化等
"""
# 记录插件初始化日志信息
logger.info(f'{self.PLUGIN_NAME} initialized.')
def register_hooks(self):
"""
注册插件钩子
子类可以重写此方法来注册特定的钩子
钩子是插件系统中用于在特定时机执行代码的机制
例如在文章发布前用户注册后等事件触发时执行插件逻辑
"""
# 基类中的默认实现为空,子类可以根据需要实现具体逻辑
pass
def get_plugin_info(self):
"""
获取插件信息
返回插件的基本元数据信息供插件管理系统使用
:return: 包含插件元数据的字典
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
'name': self.PLUGIN_NAME, # 插件名称
'description': self.PLUGIN_DESCRIPTION, # 插件描述
'version': self.PLUGIN_VERSION # 插件版本
}

@ -1,7 +1,21 @@
ARTICLE_DETAIL_LOAD = 'article_detail_load'
ARTICLE_CREATE = 'article_create'
ARTICLE_UPDATE = 'article_update'
ARTICLE_DELETE = 'article_delete'
# hook_constants.py - 插件钩子常量定义文件
# 定义了插件系统中可用的各种钩子常量,用于在特定事件发生时触发插件功能
ARTICLE_CONTENT_HOOK_NAME = "the_content"
# 文章相关操作的钩子常量
ARTICLE_DETAIL_LOAD = 'article_detail_load' # 文章详情页面加载时触发的钩子
# 当用户访问文章详情页时执行相关插件逻辑
ARTICLE_CREATE = 'article_create' # 文章创建时触发的钩子
# 当新文章被创建时执行相关插件逻辑
ARTICLE_UPDATE = 'article_update' # 文章更新时触发的钩子
# 当文章被修改更新时执行相关插件逻辑
ARTICLE_DELETE = 'article_delete' # 文章删除时触发的钩子
# 当文章被删除时执行相关插件逻辑
# 文章内容处理钩子名称
ARTICLE_CONTENT_HOOK_NAME = "the_content" # 文章内容处理钩子
# 用于在文章内容显示前对其进行处理或修改
# 例如:添加广告、插入相关链接、内容过滤等

@ -1,44 +1,109 @@
import logging
# hooks.py - 插件钩子管理系统
# 提供插件钩子的注册和执行功能,是插件系统的核心组件
logger = logging.getLogger(__name__)
import logging # 导入日志模块,用于记录钩子执行过程中的信息
logger = logging.getLogger(__name__) # 创建当前模块的日志记录器
# 全局钩子注册表,存储所有已注册的钩子回调函数
# 数据结构为字典:{钩子名称: [回调函数列表]}
_hooks = {}
def register(hook_name: str, callback: callable):
"""
注册一个钩子回调
注册一个钩子回调函数
参数:
hook_name (str): 钩子名称用于标识特定的钩子事件
callback (callable): 回调函数当钩子被触发时执行的函数
功能:
将回调函数添加到指定钩子的回调列表中
如果钩子名称不存在则创建新的钩子条目
"""
# 检查钩子名称是否已存在于钩子注册表中
if hook_name not in _hooks:
# 如果不存在,则创建一个新的空列表用于存储回调函数
_hooks[hook_name] = []
# 将回调函数添加到对应钩子的回调列表中
_hooks[hook_name].append(callback)
# 记录调试日志,显示已注册的钩子和回调函数名称
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
def run_action(hook_name: str, *args, **kwargs):
"""
执行一个 Action Hook
它会按顺序执行所有注册到该钩子上的回调函数
执行一个 Action Hook动作钩子
Action Hook 特点
- 不需要返回值
- 按顺序执行所有注册到该钩子上的回调函数
- 通常用于在特定事件发生时执行副作用操作
参数:
hook_name (str): 要执行的钩子名称
*args: 传递给回调函数的位置参数
**kwargs: 传递给回调函数的关键字参数
功能:
依次执行所有注册到指定钩子上的回调函数
每个回调函数独立执行一个回调函数的异常不会影响其他回调函数的执行
"""
# 检查指定的钩子是否存在已注册的回调函数
if hook_name in _hooks:
# 记录调试日志,显示正在执行的动作钩子
logger.debug(f"Running action hook '{hook_name}'")
# 遍历并执行所有注册到该钩子的回调函数
for callback in _hooks[hook_name]:
try:
# 执行回调函数,传递参数
callback(*args, **kwargs)
except Exception as e:
# 如果回调函数执行出错,记录错误日志但继续执行其他回调函数
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
执行一个 Filter Hook
它会把 value 依次传递给所有注册的回调函数进行处理
执行一个 Filter Hook过滤器钩子
Filter Hook 特点
- 需要处理并返回一个值
- 将值依次传递给所有注册的回调函数进行处理
- 每个回调函数的返回值作为下一个回调函数的输入
参数:
hook_name (str): 要执行的钩子名称
value: 需要被处理的初始值
*args: 传递给回调函数的位置参数
**kwargs: 传递给回调函数的关键字参数
返回:
处理后的最终值经过所有回调函数处理的结果
功能:
将初始值依次传递给所有注册到指定钩子的回调函数
每个回调函数处理值并返回处理结果作为下一个回调函数的输入
如果某个回调函数执行出错记录错误但继续执行其他回调函数
"""
# 检查指定的钩子是否存在已注册的回调函数
if hook_name in _hooks:
# 记录调试日志,显示正在应用的过滤器钩子
logger.debug(f"Applying filter hook '{hook_name}'")
# 遍历所有注册到该钩子的回调函数
for callback in _hooks[hook_name]:
try:
# 将当前值传递给回调函数处理,并将返回值作为新的值
# 实现链式处理value = callback3(callback2(callback1(value)))
value = callback(value, *args, **kwargs)
except Exception as e:
# 如果回调函数执行出错,记录错误日志但继续执行其他回调函数
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
# 返回经过所有回调函数处理后的最终值
return value

@ -1,19 +1,49 @@
import os
import logging
from django.conf import settings
# loader.py - 插件加载器模块
# 负责动态加载和初始化插件系统中的插件
import os # 导入操作系统接口模块,用于文件路径操作
import logging # 导入日志模块,用于记录插件加载过程中的信息
from django.conf import settings # 从 Django 配置中导入设置模块
logger = logging.getLogger(__name__) # 创建当前模块的日志记录器
logger = logging.getLogger(__name__)
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
动态加载并初始化 'plugins' 目录中的插件
此函数应在 Django 应用注册表准备就绪后调用确保所有 Django 组件都已正确初始化
加载过程
1. 遍历配置中激活的插件列表
2. 检查插件目录和必要文件是否存在
3. 动态导入插件模块
4. 记录加载成功或失败的日志信息
插件目录结构要求
- 插件必须位于 settings.PLUGINS_DIR 指定的目录下
- 每个插件应是一个独立的目录
- 插件目录中必须包含 plugin.py 文件作为插件入口点
"""
# 遍历配置文件中定义的激活插件列表 (ACTIVE_PLUGINS)
for plugin_name in settings.ACTIVE_PLUGINS:
# 构建插件目录的完整路径
# os.path.join 确保路径格式正确(跨平台兼容)
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
# 检查插件目录是否存在且包含 plugin.py 入口文件
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
# 动态导入插件模块
# 使用 __import__ 函数导入插件模块,触发插件的初始化
# 导入路径格式:'plugins.{插件名}.plugin'
__import__(f'plugins.{plugin_name}.plugin')
# 记录插件成功加载的日志信息
logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e:
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
# 如果插件导入失败,记录错误日志
# exc_info=True 参数会包含完整的异常堆栈信息,便于调试
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -9,335 +9,367 @@ https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
# 导入系统模块
import os
import sys
from pathlib import Path
from django.utils.translation import gettext_lazy as _
from pathlib import Path # 用于处理文件路径的现代化方式
from django.utils.translation import gettext_lazy as _ # 国际化翻译函数
# 定义环境变量转布尔值的辅助函数
def env_to_bool(env, default):
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
"""
将环境变量转换为布尔值
参数:
env: 环境变量名称
default: 默认值
返回:
布尔值
"""
str_val = os.environ.get(env) # 获取环境变量值
return default if str_val is None else str_val == 'True' # 如果未设置则使用默认值,否则判断是否为 'True'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
# 构建项目内的路径,使用 Path 对象更安全和跨平台
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
# 安全警告:在生产环境中保持密钥的机密性
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# 安全警告:在生产环境中不要开启调试模式
DEBUG = env_to_bool('DJANGO_DEBUG', True) # 从环境变量获取调试设置,默认为 True
# DEBUG = False # 注释掉的代码:手动设置调试模式为 False
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test' # 判断是否在运行测试
# ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# django 4.0新增配置
# ALLOWED_HOSTS = [] # 注释掉的默认配置
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com'] # 允许的主机列表,* 表示允许所有主机
# django 4.0新增配置 - CSRF 可信来源
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
# Application definition
# 应用定义:列出项目中使用的所有 Django 应用
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.sitemaps',
'mdeditor',
'haystack',
'blog',
'accounts',
'comments',
'oauth',
'servermanager',
'owntracks',
'compressor',
'djangoblog'
# 'django.contrib.admin', # 注释掉的默认管理应用
'django.contrib.admin.apps.SimpleAdminConfig', # 使用简化配置的管理应用
'django.contrib.auth', # 用户认证系统
'django.contrib.contenttypes', # 内容类型框架
'django.contrib.sessions', # 会话框架
'django.contrib.messages', # 消息框架
'django.contrib.staticfiles', # 静态文件管理
'django.contrib.sites', # 站点框架
'django.contrib.sitemaps', # 站点地图框架
'mdeditor', # Markdown 编辑器
'haystack', # 全文搜索框架
'blog', # 博客应用
'accounts', # 账户管理应用
'comments', # 评论应用
'oauth', # OAuth 认证应用
'servermanager', # 服务器管理应用
'owntracks', # OwnTracks 位置追踪应用
'compressor', # 静态文件压缩工具
'djangoblog' # 主应用
]
# 中间件配置:定义请求/响应处理的顺序
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.gzip.GZipMiddleware',
# 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.cache.FetchFromCacheMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'blog.middleware.OnlineMiddleware'
'django.middleware.security.SecurityMiddleware', # 安全中间件
'django.contrib.sessions.middleware.SessionMiddleware', # 会话中间件
'django.middleware.locale.LocaleMiddleware', # 国际化中间件
'django.middleware.gzip.GZipMiddleware', # GZip 压缩中间件
# 'django.middleware.cache.UpdateCacheMiddleware', # 注释掉的缓存更新中间件
'django.middleware.common.CommonMiddleware', # 通用中间件
# 'django.middleware.cache.FetchFromCacheMiddleware', # 注释掉的缓存获取中间件
'django.middleware.csrf.CsrfViewMiddleware', # CSRF 保护中间件
'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证中间件
'django.contrib.messages.middleware.MessageMiddleware', # 消息中间件
'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持保护中间件
'django.middleware.http.ConditionalGetMiddleware', # 条件 GET 中间件
'blog.middleware.OnlineMiddleware' # 自定义在线用户统计中间件
]
ROOT_URLCONF = 'djangoblog.urls'
ROOT_URLCONF = 'djangoblog.urls' # 根 URL 配置文件
# 模板配置
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'BACKEND': 'django.template.backends.django.DjangoTemplates', # 使用 Django 模板引擎
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 模板目录
'APP_DIRS': True, # 自动在应用目录中查找模板
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'blog.context_processors.seo_processor'
'context_processors': [ # 上下文处理器
'django.template.context_processors.debug', # 调试上下文处理器
'django.template.context_processors.request', # 请求上下文处理器
'django.contrib.auth.context_processors.auth', # 认证上下文处理器
'django.contrib.messages.context_processors.messages', # 消息上下文处理器
'blog.context_processors.seo_processor' # 自定义 SEO 上下文处理器
],
},
},
]
WSGI_APPLICATION = 'djangoblog.wsgi.application'
WSGI_APPLICATION = 'djangoblog.wsgi.application' # WSGI 应用入口
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
# 数据库配置:使用 MySQL
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or '123456',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'ENGINE': 'django.db.backends.mysql', # 数据库引擎
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog', # 数据库名称
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root', # 数据库用户名
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or '123456', # 数据库密码
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', # 数据库主机
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
os.environ.get('DJANGO_MYSQL_PORT') or 3306), # 数据库端口
'OPTIONS': {
'charset': 'utf8mb4'},
'charset': 'utf8mb4'}, # 字符集配置
}}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
# 密码验证器配置
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # 用户属性相似性验证
},
{
'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', # 数字密码验证
},
]
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
# 国际化配置
LANGUAGES = ( # 支持的语言列表
('en', _('English')), # 英语
('zh-hans', _('Simplified Chinese')), # 简体中文
('zh-hant', _('Traditional Chinese')), # 繁体中文
)
LOCALE_PATHS = (
LOCALE_PATHS = ( # 国际化文件路径
os.path.join(BASE_DIR, 'locale'),
)
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
LANGUAGE_CODE = 'zh-hans' # 默认语言代码
TIME_ZONE = 'Asia/Shanghai' # 时区设置
USE_I18N = True # 启用国际化
USE_L10N = True # 启用本地化
USE_TZ = False # 不使用时区感知
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
# 静态文件配置
HAYSTACK_CONNECTIONS = {
HAYSTACK_CONNECTIONS = { # Haystack 搜索引擎配置
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine', # 使用 Whoosh 搜索引擎(中文支持)
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'), # 索引文件路径
},
}
# Automatically update searching index
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Allow user login with username and password
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' # 实时更新搜索索引
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
# Allow user login with username and password
AUTHENTICATION_BACKENDS = [ # 认证后端配置
'accounts.user_login_backend.EmailOrUsernameModelBackend' # 允许使用邮箱或用户名登录
]
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic') # 静态文件收集目录
STATIC_URL = '/static/' # 静态文件 URL 前缀
STATICFILES = os.path.join(BASE_DIR, 'static') # 静态文件目录
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
AUTH_USER_MODEL = 'accounts.BlogUser' # 自定义用户模型
LOGIN_URL = '/login/' # 登录 URL
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S' # 时间格式
DATE_TIME_FORMAT = '%Y-%m-%d' # 日期格式
# bootstrap color styles
BOOTSTRAP_COLOR_TYPES = [
BOOTSTRAP_COLOR_TYPES = [ # Bootstrap 颜色样式列表
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# paginate
PAGINATE_BY = 10
PAGINATE_BY = 10 # 分页每页显示数量
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
CACHE_CONTROL_MAX_AGE = 2592000 # HTTP 缓存最大年龄30天
# cache setting
CACHES = {
CACHES = { # 缓存配置
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800,
'LOCATION': 'unique-snowflake',
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', # 使用本地内存缓存
'TIMEOUT': 10800, # 缓存超时时间3小时
'LOCATION': 'unique-snowflake', # 缓存位置标识
}
}
# 使用redis作为缓存
# 使用 redis 作为缓存(如果配置了环境变量)
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
'BACKEND': 'django.core.cache.backends.redis.RedisCache', # 使用 Redis 缓存
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', # Redis 连接地址
}
}
SITE_ID = 1
SITE_ID = 1 # 站点框架 ID
# 百度搜索引擎主动推送 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'
# Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER
# Email 配置:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # 使用 SMTP 发送邮件
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) # 是否使用 TLS
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) # 是否使用 SSL
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' # 邮件服务器主机
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) # 邮件服务器端口
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER') # 邮件用户名
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD') # 邮件密码
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER # 默认发件人邮箱
SERVER_EMAIL = EMAIL_HOST_USER # 服务器错误邮件发送地址
# Setting debug=false did NOT handle except email notifications
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)
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7' # 微信管理员密码(两次 MD5 加密)
LOG_PATH = os.path.join(BASE_DIR, 'logs')
# 日志配置
LOG_PATH = os.path.join(BASE_DIR, 'logs') # 日志文件路径
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
os.makedirs(LOG_PATH, exist_ok=True) # 创建日志目录
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'root': {
'level': 'INFO',
'handlers': ['console', 'log_file'],
'version': 1, # 日志配置版本
'disable_existing_loggers': False, # 不禁用现有的日志记录器
'root': { # 根日志记录器
'level': 'INFO', # 日志级别
'handlers': ['console', 'log_file'], # 处理器
},
'formatters': {
'formatters': { # 日志格式化器
'verbose': {
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
}
},
'filters': {
'filters': { # 日志过滤器
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
'()': 'django.utils.log.RequireDebugFalse', # 要求调试为 False
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
'()': 'django.utils.log.RequireDebugTrue', # 要求调试为 True
},
},
'handlers': {
'handlers': { # 日志处理器
'log_file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
'when': 'D',
'formatter': 'verbose',
'interval': 1,
'delay': True,
'backupCount': 5,
'encoding': 'utf-8'
'level': 'INFO', # 处理器级别
'class': 'logging.handlers.TimedRotatingFileHandler', # 按时间轮转的文件处理器
'filename': os.path.join(LOG_PATH, 'djangoblog.log'), # 日志文件名
'when': 'D', # 每天轮转
'formatter': 'verbose', # 使用详细格式
'interval': 1, # 轮转间隔
'delay': True, # 延迟创建文件
'backupCount': 5, # 保留备份数量
'encoding': 'utf-8' # 文件编码
},
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
'level': 'DEBUG', # 控制台处理器级别
'filters': ['require_debug_true'], # 仅在调试模式下使用
'class': 'logging.StreamHandler', # 控制台处理器
'formatter': 'verbose' # 使用详细格式
},
'null': {
'class': 'logging.NullHandler',
'class': 'logging.NullHandler', # 空处理器
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
'level': 'ERROR', # 邮件处理器级别
'filters': ['require_debug_false'], # 仅在非调试模式下使用
'class': 'django.utils.log.AdminEmailHandler' # 管理员邮件处理器
}
},
'loggers': {
'loggers': { # 日志记录器配置
'djangoblog': {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
'handlers': ['log_file', 'console'], # 使用的处理器
'level': 'INFO', # 日志级别
'propagate': True, # 是否传播到父级记录器
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
'handlers': ['mail_admins'], # 错误请求发送邮件给管理员
'level': 'ERROR', # 错误级别
'propagate': False, # 不传播到父级记录器
}
}
}
# 静态文件查找器配置
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'django.contrib.staticfiles.finders.FileSystemFinder', # 文件系统查找器
'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 应用目录查找器
# other
'compressor.finders.CompressorFinder',
'compressor.finders.CompressorFinder', # 压缩文件查找器
)
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
COMPRESS_ENABLED = True # 启用静态文件压缩
# COMPRESS_OFFLINE = True # 注释掉的离线压缩配置
# CSS 压缩过滤器
COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.css_default.CssAbsoluteFilter', # 创建绝对 URL
# css minimizer
'compressor.filters.cssmin.CSSMinFilter'
'compressor.filters.cssmin.CSSMinFilter' # CSS 最小化
]
# JavaScript 压缩过滤器
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
'compressor.filters.jsmin.JSMinFilter' # JS 最小化
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') # 媒体文件上传目录
MEDIA_URL = '/media/' # 媒体文件 URL 前缀
X_FRAME_OPTIONS = 'SAMEORIGIN' # X-Frame-Options 安全头设置
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # 默认自动字段类型
# 如果配置了 Elasticsearch则使用 Elasticsearch 作为搜索后端
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') # Elasticsearch 主机地址
},
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine', # 使用自定义 Elasticsearch 后端
},
}
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
'article_copyright',
'reading_time',
'external_links',
'view_count',
'seo_optimizer'
]
# 插件系统配置
PLUGINS_DIR = BASE_DIR / 'plugins' # 插件目录路径
ACTIVE_PLUGINS = [ # 激活的插件列表
'article_copyright', # 文章版权插件
'reading_time', # 阅读时间插件
'external_links', # 外部链接插件
'view_count', # 浏览量统计插件
'seo_optimizer' # SEO 优化插件
]

@ -1,59 +1,161 @@
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
# sitemap.py - Django 网站地图生成模块
# 用于生成符合 Google Sitemap 协议的 XML 网站地图,帮助搜索引擎更好地索引网站内容
from blog.models import Article, Category, Tag
# 从 Django 和项目模块导入相关组件
from django.contrib.sitemaps import Sitemap # Django 网站地图基类
from django.urls import reverse # URL 反向解析函数
# 从博客应用导入相关模型
from blog.models import Article, Category, Tag # 文章、分类、标签模型
class StaticViewSitemap(Sitemap):
priority = 0.5
changefreq = 'daily'
"""
静态页面网站地图类
继承自 Django Sitemap 用于生成静态页面的网站地图
"""
priority = 0.5 # 页面优先级0.0-1.0),默认中等优先级
changefreq = 'daily' # 页面更新频率:每天
def items(self):
return ['blog:index', ]
"""
返回网站地图包含的项目列表
返回:
包含静态页面 URL 名称的列表
"""
return ['blog:index', ] # 返回首页的 URL 名称
def location(self, item):
return reverse(item)
"""
返回项目的绝对 URL
参数:
item: URL 名称
返回:
对应的绝对 URL 路径
"""
return reverse(item) # 使用 reverse 函数将 URL 名称转换为绝对路径
class ArticleSiteMap(Sitemap):
changefreq = "monthly"
priority = "0.6"
"""
文章网站地图类
用于生成文章页面的网站地图
"""
changefreq = "monthly" # 页面更新频率:每月
priority = "0.6" # 页面优先级:中等偏上
def items(self):
"""
返回网站地图包含的文章项目列表
返回:
已发布文章的查询集
"""
# 只包含状态为已发布的文章status='p'
return Article.objects.filter(status='p')
def lastmod(self, obj):
return obj.last_modify_time
"""
返回文章最后修改时间
参数:
obj: Article 对象
返回:
文章最后修改时间
"""
return obj.last_modify_time # 返回文章的最后修改时间字段
class CategorySiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.6"
"""
分类网站地图类
用于生成文章分类页面的网站地图
"""
changefreq = "Weekly" # 页面更新频率:每周
priority = "0.6" # 页面优先级:中等偏上
def items(self):
return Category.objects.all()
"""
返回网站地图包含的分类项目列表
返回:
所有分类的查询集
"""
return Category.objects.all() # 返回所有分类
def lastmod(self, obj):
return obj.last_modify_time
"""
返回分类最后修改时间
参数:
obj: Category 对象
返回:
分类最后修改时间
"""
return obj.last_modify_time # 返回分类的最后修改时间字段
class TagSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
"""
标签网站地图类
用于生成文章标签页面的网站地图
"""
changefreq = "Weekly" # 页面更新频率:每周
priority = "0.3" # 页面优先级:较低
def items(self):
return Tag.objects.all()
"""
返回网站地图包含的标签项目列表
返回:
所有标签的查询集
"""
return Tag.objects.all() # 返回所有标签
def lastmod(self, obj):
return obj.last_modify_time
"""
返回标签最后修改时间
参数:
obj: Tag 对象
返回:
标签最后修改时间
"""
return obj.last_modify_time # 返回标签的最后修改时间字段
class UserSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
"""
用户网站地图类
用于生成用户页面的网站地图
"""
changefreq = "Weekly" # 页面更新频率:每周
priority = "0.3" # 页面优先级:较低
def items(self):
"""
返回网站地图包含的用户项目列表
返回:
所有文章作者的用户对象列表去重
"""
# 获取所有文章的作者,使用 set 去重,再转换为列表
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
return obj.date_joined
"""
返回用户最后修改时间注册时间
参数:
obj: User 对象
返回:
用户注册时间
"""
return obj.date_joined # 返回用户的注册时间字段

@ -1,21 +1,63 @@
import logging
# spider_notify.py - 搜索引擎主动推送通知模块
# 用于主动向搜索引擎推送网站更新的 URL帮助搜索引擎及时发现和索引新内容
import requests
from django.conf import settings
import logging # 导入日志模块,用于记录推送过程中的信息和错误
logger = logging.getLogger(__name__)
import requests # 导入 HTTP 请求库,用于向搜索引擎发送推送请求
from django.conf import settings # 从 Django 配置中导入设置模块
logger = logging.getLogger(__name__) # 创建当前模块的日志记录器
class SpiderNotify():
"""
搜索引擎通知类
提供向各大搜索引擎主动推送 URL 的功能帮助搜索引擎及时抓取网站更新
"""
@staticmethod
def baidu_notify(urls):
"""
向百度搜索引擎主动推送 URL
百度主动推送是百度提供的一种快速向百度提交网页的工具
可以帮助新站快速被百度发现提高收录速度
参数:
urls (list): 需要推送的 URL 列表
功能:
URL 列表通过 HTTP POST 请求发送到百度主动推送接口
记录推送结果或错误信息
"""
try:
# 将 URL 列表转换为换行符分隔的字符串格式
# 这是百度主动推送接口要求的数据格式
data = '\n'.join(urls)
# 向百度主动推送接口发送 POST 请求
# settings.BAIDU_NOTIFY_URL 包含了推送接口地址和 token 等参数
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
# 记录推送结果日志
logger.info(result.text)
except Exception as e:
# 如果推送过程中出现异常,记录错误日志
logger.error(e)
@staticmethod
def notify(url):
"""
通用通知方法当前仅实现百度推送
参数:
url (str list): 需要推送的 URL URL 列表
功能:
作为统一的通知入口调用具体的搜索引擎推送方法
目前只实现了百度推送功能
"""
# 调用百度推送方法
# 注意:参数名是 url但实际传递给 baidu_notify 的是 urls列表
SpiderNotify.baidu_notify(url)

@ -1,32 +1,17 @@
from django.test import TestCase
from djangoblog.utils import *
class DjangoBlogTest(TestCase):
def setUp(self):
pass
def test_utils(self):
md5 = get_sha256('test')
self.assertIsNotNone(md5)
c = CommonMarkdown.get_markdown('''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''')
self.assertIsNotNone(c)
''') # 测试包含标题、代码块和链接的 Markdown 文本
self.assertIsNotNone(c) # 断言转换结果不为 None
# 测试字典转 URL 参数功能
# 准备测试数据字典
d = {
'd': 'key1',
'd2': 'key2'
'd': 'key1', # 键值对1
'd2': 'key2' # 键值对2
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)
# 验证 parse_dict_to_url 函数能够正确将字典转换为 URL 参数字符串
data = parse_dict_to_url(d) # 将字典转换为 URL 参数格式
self.assertIsNotNone(data) # 断言转换结果不为 None

@ -1,6 +1,6 @@
"""djangoblog URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
The [urlpatterns](file://D:\zyd2025\src\djangoblog\urls.py#L42-L44) list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
@ -13,52 +13,85 @@ Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
# 从 Django 和项目模块导入相关组件
from django.conf import settings # Django 配置
from django.conf.urls.i18n import i18n_patterns # 国际化 URL 模式
from django.conf.urls.static import static # 静态文件 URL 配置
from django.contrib.sitemaps.views import sitemap # 网站地图视图
from django.urls import path, include # URL 配置函数
from django.urls import re_path # 正则表达式路径函数
from haystack.views import search_view_factory # 搜索视图工厂
sitemaps = {
# 从项目模块导入相关组件
from blog.views import EsSearchView # Elasticsearch 搜索视图
from djangoblog.admin_site import admin_site # 自定义管理站点
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm # Elasticsearch 搜索表单
from djangoblog.feeds import DjangoBlogFeed # RSS 订阅
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap # 网站地图
'blog': ArticleSiteMap,
'Category': CategorySiteMap,
'Tag': TagSiteMap,
'User': UserSiteMap,
'static': StaticViewSitemap
# 定义网站地图配置字典
sitemaps = {
'blog': ArticleSiteMap, # 文章网站地图
'Category': CategorySiteMap, # 分类网站地图
'Tag': TagSiteMap, # 标签网站地图
'User': UserSiteMap, # 用户网站地图
'static': StaticViewSitemap # 静态页面网站地图
}
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
# 自定义错误处理视图
handler404 = 'blog.views.page_not_found_view' # 404 页面未找到处理视图
handler500 = 'blog.views.server_error_view' # 500 服务器错误处理视图
handle403 = 'blog.views.permission_denied_view' # 403 权限拒绝处理视图
# URL 模式配置列表
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
# 国际化 URL 配置
path('i18n/', include('django.conf.urls.i18n')), # 包含国际化相关的 URL
]
# 国际化 URL 模式配置
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls),
re_path(r'', include('blog.urls', namespace='blog')),
re_path(r'mdeditor/', include('mdeditor.urls')),
re_path(r'', include('comments.urls', namespace='comment')),
re_path(r'', include('accounts.urls', namespace='account')),
re_path(r'', include('oauth.urls', namespace='oauth')),
# 管理后台 URL
re_path(r'^admin/', admin_site.urls), # 自定义管理站点 URL
# 博客应用 URL
re_path(r'', include('blog.urls', namespace='blog')), # 包含博客应用的 URL命名空间为 'blog'
# Markdown 编辑器 URL
re_path(r'mdeditor/', include('mdeditor.urls')), # 包含 Markdown 编辑器的 URL
# 评论应用 URL
re_path(r'', include('comments.urls', namespace='comment')), # 包含评论应用的 URL命名空间为 'comment'
# 账户应用 URL
re_path(r'', include('accounts.urls', namespace='account')), # 包含账户应用的 URL命名空间为 'account'
# OAuth 认证 URL
re_path(r'', include('oauth.urls', namespace='oauth')), # 包含 OAuth 应用的 URL命名空间为 'oauth'
# 网站地图 URL
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
name='django.contrib.sitemaps.views.sitemap'), # 网站地图 XML 文件访问路径
# RSS 订阅 URL
re_path(r'^feed/$', DjangoBlogFeed()), # RSS 订阅 feed 路径
re_path(r'^rss/$', DjangoBlogFeed()), # RSS 订阅 rss 路径(别名)
# 搜索功能 URL
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
name='search'), # 搜索功能路径,使用 Elasticsearch 搜索视图和表单
# 服务器管理应用 URL
re_path(r'', include('servermanager.urls', namespace='servermanager')), # 包含服务器管理应用的 URL
# OwnTracks 位置追踪 URL
re_path(r'', include('owntracks.urls', namespace='owntracks')), # 包含 OwnTracks 应用的 URL
# 国际化配置:不为默认语言添加前缀
prefix_default_language=False
) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # 添加静态文件 URL 配置
# 调试模式下的媒体文件 URL 配置
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
document_root=settings.MEDIA_ROOT) # 在调试模式下提供媒体文件服务

@ -1,232 +1,390 @@
#!/usr/bin/env python
# encoding: utf-8
import logging
import os
import random
import string
import uuid
from hashlib import sha256
import bleach
import markdown
import requests
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
logger = logging.getLogger(__name__)
"""
utils.py - DjangoBlog 通用工具函数模块
包含项目中使用的各种辅助函数和工具类
"""
# 导入系统和第三方模块
import logging # 日志记录模块
import os # 操作系统接口模块
import random # 随机数生成模块
import string # 字符串处理模块
import uuid # UUID 生成模块
from hashlib import sha256 # SHA256 哈希函数
import bleach # HTML 清理库
import markdown # Markdown 解析库
import requests # HTTP 请求库
from django.conf import settings # Django 配置
from django.contrib.sites.models import Site # Django 站点框架模型
from django.core.cache import cache # Django 缓存系统
from django.templatetags.static import static # Django 静态文件处理
logger = logging.getLogger(__name__) # 创建当前模块的日志记录器
def get_max_articleid_commentid():
"""
获取文章和评论的最大 ID
返回:
tuple: (最大文章ID, 最大评论ID)
"""
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
m = sha256(str.encode('utf-8'))
return m.hexdigest()
"""
计算字符串的 SHA256 哈希值
参数:
str (str): 要计算哈希的字符串
返回:
str: SHA256 哈希值的十六进制表示
"""
m = sha256(str.encode('utf-8')) # 创建 SHA256 哈希对象并编码字符串
return m.hexdigest() # 返回十六进制格式的哈希值
def cache_decorator(expiration=3 * 60):
"""
缓存装饰器用于缓存函数的返回值
参数:
expiration (int): 缓存过期时间默认3分钟
返回:
function: 装饰器函数
"""
def wrapper(func):
def news(*args, **kwargs):
try:
# 尝试从视图对象获取缓存键
view = args[0]
key = view.get_cache_key()
except:
key = None
# 如果没有获取到缓存键,则生成一个唯一的键
if not key:
unique_str = repr((func, args, kwargs))
unique_str = repr((func, args, kwargs)) # 生成函数调用的唯一标识字符串
m = sha256(unique_str.encode('utf-8')) # 计算哈希值
key = m.hexdigest() # 用作缓存键
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
# 尝试从缓存中获取值
value = cache.get(key)
if value is not None:
# 如果缓存中存在值
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
if str(value) == '__default_cache_value__':
return None
return None # 特殊值表示空缓存
else:
return value
return value # 返回缓存值
else:
# 如果缓存中不存在值,执行函数并缓存结果
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
value = func(*args, **kwargs)
value = func(*args, **kwargs) # 执行原函数
# 根据函数返回值设置缓存
if value is None:
cache.set(key, '__default_cache_value__', expiration)
cache.set(key, '__default_cache_value__', expiration) # 空值特殊处理
else:
cache.set(key, value, expiration)
return value
cache.set(key, value, expiration) # 缓存实际值
return value # 返回函数执行结果
return news
return news # 返回包装后的函数
return wrapper
return wrapper # 返回装饰器
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
刷新视图缓存删除指定 URL 的缓存
参数:
path: URL 路径
servername: 主机名
serverport: 端口号
key_prefix: 缓存键前缀
返回:
bool: 是否成功删除缓存
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
# 创建模拟的 HTTP 请求对象
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
# 生成缓存键
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path))
if cache.get(key):
cache.delete(key)
if cache.get(key): # 如果缓存存在
cache.delete(key) # 删除缓存
return True
return False
@cache_decorator()
@cache_decorator() # 使用缓存装饰器缓存站点信息
def get_current_site():
site = Site.objects.get_current()
"""
获取当前站点信息
返回:
Site: 当前站点对象
"""
site = Site.objects.get_current() # 获取当前站点
return site
class CommonMarkdown:
"""
Markdown 处理工具类
提供 Markdown 文本到 HTML 的转换功能
"""
@staticmethod
def _convert_markdown(value):
"""
内部方法 Markdown 转换为 HTML
参数:
value (str): Markdown 格式的文本
返回:
tuple: (HTML 内容, TOC 目录)
"""
# 创建 Markdown 解析器,启用多个扩展功能
md = markdown.Markdown(
extensions=[
'extra',
'codehilite',
'toc',
'tables',
'extra', # 额外的 Markdown 功能
'codehilite', # 代码高亮
'toc', # 目录生成
'tables', # 表格支持
]
)
body = md.convert(value)
toc = md.toc
body = md.convert(value) # 转换 Markdown 为 HTML
toc = md.toc # 获取目录
return body, toc
@staticmethod
def get_markdown_with_toc(value):
"""
Markdown 转换为 HTML 并返回目录
参数:
value (str): Markdown 格式的文本
返回:
tuple: (HTML 内容, TOC 目录)
"""
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
"""
Markdown 转换为 HTML不返回目录
参数:
value (str): Markdown 格式的文本
返回:
str: HTML 格式的内容
"""
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
"""
发送邮件
参数:
emailto (list): 收件人邮箱列表
title (str): 邮件标题
content (str): 邮件内容
"""
from djangoblog.blog_signals import send_email_signal
# 发送自定义的邮件发送信号
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
send_email.__class__, # 发送者
emailto=emailto, # 收件人
title=title, # 标题
content=content) # 内容
def generate_code() -> str:
"""生成随机数验证码"""
return ''.join(random.sample(string.digits, 6))
"""
生成随机数验证码6位数字
返回:
str: 6位数字组成的验证码字符串
"""
return ''.join(random.sample(string.digits, 6)) # 从数字字符串中随机采样6个字符
def parse_dict_to_url(dict):
from urllib.parse import quote
"""
将字典转换为 URL 查询参数格式
参数:
dict (dict): 要转换的字典
返回:
str: URL 查询参数字符串key1=value1&key2=value2 格式
"""
from urllib.parse import quote # URL 编码函数
# 将字典中的每个键值对转换为 URL 编码的键值对,并用 & 连接
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
def get_blog_setting():
value = cache.get('get_blog_setting')
"""
获取博客设置信息带缓存
返回:
BlogSettings: 博客设置对象
"""
value = cache.get('get_blog_setting') # 从缓存中获取设置
if value:
return value
return value # 如果缓存中有值,直接返回
else:
from blog.models import BlogSettings
# 如果没有设置记录,则创建默认设置
if not BlogSettings.objects.count():
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
setting.site_seo_description = '基于Django的博客系统'
setting.site_keywords = 'Django,Python'
setting.article_sub_length = 300
setting.sidebar_article_count = 10
setting.sidebar_comment_count = 5
setting.show_google_adsense = False
setting.open_site_comment = True
setting.analytics_code = ''
setting.beian_code = ''
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
value = BlogSettings.objects.first()
setting.site_name = 'djangoblog' # 站点名称
setting.site_description = '基于Django的博客系统' # 站点描述
setting.site_seo_description = '基于Django的博客系统' # SEO 描述
setting.site_keywords = 'Django,Python' # SEO 关键词
setting.article_sub_length = 300 # 文章摘要长度
setting.sidebar_article_count = 10 # 侧边栏文章数量
setting.sidebar_comment_count = 5 # 侧边栏评论数量
setting.show_google_adsense = False # 是否显示 Google 广告
setting.open_site_comment = True # 是否开放站点评论
setting.analytics_code = '' # 分析代码
setting.beian_code = '' # 备案号
setting.show_gongan_code = False # 是否显示公安备案号
setting.comment_need_review = False # 评论是否需要审核
setting.save() # 保存默认设置
value = BlogSettings.objects.first() # 获取第一个设置对象
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value)
cache.set('get_blog_setting', value) # 缓存设置对象
return value
def save_user_avatar(url):
'''
保存用户头像
:param url:头像url
:return: 本地路径
保存用户头像到本地
参数:
url (str): 头像图片的 URL 地址
返回:
str: 本地头像文件的静态 URL 路径
'''
logger.info(url)
logger.info(url) # 记录头像 URL
try:
# 构建本地存储路径
basedir = os.path.join(settings.STATICFILES, 'avatar')
# 下载头像图片
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
if rsp.status_code == 200: # 如果下载成功
# 如果目录不存在则创建
if not os.path.exists(basedir):
os.makedirs(basedir)
# 检查 URL 是否为图片格式
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
ext = os.path.splitext(url)[1] if isimage else '.jpg' # 获取文件扩展名
# 生成唯一文件名
save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像:' + basedir + save_filename)
# 保存图片文件
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
# 返回静态文件 URL
return static('avatar/' + save_filename)
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png')
logger.error(e) # 记录错误
return static('blog/img/avatar.png') # 返回默认头像
def delete_sidebar_cache():
"""
删除侧边栏缓存
"""
from blog.models import LinkShowType
# 生成所有侧边栏缓存键
keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys:
logger.info('delete sidebar key:' + k)
cache.delete(k)
cache.delete(k) # 删除每个缓存键
def delete_view_cache(prefix, keys):
"""
删除模板片段缓存
参数:
prefix (str): 缓存前缀
keys (list): 缓存键列表
"""
from django.core.cache.utils import make_template_fragment_key
# 生成模板片段缓存键
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
cache.delete(key) # 删除缓存
def get_resource_url():
"""
获取资源文件的基础 URL
返回:
str: 静态资源的基础 URL
"""
if settings.STATIC_URL:
return settings.STATIC_URL
return settings.STATIC_URL # 如果配置了静态 URL直接返回
else:
site = get_current_site()
site = get_current_site() # 否则根据当前站点生成
return 'http://' + site.domain + '/static/'
# 允许的 HTML 标签列表(用于安全清理)
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
# 允许的 HTML 属性列表
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
def sanitize_html(html):
"""
清理 HTML 内容移除不安全的标签和属性
参数:
html (str): 原始 HTML 内容
返回:
str: 清理后的安全 HTML 内容
"""
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

File diff suppressed because it is too large Load Diff

@ -1,16 +1,40 @@
"""
WSGI config for djangoblog project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
# WSGI (Web Server Gateway Interface) 配置文件
# 用于配置 Django 应用与 Web 服务器之间的接口
import os
import os # 导入操作系统接口模块
# 从 Django 核心模块导入 WSGI 应用工厂函数
from django.core.wsgi import get_wsgi_application
# 设置默认的 Django 配置模块环境变量
# 如果环境变量 DJANGO_SETTINGS_MODULE 未设置,则使用 "djangoblog.settings"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
# 获取 WSGI 应用实例
# 这个 application 对象是 WSGI 可调用对象Web 服务器将通过它来处理 HTTP 请求
application = get_wsgi_application()
```
这个 WSGI 配置文件的作用包括
1. **环境配置**
- 设置 `DJANGO_SETTINGS_MODULE` 环境变量指定 Django 项目的配置文件位置
- 确保即使在没有明确设置环境变量的情况下也能正确加载配置
2. **应用实例创建**
- 通过 `get_wsgi_application()` 函数创建 Django WSGI 应用实例
- 这个实例是 Django 应用与 Web 服务器通信的入口点
3. **服务器集成**
- 生产环境的 Web 服务器 GunicornuWSGIApache with mod_wsgi
会导入这个 [application](file://D:\zyd2025\src\djangoblog\wsgi.py#L15-L15) 对象来处理 HTTP 请求
- 遵循 WSGI 标准确保与各种 WSGI 兼容的服务器协同工作
4. **模块级变量暴露**
- WSGI 可调用对象作为模块级变量 [application](file://D:\zyd2025\src\djangoblog\wsgi.py#L15-L15) 暴露出来
- 便于 Web 服务器直接导入和使用
这是 Django 项目部署时的标准配置文件是连接 Django 应用与生产环境 Web 服务器的关键桥梁
Loading…
Cancel
Save