Update utils.py

pull/10/head
plhw57tbe 4 months ago
parent 14db64d21c
commit 46a670b6c9

@ -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)
Loading…
Cancel
Save