DjangoBlog插件开发指南(四):复杂插件实战案例


目录:

预计阅读时间:16 分钟

前言

前三篇我们学习了插件的理论知识,今天我们将通过两个真实的复杂插件案例,学习如何设计和实现生产级别的插件。

本文核心内容: - SEO 优化插件完整剖析 - 智能推荐插件设计与实现 - 性能优化技巧 - 开发最佳实践


一、案例一:SEO 优化插件

1.1 功能需求分析

核心功能: - 根据页面类型动态生成 SEO meta 标签 - 生成 Open Graph 标签(社交分享优化) - 生成 JSON-LD 结构化数据(搜索引擎优化) - 支持文章、分类、首页等多种页面类型

技术要点: - 路由感知与上下文分发 - Schema.org 结构化数据标准 - Open Graph Protocol 协议

1.2 插件架构设计

SeoOptimizerPlugin
├── 路由分发器
│   ├── 识别当前页面类型
│   └── 调用对应的SEO生成器
├── SEO数据生成器
│   ├── 文章页SEO生成
│   ├── 分类页SEO生成
│   └── 默认页SEO生成
└── 输出格式化器
    ├── meta标签
    ├── Open Graph标签
    └── JSON-LD脚本

1.3 核心实现

# plugins/seo_optimizer/plugin.py
import json
from django.utils.html import strip_tags
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from blog.models import Article, Category
from djangoblog.utils import get_blog_setting

class SeoOptimizerPlugin(BasePlugin):
    PLUGIN_NAME = 'SEO 优化器'
    PLUGIN_DESCRIPTION = '动态生成 SEO meta 标签和结构化数据'
    PLUGIN_VERSION = '0.2.0'
    PLUGIN_AUTHOR = 'liangliangyy'

    def register_hooks(self):
        """注册到 head_meta 过滤器钩子"""
        hooks.register('head_meta', self.dispatch_seo_generation)

    def dispatch_seo_generation(self, metas, context):
        """
        路由分发器:根据页面类型分发到不同的SEO生成方法
        """
        request = context.get('request')
        if not request:
            return metas

        # 获取当前视图名称
        view_name = request.resolver_match.view_name
        blog_setting = get_blog_setting()

        # 根据视图名称分发
        seo_data = None
        if view_name == 'blog:detailbyid':
            # 文章详情页
            seo_data = self._get_article_seo_data(
                context, request, blog_setting
            )
        elif view_name == 'blog:category_detail':
            # 分类页
            seo_data = self._get_category_seo_data(
                context, request, blog_setting
            )

        if not seo_data:
            # 默认页面(首页等)
            seo_data = self._get_default_seo_data(
                context, request, blog_setting
            )

        # 生成最终HTML
        return metas + self._format_seo_html(seo_data)

1.4 文章页SEO生成

def _get_article_seo_data(self, context, request, blog_setting):
    """生成文章页面的SEO数据"""
    article = context.get('article')
    if not isinstance(article, Article):
        return None

    # 1. 生成描述(截取前150字符)
    description = strip_tags(article.body)[:150]

    # 2. 生成关键词(使用标签)
    keywords = ",".join([tag.name for tag in article.tags.all()])
    if not keywords:
        keywords = blog_setting.site_keywords

    # 3. Open Graph 标签(社交分享优化)
    meta_tags = f'''
    <meta property="og:type" content="article"/>
    <meta property="og:title" content="{article.title}"/>
    <meta property="og:description" content="{description}"/>
    <meta property="og:url" content="{request.build_absolute_uri()}"/>
    <meta property="article:published_time" content="{article.pub_time.isoformat()}"/>
    <meta property="article:modified_time" content="{article.last_modify_time.isoformat()}"/>
    <meta property="article:author" content="{article.author.username}"/>
    <meta property="article:section" content="{article.category.name}"/>
    '''

    # 添加标签
    for tag in article.tags.all():
        meta_tags += f'<meta property="article:tag" content="{tag.name}"/>'

    meta_tags += f'<meta property="og:site_name" content="{blog_setting.site_name}"/>'

    # 4. JSON-LD 结构化数据(搜索引擎优化)
    structured_data = {
        "@context": "https://schema.org",
        "@type": "Article",
        "mainEntityOfPage": {
            "@type": "WebPage",
            "@id": request.build_absolute_uri()
        },
        "headline": article.title,
        "description": description,
        "image": request.build_absolute_uri(article.get_first_image_url()),
        "datePublished": article.pub_time.isoformat(),
        "dateModified": article.last_modify_time.isoformat(),
        "author": {
            "@type": "Person",
            "name": article.author.username
        },
        "publisher": {
            "@type": "Organization",
            "name": blog_setting.site_name
        }
    }

    # 移除空图片字段
    if not structured_data.get("image"):
        del structured_data["image"]

    return {
        "title": f"{article.title} | {blog_setting.site_name}",
        "description": description,
        "keywords": keywords,
        "meta_tags": meta_tags,
        "json_ld": structured_data
    }

1.5 分类页SEO生成

def _get_category_seo_data(self, context, request, blog_setting):
    """生成分类页的SEO数据"""
    category_name = context.get('tag_name')
    if not category_name:
        return None

    category = Category.objects.filter(name=category_name).first()
    if not category:
        return None

    # 生成面包屑导航结构化数据
    breadcrumb_items = [
        {
            "@type": "ListItem",
            "position": 1,
            "name": "首页",
            "item": request.build_absolute_uri('/')
        },
        {
            "@type": "ListItem",
            "position": 2,
            "name": category.name,
            "item": request.build_absolute_uri()
        }
    ]

    structured_data = {
        "@context": "https://schema.org",
        "@type": "BreadcrumbList",
        "itemListElement": breadcrumb_items
    }

    return {
        "title": f"{category.name} | {blog_setting.site_name}",
        "description": strip_tags(category.name) or blog_setting.site_description,
        "keywords": category.name,
        "meta_tags": "",
        "json_ld": structured_data
    }

1.6 默认页SEO生成

def _get_default_seo_data(self, context, request, blog_setting):
    """生成首页和其他页面的SEO数据"""
    structured_data = {
        "@context": "https://schema.org",
        "@type": "WebSite",
        "name": blog_setting.site_name,
        "description": blog_setting.site_description,
        "url": request.build_absolute_uri('/'),
        "potentialAction": {
            "@type": "SearchAction",
            "target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}",
            "query-input": "required name=search_term_string"
        }
    }

    return {
        "title": f"{blog_setting.site_name} | {blog_setting.site_description}",
        "description": blog_setting.site_description,
        "keywords": blog_setting.site_keywords,
        "meta_tags": "",
        "json_ld": structured_data
    }

1.7 格式化输出

def _format_seo_html(self, seo_data):
    """格式化SEO数据为HTML"""
    json_ld_script = (
        '<script type="application/ld+json">'
        f'{json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}'
        '</script>'
    )

    return f"""
    <title>{seo_data.get("title", "")}</title>
    <meta name="description" content="{seo_data.get("description", "")}">
    <meta name="keywords" content="{seo_data.get("keywords", "")}">
    {seo_data.get("meta_tags", "")}
    {json_ld_script}
    """

# 实例化
plugin = SeoOptimizerPlugin()

1.8 设计亮点

  1. 路由感知:通过 request.resolver_match.view_name 识别页面类型
  2. 结构化数据:使用 Schema.org 标准,提升搜索引擎理解
  3. 社交优化:Open Graph 标签优化社交媒体分享效果
  4. 降级处理:无法生成特定页面SEO时,使用默认数据兜底

二、案例二:智能推荐插件

2.1 功能需求分析

核心功能: - 基于标签的相关文章推荐 - 基于分类的相似文章推荐 - 热门文章兜底机制 - 多位置展示支持 - 性能优化(缓存)

推荐算法策略:

1. 基于标签推荐相关性最高
    不足
2. 基于分类推荐中等相关性
    仍不足
3. 热门文章兜底确保有推荐

2.2 插件架构

ArticleRecommendationPlugin
├── 位置渲染
│   ├── 文章底部渲染
│   └── 侧边栏渲染
├── 推荐算法
│   ├── 标签推荐
│   ├── 分类推荐
│   └── 热门兜底
└── 数据处理
    ├── 去重
    ├── 过滤无效文章
    └── 结果排序

2.3 核心实现

# plugins/article_recommendation/plugin.py
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', 'sidebar']

    # 位置优先级
    POSITION_PRIORITIES = {
        'article_bottom': 80,
        'sidebar': 150,
    }

    # 插件配置
    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):
        """文章详情页加载时预加载推荐数据"""
        recommendations = self.get_recommendations(article)
        context['article_recommendations'] = recommendations

2.4 条件显示逻辑

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)
        # 必须有文章且不是索引页
        return article is not None and not is_index

    if position == 'sidebar':
        return True

    return False

2.5 位置渲染实现

def render_article_bottom_widget(self, context, **kwargs):
    """渲染文章底部推荐"""
    article = kwargs.get('article') or context.get('article')
    if not article:
        return None

    count = kwargs.get('count', self.CONFIG['article_bottom_count'])
    recommendations = self.get_recommendations(article, count=count)

    if not recommendations:
        return None

    # 转换上下文为普通字典
    context_dict = self._flatten_context(context)

    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')
    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

    context_dict = self._flatten_context(context)

    template_context = {
        'recommendations': recommendations,
        'title': title,
        **context_dict
    }

    return self.render_template('sidebar_widget.html', template_context)

def _flatten_context(self, context):
    """将RequestContext转换为普通字典"""
    context_dict = {}
    if hasattr(context, 'flatten'):
        context_dict = context.flatten()
    elif hasattr(context, 'dicts'):
        for d in context.dicts:
            context_dict.update(d)
    return context_dict

2.6 推荐算法实现

def get_recommendations(self, article, count=5):
    """
    多层次推荐算法

    策略:
    1. 基于标签推荐(最相关)
    2. 基于分类推荐(中等相关)
    3. 热门文章兜底(确保有结果)
    """
    if not article:
        return []

    recommendations = []

    # === 第一层:基于标签推荐 ===
    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)
        logger.info(f"标签推荐: {len(tag_based)} 篇")

    # === 第二层:基于分类推荐 ===
    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)
        logger.info(f"分类推荐: {len(category_based)} 篇")

    # === 第三层:热门文章兜底 ===
    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)
        logger.info(f"热门兜底: {len(popular_articles)} 篇")

    # 过滤无效文章
    valid_recommendations = self._filter_valid_articles(recommendations)

    return valid_recommendations[:count]

def _filter_valid_articles(self, articles):
    """过滤掉无效文章"""
    valid = []
    for article in articles:
        if article.title and len(article.title.strip()) > 0:
            valid.append(article)
        else:
            logger.warning(f"过滤空标题文章: ID={article.id}")
    return valid

def get_popular_articles(self, count=3):
    """获取热门文章"""
    return list(
        Article.objects.filter(status='p')
        .order_by('-views')[:count]
    )

2.7 静态资源

def get_css_files(self):
    return ['css/recommendation.css']

def get_js_files(self):
    return ['js/recommendation.js']

# 实例化
plugin = ArticleRecommendationPlugin()

2.8 模板示例

文件: plugins/article_recommendation/templates/article_recommendation/bottom_widget.html

<div class="recommendation-widget">
    <h3 class="widget-title">{{ title }}</h3>

    <div class="recommendation-grid">
        {% for rec in recommendations %}
        <article class="recommendation-card">
            <a href="{{ rec.get_absolute_url }}" class="card-link">
                <h4 class="card-title">{{ rec.title }}</h4>
                <div class="card-meta">
                    <span class="category">{{ rec.category.name }}</span>
                    <span class="views">{{ rec.views }} 次浏览</span>
                </div>
            </a>
        </article>
        {% empty %}
        <p class="no-recommendations">暂无推荐</p>
        {% endfor %}
    </div>
</div>

2.9 设计亮点

  1. 多层次算法:标签 → 分类 → 热门,确保总有推荐
  2. 去重处理:避免推荐当前文章或已推荐的文章
  3. 可配置性:通过 CONFIG 控制各种策略开关
  4. 数据验证:过滤无效文章,避免显示空标题
  5. 性能考虑:使用 distinct()exclude() 优化查询

三、性能优化技巧

3.1 数据库查询优化

# ❌ N+1 查询问题
for article in articles:
    print(article.category.name)  # 每次都查询
    for tag in article.tags.all():  # 每次都查询
        print(tag.name)

# ✅ 使用 select_related 和 prefetch_related
articles = Article.objects.filter(status='p') \
    .select_related('category', 'author') \
    .prefetch_related('tags') \
    .order_by('-views')[:10]

3.2 缓存策略

from django.core.cache import cache

def _get_cached_data(self):
    """使用缓存"""
    cache_key = f'{self.plugin_slug}_data'
    data = cache.get(cache_key)

    if data is None:
        data = self._expensive_query()
        cache.set(cache_key, data, timeout=300)  # 5分钟

    return data

3.3 懒加载

class LazyPlugin(BasePlugin):
    def __init__(self):
        super().__init__()
        self._data = None  # 延迟加载

    @property
    def data(self):
        """只在需要时才加载"""
        if self._data is None:
            self._data = self._load_data()
        return self._data

四、开发最佳实践

4.1 日志记录

import logging

logger = logging.getLogger(__name__)

class MyPlugin(BasePlugin):
    def init_plugin(self):
        logger.info(f'{self.PLUGIN_NAME} v{self.PLUGIN_VERSION} 初始化')

    def some_method(self):
        logger.debug(f'处理数据: {data}')
        try:
            result = self._process(data)
            logger.info(f'处理成功: {result}')
        except Exception as e:
            logger.error(f'处理失败: {e}', exc_info=True)

4.2 配置管理

class ConfigurablePlugin(BasePlugin):
    CONFIG = {
        'enabled': True,
        'count': 10,
        'cache_timeout': 300,
    }

    def get_config(self, key, default=None):
        return self.CONFIG.get(key, default)

    def set_config(self, key, value):
        self.CONFIG[key] = value
        logger.info(f'配置更新: {key}={value}')

4.3 错误处理

def render_widget(self, context, **kwargs):
    try:
        data = self._fetch_data()
        return self.render_template('widget.html', {'data': data})
    except TemplateDoesNotExist:
        logger.warning(f'模板不存在')
        return None
    except Exception as e:
        logger.error(f'渲染失败: {e}', exc_info=True)
        return None  # 确保不影响页面显示

本文由 liangliangyy 原创,转载请注明出处。

📖相关推荐