diff --git a/src/DjangoBlog/blog/documents.py b/src/DjangoBlog/blog/documents.py index 9367528..880192b 100644 --- a/src/DjangoBlog/blog/documents.py +++ b/src/DjangoBlog/blog/documents.py @@ -1,4 +1,5 @@ import time +import logging import elasticsearch.client from django.conf import settings from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean @@ -6,14 +7,18 @@ from elasticsearch_dsl.connections import connections from blog.models import Article +logger = logging.getLogger(__name__) + ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL') # 是否启用 Elasticsearch # 如果启用,则建立连接 if ELASTICSEARCH_ENABLED: connections.create_connection(hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]) from elasticsearch import Elasticsearch + es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts']) from elasticsearch.client import IngestClient + c = IngestClient(es) try: c.get_pipeline('geoip') @@ -27,6 +32,7 @@ if ELASTICSEARCH_ENABLED: } ''') + # 定义 IP 地理位置信息内部文档 class GeoIp(InnerDoc): continent_name = Keyword() @@ -34,19 +40,23 @@ class GeoIp(InnerDoc): country_name = Keyword() location = GeoPoint() + # 用户代理(浏览器/设备/操作系统)相关内部类 class UserAgentBrowser(InnerDoc): Family = Keyword() Version = Keyword() + class UserAgentOS(UserAgentBrowser): pass + class UserAgentDevice(InnerDoc): Family = Keyword() Brand = Keyword() Model = Keyword() + class UserAgent(InnerDoc): browser = Object(UserAgentBrowser, required=False) os = Object(UserAgentOS, required=False) @@ -54,6 +64,7 @@ class UserAgent(InnerDoc): string = Text() is_bot = Boolean() + # 性能监控文档:记录每个请求的 URL、耗时、IP、用户代理等 class ElapsedTimeDocument(Document): url = Keyword() @@ -67,6 +78,7 @@ class ElapsedTimeDocument(Document): name = 'performance' settings = {"number_of_shards": 1, "number_of_replicas": 0} + # 文章搜索文档:用于全文检索 class ArticleDocument(Document): body = Text(analyzer='ik_max_word', search_analyzer='ik_smart') # 使用IK中文分词 @@ -85,4 +97,305 @@ class ArticleDocument(Document): name = 'blog' settings = {"number_of_shards": 1, "number_of_replicas": 0} -# Elasticsearch 索引管理工具类(略,见原文档) \ No newline at end of file + +# 新增:ArticleDocumentManager 类 +class ArticleDocumentManager: + """ + ArticleDocument 管理器类 + 用于处理 Elasticsearch 相关的文章搜索操作 + """ + + def __init__(self, document_class=ArticleDocument): + self.document_class = document_class + self.es_enabled = ELASTICSEARCH_ENABLED + + def search_articles(self, query, category=None, author=None, tags=None, size=10, from_index=0): + """ + 搜索文章 + """ + if not self.es_enabled: + # 如果 Elasticsearch 未启用,返回空结果 + return [], 0 + + try: + search = self.document_class.search() + + if query: + # 多字段匹配搜索,标题权重更高 + search = search.query( + "multi_match", + query=query, + fields=[ + 'title^3', # 标题权重最高 + 'body^2', # 正文权重次之 + 'tags.name^2', # 标签权重 + 'category.name^1.5', # 分类权重 + 'author.nickname^1' # 作者权重 + ], + fuzziness="AUTO" # 自动模糊匹配 + ) + + # 应用过滤器 + if category: + search = search.filter('term', category__name=category) + + if author: + search = search.filter('term', author__nickname=author) + + if tags: + if isinstance(tags, str): + tags = [tags] + for tag in tags: + search = search.filter('term', tags__name=tag) + + # 只搜索已发布的文章 + search = search.filter('term', status='p') + + # 获取总数 + total = search.count() + + # 应用分页 + search = search[from_index:from_index + size] + + # 执行搜索 + response = search.execute() + + return response, total + + except Exception as e: + logger.error(f"Elasticsearch search failed: {e}") + return [], 0 + + def get_popular_articles(self, size=10): + """ + 获取热门文章(按浏览量排序) + """ + if not self.es_enabled: + return [] + + try: + search = self.document_class.search() + search = search.filter('term', status='p') + search = search.sort('-views') # 按浏览量降序 + search = search[:size] + response = search.execute() + return response + except Exception as e: + logger.error(f"Failed to get popular articles: {e}") + return [] + + def get_recent_articles(self, size=10): + """ + 获取最新文章(按发布时间排序) + """ + if not self.es_enabled: + return [] + + try: + search = self.document_class.search() + search = search.filter('term', status='p') + search = search.sort('-pub_time') # 按发布时间降序 + search = search[:size] + response = search.execute() + return response + except Exception as e: + logger.error(f"Failed to get recent articles: {e}") + return [] + + def get_articles_by_category(self, category_name, size=10): + """ + 根据分类获取文章 + """ + if not self.es_enabled: + return [] + + try: + search = self.document_class.search() + search = search.filter('term', category__name=category_name) + search = search.filter('term', status='p') + search = search.sort('-pub_time') + search = search[:size] + response = search.execute() + return response + except Exception as e: + logger.error(f"Failed to get articles by category: {e}") + return [] + + def get_articles_by_tag(self, tag_name, size=10): + """ + 根据标签获取文章 + """ + if not self.es_enabled: + return [] + + try: + search = self.document_class.search() + search = search.filter('term', tags__name=tag_name) + search = search.filter('term', status='p') + search = search.sort('-pub_time') + search = search[:size] + response = search.execute() + return response + except Exception as e: + logger.error(f"Failed to get articles by tag: {e}") + return [] + + def get_similar_articles(self, article_id, size=5): + """ + 获取相似文章(基于更多相似项) + """ + if not self.es_enabled: + return [] + + try: + search = self.document_class.search() + + # 使用 more_like_this 查询找到相似文章 + search = search.query( + 'more_like_this', + fields=['title', 'body', 'tags.name', 'category.name'], + like={"_id": article_id}, + min_term_freq=1, + max_query_terms=12 + ) + + search = search.filter('term', status='p') + search = search.exclude('term', _id=article_id) # 排除当前文章 + search = search[:size] + + response = search.execute() + return response + + except Exception as e: + logger.error(f"Failed to get similar articles: {e}") + return [] + + def rebuild_index(self): + """ + 重建文章索引 + """ + if not self.es_enabled: + return False + + try: + # 删除现有索引 + self.document_class._index.delete(ignore=404) + + # 创建新索引 + self.document_class._index.create() + + # 重新索引所有文章 + self.document_class.init() + + logger.info("Article index rebuilt successfully") + return True + + except Exception as e: + logger.error(f"Failed to rebuild article index: {e}") + return False + + def get_index_stats(self): + """ + 获取索引统计信息 + """ + if not self.es_enabled: + return {} + + try: + from elasticsearch.client import IndicesClient + client = IndicesClient(connections.get_connection()) + stats = client.stats(index='blog') + return stats + except Exception as e: + logger.error(f"Failed to get index stats: {e}") + return {} + + +# 创建全局实例 +article_document_manager = ArticleDocumentManager() + + +# Elasticsearch 索引管理工具类 +class ElasticsearchManager: + """ + Elasticsearch 索引管理工具类 + """ + + def __init__(self): + self.es_enabled = ELASTICSEARCH_ENABLED + + def create_all_indices(self): + """ + 创建所有索引 + """ + if not self.es_enabled: + return False + + try: + # 创建文章索引 + ArticleDocument.init() + + # 创建性能监控索引 + ElapsedTimeDocument.init() + + logger.info("All Elasticsearch indices created successfully") + return True + + except Exception as e: + logger.error(f"Failed to create indices: {e}") + return False + + def delete_all_indices(self): + """ + 删除所有索引 + """ + if not self.es_enabled: + return False + + try: + ArticleDocument._index.delete(ignore=404) + ElapsedTimeDocument._index.delete(ignore=404) + + logger.info("All Elasticsearch indices deleted successfully") + return True + + except Exception as e: + logger.error(f"Failed to delete indices: {e}") + return False + + def refresh_all_indices(self): + """ + 刷新所有索引 + """ + if not self.es_enabled: + return False + + try: + from elasticsearch.client import IndicesClient + client = IndicesClient(connections.get_connection()) + client.refresh(index='_all') + + logger.info("All Elasticsearch indices refreshed successfully") + return True + + except Exception as e: + logger.error(f"Failed to refresh indices: {e}") + return False + + def get_cluster_health(self): + """ + 获取集群健康状态 + """ + if not self.es_enabled: + return {} + + try: + health = connections.get_connection().cluster.health() + return health + except Exception as e: + logger.error(f"Failed to get cluster health: {e}") + return {} + + +# 创建全局实例 +elasticsearch_manager = ElasticsearchManager() \ No newline at end of file diff --git a/src/DjangoBlog/blog/middleware.py b/src/DjangoBlog/blog/middleware.py index 9baa6fa..2ae8fbc 100644 --- a/src/DjangoBlog/blog/middleware.py +++ b/src/DjangoBlog/blog/middleware.py @@ -1,10 +1,67 @@ -# 记录每个请求的加载时间、IP、用户代理,可选地存入 Elasticsearch -class OnlineMiddleware(object): +import time # 添加这行 +import logging +from django.conf import settings +from django.utils import timezone + +logger = logging.getLogger(__name__) + + +class OnlineMiddleware: + """ + 在线用户中间件 - 记录每个请求的加载时间、IP、用户代理,可选地存入 Elasticsearch + """ + def __init__(self, get_response=None): self.get_response = get_response def __call__(self, request): + # 记录请求开始时间 start_time = time.time() + + # 处理请求 response = self.get_response(request) + # 计算耗时,记录并显示到页面 - ... \ No newline at end of file + duration = time.time() - start_time + + # 获取客户端IP + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + + # 获取用户代理 + user_agent = request.META.get('HTTP_USER_AGENT', '') + + # 记录日志 + logger.info( + f"Request: {request.method} {request.path} - " + f"IP: {ip} - " + f"Time: {duration:.3f}s" + ) + + # 可选:存入 Elasticsearch + if hasattr(settings, 'ELASTICSEARCH_DSL') and settings.ELASTICSEARCH_DSL: + try: + from blog.documents import ElapsedTimeDocument + doc = ElapsedTimeDocument( + url=request.path, + time_taken=int(duration * 1000), + log_datetime=timezone.now(), + ip=ip, + useragent={'string': user_agent} + ) + doc.save() + except Exception as e: + logger.warning(f"Failed to save to Elasticsearch: {e}") + + # 添加处理时间到响应头 + response['X-Response-Time'] = f'{duration:.3f}s' + + return response + + def process_exception(self, request, exception): + """处理异常""" + logger.error(f"Middleware exception: {exception}") + return None \ No newline at end of file diff --git a/src/DjangoBlog/djangoblog/admin_site.py b/src/DjangoBlog/djangoblog/admin_site.py index 63d3c02..9432255 100644 --- a/src/DjangoBlog/djangoblog/admin_site.py +++ b/src/DjangoBlog/djangoblog/admin_site.py @@ -84,7 +84,7 @@ class DjangoBlogAdminSite(AdminSite): admin_site = DjangoBlogAdminSite(name='admin') # 注册博客相关模型到后台管理 -admin_site.register(Article, ArticlelAdmin) # 文章模型 +admin_site.register(Article, ArticleAdmin) # 文章模型 admin_site.register(Category, CategoryAdmin) # 分类模型 admin_site.register(Tag, TagAdmin) # 标签模型 admin_site.register(Links, LinksAdmin) # 友情链接模型 diff --git a/src/DjangoBlog/oauth/migrations/0001_initial.py b/src/DjangoBlog/oauth/migrations/0001_initial.py index e8776b0..9efb89b 100644 --- a/src/DjangoBlog/oauth/migrations/0001_initial.py +++ b/src/DjangoBlog/oauth/migrations/0001_initial.py @@ -1,197 +1,56 @@ -""" -<<<<<<< HEAD -Django 数据库迁移模块 - OAuth 认证配置 -======= -OAuth应用数据库迁移文件 +# oauth/migrations/0001_initial.py -本迁移文件由Django自动生成,用于创建OAuth认证相关的数据库表结构。 -包含OAuth配置和OAuth用户两个主要模型,支持多种第三方登录方式。 - -生成的表结构: -- oauth_oauthconfig: OAuth服务提供商配置表 -- oauth_oauthuser: OAuth用户信息表 - -迁移依赖: -- 依赖于Django内置的用户模型(AUTH_USER_MODEL) -""" - -# Generated by Django 4.1.7 on 2023-03-07 09:53 ->>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5 - -该模块用于创建OAuth认证相关的数据库表结构,包含OAuth服务提供商配置和OAuth用户信息两个主要模型。 -这是Django迁移系统自动生成的迁移文件,在Django 4.1.7版本中创建于2023-03-07。 -""" - -# 导入Django核心模块 -from django.conf import settings # 导入Django设置 -from django.db import migrations, models # 导入数据库迁移和模型相关功能 -import django.db.models.deletion # 导入外键删除操作 -import django.utils.timezone # 导入时区工具 +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone class Migration(migrations.Migration): - """ -<<<<<<< HEAD - OAuth认证系统的数据库迁移类 -======= - OAuth应用初始迁移类 - 继承自migrations.Migration,定义数据库表结构的创建操作。 - initial = True 表示这是该应用的第一个迁移文件。 - """ ->>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5 - - 这个迁移类负责创建OAuth认证功能所需的数据库表结构, - 包括OAuth服务提供商配置和第三方登录用户信息存储。 - """ - - # 标记为初始迁移 initial = True - # 定义依赖关系 - 依赖于可切换的用户模型 dependencies = [ - # 声明对Django用户模型的依赖 migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] - # 定义要执行的数据库操作 operations = [ -<<<<<<< HEAD - # 创建OAuthConfig模型对应的数据库表 - migrations.CreateModel( - name='OAuthConfig', - fields=[ - # 主键ID字段,自增BigAutoField - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - # OAuth服务类型选择字段,支持多种第三方登录 - ('type', models.CharField( - choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), - ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')), - # OAuth应用的AppKey字段 - ('appkey', models.CharField(max_length=200, verbose_name='AppKey')), - # OAuth应用的AppSecret字段,用于安全认证 - ('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')), - # OAuth回调地址字段,用于接收授权码 - ('callback_url', - models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')), - # 是否启用该OAuth配置的布尔字段 - ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), - # 记录创建时间,默认使用当前时间 - ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - # 记录最后修改时间,默认使用当前时间 - ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), - ], - options={ - # 设置模型在Admin中的单数显示名称 -======= - # 创建OAuth配置表 migrations.CreateModel( name='OAuthConfig', fields=[ - # 主键字段 - 使用BigAutoField作为自增主键 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - # OAuth类型字段 - 使用选择框限定支持的第三方登录类型 - ('type', models.CharField( - choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), - ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')), - # AppKey字段 - 存储OAuth应用的密钥标识 + ('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')), ('appkey', models.CharField(max_length=200, verbose_name='AppKey')), - # AppSecret字段 - 存储OAuth应用的密钥 ('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')), - # 回调地址字段 - 默认设置为百度首页 - ('callback_url', - models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')), - # 启用状态字段 - 控制该OAuth配置是否可用 + ('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')), ('is_enable', models.BooleanField(default=True, verbose_name='是否显示')), - # 创建时间字段 - 自动记录创建时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - # 最后修改时间字段 - 自动记录最后修改时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), ], options={ - # 管理后台显示名称 ->>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5 'verbose_name': 'oauth配置', - # 设置模型在Admin中的复数显示名称 'verbose_name_plural': 'oauth配置', -<<<<<<< HEAD - # 设置默认排序字段,按创建时间降序排列 - 'ordering': ['-created_time'], - }, - ), - # 创建OAuthUser模型对应的数据库表 - migrations.CreateModel( - name='OAuthUser', - fields=[ - # 主键ID字段,自增BigAutoField - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - # 第三方平台的用户唯一标识 - ('openid', models.CharField(max_length=50)), - # 用户在第三方平台的昵称 - ('nickname', models.CharField(max_length=50, verbose_name='昵称')), - # OAuth访问令牌,可为空 - ('token', models.CharField(blank=True, max_length=150, null=True)), - # 用户头像URL,可为空 - ('picture', models.CharField(blank=True, max_length=350, null=True)), - # OAuth服务类型 - ('type', models.CharField(max_length=50)), - # 用户邮箱,可为空 - ('email', models.CharField(blank=True, max_length=50, null=True)), - # 存储额外的元数据信息,使用Text字段 - ('metadata', models.TextField(blank=True, null=True)), - # 记录创建时间,默认使用当前时间 - ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - # 记录最后修改时间,默认使用当前时间 - ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), - # 外键关联到本地用户模型,建立第三方账号与本地用户的关联 -======= - # 默认排序规则 - 按创建时间倒序 'ordering': ['-created_time'], }, ), - # 创建OAuth用户表 migrations.CreateModel( name='OAuthUser', fields=[ - # 主键字段 ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - # 第三方平台用户唯一标识 ('openid', models.CharField(max_length=50)), - # 用户在第三方平台的昵称 ('nickname', models.CharField(max_length=50, verbose_name='昵称')), - # OAuth访问令牌 - 可为空 ('token', models.CharField(blank=True, max_length=150, null=True)), - # 用户头像URL - 可为空 ('picture', models.CharField(blank=True, max_length=350, null=True)), - # OAuth类型 - 标识来自哪个第三方平台 ('type', models.CharField(max_length=50)), - # 用户邮箱 - 可为空 ('email', models.CharField(blank=True, max_length=50, null=True)), - # 元数据字段 - 存储额外的用户信息(JSON格式) ('metadata', models.TextField(blank=True, null=True)), - # 创建时间 ('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')), - # 最后修改时间 ('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')), - # 外键关联到本地用户 - 建立第三方账号与本地账号的关联 ->>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5 - ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL, verbose_name='用户')), + ('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), ], options={ -<<<<<<< HEAD - # 设置模型在Admin中的单数显示名称 -======= - # 管理后台显示名称 ->>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5 'verbose_name': 'oauth用户', - # 设置模型在Admin中的复数显示名称 'verbose_name_plural': 'oauth用户', -<<<<<<< HEAD - # 设置默认排序字段,按创建时间降序排列 -======= - # 默认排序规则 ->>>>>>> d4786ee23b15aa002b21504f1056098d46f303c5 'ordering': ['-created_time'], }, ),