diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..35410ca
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
diff --git a/src/blog/views.py b/src/blog/views.py
index d5dc7ec..c694ced 100644
--- a/src/blog/views.py
+++ b/src/blog/views.py
@@ -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
diff --git a/src/deploy/docker-compose/docker-compose.es.yml b/src/deploy/docker-compose/docker-compose.es.yml
index 83e35ff..e95c35a 100644
--- a/src/deploy/docker-compose/docker-compose.es.yml
+++ b/src/deploy/docker-compose/docker-compose.es.yml
@@ -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 映射到容器端口 5601(Kibana 默认端口)
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 映射到容器端口 8000(Django 默认端口)
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
diff --git a/src/deploy/docker-compose/docker-compose.yml b/src/deploy/docker-compose/docker-compose.yml
index 9609af3..25facd5 100644
--- a/src/deploy/docker-compose/docker-compose.yml
+++ b/src/deploy/docker-compose/docker-compose.yml
@@ -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 映射到容器端口 3306(MySQL 默认端口)
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 映射到容器端口 8000(Django 默认端口)
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 映射到容器端口 6379(Redis 默认端口)
diff --git a/src/deploy/entrypoint.sh b/src/deploy/entrypoint.sh
index 2fb6491..e6a2950 100644
--- a/src/deploy/entrypoint.sh
+++ b/src/deploy/entrypoint.sh
@@ -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 # 每个工作进程使用的线程数
diff --git a/src/deploy/k8s/configmap.yaml b/src/deploy/k8s/configmap.yaml
index 835d4ad..9ce20a3 100644
--- a/src/deploy/k8s/configmap.yaml
+++ b/src/deploy/k8s/configmap.yaml
@@ -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
diff --git a/src/deploy/k8s/deployment.yaml b/src/deploy/k8s/deployment.yaml
index 414fdcc..476edd3 100644
--- a/src/deploy/k8s/deployment.yaml
+++ b/src/deploy/k8s/deployment.yaml
@@ -1,121 +1,128 @@
-apiVersion: apps/v1
-kind: Deployment
+# deployment.yaml - Kubernetes Deployment 资源配置文件
+# 用于定义应用的部署配置,包括副本数量、容器镜像、资源限制等
+
+---
+# 第一个 Deployment:DjangoBlog 应用部署配置
+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 的持久卷声明
---
+# 第二个 Deployment:Redis 缓存服务部署配置
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 # 内存限制
+
---
+# 第三个 Deployment:MySQL 数据库部署配置
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 的持久卷声明
+
---
+# 第四个 Deployment:Nginx 反向代理部署配置
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 # 使用资源持久卷声明
---
+# 第五个 Deployment:Elasticsearch 搜索引擎部署配置
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 的持久卷声明
diff --git a/src/deploy/k8s/gateway.yaml b/src/deploy/k8s/gateway.yaml
index a8de073..5ff5360 100644
--- a/src/deploy/k8s/gateway.yaml
+++ b/src/deploy/k8s/gateway.yaml
@@ -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
\ No newline at end of file
+ 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 端口
diff --git a/src/deploy/k8s/pv.yaml b/src/deploy/k8s/pv.yaml
index 874b72f..e0e44e3 100644
--- a/src/deploy/k8s/pv.yaml
+++ b/src/deploy/k8s/pv.yaml
@@ -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
\ No newline at end of file
+ - master # 指定只能在名为 master 的节点上使用此 PV
diff --git a/src/deploy/k8s/pvc.yaml b/src/deploy/k8s/pvc.yaml
index ef238c5..78dfddc 100644
--- a/src/deploy/k8s/pvc.yaml
+++ b/src/deploy/k8s/pvc.yaml
@@ -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 # 直接绑定到指定的 PV(local-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 # 直接绑定到指定的 PV(local-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 # 直接绑定到指定的 PV(local-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 # 直接绑定到指定的 PV(local-pv-elasticsearch)
+ storageClassName: local-storage # 存储类名称
accessModes:
- - ReadWriteOnce
+ - ReadWriteOnce # 单节点读写模式
resources:
requests:
- storage: 5Gi
-
\ No newline at end of file
+ storage: 5Gi # 请求存储空间为 5GB
diff --git a/src/deploy/k8s/service.yaml b/src/deploy/k8s/service.yaml
index 4ef2931..514b944 100644
--- a/src/deploy/k8s/service.yaml
+++ b/src/deploy/k8s/service.yaml
@@ -1,80 +1,94 @@
-apiVersion: v1
-kind: Service
+# service.yaml - Kubernetes Service 资源配置文件
+# 用于定义服务,为 Pod 提供稳定的网络访问入口和负载均衡
+
+# 注意:文件开头有一个拼写错误 "tapiVersion",应该是 "apiVersion"
+
+---
+# 第一个 Service:DjangoBlog 应用服务
+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(仅在集群内部可访问)
+
---
+# 第二个 Service:Nginx 服务
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
+
---
+# 第三个 Service:Redis 缓存服务
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
+
---
+# 第四个 Service:MySQL 数据库服务
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
+
---
+# 第五个 Service:Elasticsearch 搜索引擎服务
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
diff --git a/src/deploy/k8s/storageclass.yaml b/src/deploy/k8s/storageclass.yaml
index 5d5a14c..3d55726 100644
--- a/src/deploy/k8s/storageclass.yaml
+++ b/src/deploy/k8s/storageclass.yaml
@@ -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
diff --git a/src/deploy/nginx.conf b/src/deploy/nginx.conf
index 32161d8..e69de29 100644
--- a/src/deploy/nginx.conf
+++ b/src/deploy/nginx.conf
@@ -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;
- }
- }
- }
-}
diff --git a/src/djangoblog/__init__.py b/src/djangoblog/__init__.py
index 1e205f4..1c956ab 100644
--- a/src/djangoblog/__init__.py
+++ b/src/djangoblog/__init__.py
@@ -1 +1,6 @@
+# __init__.py - Django 应用初始化配置文件
+# 该文件用于配置 Django 应用的默认应用配置类
+
+# 指定 Django 应用的默认配置类
+# 当 Django 启动时,会自动加载这个配置类来初始化应用
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'
diff --git a/src/djangoblog/admin_site.py b/src/djangoblog/admin_site.py
index f120405..215e097 100644
--- a/src/djangoblog/admin_site.py
+++ b/src/djangoblog/admin_site.py
@@ -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) # 注册日志条目模型和自定义管理员类
diff --git a/src/djangoblog/apps.py b/src/djangoblog/apps.py
index d29e318..a33fe7d 100644
--- a/src/djangoblog/apps.py
+++ b/src/djangoblog/apps.py
@@ -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()
\ No newline at end of file
+
+ # 在此处导入并加载插件
+ # 将插件加载放在 ready 方法中可以确保 Django 应用完全初始化后再加载插件
+ # 避免在应用未准备就绪时尝试访问 Django 组件导致的问题
+ from .plugin_manage.loader import load_plugins # 导入插件加载器
+ load_plugins() # 调用插件加载函数,动态加载已激活的插件
diff --git a/src/djangoblog/blog_signals.py b/src/djangoblog/blog_signals.py
index 393f441..0511ac5 100644
--- a/src/djangoblog/blog_signals.py
+++ b/src/djangoblog/blog_signals.py
@@ -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() # 注释掉的代码:清除所有缓存
diff --git a/src/djangoblog/elasticsearch_backend.py b/src/djangoblog/elasticsearch_backend.py
index 4afe498..6519b51 100644
--- a/src/djangoblog/elasticsearch_backend.py
+++ b/src/djangoblog/elasticsearch_backend.py
@@ -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 # 指定查询类
diff --git a/src/djangoblog/feeds.py b/src/djangoblog/feeds.py
index 8c4e851..b1d9beb 100644
--- a/src/djangoblog/feeds.py
+++ b/src/djangoblog/feeds.py
@@ -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
diff --git a/src/djangoblog/logentryadmin.py b/src/djangoblog/logentryadmin.py
index 2f6a535..a7148e7 100644
--- a/src/djangoblog/logentryadmin.py
+++ b/src/djangoblog/logentryadmin.py
@@ -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 = '{}'.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 = '{}'.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
diff --git a/src/djangoblog/plugin_manage/base_plugin.py b/src/djangoblog/plugin_manage/base_plugin.py
index 2b4be5c..6621d02 100644
--- a/src/djangoblog/plugin_manage/base_plugin.py
+++ b/src/djangoblog/plugin_manage/base_plugin.py
@@ -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 # 插件版本
}
diff --git a/src/djangoblog/plugin_manage/hook_constants.py b/src/djangoblog/plugin_manage/hook_constants.py
index 6685b7c..b02bbab 100644
--- a/src/djangoblog/plugin_manage/hook_constants.py
+++ b/src/djangoblog/plugin_manage/hook_constants.py
@@ -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" # 文章内容处理钩子
+ # 用于在文章内容显示前对其进行处理或修改
+ # 例如:添加广告、插入相关链接、内容过滤等
diff --git a/src/djangoblog/plugin_manage/hooks.py b/src/djangoblog/plugin_manage/hooks.py
index d712540..c694ced 100644
--- a/src/djangoblog/plugin_manage/hooks.py
+++ b/src/djangoblog/plugin_manage/hooks.py
@@ -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
diff --git a/src/djangoblog/plugin_manage/loader.py b/src/djangoblog/plugin_manage/loader.py
index 12e824b..05c4217 100644
--- a/src/djangoblog/plugin_manage/loader.py
+++ b/src/djangoblog/plugin_manage/loader.py
@@ -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)
\ No newline at end of file
+ # 如果插件导入失败,记录错误日志
+ # exc_info=True 参数会包含完整的异常堆栈信息,便于调试
+ logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
diff --git a/src/djangoblog/settings.py b/src/djangoblog/settings.py
index 30f9ac5..dcc399d 100644
--- a/src/djangoblog/settings.py
+++ b/src/djangoblog/settings.py
@@ -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'
-]
\ No newline at end of file
+# 插件系统配置
+PLUGINS_DIR = BASE_DIR / 'plugins' # 插件目录路径
+ACTIVE_PLUGINS = [ # 激活的插件列表
+ 'article_copyright', # 文章版权插件
+ 'reading_time', # 阅读时间插件
+ 'external_links', # 外部链接插件
+ 'view_count', # 浏览量统计插件
+ 'seo_optimizer' # SEO 优化插件
+]
diff --git a/src/djangoblog/sitemap.py b/src/djangoblog/sitemap.py
index 8b7d446..561cefe 100644
--- a/src/djangoblog/sitemap.py
+++ b/src/djangoblog/sitemap.py
@@ -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 # 返回用户的注册时间字段
diff --git a/src/djangoblog/spider_notify.py b/src/djangoblog/spider_notify.py
index 7b909e9..b2b44f6 100644
--- a/src/djangoblog/spider_notify.py
+++ b/src/djangoblog/spider_notify.py
@@ -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)
diff --git a/src/djangoblog/tests.py b/src/djangoblog/tests.py
index 01237d9..1815d9d 100644
--- a/src/djangoblog/tests.py
+++ b/src/djangoblog/tests.py
@@ -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
diff --git a/src/djangoblog/urls.py b/src/djangoblog/urls.py
index 4aae58a..47bb2b2 100644
--- a/src/djangoblog/urls.py
+++ b/src/djangoblog/urls.py
@@ -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) # 在调试模式下提供媒体文件服务
diff --git a/src/djangoblog/utils.py b/src/djangoblog/utils.py
index 57f63dc..2461be6 100644
--- a/src/djangoblog/utils.py
+++ b/src/djangoblog/utils.py
@@ -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)
diff --git a/src/djangoblog/whoosh_cn_backend.py b/src/djangoblog/whoosh_cn_backend.py
index 04e3f7f..700d58f 100644
--- a/src/djangoblog/whoosh_cn_backend.py
+++ b/src/djangoblog/whoosh_cn_backend.py
@@ -1,7 +1,12 @@
# encoding: utf-8
+"""
+whoosh_cn_backend.py - Django Haystack 的 Whoosh 中文搜索后端实现
+提供基于 Whoosh 搜索引擎的中文全文搜索功能,集成了 jieba 中文分词器
+"""
from __future__ import absolute_import, division, print_function, unicode_literals
+# 导入系统和第三方库模块
import json
import os
import re
@@ -9,61 +14,68 @@ import shutil
import threading
import warnings
-import six
-from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
-from datetime import datetime
-from django.utils.encoding import force_str
-from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query
-from haystack.constants import DJANGO_CT, DJANGO_ID, ID
-from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument
-from haystack.inputs import Clean, Exact, PythonData, Raw
-from haystack.models import SearchResult
-from haystack.utils import get_identifier, get_model_ct
-from haystack.utils import log as logging
-from haystack.utils.app_loading import haystack_get_model
-from jieba.analyse import ChineseAnalyzer
-from whoosh import index
-from whoosh.analysis import StemmingAnalyzer
-from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT
-from whoosh.fields import ID as WHOOSH_ID
-from whoosh.filedb.filestore import FileStorage, RamStorage
-from whoosh.highlight import ContextFragmenter, HtmlFormatter
-from whoosh.highlight import highlight as whoosh_highlight
-from whoosh.qparser import QueryParser
-from whoosh.searching import ResultsPage
-from whoosh.writing import AsyncWriter
-
+import six # Python 2/3 兼容库
+from django.conf import settings # Django 配置
+from django.core.exceptions import ImproperlyConfigured # 配置错误异常
+from datetime import datetime # 日期时间处理
+from django.utils.encoding import force_str # 字符串编码处理
+from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query # Haystack 基类
+from haystack.constants import DJANGO_CT, DJANGO_ID, ID # Haystack 常量
+from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument # Haystack 异常
+from haystack.inputs import Clean, Exact, PythonData, Raw # Haystack 输入类型
+from haystack.models import SearchResult # Haystack 搜索结果模型
+from haystack.utils import get_identifier, get_model_ct # Haystack 工具函数
+from haystack.utils import log as logging # Haystack 日志
+from haystack.utils.app_loading import haystack_get_model # Haystack 模型加载
+from jieba.analyse import ChineseAnalyzer # jieba 中文分词分析器
+from whoosh import index # Whoosh 索引模块
+from whoosh.analysis import StemmingAnalyzer # Whoosh 词干分析器
+from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT # Whoosh 字段类型
+from whoosh.fields import ID as WHOOSH_ID # Whoosh ID 字段
+from whoosh.filedb.filestore import FileStorage, RamStorage # Whoosh 存储模块
+from whoosh.highlight import ContextFragmenter, HtmlFormatter # Whoosh 高亮模块
+from whoosh.highlight import highlight as whoosh_highlight # Whoosh 高亮函数
+from whoosh.qparser import QueryParser # Whoosh 查询解析器
+from whoosh.searching import ResultsPage # Whoosh 结果页面
+from whoosh.writing import AsyncWriter # Whoosh 异步写入器
+
+# 检查是否安装了 Whoosh 库
try:
import whoosh
except ImportError:
raise MissingDependency(
"The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.")
-# Handle minimum requirement.
+# 检查 Whoosh 版本要求(至少 2.5.0)
if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0):
raise MissingDependency(
"The 'whoosh' backend requires version 2.5.0 or greater.")
-# Bubble up the correct error.
-
+# 日期时间正则表达式,用于解析日期时间字符串
DATETIME_REGEX = re.compile(
'^(?P