|
|
import logging
|
|
|
from djangoblog.plugin_manage.base_plugin import BasePlugin
|
|
|
from djangoblog.plugin_manage import hooks
|
|
|
from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD
|
|
|
from blog.models import Article
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
class ArticleRecommendationPlugin(BasePlugin):
|
|
|
PLUGIN_NAME = '文章推荐'
|
|
|
PLUGIN_DESCRIPTION = '智能文章推荐系统,支持多位置展示'
|
|
|
PLUGIN_VERSION = '1.0.0'
|
|
|
PLUGIN_AUTHOR = 'liangliangyy'
|
|
|
|
|
|
# 支持的位置
|
|
|
SUPPORTED_POSITIONS = ['article_bottom']
|
|
|
|
|
|
# 各位置优先级
|
|
|
POSITION_PRIORITIES = {
|
|
|
'article_bottom': 80, # 文章底部优先级
|
|
|
}
|
|
|
|
|
|
# 插件配置
|
|
|
CONFIG = {
|
|
|
'article_bottom_count': 8, # 文章底部推荐数量
|
|
|
'sidebar_count': 5, # 侧边栏推荐数量
|
|
|
'enable_category_fallback': True, # 启用分类回退
|
|
|
'enable_popular_fallback': True, # 启用热门文章回退
|
|
|
}
|
|
|
|
|
|
def register_hooks(self):
|
|
|
"""注册钩子"""
|
|
|
hooks.register(ARTICLE_DETAIL_LOAD, self.on_article_detail_load)
|
|
|
|
|
|
def on_article_detail_load(self, article, context, request, *args, **kwargs):
|
|
|
"""文章详情页加载时的处理"""
|
|
|
# 可以在这里预加载推荐数据到context中
|
|
|
recommendations = self.get_recommendations(article)
|
|
|
context['article_recommendations'] = recommendations
|
|
|
|
|
|
def should_display(self, position, context, **kwargs):
|
|
|
"""条件显示逻辑"""
|
|
|
# 只在文章详情页底部显示
|
|
|
if position == 'article_bottom':
|
|
|
article = kwargs.get('article') or context.get('article')
|
|
|
# 检查是否有文章对象,以及是否不是索引页面
|
|
|
is_index = context.get('isindex', False) if hasattr(context, 'get') else False
|
|
|
return article is not None and not is_index
|
|
|
|
|
|
return False
|
|
|
|
|
|
def render_article_bottom_widget(self, context, **kwargs):
|
|
|
"""渲染文章底部推荐"""
|
|
|
article = kwargs.get('article') or context.get('article')
|
|
|
if not article:
|
|
|
return None
|
|
|
|
|
|
# 使用配置的数量,也可以通过kwargs覆盖
|
|
|
count = kwargs.get('count', self.CONFIG['article_bottom_count'])
|
|
|
recommendations = self.get_recommendations(article, count=count)
|
|
|
if not recommendations:
|
|
|
return None
|
|
|
|
|
|
# 将RequestContext转换为普通字典
|
|
|
context_dict = {}
|
|
|
if hasattr(context, 'flatten'):
|
|
|
context_dict = context.flatten()
|
|
|
elif hasattr(context, 'dicts'):
|
|
|
# 合并所有上下文字典
|
|
|
for d in context.dicts:
|
|
|
context_dict.update(d)
|
|
|
|
|
|
template_context = {
|
|
|
'recommendations': recommendations,
|
|
|
'article': article,
|
|
|
'title': '相关推荐',
|
|
|
**context_dict
|
|
|
}
|
|
|
|
|
|
return self.render_template('bottom_widget.html', template_context)
|
|
|
|
|
|
def render_sidebar_widget(self, context, **kwargs):
|
|
|
"""渲染侧边栏推荐"""
|
|
|
article = context.get('article')
|
|
|
|
|
|
# 使用配置的数量,也可以通过kwargs覆盖
|
|
|
count = kwargs.get('count', self.CONFIG['sidebar_count'])
|
|
|
|
|
|
if article:
|
|
|
# 文章页面,显示相关文章
|
|
|
recommendations = self.get_recommendations(article, count=count)
|
|
|
title = '相关文章'
|
|
|
else:
|
|
|
# 其他页面,显示热门文章
|
|
|
recommendations = self.get_popular_articles(count=count)
|
|
|
title = '热门推荐'
|
|
|
|
|
|
if not recommendations:
|
|
|
return None
|
|
|
|
|
|
# 将RequestContext转换为普通字典
|
|
|
context_dict = {}
|
|
|
if hasattr(context, 'flatten'):
|
|
|
context_dict = context.flatten()
|
|
|
elif hasattr(context, 'dicts'):
|
|
|
# 合并所有上下文字典
|
|
|
for d in context.dicts:
|
|
|
context_dict.update(d)
|
|
|
|
|
|
template_context = {
|
|
|
'recommendations': recommendations,
|
|
|
'title': title,
|
|
|
**context_dict
|
|
|
}
|
|
|
|
|
|
return self.render_template('sidebar_widget.html', template_context)
|
|
|
|
|
|
def get_css_files(self):
|
|
|
"""返回CSS文件"""
|
|
|
return ['css/recommendation.css']
|
|
|
|
|
|
def get_js_files(self):
|
|
|
"""返回JS文件"""
|
|
|
return ['js/recommendation.js']
|
|
|
|
|
|
def get_recommendations(self, article, count=5):
|
|
|
"""获取推荐文章"""
|
|
|
if not article:
|
|
|
return []
|
|
|
|
|
|
recommendations = []
|
|
|
|
|
|
# 1. 基于标签的推荐
|
|
|
if article.tags.exists():
|
|
|
tag_ids = list(article.tags.values_list('id', flat=True))
|
|
|
tag_based = list(Article.objects.filter(
|
|
|
status='p',
|
|
|
tags__id__in=tag_ids
|
|
|
).exclude(
|
|
|
id=article.id
|
|
|
).exclude(
|
|
|
title__isnull=True
|
|
|
).exclude(
|
|
|
title__exact=''
|
|
|
).distinct().order_by('-views')[:count])
|
|
|
recommendations.extend(tag_based)
|
|
|
|
|
|
# 2. 如果数量不够,基于分类推荐
|
|
|
if len(recommendations) < count and self.CONFIG['enable_category_fallback']:
|
|
|
needed = count - len(recommendations)
|
|
|
existing_ids = [r.id for r in recommendations] + [article.id]
|
|
|
|
|
|
category_based = list(Article.objects.filter(
|
|
|
status='p',
|
|
|
category=article.category
|
|
|
).exclude(
|
|
|
id__in=existing_ids
|
|
|
).exclude(
|
|
|
title__isnull=True
|
|
|
).exclude(
|
|
|
title__exact=''
|
|
|
).order_by('-views')[:needed])
|
|
|
recommendations.extend(category_based)
|
|
|
|
|
|
# 3. 如果还是不够,推荐热门文章
|
|
|
if len(recommendations) < count and self.CONFIG['enable_popular_fallback']:
|
|
|
needed = count - len(recommendations)
|
|
|
existing_ids = [r.id for r in recommendations] + [article.id]
|
|
|
|
|
|
popular_articles = list(Article.objects.filter(
|
|
|
status='p'
|
|
|
).exclude(
|
|
|
id__in=existing_ids
|
|
|
).exclude(
|
|
|
title__isnull=True
|
|
|
).exclude(
|
|
|
title__exact=''
|
|
|
).order_by('-views')[:needed])
|
|
|
recommendations.extend(popular_articles)
|
|
|
|
|
|
# 过滤掉无效的推荐
|
|
|
valid_recommendations = []
|
|
|
for rec in recommendations:
|
|
|
if rec.title and len(rec.title.strip()) > 0:
|
|
|
valid_recommendations.append(rec)
|
|
|
else:
|
|
|
logger.warning(f"过滤掉空标题文章: ID={rec.id}, 标题='{rec.title}'")
|
|
|
|
|
|
# 调试:记录推荐结果
|
|
|
logger.info(f"原始推荐数量: {len(recommendations)}, 有效推荐数量: {len(valid_recommendations)}")
|
|
|
for i, rec in enumerate(valid_recommendations):
|
|
|
logger.info(f"推荐 {i+1}: ID={rec.id}, 标题='{rec.title}', 长度={len(rec.title)}")
|
|
|
|
|
|
return valid_recommendations[:count]
|
|
|
|
|
|
def get_popular_articles(self, count=3):
|
|
|
"""获取热门文章"""
|
|
|
return list(Article.objects.filter(
|
|
|
status='p'
|
|
|
).order_by('-views')[:count])
|
|
|
|
|
|
|
|
|
# 实例化插件
|
|
|
plugin = ArticleRecommendationPlugin()
|