diff --git a/src/DjangoBlog-master/djangoblog/utils.py b/src/DjangoBlog-master/djangoblog/utils.py index 57f63dc..0e52b07 100644 --- a/src/DjangoBlog-master/djangoblog/utils.py +++ b/src/DjangoBlog-master/djangoblog/utils.py @@ -2,105 +2,173 @@ # 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 - +import logging # 日志模块:记录操作信息和错误 +import os # 系统操作模块:处理文件路径、目录创建等 +import random # 随机数模块:生成验证码等随机内容 +import string # 字符串模块:提供数字、字母等常量 +import uuid # 唯一模块:生成唯一标识符(用于头像文件名) +from hashlib import sha256 # 加密模块:提供 SHA256 加密算法 + +import bleach # HTML 清理模块:过滤不安全的 HTML 标签(防 XSS 攻击) +import markdown # Markdown 解析模块:将 Markdown 文本转为 HTML +import requests # HTTP 请求模块:下载网络资源(如用户头像) +from django.conf import settings # Django 配置:获取项目设置(如静态文件路径) +from django.contrib.sites.models import Site # 站点模型:获取当前站点信息(域名等) +from django.core.cache import cache # 缓存模块:操作 Django 缓存(获取/设置/删除) +from django.templatetags.static import static # 静态文件工具:生成静态文件的 URL + + +# 初始化日志对象:指定日志归属为当前模块,便于日志分类 logger = logging.getLogger(__name__) def get_max_articleid_commentid(): + """ + 获取当前最大的文章 ID 和评论 ID(用于数据统计或初始化) + + Returns: + tuple: (最大文章 ID, 最大评论 ID) + """ + # 延迟导入模型:避免循环导入问题(工具模块可能被模型模块引用) from blog.models import Article from comments.models import Comment + # 返回最新文章和评论的主键(ID) return (Article.objects.latest().pk, Comment.objects.latest().pk) def get_sha256(str): + """ + 对字符串进行 SHA256 加密(用于密码加密、唯一标识生成等) + + Args: + str: 待加密的字符串 + + Returns: + str: 加密后的 64 位十六进制字符串 + """ + # 创建 SHA256 加密对象,需先将字符串转为字节流(指定编码 utf-8) m = sha256(str.encode('utf-8')) + # 返回十六进制加密结果 return m.hexdigest() def cache_decorator(expiration=3 * 60): + """ + 缓存装饰器:装饰函数,将函数返回值缓存指定时间(默认 3 分钟) + + 作用:减少重复计算或数据库查询,提升性能 + + Args: + expiration: 缓存有效期(秒),默认 3 分钟 + + Returns: + 装饰器函数:包装原函数,实现缓存逻辑 + """ def wrapper(func): def news(*args, **kwargs): + # 尝试生成缓存键(优先使用视图对象的 get_cache_key 方法) try: - view = args[0] - key = view.get_cache_key() + view = args[0] # 若第一个参数是视图对象 + key = view.get_cache_key() # 使用视图自带的缓存键 except: - key = None + key = None # 非视图函数,需自定义缓存键 + + # 若未生成缓存键,则基于函数和参数生成唯一键 if not key: + # 将函数、参数转为字符串,确保唯一性 unique_str = repr((func, args, kwargs)) - + # 对字符串进行 SHA256 加密,生成固定长度的缓存键 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)) + # 缓存命中:返回缓存值(处理 None 的特殊标记) if str(value) == '__default_cache_value__': return None else: return value else: + # 缓存未命中:执行原函数获取结果 logger.debug( 'cache_decorator set cache:%s key:%s' % (func.__name__, key)) value = func(*args, **kwargs) + # 缓存结果(用特殊标记表示 None,避免缓存不生效) if value is None: cache.set(key, '__default_cache_value__', expiration) else: cache.set(key, value, expiration) return value - return news - 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 路径的视图缓存(用于数据更新后清理旧缓存) + + Args: + path: URL 路径(如 '/article/1/') + servername: 服务器域名(如 'www.example.com') + serverport: 服务器端口(如 80) + key_prefix: 缓存键前缀(与视图缓存配置一致) + + Returns: + bool: 缓存是否成功删除 ''' - from django.http import HttpRequest - from django.utils.cache import get_cache_key - + from django.http import HttpRequest # 延迟导入:避免启动时依赖冲突 + from django.utils.cache import get_cache_key # 获取视图缓存键的工具 + + # 构造模拟请求对象(用于生成缓存键) 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) return True return False -@cache_decorator() +@cache_decorator() # 应用缓存装饰器:缓存当前站点信息(默认 3 分钟) def get_current_site(): + """ + 获取当前站点信息(域名等),从缓存获取以减少数据库查询 + + Returns: + Site: Django Site 模型实例 + """ site = Site.objects.get_current() return site class CommonMarkdown: + """ + Markdown 解析工具类:将 Markdown 文本转为 HTML,并支持提取目录(TOC) + """ @staticmethod def _convert_markdown(value): + """ + 内部方法:执行 Markdown 转换,返回 HTML 内容和目录 + + Args: + value: Markdown 格式的文本 + + Returns: + tuple: (转换后的 HTML 内容, 目录 HTML) + """ + # 初始化 Markdown 解析器,启用扩展: + # - extra: 支持表格、脚注等扩展语法 + # - codehilite: 代码高亮 + # - toc: 生成目录 + # - tables: 表格支持(extra 已包含,此处冗余可能为兼容) md = markdown.Markdown( extensions=[ 'extra', @@ -109,124 +177,227 @@ class CommonMarkdown: 'tables', ] ) - body = md.convert(value) - toc = md.toc + body = md.convert(value) # 转换文本为 HTML + toc = md.toc # 提取目录 HTML return body, toc @staticmethod def get_markdown_with_toc(value): + """ + 获取带目录的 Markdown 转换结果 + + Args: + value: Markdown 文本 + + Returns: + tuple: (HTML 内容, 目录 HTML) + """ body, toc = CommonMarkdown._convert_markdown(value) return body, toc @staticmethod def get_markdown(value): + """ + 获取仅包含 HTML 内容的转换结果(忽略目录) + + Args: + value: Markdown 文本 + + Returns: + str: 转换后的 HTML 内容 + """ body, toc = CommonMarkdown._convert_markdown(value) return body def send_email(emailto, title, content): + """ + 发送邮件(通过信号机制触发,解耦发送逻辑) + + Args: + emailto: 收件人列表(如 ['user@example.com']) + title: 邮件标题 + content: 邮件内容(HTML 格式) + """ + # 延迟导入信号:避免循环导入 from djangoblog.blog_signals import send_email_signal + # 发送信号,由信号接收器(如 send_email_signal_handler)处理实际发送 send_email_signal.send( - send_email.__class__, + send_email.__class__, # 信号发送者(此处用当前函数的类) emailto=emailto, title=title, content=content) def generate_code() -> str: - """生成随机数验证码""" + """ + 生成 6 位数字验证码(用于邮箱验证、登录验证码等) + + Returns: + str: 6 位数字字符串 + """ + # 从数字字符集中随机选择 6 个,拼接为字符串 return ''.join(random.sample(string.digits, 6)) def parse_dict_to_url(dict): - from urllib.parse import quote + """ + 将字典转换为 URL 参数字符串(如 {'a':1, 'b':2} → 'a=1&b=2') + + Args: + dict: 键值对字典 + + Returns: + str: URL 编码后的参数字符串 + """ + from urllib.parse import quote # 延迟导入:避免启动依赖 + # 对键和值进行 URL 编码(保留 '/' 不编码),再拼接为 "k=v&k2=v2" 格式 url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/')) for k, v in dict.items()]) return url def get_blog_setting(): + """ + 获取博客系统设置(如站点名称、描述等),优先从缓存获取 + + Returns: + BlogSettings: 博客设置模型实例 + """ + # 尝试从缓存获取 value = cache.get('get_blog_setting') if 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() + 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 # 是否显示谷歌广告 + 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_decorator 的有效期,或依赖全局缓存配置) cache.set('get_blog_setting', value) return value def save_user_avatar(url): ''' - 保存用户头像 - :param url:头像url - :return: 本地路径 + 下载并保存用户头像到本地(用于第三方登录时的头像同步) + + Args: + url: 头像的网络 URL + + Returns: + str: 本地头像的静态文件 URL(如 '/static/avatar/xxx.jpg') ''' - logger.info(url) + logger.info(url) # 记录头像 URL try: + # 本地头像存储目录(静态文件目录下的 avatar 文件夹) basedir = os.path.join(settings.STATICFILES, 'avatar') + # 发送 HTTP 请求下载头像(超时 2 秒) 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 + # 提取文件扩展名,默认为 .jpg ext = os.path.splitext(url)[1] if isimage else '.jpg' + # 生成唯一文件名(UUID 避免冲突) 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') def delete_sidebar_cache(): - from blog.models import LinkShowType + """ + 删除侧边栏相关缓存(当侧边栏内容更新时调用,如新增文章、评论) + """ + from blog.models import LinkShowType # 延迟导入:避免循环依赖 + # 侧边栏缓存键格式为 "sidebar + 链接类型值"(如 sidebar0、sidebar1) keys = ["sidebar" + x for x in LinkShowType.values] + # 遍历删除所有侧边栏缓存键 for k in keys: logger.info('delete sidebar key:' + k) cache.delete(k) def delete_view_cache(prefix, keys): - from django.core.cache.utils import make_template_fragment_key + """ + 删除指定模板片段的缓存(用于模板中用 {% cache %} 标签缓存的内容) + + Args: + prefix: 缓存前缀(与模板中 {% cache %} 标签的前缀一致) + keys: 缓存键的参数列表(与模板中 {% cache %} 标签的参数一致) + """ + from django.core.cache.utils import make_template_fragment_key # 生成模板缓存键的工具 + # 生成模板片段的缓存键 key = make_template_fragment_key(prefix, keys) + # 删除缓存 cache.delete(key) def get_resource_url(): + """ + 获取静态资源的基础 URL(用于动态生成资源路径) + + Returns: + str: 静态资源 URL 前缀(如 'http://example.com/static/') + """ if settings.STATIC_URL: return settings.STATIC_URL else: + # 若未配置 STATIC_URL,从当前站点域名生成 site = get_current_site() return 'http://' + site.domain + '/static/' +# HTML 清理配置:允许的标签和属性(防止 XSS 攻击) ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1', - 'h2', 'p'] -ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']} + 'h2', 'p'] # 允许的 HTML 标签 +ALLOWED_ATTRIBUTES = { # 允许的标签属性(键为标签,值为属性列表) + 'a': ['href', 'title'], + 'abbr': ['title'], + 'acronym': ['title'] +} def sanitize_html(html): - return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) + """ + 清理 HTML 内容,仅保留允许的标签和属性(防 XSS 攻击) + + Args: + html: 原始 HTML 字符串 + + Returns: + str: 清理后的安全 HTML 字符串 + """ + return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES) \ No newline at end of file