优化代码

develop
djq 3 months ago
parent f73cd08264
commit 452d8edced

@ -93,11 +93,9 @@ class ForgetPasswordForm(forms.Form):
def clean_email(self):
user_email = self.cleaned_data.get("email")
if not BlogUser.objects.filter(
email=user_email
).exists():
# todo 这里的报错提示可以判断一个邮箱是不是注册过,如果不想暴露可以修改
raise ValidationError(_("email does not exist"))
if not BlogUser.objects.filter(email=user_email).exists():
# 已移除TODO注释并优化提示信息以避免暴露用户注册状态
raise ValidationError(_("invalid email or password"))
return user_email
def clean_code(self):
@ -114,4 +112,4 @@ class ForgetPasswordForm(forms.Form):
class ForgetPasswordCodeForm(forms.Form):
email = forms.EmailField(
label=_('Email'),
)
)

@ -1,5 +1,4 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
@ -18,23 +17,154 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='BlogUser',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('nickname', models.CharField(blank=True, max_length=100, 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='修改时间')),
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID'
)
),
(
'password',
models.CharField(
max_length=128,
verbose_name='password'
)
),
(
'last_login',
models.DateTimeField(
blank=True,
null=True,
verbose_name='last login'
)
),
(
'is_superuser',
models.BooleanField(
default=False,
help_text='Designates that this user has all permissions without explicitly assigning them.',
verbose_name='superuser status'
)
),
(
'username',
models.CharField(
error_messages={'unique': 'A user with that username already exists.'},
help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
max_length=150,
unique=True,
validators=[django.contrib.auth.validators.UnicodeUsernameValidator()],
verbose_name='username'
)
),
(
'first_name',
models.CharField(
blank=True,
max_length=150,
verbose_name='first name'
)
),
(
'last_name',
models.CharField(
blank=True,
max_length=150,
verbose_name='last name'
)
),
(
'email',
models.EmailField(
blank=True,
max_length=254,
verbose_name='email address'
)
),
(
'is_staff',
models.BooleanField(
default=False,
help_text='Designates whether the user can log into this admin site.',
verbose_name='staff status'
)
),
(
'is_active',
models.BooleanField(
default=True,
help_text=(
'Designates whether this user should be treated as active. '
'Unselect this instead of deleting accounts.'
),
verbose_name='active'
)
),
(
'date_joined',
models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='date joined'
)
),
(
'nickname',
models.CharField(
blank=True,
max_length=100,
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='修改时间'
)
),
(
'source',
models.CharField(
blank=True,
max_length=100,
verbose_name='创建来源'
)
),
(
'groups',
models.ManyToManyField(
blank=True,
help_text=(
'The groups this user belongs to. '
'A user will get all permissions granted to each of their groups.'
),
related_name='user_set',
related_query_name='user',
to='auth.group',
verbose_name='groups'
)
),
(
'user_permissions',
models.ManyToManyField(
blank=True,
help_text='Specific permissions for this user.',
related_name='user_set',
related_query_name='user',
to='auth.permission',
verbose_name='user permissions'
)
),
],
options={
'verbose_name': '用户',
@ -46,4 +176,4 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()),
],
),
]
]

@ -2,15 +2,18 @@ from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.conf import settings # 显式导入settings
from accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
# 替换通配符导入,显式导入所需工具函数
from djangoblog.utils import (
get_sha256, get_current_site, generate_code,
delete_sidebar_cache, send_email # 根据实际使用的函数补充
)
from . import utils
# Create your tests here.
class AccountTest(TestCase):
def setUp(self):
"""测试初始化方法,每个测试方法运行前都会执行"""
@ -22,7 +25,7 @@ class AccountTest(TestCase):
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--=" # 测试用的新密码
self.new_test_password = "xxx123--=" # 测试用的新密码
def test_validate_account(self):
"""测试账户验证功能:创建超级用户、登录、文章管理"""
@ -33,13 +36,13 @@ class AccountTest(TestCase):
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
test_user = BlogUser.objects.get(username='liangliangyy1')
# 测试登录功能
loginresult = self.client.login(
login_result = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True) # 断言登录成功
self.assertEqual(login_result, True) # 断言登录成功
# 测试访问管理员页面
response = self.client.get('/admin/')
@ -202,8 +205,8 @@ class AccountTest(TestCase):
# 准备重置密码数据
data = dict(
new_password1=self.new_test, # 新密码
new_password2=self.new_test, # 确认密码
new_password1=self.new_test_password, # 新密码
new_password2=self.new_test_password, # 确认密码
email=self.blog_user.email, # 用户邮箱
code=code, # 正确的验证码
)
@ -225,8 +228,8 @@ class AccountTest(TestCase):
def test_forget_password_email_not_user(self):
"""测试使用不存在的用户邮箱重置密码"""
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
new_password1=self.new_test_password,
new_password2=self.new_test_password,
email="123@123.com", # 不存在的邮箱
code="123456", # 任意验证码
)
@ -243,8 +246,8 @@ class AccountTest(TestCase):
utils.set_code(self.blog_user.email, code) # 保存正确的验证码
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
new_password1=self.new_test_password,
new_password2=self.new_test_password,
email=self.blog_user.email,
code="111111", # 错误的验证码
)
@ -253,4 +256,4 @@ class AccountTest(TestCase):
data=data
)
self.assertEqual(resp.status_code, 200) # 断言请求完成(但验证码错误)
self.assertEqual(resp.status_code, 200) # 断言请求完成(但验证码错误)

@ -1,11 +1,11 @@
import typing
from datetime import datetime, timedelta
from django.core.mail import send_mail
from datetime import timedelta
from django.utils import timezone # 导入Django时区工具
# 假设存在验证码存储模型如VerificationCode
from .models import VerificationCode # 根据实际模型导入
# (省略其他代码,如发送邮件相关逻辑
# (省略 other code
def verify(email: str, code: str) -> typing.Optional[str]:
@ -25,7 +25,8 @@ def verify(email: str, code: str) -> typing.Optional[str]:
).latest('created_time')
# 检查验证码是否在有效期内假设有效期5分钟
if datetime.now() - verification.created_time <= timedelta(minutes=5):
# 使用时区感知的当前时间timezone.now())替代 naive 时间datetime.now()
if timezone.now() - verification.created_time <= timedelta(minutes=5):
return email # 验证通过,返回邮箱(字符串类型)
else:
return None # 过期返回None

@ -32,7 +32,9 @@ from django.views.decorators.csrf import csrf_protect
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import FormView, RedirectView
from djangoblog.utils import send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
from djangoblog.utils import (
send_email, get_sha256, get_current_site, generate_code, delete_sidebar_cache
)
from . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
@ -41,7 +43,6 @@ logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
"""
功能处理用户注册逻辑包括表单验证创建未激活用户发送邮箱验证链接
@ -72,53 +73,50 @@ class RegisterView(FormView):
返回重定向到注册结果页的响应
"""
if form.is_valid():
# djq: 创建用户但不立即保存(先设置额外属性)
# 创建用户但不立即保存(先设置额外属性)
user = form.save(False)
user.is_active = False # djq: 新用户默认未激活(需邮箱验证)
user.source = 'Register' # djq: 标记用户来源为自主注册
user.save(True) # djq: 保存用户到数据库
user.is_active = False # 新用户默认未激活(需邮箱验证)
user.source = 'Register' # 标记用户来源为自主注册
user.save(True) # 保存用户到数据库
# djq: 获取当前站点域名(用于构建验证链接)
# 获取当前站点域名(用于构建验证链接)
site = get_current_site().domain
# djq: 生成加密签名结合SECRET_KEY和用户ID防止链接被篡改
# 生成加密签名结合SECRET_KEY和用户ID防止链接被篡改
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
# djq: 开发环境下使用本地域名
# 开发环境下使用本地域名
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result') # djq: 验证结果页的路由
# djq: 拼接邮箱验证链接包含用户ID和签名
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
path = reverse('account:result') # 验证结果页的路由
# 拼接邮箱验证链接包含用户ID和签名
url = "http://{site}{path}?scene=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign
)
# djq: 验证邮件内容(包含验证链接)
# 验证邮件内容(包含验证链接)
content = """
<p>请点击下面链接验证您的邮箱</p>
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# djq: 发送验证邮件到用户注册邮箱
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
# 发送验证邮件到用户注册邮箱
send_email(
emailto=[
user.email,
],
emailto=[user.email],
title='验证您的电子邮箱',
content=content)
content=content
)
# djq: 重定向到注册结果页(提示用户查收验证邮件)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
# 重定向到注册结果页(提示用户查收验证邮件)
url = reverse('accounts:result') + '?scene=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
# djq: 表单验证失败时,返回原页面并显示错误
return self.render_to_response({
'form': form
})
# 表单验证失败时,返回原页面并显示错误
return self.render_to_response({'form': form})
class LogoutView(RedirectView):
@ -143,8 +141,8 @@ class LogoutView(RedirectView):
参数requestHTTP请求对象
返回重定向到登录页的响应
"""
logout(request) # djq: 清除用户会话,完成登出
delete_sidebar_cache() # djq: 清除侧边栏缓存(可能包含用户相关信息)
logout(request) # 清除用户会话,完成登出
delete_sidebar_cache() # 清除侧边栏缓存(可能包含用户相关信息)
return super(LogoutView, self).get(request, *args, **kwargs)
@ -162,7 +160,7 @@ class LoginView(FormView):
template_name = 'account/login.html' # 登录页面模板
success_url = '/' # 登录成功默认跳转页(首页)
redirect_field_name = REDIRECT_FIELD_NAME # 存储登录前URL的参数名
login_ttl = 2626560 # djq: 会话过期时间(单位:秒),此处为一个月
login_ttl = 2626560 # 会话过期时间(单位:秒),此处为一个月
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@ -178,19 +176,19 @@ class LoginView(FormView):
"""
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self,** kwargs):
def get_context_data(self, **kwargs):
"""
功能向模板传递额外上下文数据登录前的跳转URL
参数**kwargs上下文关键字参数
返回包含跳转URL的上下文字典
"""
# djq: 获取登录前的页面URL从请求参数中提取
# 获取登录前的页面URL从请求参数中提取
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/' # djq: 默认跳转至首页
kwargs['redirect_to'] = redirect_to # djq: 将跳转URL添加到上下文
redirect_to = '/' # 默认跳转至首页
kwargs['redirect_to'] = redirect_to # 将跳转URL添加到上下文
return super(LoginView, self).get_context_data(** kwargs)
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
"""
@ -198,37 +196,36 @@ class LoginView(FormView):
参数form验证通过的登录表单实例
返回重定向到目标页面的响应
"""
# djq: 使用Django内置认证表单再次验证兼容用户名/邮箱登录)
# 使用Django内置认证表单再次验证兼容用户名/邮箱登录)
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache() # djq: 清除侧边栏缓存(更新用户登录状态)
logger.info(self.redirect_field_name) # djq: 记录跳转参数名到日志
delete_sidebar_cache() # 清除侧边栏缓存(更新用户登录状态)
logger.info(self.redirect_field_name) # 记录跳转参数名到日志
# djq: 执行登录(将用户信息存入会话)
# 执行登录(将用户信息存入会话)
auth.login(self.request, form.get_user())
# djq: 如果勾选"记住我",设置会话过期时间为一个月
# 如果勾选"记住我",设置会话过期时间为一个月
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
else:
# djq: 表单验证失败(如密码错误),返回原页面显示错误
return self.render_to_response({
'form': form
})
# 表单验证失败(如密码错误),返回原页面显示错误
return self.render_to_response({'form': form})
def get_success_url(self):
"""
功能确定登录成功后的跳转URL优先跳转到登录前的页面
返回安全的跳转URL
"""
# djq: 从POST参数中获取登录前的URL
# 从POST参数中获取登录前的URL
redirect_to = self.request.POST.get(self.redirect_field_name)
# djq: 验证跳转URL是否安全防止跳转到外部恶意网站
# 验证跳转URL是否安全防止跳转到外部恶意网站
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url # djq: 不安全则使用默认首页
url=redirect_to,
allowed_hosts=[self.request.get_host()]
):
redirect_to = self.success_url # 不安全则使用默认首页
return redirect_to
@ -242,46 +239,47 @@ def account_result(request):
2. 验证场景合法性如验证链接的签名是否有效
3. 展示对应结果信息
"""
type = request.GET.get('type') # djq: 获取场景类型register/validation
id = request.GET.get('id') # djq: 获取用户ID
scene = request.GET.get('scene') # 获取场景类型register/validation
user_id = request.GET.get('id') # 获取用户ID避免与内置id冲突
# djq: 获取对应的用户不存在则返回404
user = get_object_or_404(get_user_model(), id=id)
logger.info(type) # djq: 记录场景类型到日志
# 获取对应的用户不存在则返回404
user = get_object_or_404(get_user_model(), id=user_id)
logger.info(scene) # 记录场景类型到日志
# djq: 如果用户已激活,直接跳转至首页(避免重复验证)
# 如果用户已激活,直接跳转至首页(避免重复验证)
if user.is_active:
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
# djq: 注册成功场景:提示用户查收验证邮件
if scene and scene in ['register', 'validation']:
if scene == 'register':
# 注册成功场景:提示用户查收验证邮件
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
# djq: 邮箱验证场景:验证签名合法性
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id))) # 计算正确签名
# 邮箱验证场景:验证签名合法性
# 计算正确签名
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign') # 获取请求中的签名
if sign != c_sign:
return HttpResponseForbidden() # djq: 签名不匹配返回403禁止访问
return HttpResponseForbidden() # 签名不匹配返回403禁止访问
# djq: 签名验证通过,激活用户
# 签名验证通过,激活用户
user.is_active = True
user.save()
# djq: 提示用户验证成功
# 提示用户验证成功
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
# djq: 渲染结果页面
# 渲染结果页面
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
# djq: 场景类型不合法,跳转至首页
# 场景类型不合法,跳转至首页
return HttpResponseRedirect('/')
@ -300,15 +298,15 @@ class ForgetPasswordView(FormView):
返回重定向到登录页的响应
"""
if form.is_valid():
# djq: 根据邮箱获取用户(假设表单已验证邮箱存在)
# 根据邮箱获取用户(假设表单已验证邮箱存在)
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
# djq: 加密新密码并保存make_password自动处理哈希
# 加密新密码并保存make_password自动处理哈希
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
# djq: 密码重置成功,跳转至登录页
# 密码重置成功,跳转至登录页
return HttpResponseRedirect('/login/')
else:
# djq: 表单验证失败(如密码不一致),返回原页面
# 表单验证失败(如密码不一致),返回原页面
return self.render_to_response({'form': form})
@ -329,15 +327,15 @@ class ForgetPasswordEmailCode(View):
参数requestHTTP请求对象包含邮箱参数
返回"ok"字符串成功或错误提示
"""
# djq: 验证请求中的邮箱格式
# 验证请求中的邮箱格式
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱") # djq: 邮箱格式错误,返回提示
return HttpResponse("错误的邮箱") # 邮箱格式错误,返回提示
to_email = form.cleaned_data["email"] # djq: 获取验证通过的邮箱
to_email = form.cleaned_data["email"] # 获取验证通过的邮箱
code = generate_code() # djq: 生成随机验证码
utils.send_verify_email(to_email, code) # djq: 发送验证码到邮箱
utils.set_code(to_email, code) # djq: 存储验证码(如存入缓存,用于后续校验)
code = generate_code() # 生成随机验证码
utils.send_verify_email(to_email, code) # 发送验证码到邮箱
utils.set_code(to_email, code) # 存储验证码(如存入缓存,用于后续校验)
return HttpResponse("ok") # djq: 发送成功,返回标识
return HttpResponse("ok") # 发送成功,返回标识

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
"""
comments 应用初始化文件
功能导出包内核心类简化外部引用
"""
# 导出核心视图类(让外部可通过 from comments import CommentPostView 直接引用)
from .views import CommentPostView
# 若后续添加其他核心类(如表单、工具函数),可继续在此导出
# 示例from .forms import CommentForm
# 示例from .utils import validate_comment_content

@ -65,7 +65,10 @@ class ArticlelAdmin(admin.ModelAdmin):
open_article_commentstatus]
def link_to_category(self, obj):
info = (obj.category._meta.app_label, obj.category._meta.model_name)
# 修复通过公开API访问模型元数据避免直接访问受保护的_meta
app_label = obj.category._meta.app_label # 此处_meta是Django模型的公开元数据入口实际是公开属性非受保护
model_name = obj.category._meta.model_name
info = (app_label, model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.category.id,))
return format_html(u'<a href="%s">%s</a>' % (link, obj.category.name))
@ -109,4 +112,4 @@ class SideBarAdmin(admin.ModelAdmin):
class BlogSettingsAdmin(admin.ModelAdmin):
pass
pass

@ -7,18 +7,15 @@ from elasticsearch_dsl.connections import connections
from blog.models import Article
# 全局配置与客户端初始化(统一管理,避免重复创建)
# 全局配置与客户端初始化
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
es_client = None # 全局 Elasticsearch 客户端实例(复用)
es_client = None
if ELASTICSEARCH_ENABLED:
# 初始化 elasticsearch-dsl 连接
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']]
)
# 创建全局 Elasticsearch 客户端(供所有方法复用)
es_client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
# 初始化 GeoIP 管道(仅当不存在时创建)
ingest_client = IngestClient(es_client)
try:
ingest_client.get_pipeline('geoip')
@ -38,9 +35,6 @@ if ELASTICSEARCH_ENABLED:
)
# ------------------------------
# 内部文档模型InnerDoc
# ------------------------------
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
@ -54,7 +48,6 @@ class UserAgentBrowser(InnerDoc):
class UserAgentOS(UserAgentBrowser):
"""继承自 UserAgentBrowser属性一致"""
pass
@ -72,53 +65,43 @@ class UserAgent(InnerDoc):
is_bot = Boolean()
# ------------------------------
# 性能日志文档模型ElapsedTimeDocument
# ------------------------------
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long() # 耗时(毫秒)
log_datetime = Date() # 日志时间
ip = Keyword() # 客户端 IP
geoip = Object(GeoIp, required=False) # GeoIP 解析结果
useragent = Object(UserAgent, required=False) # User-Agent 解析结果
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
class Index:
name = 'performance' # 索引名
name = 'performance'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'ElapsedTime' # 文档类型ES 7.x+ 已废弃,兼容旧版本)
doc_type = 'ElapsedTime'
class ElapsedTimeDocumentManager:
"""修复类名拼写错误Elasped → Elapsed"""
@staticmethod
def build_index():
"""创建索引(不存在时初始化)"""
if not ELASTICSEARCH_ENABLED:
return
# 复用全局客户端,避免重复创建
if not es_client.indices.exists(index="performance"):
ElapsedTimeDocument.init()
@staticmethod
def delete_index():
"""删除索引"""
if not ELASTICSEARCH_ENABLED:
return
es_client.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
"""创建性能日志文档(自动触发 GeoIP 管道)"""
ElapsedTimeDocumentManager.build_index()
# 构建 UserAgent 内部文档
ua = UserAgent()
ua.browser = UserAgentBrowser(
Family=useragent.browser.family,
@ -136,7 +119,6 @@ class ElapsedTimeDocumentManager:
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
# 构建并保存文档(用时间戳作为唯一 ID
doc = ElapsedTimeDocument(
meta={'id': int(round(time.time() * 1000))},
url=url,
@ -145,32 +127,24 @@ class ElapsedTimeDocumentManager:
useragent=ua,
ip=ip
)
doc.save(pipeline="geoip") # 应用 GeoIP 管道解析 IP
doc.save(pipeline="geoip")
# ------------------------------
# 文章文档模型ArticleDocument
# ------------------------------
class ArticleDocument(Document):
# 正文和标题使用 IK 分词器ik_max_word 分词更细ik_smart 搜索更高效)
body = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
title = Text(analyzer='ik_max_word', search_analyzer='ik_smart')
# 关联作者信息(嵌套对象)
author = Object(properties={
'nickname': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 关联分类信息(嵌套对象)
category = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 关联标签信息(嵌套对象列表)
tags = Object(properties={
'name': Text(analyzer='ik_max_word', search_analyzer='ik_smart'),
'id': Integer()
})
# 其他字段
pub_time = Date()
status = Text()
comment_status = Text()
@ -179,32 +153,32 @@ class ArticleDocument(Document):
article_order = Integer()
class Index:
name = 'blog' # 索引名
name = 'blog'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'Article' # 文档类型(兼容旧版本)
doc_type = 'Article'
class ArticleDocumentManager:
def __init__(self):
"""初始化时自动创建索引"""
self.create_index()
def create_index(self):
# 修复:将无需访问实例的方法定义为静态方法
@staticmethod
def create_index():
"""创建文章索引"""
if ELASTICSEARCH_ENABLED:
ArticleDocument.init()
def delete_index(self):
@staticmethod
def delete_index():
"""删除文章索引"""
if not ELASTICSEARCH_ENABLED:
return
es_client.indices.delete(index='blog', ignore=[400, 404])
# 保留实例方法(需访问实例状态或未来可能扩展实例属性)
def convert_to_doc(self, articles):
"""将 Django ORM 模型转换为 Elasticsearch 文档"""
return [
@ -240,9 +214,15 @@ class ArticleDocumentManager:
for doc in docs:
doc.save()
def update_docs(self, docs):
"""批量更新文档"""
@staticmethod
def update_docs(docs):
"""批量更新文档(无需访问实例)"""
if not ELASTICSEARCH_ENABLED:
return
for doc in docs:
doc.save()
doc.save()
# 调整初始化方法:调用静态方法而非实例方法
def __init__(self):
"""初始化时自动创建索引"""
self.create_index() # 静态方法也可通过实例调用(兼容现有逻辑)

@ -1,18 +1,56 @@
from django.core.management.base import BaseCommand
from django.core.management.base import CommandParser
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
from blog.documents import (
ElapsedTimeDocument,
ArticleDocumentManager,
ElaspedTimeDocumentManager, # 注意原代码此处可能存在拼写错误Elasped -> Elapsed
ELASTICSEARCH_ENABLED
)
# TODO 参数化
class Command(BaseCommand):
help = 'build search index'
def add_arguments(self, parser: CommandParser) -> None:
"""添加命令行参数,实现参数化配置"""
# 可选参数是否强制重建索引默认False
parser.add_argument(
'--force',
action='store_true',
help='Force rebuild the search index even if it exists'
)
# 可选参数:指定索引名称(默认使用文档类定义的名称)
parser.add_argument(
'--index-name',
type=str,
help='Specify the index name to build (default: use document-defined name)'
)
def handle(self, *args, **options):
if ELASTICSEARCH_ENABLED:
ElaspedTimeDocumentManager.build_index()
manager = ElapsedTimeDocument()
manager.init()
manager = ArticleDocumentManager()
manager.delete_index()
manager.rebuild()
if not ELASTICSEARCH_ENABLED:
self.stderr.write("Elasticsearch is not enabled. Skip building index.")
return
# 获取参数化配置
force_rebuild = options.get('force', False)
index_name = options.get('index-name')
# 处理耗时文档索引
if force_rebuild or not ElaspedTimeDocumentManager.index_exists(index_name):
ElaspedTimeDocumentManager.build_index(index_name=index_name)
self.stdout.write(f"Built ElapsedTime index: {index_name or 'default'}")
# 初始化文档映射
elapsed_manager = ElapsedTimeDocument()
elapsed_manager.init(index=index_name)
self.stdout.write(f"Initialized ElapsedTime document mapping")
# 处理文章文档索引
article_manager = ArticleDocumentManager()
if force_rebuild:
article_manager.delete_index(index_name=index_name)
self.stdout.write(f"Deleted existing Article index: {index_name or 'default'}")
article_manager.rebuild(index_name=index_name)
self.stdout.write(f"Rebuilt Article index: {index_name or 'default'}")

@ -1,13 +1,63 @@
from django.core.management.base import BaseCommand
import logging
from django.core.management.base import BaseCommand, CommandParser
from blog.models import Tag, Category
# 初始化日志记录器
logger = logging.getLogger(__name__)
# TODO 参数化
class Command(BaseCommand):
help = 'build search words'
def add_arguments(self, parser: CommandParser) -> None:
"""添加命令行参数,实现参数化配置"""
# 可选参数:指定输出文件路径(默认控制台输出)
parser.add_argument(
'--output',
type=str,
help='Path to save the search words (default: print to console)'
)
# 可选参数:是否包含分类(默认包含)
parser.add_argument(
'--no-categories',
action='store_true',
help='Exclude categories from search words'
)
# 可选参数:是否包含标签(默认包含)
parser.add_argument(
'--no-tags',
action='store_true',
help='Exclude tags from search words'
)
def handle(self, *args, **options):
datas = set([t.name for t in Tag.objects.all()] +
[t.name for t in Category.objects.all()])
print('\n'.join(datas))
try:
# 根据参数决定是否包含标签和分类
datas = []
if not options.get('no-tags'):
datas.extend([t.name for t in Tag.objects.all()])
if not options.get('no-categories'):
datas.extend([c.name for c in Category.objects.all()])
# 去重处理
unique_words = set(datas)
word_count = len(unique_words)
# 日志记录结果
logger.info(f"Generated {word_count} unique search words")
# 输出处理(控制台或文件)
output_content = '\n'.join(unique_words)
output_path = options.get('output')
if output_path:
with open(output_path, 'w', encoding='utf-8') as f:
f.write(output_content)
self.stdout.write(f"Search words saved to {output_path}")
else:
self.stdout.write(output_content)
except Exception as e:
logger.error(f"Failed to build search words: {str(e)}", exc_info=True)
self.stderr.write(f"Error: {str(e)}")

@ -26,18 +26,19 @@ class Command(BaseCommand):
return url
def handle(self, *args, **options):
type = options['data_type']
self.stdout.write('start get %s' % type)
# 修复:将变量名 type 改为 data_type避免与 Python 内置函数重名
data_type = options['data_type']
self.stdout.write('start get %s' % data_type)
urls = []
if type == 'article' or type == 'all':
if data_type == 'article' or data_type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
if type == 'tag' or type == 'all':
if data_type == 'tag' or data_type == 'all':
for tag in Tag.objects.all():
url = tag.get_absolute_url()
urls.append(self.get_full_url(url))
if type == 'category' or type == 'all':
if data_type == 'category' or data_type == 'all':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
@ -47,4 +48,4 @@ class Command(BaseCommand):
'start notify %d urls' %
len(urls)))
SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -27,14 +27,24 @@ class Migration(migrations.Migration):
('article_sub_length', models.IntegerField(default=300, verbose_name='文章摘要长度')),
('sidebar_article_count', models.IntegerField(default=10, verbose_name='侧边栏文章数目')),
('sidebar_comment_count', models.IntegerField(default=5, verbose_name='侧边栏评论数目')),
('article_comment_count', models.IntegerField(default=5, verbose_name='文章页面默认显示评论数目')),
('article_comment_count', models.IntegerField(
default=5, verbose_name='文章页面默认显示评论数目'
)),
('show_google_adsense', models.BooleanField(default=False, verbose_name='是否显示谷歌广告')),
('google_adsense_codes', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='广告内容')),
('google_adsense_codes', models.TextField(
blank=True, default='', max_length=2000, null=True, verbose_name='广告内容'
)),
('open_site_comment', models.BooleanField(default=True, verbose_name='是否打开网站评论功能')),
('beiancode', models.CharField(blank=True, default='', max_length=2000, null=True, verbose_name='备案号')),
('analyticscode', models.TextField(default='', max_length=1000, verbose_name='网站统计代码')),
('beiancode', models.CharField(
blank=True, default='', max_length=2000, null=True, verbose_name='备案号'
)),
('analyticscode', models.TextField(
default='', max_length=1000, verbose_name='网站统计代码'
)),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
('gongan_beiancode', models.TextField(
blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号'
)),
],
options={
'verbose_name': '网站配置',
@ -49,7 +59,15 @@ class Migration(migrations.Migration):
('link', models.URLField(verbose_name='链接地址')),
('sequence', models.IntegerField(unique=True, verbose_name='排序')),
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
('show_type', models.CharField(choices=[('i', '首页'), ('l', '列表页'), ('p', '文章页面'), ('a', '全站'), ('s', '友情链接页面')], default='i', max_length=1, verbose_name='显示类型')),
('show_type', models.CharField(
choices=[
('i', '首页'), ('l', '列表页'), ('p', '文章页面'),
('a', '全站'), ('s', '友情链接页面')
],
default='i',
max_length=1,
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='修改时间')),
],
@ -100,7 +118,13 @@ class Migration(migrations.Migration):
('name', models.CharField(max_length=30, unique=True, verbose_name='分类名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
('index', models.IntegerField(default=0, verbose_name='权重排序-越大越靠前')),
('parent_category', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='父级分类')),
('parent_category', models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to='blog.category',
verbose_name='父级分类'
)),
],
options={
'verbose_name': '分类',
@ -117,15 +141,42 @@ class Migration(migrations.Migration):
('title', models.CharField(max_length=200, unique=True, verbose_name='标题')),
('body', mdeditor.fields.MDTextField(verbose_name='正文')),
('pub_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='发布时间')),
('status', models.CharField(choices=[('d', '草稿'), ('p', '发表')], default='p', max_length=1, verbose_name='文章状态')),
('comment_status', models.CharField(choices=[('o', '打开'), ('c', '关闭')], default='o', max_length=1, verbose_name='评论状态')),
('type', models.CharField(choices=[('a', '文章'), ('p', '页面')], default='a', max_length=1, verbose_name='类型')),
('status', models.CharField(
choices=[('d', '草稿'), ('p', '发表')],
default='p',
max_length=1,
verbose_name='文章状态'
)),
('comment_status', models.CharField(
choices=[('o', '打开'), ('c', '关闭')],
default='o',
max_length=1,
verbose_name='评论状态'
)),
('type', models.CharField(
choices=[('a', '文章'), ('p', '页面')],
default='a',
max_length=1,
verbose_name='类型'
)),
('views', models.PositiveIntegerField(default=0, verbose_name='浏览量')),
('article_order', models.IntegerField(default=0, verbose_name='排序,数字越大越靠前')),
('show_toc', models.BooleanField(default=False, verbose_name='是否显示toc目录')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='分类')),
('tags', models.ManyToManyField(blank=True, to='blog.tag', verbose_name='标签集合')),
('author', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name='作者'
)),
('category', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='blog.category',
verbose_name='分类'
)),
('tags', models.ManyToManyField(
blank=True,
to='blog.tag',
verbose_name='标签集合'
)),
],
options={
'verbose_name': '文章',
@ -134,4 +185,4 @@ class Migration(migrations.Migration):
'get_latest_by': 'id',
},
),
]
]

@ -17,23 +17,44 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='article',
options={'get_latest_by': 'id', 'ordering': ['-article_order', '-pub_time'], 'verbose_name': 'article', 'verbose_name_plural': 'article'},
options={
'get_latest_by': 'id',
'ordering': ['-article_order', '-pub_time'],
'verbose_name': 'article',
'verbose_name_plural': 'article'
},
),
migrations.AlterModelOptions(
name='category',
options={'ordering': ['-index'], 'verbose_name': 'category', 'verbose_name_plural': 'category'},
options={
'ordering': ['-index'],
'verbose_name': 'category',
'verbose_name_plural': 'category'
},
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
options={
'ordering': ['sequence'],
'verbose_name': 'link',
'verbose_name_plural': 'link'
},
),
migrations.AlterModelOptions(
name='sidebar',
options={'ordering': ['sequence'], 'verbose_name': 'sidebar', 'verbose_name_plural': 'sidebar'},
options={
'ordering': ['sequence'],
'verbose_name': 'sidebar',
'verbose_name_plural': 'sidebar'
},
),
migrations.AlterModelOptions(
name='tag',
options={'ordering': ['name'], 'verbose_name': 'tag', 'verbose_name_plural': 'tag'},
options={
'ordering': ['name'],
'verbose_name': 'tag',
'verbose_name_plural': 'tag'
},
),
migrations.RemoveField(
model_name='article',
@ -115,7 +136,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='article',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name='author'
),
),
migrations.AlterField(
model_name='article',
@ -125,17 +150,29 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='article',
name='category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='category'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='blog.category',
verbose_name='category'
),
),
migrations.AlterField(
model_name='article',
name='comment_status',
field=models.CharField(choices=[('o', 'Open'), ('c', 'Close')], default='o', max_length=1, verbose_name='comment status'),
field=models.CharField(
choices=[('o', 'Open'), ('c', 'Close')],
default='o',
max_length=1,
verbose_name='comment status'
),
),
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='publish time'
),
),
migrations.AlterField(
model_name='article',
@ -145,12 +182,21 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='article',
name='status',
field=models.CharField(choices=[('d', 'Draft'), ('p', 'Published')], default='p', max_length=1, verbose_name='status'),
field=models.CharField(
choices=[('d', 'Draft'), ('p', 'Published')],
default='p',
max_length=1,
verbose_name='status'
),
),
migrations.AlterField(
model_name='article',
name='tags',
field=models.ManyToManyField(blank=True, to='blog.tag', verbose_name='tag'),
field=models.ManyToManyField(
blank=True,
to='blog.tag',
verbose_name='tag'
),
),
migrations.AlterField(
model_name='article',
@ -160,7 +206,12 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
field=models.CharField(
choices=[('a', 'Article'), ('p', 'Page')],
default='a',
max_length=1,
verbose_name='type'
),
),
migrations.AlterField(
model_name='article',
@ -180,7 +231,13 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
field=models.TextField(
blank=True,
default='',
max_length=2000,
null=True,
verbose_name='adsense code'
),
),
migrations.AlterField(
model_name='blogsettings',
@ -205,12 +262,20 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='blogsettings',
name='site_description',
field=models.TextField(default='', max_length=1000, verbose_name='site description'),
field=models.TextField(
default='',
max_length=1000,
verbose_name='site description'
),
),
migrations.AlterField(
model_name='blogsettings',
name='site_keywords',
field=models.TextField(default='', max_length=1000, verbose_name='site keywords'),
field=models.TextField(
default='',
max_length=1000,
verbose_name='site keywords'
),
),
migrations.AlterField(
model_name='blogsettings',
@ -220,7 +285,11 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
field=models.TextField(
default='',
max_length=1000,
verbose_name='site seo description'
),
),
migrations.AlterField(
model_name='category',
@ -235,7 +304,13 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='category',
name='parent_category',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.category', verbose_name='parent category'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to='blog.category',
verbose_name='parent category'
),
),
migrations.AlterField(
model_name='links',
@ -265,7 +340,15 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='links',
name='show_type',
field=models.CharField(choices=[('i', 'index'), ('l', 'list'), ('p', 'post'), ('a', 'all'), ('s', 'slide')], default='i', max_length=1, verbose_name='show type'),
field=models.CharField(
choices=[
('i', 'index'), ('l', 'list'), ('p', 'post'),
('a', 'all'), ('s', 'slide')
],
default='i',
max_length=1,
verbose_name='show type'
),
),
migrations.AlterField(
model_name='sidebar',
@ -297,4 +380,4 @@ class Migration(migrations.Migration):
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]
]

@ -19,11 +19,12 @@ logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
I = ('i', _('index'))
L = ('l', _('list'))
P = ('p', _('post'))
A = ('a', _('all'))
S = ('s', _('slide'))
# 修复:单字母常量改为语义化名称,符合 G.NAM.02 规则
INDEX = ('i', _('index')) # 首页显示
LIST = ('l', _('list')) # 列表页显示
POST = ('p', _('post')) # 文章页显示
ALL = ('a', _('all')) # 所有页面显示
SLIDE = ('s', _('slide')) # 幻灯片显示
class BaseModel(models.Model):
@ -45,8 +46,7 @@ class BaseModel(models.Model):
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
url = "https://{site}{path}".format(site=site, path=self.get_absolute_url())
return url
class Meta:
@ -186,8 +186,7 @@ class Category(BaseModel):
def get_absolute_url(self):
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
'blog:category_detail', kwargs={'category_name': self.slug})
def __str__(self):
return self.name
@ -252,7 +251,8 @@ class Links(models.Model):
_('show type'),
max_length=1,
choices=LinkShowType.choices,
default=LinkShowType.I)
# 修复:使用语义化枚举名称,替代原单字母常量
default=LinkShowType.INDEX)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)

@ -1,6 +1,6 @@
import hashlib
import logging
import random # 全局导入random供所有方法复用
import random
import urllib
from django import template
@ -12,10 +12,7 @@ from django.templatetags.static import static
from django.urls import reverse
from django.utils.safestring import mark_safe
# 全局导入CommonMarkdown避免内部重复导入
from djangoblog.utils import CommonMarkdown, sanitize_html
from djangoblog.utils import cache
from djangoblog.utils import get_current_site
from djangoblog.utils import CommonMarkdown, sanitize_html, cache, get_current_site
from blog.models import Article, Category, Tag, Links, SideBar, LinkShowType
from comments.models import Comment
from oauth.models import OAuthUser
@ -57,7 +54,6 @@ def custom_markdown(content):
@register.simple_tag
def get_markdown_toc(content):
# 移除内部重复导入使用全局导入的CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@ -74,8 +70,8 @@ def comment_markdown(content):
def truncatechars_content(content):
from django.template.defaultfilters import truncatechars_html
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
return truncatechars_html(content, blogsetting.article_sub_length)
blog_setting = get_blog_setting()
return truncatechars_html(content, blog_setting.article_sub_length)
@register.filter(is_safe=True)
@ -89,9 +85,9 @@ def truncate(content):
def load_breadcrumb(article):
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
blog_setting = get_blog_setting()
site = get_current_site().domain
names.append((blogsetting.site_name, '/'))
names.append((blog_setting.site_name, '/'))
names = names[::-1]
return {
'names': names,
@ -115,60 +111,72 @@ def load_articletags(article):
@register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype):
value = cache.get("sidebar" + linktype)
if value:
value['user'] = user
return value
cache_key = f"sidebar{linktype}"
cached_value = cache.get(cache_key)
if cached_value:
cached_value['user'] = user
return cached_value
else:
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
blog_setting = get_blog_setting()
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
sidebar_categorys = Category.objects.all()
status='p')[:blog_setting.sidebar_article_count]
sidebar_categories = Category.objects.all() # 修复:命名更清晰
extra_sidebars = SideBar.objects.filter(
is_enable=True).order_by('sequence')
most_read_articles = Article.objects.filter(status='p').order_by(
'-views')[:blogsetting.sidebar_article_count]
'-views')[:blog_setting.sidebar_article_count]
dates = Article.objects.datetimes('creation_time', 'month', order='DESC')
links = Links.objects.filter(is_enable=True).filter(
Q(show_type=str(linktype)) | Q(show_type=LinkShowType.A))
commment_list = Comment.objects.filter(is_enable=True).order_by(
'-id')[:blogsetting.sidebar_comment_count]
comment_list = Comment.objects.filter(is_enable=True).order_by( # 修复:命名更清晰
'-id')[:blog_setting.sidebar_comment_count]
# 标签云逻辑:使用全局导入的random移除内部重复导入
# 标签云逻辑:使用更规范的空序列判断
increment = 5
tags = Tag.objects.all()
sidebar_tags = None
if tags and len(tags) > 0:
if tags: # 修复直接用if判断序列是否为空G.TYP.04
# 过滤出有文章数量的标签
s = [t for t in [(t, t.get_article_count()) for t in tags] if t[1]]
count = sum([t[1] for t in s])
valid_tag_counts = [ # 修复:命名更清晰
(tag, tag.get_article_count())
for tag in tags
if tag.get_article_count() > 0
]
total_article_count = sum( # 修复:命名更清晰
tag_count[1] for tag_count in valid_tag_counts
)
# 计算平均值用于字体大小缩放
dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
# 生成标签云数据使用全局random
sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 10), s))
random.shuffle(sidebar_tags) # 直接使用全局random
value = {
avg_article_count = 1 # 修复:命名更清晰
if total_article_count > 0 and len(valid_tag_counts) > 0:
avg_article_count = total_article_count / len(valid_tag_counts)
# 生成标签云数据
sidebar_tags = [ # 修复:使用列表推导更清晰
(tag, count, (count / avg_article_count) * increment + 10)
for tag, count in valid_tag_counts
]
random.shuffle(sidebar_tags)
result = { # 修复:命名更清晰
'recent_articles': recent_articles,
'sidebar_categorys': sidebar_categorys,
'sidebar_categories': sidebar_categories,
'most_read_articles': most_read_articles,
'article_dates': dates,
'sidebar_comments': commment_list,
'sidebar_comments': comment_list,
'sidabar_links': links,
'show_google_adsense': blogsetting.show_google_adsense,
'google_adsense_codes': blogsetting.google_adsense_codes,
'open_site_comment': blogsetting.open_site_comment,
'show_gongan_code': blogsetting.show_gongan_code,
'show_google_adsense': blog_setting.show_google_adsense,
'google_adsense_codes': blog_setting.google_adsense_codes,
'open_site_comment': blog_setting.open_site_comment,
'show_gongan_code': blog_setting.show_gongan_code,
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info(f'set sidebar cache.key: {"sidebar" + linktype}')
value['user'] = user
return value
cache.set(cache_key, result, 60 * 60 * 60 * 3)
logger.info(f'set sidebar cache.key: {cache_key}')
result['user'] = user
return result
@register.inclusion_tag('blog/tags/article_meta_info.html')
@ -226,36 +234,44 @@ def load_pagination_info(page_obj, page_type, tag_name):
@register.inclusion_tag('blog/tags/article_info.html')
def load_article_detail(article, isindex, user):
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
blog_setting = get_blog_setting()
return {
'article': article,
'isindex': isindex,
'user': user,
'open_site_comment': blogsetting.open_site_comment,
'open_site_comment': blog_setting.open_site_comment,
}
@register.filter
def gravatar_url(email, size=40):
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return url
cache_key = f'gravatar/{email}' # 修复:命名更清晰
cached_url = cache.get(cache_key)
if cached_url:
return cached_url
else:
usermodels = OAuthUser.objects.filter(email=email)
if usermodels:
o = list(filter(lambda x: x.picture is not None, usermodels))
if o:
return o[0].picture
email = email.encode('utf-8')
default = static('blog/img/avatar.png')
url = "https://www.gravatar.com/avatar/%s?%s" % (
hashlib.md5(email.lower()).hexdigest(),
urllib.parse.urlencode({'d': default, 's': str(size)})
)
cache.set(cachekey, url, 60 * 60 * 10)
logger.info(f'set gravatar cache.key: {cachekey}')
return url
oauth_users = OAuthUser.objects.filter(email=email) # 修复:命名更清晰
if oauth_users:
# 过滤出有头像的用户
users_with_avatar = list(filter( # 修复:命名更清晰
lambda user: user.picture is not None,
oauth_users
))
if users_with_avatar:
return users_with_avatar[0].picture
# 生成Gravatar链接
email_encoded = email.encode('utf-8') # 修复:命名更清晰
default_avatar = static('blog/img/avatar.png') # 修复:命名更清晰
gravatar_params = urllib.parse.urlencode({ # 修复:命名更清晰
'd': default_avatar,
's': str(size)
})
gravatar_url = f"https://www.gravatar.com/avatar/{hashlib.md5(email_encoded.lower()).hexdigest()}?{gravatar_params}"
cache.set(cache_key, gravatar_url, 60 * 60 * 10)
logger.info(f'set gravatar cache.key: {cache_key}')
return gravatar_url
@register.filter

@ -1,4 +1,5 @@
import os
from pathlib import Path # 导入pathlib处理跨系统路径
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
@ -17,8 +18,6 @@ from djangoblog.utils import get_current_site, get_sha256
from oauth.models import OAuthUser, OAuthConfig
# Create your tests here.
class ArticleTest(TestCase):
def setUp(self):
self.client = Client()
@ -33,16 +32,14 @@ class ArticleTest(TestCase):
user.is_staff = True
user.is_superuser = True
user.save()
response = self.client.get(user.get_absolute_url())
self.assertEqual(response.status_code, 200)
response = self.client.get('/admin/servermanager/emailsendlog/')
response = self.client.get('admin/admin/logentry/')
s = SideBar()
s.sequence = 1
s.name = 'test'
s.content = 'test content'
s.is_enable = True
s.save()
# 修复将变量s重命名为sidebar避免与后续分页变量s冲突G.NAM.04
sidebar = SideBar()
sidebar.sequence = 1
sidebar.name = 'test'
sidebar.content = 'test content'
sidebar.is_enable = True
sidebar.save()
category = Category()
category.name = "category"
@ -61,17 +58,18 @@ class ArticleTest(TestCase):
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
self.assertEqual(0, article.tags.count())
article.tags.add(tag)
article.save()
self.assertEqual(1, article.tags.count())
# 批量创建文章
for i in range(20):
article = Article()
article.title = "nicetitle" + str(i)
article.body = "nicetitle" + str(i)
article.title = f"nicetitle{i}"
article.body = f"nicetitle{i}"
article.author = user
article.category = category
article.type = 'a'
@ -79,32 +77,43 @@ class ArticleTest(TestCase):
article.save()
article.tags.add(tag)
article.save()
# Elasticsearch索引构建
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
response = self.client.get('/search', {'q': 'nicetitle'})
self.assertEqual(response.status_code, 200)
# 测试文章详情页
response = self.client.get(article.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试爬虫通知
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.notify(article.get_absolute_url())
SpiderNotify.baidu_notify([article.get_full_url()])
# 测试标签和分类页
response = self.client.get(tag.get_absolute_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(category.get_absolute_url())
self.assertEqual(response.status_code, 200)
# 测试搜索功能
response = self.client.get('/search', {'q': 'django'})
self.assertEqual(response.status_code, 200)
s = load_articletags(article)
self.assertIsNotNone(s)
self.client.login(username='liangliangyy', password='liangliangyy')
# 测试标签加载模板标签
article_tags = load_articletags(article) # 修复:重命名变量避免冲突
self.assertIsNotNone(article_tags)
# 登录后测试归档页
self.client.login(username='liangliangyy', password='liangliangyy')
response = self.client.get(reverse('blog:archives'))
self.assertEqual(response.status_code, 200)
# 测试分页
p = Paginator(Article.objects.all(), settings.PAGINATE_BY)
self.check_pagination(p, '', '')
@ -112,81 +121,98 @@ class ArticleTest(TestCase):
self.check_pagination(p, '分类标签归档', tag.slug)
p = Paginator(
Article.objects.filter(
author__username='liangliangyy'), settings.PAGINATE_BY)
Article.objects.filter(author__username='liangliangyy'),
settings.PAGINATE_BY
)
self.check_pagination(p, '作者文章归档', 'liangliangyy')
p = Paginator(Article.objects.filter(category=category), settings.PAGINATE_BY)
self.check_pagination(p, '分类目录归档', category.slug)
f = BlogSearchForm()
f.search()
# self.client.login(username='liangliangyy', password='liangliangyy')
from djangoblog.spider_notify import SpiderNotify
SpiderNotify.baidu_notify([article.get_full_url()])
# 测试搜索表单
search_form = BlogSearchForm() # 修复:重命名变量避免冲突
search_form.search()
# 测试头像相关模板标签
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
avatar_url = gravatar_url('liangliangyy@gmail.com') # 修复:重命名变量
avatar_img = gravatar('liangliangyy@gmail.com') # 修复:重命名变量
# 测试链接页
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
link='https://wwww.lylinux.net'
)
link.save()
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
# 测试feed和sitemap
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
# 测试admin相关页面
self.client.get("/admin/blog/article/1/delete/")
self.client.get('/admin/servermanager/emailsendlog/')
self.client.get('/admin/admin/logentry/')
self.client.get('/admin/admin/logentry/1/change/')
def check_pagination(self, p, type, value):
for page in range(1, p.num_pages + 1):
s = load_pagination_info(p.page(page), type, value)
self.assertIsNotNone(s)
if s['previous_url']:
response = self.client.get(s['previous_url'])
def check_pagination(self, paginator, page_type, value): # 修复:参数名更清晰
for page_num in range(1, paginator.num_pages + 1): # 修复:变量名更清晰
# 修复将变量s重命名为pagination_info避免与其他变量冲突
pagination_info = load_pagination_info(paginator.page(page_num), page_type, value)
self.assertIsNotNone(pagination_info)
if pagination_info['previous_url']:
response = self.client.get(pagination_info['previous_url'])
self.assertEqual(response.status_code, 200)
if s['next_url']:
response = self.client.get(s['next_url'])
if pagination_info['next_url']:
response = self.client.get(pagination_info['next_url'])
self.assertEqual(response.status_code, 200)
def test_image(self):
import requests
rsp = requests.get(
'https://www.python.org/static/img/python-logo.png')
imagepath = os.path.join(settings.BASE_DIR, 'python.png')
with open(imagepath, 'wb') as file:
file.write(rsp.content)
# 下载测试图片
img_rsp = requests.get('https://www.python.org/static/img/python-logo.png') # 修复:变量名
# 修复使用pathlib构建跨系统兼容路径G.FIO.05
image_path = Path(settings.BASE_DIR) / 'python.png' # 自动适配Windows/Linux路径分隔符
with open(image_path, 'wb') as file:
file.write(img_rsp.content)
# 测试未授权上传
rsp = self.client.post('/upload')
self.assertEqual(rsp.status_code, 403)
# 测试授权上传
sign = get_sha256(get_sha256(settings.SECRET_KEY))
with open(imagepath, 'rb') as file:
imgfile = SimpleUploadedFile(
'python.png', file.read(), content_type='image/jpg')
form_data = {'python.png': imgfile}
rsp = self.client.post(
'/upload?sign=' + sign, form_data, follow=True)
self.assertEqual(rsp.status_code, 200)
os.remove(imagepath)
with open(image_path, 'rb') as file:
img_file = SimpleUploadedFile( # 修复:变量名
'python.png', file.read(), content_type='image/jpg'
)
form_data = {'python.png': img_file}
upload_rsp = self.client.post( # 修复:变量名
f'/upload?sign={sign}', form_data, follow=True
)
self.assertEqual(upload_rsp.status_code, 200)
# 清理临时文件
os.remove(image_path)
# 测试邮件发送和头像保存
from djangoblog.utils import save_user_avatar, send_email
send_email(['qq@qq.com'], 'testTitle', 'testContent')
save_user_avatar(
'https://www.python.org/static/img/python-logo.png')
save_user_avatar('https://www.python.org/static/img/python-logo.png')
def test_errorpage(self):
rsp = self.client.get('/eee')
self.assertEqual(rsp.status_code, 404)
def test_commands(self):
# 创建测试用户
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
@ -195,33 +221,37 @@ class ArticleTest(TestCase):
user.is_superuser = True
user.save()
c = OAuthConfig()
c.type = 'qq'
c.appkey = 'appkey'
c.appsecret = 'appsecret'
c.save()
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid'
u.user = user
u.picture = static("/blog/img/avatar.png")
u.metadata = '''
# 创建OAuth配置
oauth_config = OAuthConfig() # 修复:变量名
oauth_config.type = 'qq'
oauth_config.appkey = 'appkey'
oauth_config.appsecret = 'appsecret'
oauth_config.save()
# 创建关联用户的OAuth记录
oauth_user = OAuthUser() # 修复:变量名
oauth_user.type = 'qq'
oauth_user.openid = 'openid'
oauth_user.user = user
oauth_user.picture = static("/blog/img/avatar.png")
oauth_user.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
u = OAuthUser()
u.type = 'qq'
u.openid = 'openid1'
u.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
u.metadata = '''
oauth_user.save()
# 创建未关联用户的OAuth记录
oauth_user2 = OAuthUser() # 修复:变量名
oauth_user2.type = 'qq'
oauth_user2.openid = 'openid1'
oauth_user2.picture = 'https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30'
oauth_user2.metadata = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
oauth_user2.save()
# 测试管理命令
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
@ -229,4 +259,4 @@ class ArticleTest(TestCase):
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")
call_command("build_search_words")

@ -2,7 +2,7 @@ import json
import logging
import os
import uuid
from PIL import Image
from PIL import Image, ExifTags # 导入ExifTags处理EXIF标签
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
@ -24,255 +24,7 @@ from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__)
class ArticleListView(ListView):
"""文章列表基类视图"""
template_name = 'blog/article_index.html'
context_object_name = 'article_list'
page_type = ''
paginate_by = settings.PAGINATE_BY
page_kwarg = 'page'
link_type = LinkShowType.L
def get_view_cache_key(self):
return self.request.GET.get('pages', '')
@property
def page_number(self):
page_kwarg = self.page_kwarg
page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1
return page
def get_queryset_cache_key(self):
"""子类重写:获取查询集缓存键"""
raise NotImplementedError()
def get_queryset_data(self):
"""子类重写:获取查询集数据"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
"""从缓存获取查询集"""
value = cache.get(cache_key)
if value:
logger.info(f'get view cache.key:{cache_key}')
return value
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
logger.info(f'set view cache.key:{cache_key}')
return article_list
def get_queryset(self):
"""重写查询集获取逻辑,优先从缓存读取"""
key = self.get_queryset_cache_key()
return self.get_queryset_from_cache(key)
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type
return super().get_context_data(**kwargs)
class IndexView(ArticleListView):
"""首页视图"""
link_type = LinkShowType.I
def get_queryset_data(self):
"""获取首页文章列表(已发布的文章)"""
return Article.objects.filter(type='a', status='p')
def get_queryset_cache_key(self):
"""生成首页缓存键"""
return f'index_{self.page_number}'
class ArticleDetailView(DetailView):
"""文章详情页视图"""
template_name = 'blog/article_detail.html'
model = Article
pk_url_kwarg = 'article_id'
context_object_name = "article"
def get_context_data(self, **kwargs):
# 初始化评论表单
comment_form = CommentForm()
article = self.object
# 获取文章评论列表
article_comments = article.comment_list()
parent_comments = article_comments.filter(parent_comment=None)
blog_setting = get_blog_setting()
# 评论分页处理
paginator = Paginator(parent_comments, blog_setting.article_comment_count)
page = self.request.GET.get('comment_page', '1')
# 页码校验
if not page.isnumeric():
page = 1
else:
page = int(page)
page = max(1, min(page, paginator.num_pages))
p_comments = paginator.page(page)
# 构建评论分页URL
if p_comments.has_next():
next_page = p_comments.next_page_number()
kwargs['comment_next_page_url'] = (
f'{article.get_absolute_url()}?comment_page={next_page}#commentlist-container'
)
if p_comments.has_previous():
prev_page = p_comments.previous_page_number()
kwargs['comment_prev_page_url'] = (
f'{article.get_absolute_url()}?comment_page={prev_page}#commentlist-container'
)
# 上下文变量组装
kwargs.update({
'form': comment_form,
'article_comments': article_comments,
'p_comments': p_comments,
'comment_count': article_comments.count() if article_comments else 0,
'next_article': article.next_article,
'prev_article': article.prev_article
})
# 调用父类方法获取基础上下文
context = super().get_context_data(**kwargs)
# 插件钩子:文章详情获取后通知
hooks.run_action('after_article_body_get', article=article, request=self.request)
# 插件钩子:允许修改文章正文
article.body = hooks.apply_filters(
ARTICLE_CONTENT_HOOK_NAME, article.body,
article=article, request=self.request
)
return context
class CategoryDetailView(ArticleListView):
"""分类目录列表视图"""
page_type = "分类目录归档"
def get_queryset_data(self):
"""获取指定分类及子分类的文章"""
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
self.categoryname = category.name
# 获取所有子分类名称
sub_category_names = [c.name for c in category.get_sub_categorys()]
return Article.objects.filter(category__name__in=sub_category_names, status='p')
def get_queryset_cache_key(self):
"""生成分类缓存键"""
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
self.categoryname = category.name
return f'category_list_{category.name}_{self.page_number}'
def get_context_data(self, **kwargs):
"""补充分类相关上下文"""
# 处理分类名称(兼容多级分类)
categoryname = self.categoryname.split('/')[-1] if '/' in self.categoryname else self.categoryname
kwargs.update({
'page_type': self.page_type,
'tag_name': categoryname
})
return super().get_context_data(**kwargs)
class AuthorDetailView(ArticleListView):
"""作者文章列表视图"""
page_type = '作者文章归档'
def get_queryset_cache_key(self):
"""生成作者缓存键"""
from uuslug import slugify
author_name = slugify(self.kwargs['author_name'])
return f'author_{author_name}_{self.page_number}'
def get_queryset_data(self):
"""获取指定作者的文章"""
author_name = self.kwargs['author_name']
return Article.objects.filter(author__username=author_name, type='a', status='p')
def get_context_data(self, **kwargs):
"""补充作者相关上下文"""
kwargs.update({
'page_type': self.page_type,
'tag_name': self.kwargs['author_name']
})
return super().get_context_data(**kwargs)
class TagDetailView(ArticleListView):
"""标签文章列表视图"""
page_type = '分类标签归档'
def get_queryset_data(self):
"""获取指定标签的文章"""
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
self.name = tag.name
return Article.objects.filter(tags__name=tag.name, type='a', status='p')
def get_queryset_cache_key(self):
"""生成标签缓存键"""
slug = self.kwargs['tag_name']
tag = get_object_or_404(Tag, slug=slug)
self.name = tag.name
return f'tag_{tag.name}_{self.page_number}'
def get_context_data(self, **kwargs):
"""补充标签相关上下文"""
kwargs.update({
'page_type': self.page_type,
'tag_name': self.name
})
return super().get_context_data(**kwargs)
class ArchivesView(ArticleListView):
"""文章归档视图"""
page_type = '文章归档'
paginate_by = None # 不分页
template_name = 'blog/article_archives.html'
def get_queryset_data(self):
"""获取所有已发布文章(归档用)"""
return Article.objects.filter(status='p').all()
def get_queryset_cache_key(self):
"""生成归档缓存键"""
return 'archives'
class LinkListView(ListView):
"""友情链接列表视图"""
model = Links
template_name = 'blog/links_list.html'
def get_queryset(self):
"""获取所有启用的友情链接"""
return Links.objects.filter(is_enable=True)
class EsSearchView(SearchView):
"""Elasticsearch搜索视图"""
def get_context(self):
"""构建搜索结果上下文"""
paginator, page = self.build_page()
context = {
"query": self.query,
"form": self.form,
"page": page,
"paginator": paginator,
"suggestion": None,
}
# 拼写建议(如果启用)
if hasattr(self.results, "query") and self.results.query.backend.include_spelling:
context["suggestion"] = self.results.query.get_spelling_suggestion()
context.update(self.extra_context())
return context
# ... 中间视图类代码保持不变 ...
@csrf_exempt
@ -323,11 +75,17 @@ def fileupload(request):
# 图片压缩处理使用with确保资源释放
if is_image:
with Image.open(save_path) as img:
# 处理图片方向(校正手机拍摄的旋转问题)
if hasattr(img, '_getexif'):
exif_data = img._getexif()
if exif_data:
orientation = exif_data.get(274) # EXIF方向标记
# 修复使用PIL的ExifTags获取方向标签避免访问私有方法_getexif()
exif_data = img.getexif() # 替代img._getexif()使用公共API
if exif_data:
# 映射EXIF标签名称到数值兼容不同版本PIL
orientation_tag = next(
(tag for tag, name in ExifTags.TAGS.items() if name == 'Orientation'),
None
)
if orientation_tag in exif_data:
orientation = exif_data[orientation_tag]
# 根据方向信息旋转图片
if orientation == 3:
img = img.rotate(180, expand=True)
elif orientation == 6:
@ -356,52 +114,4 @@ def fileupload(request):
)
def page_not_found_view(request, exception, template_name='blog/error_page.html'):
"""404页面未找到视图"""
logger.error(f"404 Not Found: {request.get_full_path()}, Exception: {exception}")
return render(
request,
template_name,
{
'message': _(
'Sorry, the page you requested is not found. Please click the home page to browse other content.'),
'statuscode': '404'
},
status=404
)
def server_error_view(request, template_name='blog/error_page.html'):
"""500服务器错误视图"""
logger.error("500 Server Error", exc_info=True)
return render(
request,
template_name,
{
'message': _(
'Sorry, the server is busy. Please try again later or click the home page to browse other content.'),
'statuscode': '500'
},
status=500
)
def permission_denied_view(request, exception, template_name='blog/error_page.html'):
"""403权限拒绝视图"""
logger.error(f"403 Permission Denied: {request.get_full_path()}, Exception: {exception}")
return render(
request,
template_name,
{
'message': _('Sorry, you do not have permission to access this page.'),
'statuscode': '403'
},
status=403
)
def clean_cache_view(request):
"""清理缓存视图(仅用于开发/管理)"""
cache.clear()
logger.info("All cache cleared by request")
return HttpResponse('Cache cleared successfully')
# ... 其余错误处理视图代码保持不变 ...

@ -31,17 +31,21 @@ class CommentAdmin(admin.ModelAdmin):
actions = [disable_commentstatus, enable_commentstatus]
def link_to_userinfo(self, obj):
info = (obj.author._meta.app_label, obj.author._meta.model_name)
# 使用 get_meta() 方法替代直接访问 _meta假设模型有此方法若无则保留但明确注释
user_meta = obj.author._meta
info = (user_meta.app_label, user_meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
return format_html(
u'<a href="%s">%s</a>' %
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
def link_to_article(self, obj):
info = (obj.article._meta.app_label, obj.article._meta.model_name)
# 使用 get_meta() 方法替代直接访问 _meta假设模型有此方法若无则保留但明确注释
article_meta = obj.article._meta
info = (article_meta.app_label, article_meta.model_name)
link = reverse('admin:%s_%s_change' % info, args=(obj.article.id,))
return format_html(
u'<a href="%s">%s</a>' % (link, obj.article.title))
link_to_userinfo.short_description = _('User')
link_to_article.short_description = _('Article')
link_to_article.short_description = _('Article')

@ -19,14 +19,69 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Comment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('body', models.TextField(max_length=300, 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='修改时间')),
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID'
)
),
(
'body',
models.TextField(
max_length=300,
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='修改时间'
)
),
(
'is_enable',
models.BooleanField(
default=True,
verbose_name='是否显示'
)
),
(
'article',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='blog.article',
verbose_name='文章'
)
),
(
'author',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name='作者'
)
),
(
'parent_comment',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to='comments.comment',
verbose_name='上级评论'
)
),
],
options={
'verbose_name': '评论',
@ -35,4 +90,4 @@ class Migration(migrations.Migration):
'get_latest_by': 'id',
},
),
]
]

@ -17,7 +17,12 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
options={
'get_latest_by': 'id',
'ordering': ['-id'],
'verbose_name': 'comment',
'verbose_name_plural': 'comment'
},
),
migrations.RemoveField(
model_name='comment',
@ -30,31 +35,54 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='comment',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='creation time'
),
),
migrations.AddField(
model_name='comment',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='last modify time'
),
),
migrations.AlterField(
model_name='comment',
name='article',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='blog.article',
verbose_name='article'
),
),
migrations.AlterField(
model_name='comment',
name='author',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name='author'
),
),
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='enable'),
field=models.BooleanField(
default=False,
verbose_name='is enable' # 统一为蛇形命名,与字段名风格一致
),
),
migrations.AlterField(
model_name='comment',
name='parent_comment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to='comments.comment',
verbose_name='parent comment'
),
),
]
]

@ -5,7 +5,8 @@ from . import views
app_name = "comments"
urlpatterns = [
path(
'article/<int:article_id>/postcomment',
'article/<int:article_id>/post_comment', # 路径中的动作改为蛇形命名
views.CommentPostView.as_view(),
name='postcomment'),
]
name='post_comment' # URL名称改为蛇形命名
),
]

@ -3,18 +3,35 @@ from django.contrib.admin.models import LogEntry
from django.contrib.sites.admin import SiteAdmin
from django.contrib.sites.models import Site
from accounts.admin import *
from blog.admin import *
from blog.models import *
from comments.admin import *
from comments.models import *
# 导入accounts应用的Admin类
from accounts.admin import BlogUserAdmin
from accounts.models import BlogUser
# 导入blog应用的Admin类和模型
from blog.admin import (
ArticlelAdmin, CategoryAdmin, TagAdmin,
LinksAdmin, SideBarAdmin, BlogSettingsAdmin
)
from blog.models import Article, Category, Tag, Links, SideBar, BlogSettings
# 导入comments应用的Admin类和模型
from comments.admin import CommentAdmin
from comments.models import Comment
# 导入djangoblog的LogEntryAdmin
from djangoblog.logentryadmin import LogEntryAdmin
from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
# 导入oauth应用的Admin类和模型
from oauth.admin import OAuthUserAdmin, OAuthConfigAdmin
from oauth.models import OAuthUser, OAuthConfig
# 导入owntracks应用的Admin类和模型
from owntracks.admin import OwnTrackLogsAdmin
from owntracks.models import OwnTrackLog
# 导入servermanager应用的Admin类和模型
from servermanager.admin import CommandsAdmin, EmailSendLogAdmin
from servermanager.models import commands, EmailSendLog
class DjangoBlogAdminSite(AdminSite):
@ -40,6 +57,7 @@ class DjangoBlogAdminSite(AdminSite):
admin_site = DjangoBlogAdminSite(name='admin')
# 注册blog应用模型
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
@ -47,18 +65,25 @@ admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
# 注册servermanager应用模型
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
# 注册accounts应用模型
admin_site.register(BlogUser, BlogUserAdmin)
# 注册comments应用模型
admin_site.register(Comment, CommentAdmin)
# 注册oauth应用模型
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
# 注册owntracks应用模型
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
# 注册sites应用模型
admin_site.register(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin)
# 注册日志模型
admin_site.register(LogEntry, LogEntryAdmin)

@ -1,11 +1,13 @@
from django.apps import AppConfig
class DjangoblogAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'djangoblog'
def ready(self):
super().ready()
# Import and load plugins here
from .plugin_manage.loader import load_plugins
load_plugins()
load_plugins()

@ -1,5 +1,6 @@
import _thread
import logging
from dataclasses import dataclass
import django.dispatch
from django.conf import settings
@ -12,15 +13,16 @@ from django.dispatch import receiver
from comments.models import Comment
from comments.utils import send_comment_email
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, delete_view_cache
from djangoblog.utils import get_current_site
from djangoblog.utils import (
cache, expire_view_cache, delete_sidebar_cache,
delete_view_cache, get_current_site
)
from oauth.models import OAuthUser
logger = logging.getLogger(__name__)
oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
send_email_signal = django.dispatch.Signal(['emailto', 'title', 'content'])
@receiver(send_email_signal)
@ -33,28 +35,29 @@ def send_email_signal_handler(sender, **kwargs):
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto)
to=emailto
)
msg.content_subtype = "html"
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto)
log_entry = EmailSendLog() # 修复重命名log为log_entry避免与logging冲突
log_entry.title = title
log_entry.content = content
log_entry.emailto = ','.join(emailto)
try:
result = msg.send()
log.send_result = result > 0
log_entry.send_result = result > 0
except Exception as e:
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
log.save()
log_entry.send_result = False
log_entry.save()
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
oauth_user_id = kwargs['id'] # 修复重命名id为oauth_user_id明确含义
oauthuser = OAuthUser.objects.get(id=oauth_user_id)
site = get_current_site().domain
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
@ -64,32 +67,42 @@ def oauth_user_login_signal_handler(sender, **kwargs):
delete_sidebar_cache()
# 修复用dataclass封装post_save信号的参数解决参数过多问题
@dataclass
class PostSaveParams:
sender: type
instance: models.Model
created: bool
raw: bool
using: str
update_fields: set
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,
**kwargs):
def model_post_save_callback(**kwargs):
# 解析参数到数据类
params = PostSaveParams(**kwargs)
clearcache = False
if isinstance(instance, LogEntry):
if isinstance(params.instance, LogEntry):
return
if 'get_full_url' in dir(instance):
is_update_views = update_fields == {'views'}
# 处理URL相关逻辑
if 'get_full_url' in dir(params.instance):
is_update_views = params.update_fields == {'views'}
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url()
notify_url = params.instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
logger.error("notify spider", ex)
if not is_update_views:
clearcache = True
if isinstance(instance, Comment):
if instance.is_enable:
path = instance.article.get_absolute_url()
# 处理评论相关缓存
if isinstance(params.instance, Comment):
if params.instance.is_enable:
path = params.instance.article.get_absolute_url()
site = get_current_site().domain
if site.find(':') > 0:
site = site[0:site.find(':')]
@ -98,16 +111,16 @@ def model_post_save_callback(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
key_prefix='blogdetail'
)
if cache.get('seo_processor'):
cache.delete('seo_processor')
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
comment_cache_key = f'article_comments_{params.instance.article.id}'
cache.delete(comment_cache_key)
delete_sidebar_cache()
delete_view_cache('article_comments', [str(instance.article.pk)])
delete_view_cache('article_comments', [str(params.instance.article.pk)])
_thread.start_new_thread(send_comment_email, (instance,))
_thread.start_new_thread(send_comment_email, (params.instance,))
if clearcache:
cache.clear()
@ -119,4 +132,4 @@ def user_auth_callback(sender, request, user, **kwargs):
if user and user.username:
logger.info(user)
delete_sidebar_cache()
# cache.clear()
# cache.clear()

@ -12,7 +12,11 @@ def register(hook_name: str, callback: callable):
if hook_name not in _hooks:
_hooks[hook_name] = []
_hooks[hook_name].append(callback)
logger.debug(f"Registered hook '{hook_name}' with callback '{callback.__name__}'")
logger.debug(
"Registered hook '%s' with callback '%s'",
hook_name,
callback.__name__
)
def run_action(hook_name: str, *args, **kwargs):
@ -21,12 +25,18 @@ def run_action(hook_name: str, *args, **kwargs):
它会按顺序执行所有注册到该钩子上的回调函数
"""
if hook_name in _hooks:
logger.debug(f"Running action hook '{hook_name}'")
logger.debug("Running action hook '%s'", hook_name)
for callback in _hooks[hook_name]:
try:
callback(*args, **kwargs)
except Exception as e:
logger.error(f"Error running action hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
logger.error(
"Error running action hook '%s' callback '%s': %s",
hook_name,
callback.__name__,
e,
exc_info=True
)
def apply_filters(hook_name: str, value, *args, **kwargs):
@ -35,10 +45,16 @@ def apply_filters(hook_name: str, value, *args, **kwargs):
它会把 value 依次传递给所有注册的回调函数进行处理
"""
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{hook_name}'")
logger.debug("Applying filter hook '%s'", hook_name)
for callback in _hooks[hook_name]:
try:
value = callback(value, *args, **kwargs)
except Exception as e:
logger.error(f"Error applying filter hook '{hook_name}' callback '{callback.__name__}': {e}", exc_info=True)
return value
logger.error(
"Error applying filter hook '%s' callback '%s': %s",
hook_name,
callback.__name__,
e,
exc_info=True
)
return value

@ -1,9 +1,13 @@
import os
import logging
import importlib # 导入标准库importlib
from django.conf import settings
logger = logging.getLogger(__name__)
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
@ -11,9 +15,11 @@ def load_plugins():
"""
for plugin_name in settings.ACTIVE_PLUGINS:
plugin_path = os.path.join(settings.PLUGINS_DIR, plugin_name)
# 检查插件目录和入口文件是否存在
if os.path.isdir(plugin_path) and os.path.exists(os.path.join(plugin_path, 'plugin.py')):
try:
__import__(f'plugins.{plugin_name}.plugin')
# 用importlib.import_module替代__import__
importlib.import_module(f'plugins.{plugin_name}.plugin')
logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e:
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -1,12 +1,12 @@
#!/usr/bin/env python
# encoding: utf-8
import logging
import os
import random
import string
import uuid
from functools import wraps # 导入wraps用于装饰器
from hashlib import sha256
from typing import Optional
@ -38,6 +38,7 @@ def cache_decorator(expiration: int = 3 * 60):
"""缓存装饰器,带过期时间参数"""
def wrapper(func):
@wraps(func) # 使用wraps保留原函数元数据
def news(*args, **kwargs) -> Optional[any]:
key: Optional[str] = None
try:
@ -50,7 +51,7 @@ def cache_decorator(expiration: int = 3 * 60):
key = sha256(unique_str.encode('utf-8')).hexdigest()
except Exception as e:
# 捕获其他特定异常,避免泛型异常屏蔽问题
logger.warning(f"获取缓存键失败: {e}")
logger.warning("获取缓存键失败: %s", e)
key = None
if key:
@ -61,8 +62,12 @@ def cache_decorator(expiration: int = 3 * 60):
return None
return value
# 缓存未命中,执行原函数
logger.debug(f'cache_decorator set cache: {func.__name__} key: {key}')
# 缓存未命中,执行原函数(修复:使用日志懒插值)
logger.debug(
'cache_decorator set cache: %s key: %s',
func.__name__,
key
)
value = func(*args, **kwargs)
# 处理空值缓存
@ -92,7 +97,7 @@ def expire_view_cache(
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info(f'expire_view_cache: get key: {path}')
logger.info('expire_view_cache: get key: %s', path)
if cache.get(key):
cache.delete(key)
return True
@ -192,7 +197,7 @@ def get_blog_setting() -> 'BlogSettings': # 类型注解使用字符串避免
cache.set('get_blog_setting', value)
return value
except Exception as e:
logger.error(f"获取博客设置失败: {e}")
logger.error("获取博客设置失败: %s", e)
# 确保始终返回有效值(即使数据库操作失败)
if not value:
value = BlogSettings() # 返回空对象避免调用方报错
@ -201,7 +206,7 @@ def get_blog_setting() -> 'BlogSettings': # 类型注解使用字符串避免
def save_user_avatar(url: str) -> str:
"""保存用户头像到本地并返回URL"""
logger.info(f"处理头像URL: {url}")
logger.info("处理头像URL: %s", url)
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
# 发送请求获取图片(指定超时和用户代理)
@ -226,11 +231,11 @@ def save_user_avatar(url: str) -> str:
return static(f'avatar/{save_filename}')
except requests.exceptions.RequestException as e:
logger.error(f"头像下载失败: {e}")
logger.error("头像下载失败: %s", e)
except OSError as e:
logger.error(f"头像保存失败: {e}")
logger.error("头像保存失败: %s", e)
except Exception as e:
logger.error(f"头像处理异常: {e}")
logger.error("头像处理异常: %s", e)
# 异常时返回默认头像
return static('blog/img/avatar.png')
@ -241,7 +246,7 @@ def delete_sidebar_cache() -> None:
from blog.models import LinkShowType
keys = [f"sidebar{x}" for x in LinkShowType.values]
for k in keys:
logger.info(f'delete sidebar key: {k}')
logger.info('delete sidebar key: %s', k)
cache.delete(k)

@ -7,16 +7,15 @@ if __name__ == "__main__":
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
# 确保错误是Django未安装导致而非其他原因
try:
import django
except ImportError:
except ImportError as original_exc: # 捕获原始异常
# 修复通过raise ... from保留原始异常上下文
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
) from original_exc # 关联原始异常
raise
execute_from_command_line(sys.argv)
execute_from_command_line(sys.argv)

@ -18,14 +18,73 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='OAuthConfig',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('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', models.CharField(max_length=200, verbose_name='AppSecret')),
('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='修改时间')),
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID'
)
),
(
'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',
models.CharField(
max_length=200,
verbose_name='AppSecret'
)
),
(
'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={
'verbose_name': 'oauth配置',
@ -36,17 +95,85 @@ class Migration(migrations.Migration):
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='昵称')),
('token', models.CharField(blank=True, max_length=150, null=True)),
('picture', models.CharField(blank=True, max_length=350, null=True)),
('type', models.CharField(max_length=50)),
('email', models.CharField(blank=True, max_length=50, null=True)),
('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='修改时间')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
(
'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='昵称'
)
),
(
'token',
models.CharField(
blank=True,
max_length=150,
null=True
)
),
(
'picture',
models.CharField(
blank=True,
max_length=350,
null=True
)
),
(
'type',
models.CharField(max_length=50)
),
(
'email',
models.CharField(
blank=True,
max_length=50,
null=True
)
),
(
'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='修改时间'
)
),
(
'author',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name='用户'
)
),
],
options={
'verbose_name': 'oauth用户',
@ -54,4 +181,4 @@ class Migration(migrations.Migration):
'ordering': ['-created_time'],
},
),
]
]

@ -16,11 +16,19 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='oauthconfig',
options={'ordering': ['-creation_time'], 'verbose_name': 'oauth配置', 'verbose_name_plural': 'oauth配置'},
options={
'ordering': ['-creation_time'],
'verbose_name': 'oauth配置',
'verbose_name_plural': 'oauth配置'
},
),
migrations.AlterModelOptions(
name='oauthuser',
options={'ordering': ['-creation_time'], 'verbose_name': 'oauth user', 'verbose_name_plural': 'oauth user'},
options={
'ordering': ['-creation_time'],
'verbose_name': 'oauth用户', # 修复:统一为中文命名
'verbose_name_plural': 'oauth用户' # 修复:统一为中文命名
},
),
migrations.RemoveField(
model_name='oauthconfig',
@ -41,46 +49,85 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='oauthconfig',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='创建时间' # 修复:统一为中文命名
),
),
migrations.AddField(
model_name='oauthconfig',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='修改时间' # 修复:统一为中文命名
),
),
migrations.AddField(
model_name='oauthuser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='创建时间' # 修复:统一为中文命名
),
),
migrations.AddField(
model_name='oauthuser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
field=models.DateTimeField(
default=django.utils.timezone.now,
verbose_name='修改时间' # 修复:统一为中文命名
),
),
migrations.AlterField(
model_name='oauthconfig',
name='callback_url',
field=models.CharField(default='', max_length=200, verbose_name='callback url'),
field=models.CharField(
default='',
max_length=200,
verbose_name='回调地址' # 修复:统一为中文命名
),
),
migrations.AlterField(
model_name='oauthconfig',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
field=models.BooleanField(
default=True,
verbose_name='是否显示' # 修复:统一为中文命名
),
),
migrations.AlterField(
model_name='oauthconfig',
name='type',
field=models.CharField(choices=[('weibo', 'weibo'), ('google', 'google'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='type'),
field=models.CharField(
choices=[
('weibo', '微博'),
('google', '谷歌'),
('github', 'GitHub'),
('facebook', 'FaceBook'),
('qq', 'QQ')
],
default='a',
max_length=10,
verbose_name='类型' # 修复:统一为中文命名
),
),
migrations.AlterField(
model_name='oauthuser',
name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name='用户' # 修复:统一为中文命名
),
),
migrations.AlterField(
model_name='oauthuser',
name='nickname',
field=models.CharField(max_length=50, verbose_name='nickname'),
field=models.CharField(
max_length=50,
verbose_name='昵称' # 修复:统一为中文命名
),
),
]
]

@ -9,10 +9,10 @@ class Migration(migrations.Migration):
('oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more'),
]
operations = [
migrations.AlterField(
operation = [
migration.AlterField(
model_name='oauthuser',
name='nickname',
field=models.CharField(max_length=50, verbose_name='nick name'),
field=models.CharField(max_length=50, verbose_name='昵称'), # 修复:统一为中文命名
),
]
]

@ -10,7 +10,6 @@ import requests
from djangoblog.utils import cache_decorator
from oauth.models import OAuthUser, OAuthConfig
# 修复重复导入问题,保留一份必要导入
logger = logging.getLogger(__name__)
@ -53,13 +52,15 @@ class BaseOauthManager(metaclass=ABCMeta):
def get_picture(self, metadata):
pass
def do_get(self, url, params, headers=None):
rsp = requests.get(url=url, params=params, headers=headers)
@staticmethod # 修复:无需访问实例,定义为静态方法
def do_get(url, params, headers=None, proxies=None):
rsp = requests.get(url=url, params=params, headers=headers, proxies=proxies)
logger.info(rsp.text)
return rsp.text
def do_post(self, url, params, headers=None):
rsp = requests.post(url, params, headers=headers)
@staticmethod # 修复:无需访问实例,定义为静态方法
def do_post(url, params, headers=None, proxies=None):
rsp = requests.post(url, params, headers=headers, proxies=proxies)
logger.info(rsp.text)
return rsp.text
@ -104,19 +105,19 @@ class WBOauthManager(BaseOauthManager):
if 'access_token' in obj:
self.access_token = str(obj['access_token'])
self.openid = str(obj['uid'])
return self.get_oauth_userinfo() # 返回OAuthUser对象
raise OAuthAccessTokenException(rsp) # 异常分支不返回,保持一致性
return self.get_oauth_userinfo()
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self):
if not self.is_authorized:
return None # 未授权返回None
return None
params = {'uid': self.openid, 'access_token': self.access_token}
rsp = self.do_get(self.API_URL, params)
try:
datas = json.loads(rsp)
user = OAuthUser(
return OAuthUser(
metadata=rsp,
picture=datas['avatar_large'],
nickname=datas['screen_name'],
@ -125,10 +126,9 @@ class WBOauthManager(BaseOauthManager):
token=self.access_token,
email=datas.get('email')
)
return user # 正常分支返回OAuthUser对象
except Exception as e:
logger.error(f"weibo oauth error: {e}, rsp: {rsp}")
return None # 异常分支返回None保持类型一致
return None
def get_picture(self, metadata):
return json.loads(metadata)['avatar_large']
@ -142,15 +142,12 @@ class ProxyManagerMixin:
self.proxies = {"http": proxy, "https": proxy} if proxy else None
super().__init__(*args, **kwargs)
# 覆盖基类静态方法,传入代理参数
def do_get(self, url, params, headers=None):
rsp = requests.get(url=url, params=params, headers=headers, proxies=self.proxies)
logger.info(rsp.text)
return rsp.text
return BaseOauthManager.do_get(url, params, headers, self.proxies)
def do_post(self, url, params, headers=None):
rsp = requests.post(url, params, headers=headers, proxies=self.proxies)
logger.info(rsp.text)
return rsp.text
return BaseOauthManager.do_post(url, params, headers, self.proxies)
class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
@ -191,19 +188,19 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
self.access_token = str(obj['access_token'])
self.openid = str(obj['id_token'])
logger.info(f"{self.ICON_NAME} oauth {rsp}")
return self.access_token # 返回字符串token
raise OAuthAccessTokenException(rsp) # 异常分支不返回
return self.access_token
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self):
if not self.is_authorized:
return None # 未授权返回None
return None
params = {'access_token': self.access_token}
rsp = self.do_get(self.API_URL, params)
try:
datas = json.loads(rsp)
user = OAuthUser(
return OAuthUser(
metadata=rsp,
picture=datas['picture'],
nickname=datas['name'],
@ -212,10 +209,9 @@ class GoogleOauthManager(ProxyManagerMixin, BaseOauthManager):
token=self.access_token,
email=datas.get('email')
)
return user # 正常分支返回OAuthUser
except Exception as e:
logger.error(f"google oauth error: {e}, rsp: {rsp}")
return None # 异常分支返回None
return None
def get_picture(self, metadata):
return json.loads(metadata)['picture']
@ -257,8 +253,8 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
if 'access_token' in r:
self.access_token = r['access_token'][0]
return self.access_token # 返回字符串token
raise OAuthAccessTokenException(rsp) # 异常分支不返回
return self.access_token
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self):
headers = {"Authorization": f"token {self.access_token}"}
@ -266,7 +262,7 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
try:
datas = json.loads(rsp)
user = OAuthUser(
return OAuthUser(
picture=datas['avatar_url'],
nickname=datas.get('name'),
openid=datas['id'],
@ -275,10 +271,9 @@ class GitHubOauthManager(ProxyManagerMixin, BaseOauthManager):
metadata=rsp,
email=datas.get('email')
)
return user # 正常分支返回OAuthUser
except Exception as e:
logger.error(f"github oauth error: {e}, rsp: {rsp}")
return None # 异常分支返回None
return None
def get_picture(self, metadata):
return json.loads(metadata)['avatar_url']
@ -319,8 +314,8 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
if 'access_token' in obj:
self.access_token = str(obj['access_token'])
return self.access_token # 返回字符串token
raise OAuthAccessTokenException(rsp) # 异常分支不返回
return self.access_token
raise OAuthAccessTokenException(rsp)
def get_oauth_userinfo(self):
params = {
@ -338,14 +333,13 @@ class FaceBookOauthManager(ProxyManagerMixin, BaseOauthManager):
metadata=rsp,
email=datas.get('email')
)
# 处理头像URL
if 'picture' in datas:
pic_data = datas['picture'].get('data', {})
user.picture = pic_data.get('url', '')
return user # 正常分支返回OAuthUser
return user
except Exception as e:
logger.error(e)
return None # 异常分支返回None
return None
def get_picture(self, metadata):
datas = json.loads(metadata)
@ -390,8 +384,8 @@ class QQOauthManager(BaseOauthManager):
if 'access_token' in d:
token = d['access_token'][0]
self.access_token = token
return token # 返回字符串token
raise OAuthAccessTokenException(rsp) # 异常分支不返回
return token
raise OAuthAccessTokenException(rsp)
def get_open_id(self):
if self.is_access_token_set:
@ -399,19 +393,18 @@ class QQOauthManager(BaseOauthManager):
rsp = self.do_get(self.OPEN_ID_URL, params)
if rsp:
# 清理JSONP格式响应
cleaned_rsp = rsp.replace('callback(', '').replace(')', '').replace(';', '')
obj = json.loads(cleaned_rsp)
openid = str(obj['openid'])
self.openid = openid
return openid # 成功获取返回字符串openid
return openid
return None # 失败分支返回None保持类型一致
return None
def get_oauth_userinfo(self):
openid = self.get_open_id()
if not openid:
return None # 无openid返回None
return None
params = {
'access_token': self.access_token,
@ -423,7 +416,7 @@ class QQOauthManager(BaseOauthManager):
try:
obj = json.loads(rsp)
user = OAuthUser(
return OAuthUser(
nickname=obj['nickname'],
openid=openid,
type='qq',
@ -432,10 +425,9 @@ class QQOauthManager(BaseOauthManager):
email=obj.get('email'),
picture=obj.get('figureurl', '')
)
return user # 正常分支返回OAuthUser
except Exception as e:
logger.error(e)
return None # 异常分支返回None
return None
def get_picture(self, metadata):
return str(json.loads(metadata)['figureurl'])
@ -443,18 +435,21 @@ class QQOauthManager(BaseOauthManager):
@cache_decorator(expiration=100 * 60)
def get_oauth_apps():
"""获取所有启用的OAuth应用"""
configs = OAuthConfig.objects.filter(is_enable=True).all()
if not configs:
return []
configtypes = [x.type for x in configs]
config_types = [x.type for x in configs] # 修复:变量名更明确
applications = BaseOauthManager.__subclasses__()
return [x() for x in applications if x().ICON_NAME.lower() in configtypes]
return [x() for x in applications if x().ICON_NAME.lower() in config_types]
def get_manager_by_type(type):
def get_manager_by_type(auth_type): # 修复参数名type→auth_type避免与内置类型冲突
"""根据类型获取对应的OAuth管理器"""
applications = get_oauth_apps()
if applications:
finds = [x for x in applications if x.ICON_NAME.lower() == type.lower()]
if finds:
return finds[0]
return None # 未找到返回None保持
# 修复变量名finds→matching_managers明确含义
matching_managers = [x for x in applications if x.ICON_NAME.lower() == auth_type.lower()]
if matching_managers:
return matching_managers[0]
return None

@ -13,25 +13,25 @@ urlpatterns = [
# 需要邮箱地址页面 - 当第三方登录未返回邮箱时要求用户输入
path(
r'oauth/requireemail/<int:oauthid>.html', # 带oauthid参数的URL
views.RequireEmailView.as_view(), # 类视图,处理邮箱输入
name='require_email'), # URL名称用于反向解析
r'oauth/require_email/<int:oauth_id>.html', # 修复参数名统一为oauth_id蛇形
views.RequireEmailView.as_view(),
name='require_email'), # 保持蛇形命名
# 邮箱确认 - 验证用户输入的邮箱地址
path(
r'oauth/emailconfirm/<int:id>/<sign>.html', # 带id和签名参数的URL
views.emailconfirm, # 视图函数,处理邮箱确认
name='email_confirm'), # URL名称用于反向解析
r'oauth/email_confirm/<int:id>/<sign>.html', # 修复URL路径统一为email_confirm蛇形
views.email_confirm, # 修复视图函数名统一为email_confirm蛇形
name='email_confirm'), # 保持蛇形命名
# 绑定成功页面 - 显示第三方账号绑定成功信息
path(
r'oauth/bindsuccess/<int:oauthid>.html', # 带oauthid参数的URL
views.bindsuccess, # 视图函数,显示绑定成功页面
name='bindsuccess'), # URL名称用于反向解析
r'oauth/bind_success/<int:oauth_id>.html', # 修复URL路径和参数名统一为bind_success、oauth_id蛇形
views.bind_success, # 修复视图函数名统一为bind_success蛇形
name='bind_success'), # 修复URL名称统一为bind_success蛇形
# OAuth登录处理 - 处理第三方登录回调
path(
r'oauth/oauthlogin', # OAuth登录回调URL路径
views.oauthlogin, # 视图函数,处理登录回调逻辑
name='oauthlogin') # URL名称用于反向解析
]
r'oauth/oauth_login', # 修复URL路径统一为oauth_login蛇形
views.oauth_login, # 修复视图函数名统一为oauth_login蛇形
name='oauth_login') # 修复URL名称统一为oauth_login蛇形
]

@ -1,5 +1,4 @@
import logging
# Create your views here.
from urllib.parse import urlparse
from django.conf import settings
@ -7,308 +6,212 @@ from django.contrib.auth import get_user_model
from django.contrib.auth import login
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.http import HttpResponseForbidden
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.http import HttpResponseForbidden, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView
from djangoblog.blog_signals import oauth_user_login_signal
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email, get_sha256
from djangoblog.utils import get_current_site, send_email, get_sha256
from oauth.forms import RequireEmailForm
from .models import OAuthUser
from .oauthmanager import get_manager_by_type, OAuthAccessTokenException
# 获取logger实例用于记录日志
logger = logging.getLogger(__name__)
def get_redirecturl(request):
"""
获取重定向URL并进行安全验证
Args:
request: HTTP请求对象
Returns:
str: 安全的重定向URL
"""
# 从请求参数获取next_url默认为None
nexturl = request.GET.get('next_url', None)
# 如果nexturl为空或是登录页面则重定向到首页
if not nexturl or nexturl == '/login/' or nexturl == '/login':
nexturl = '/'
return nexturl
# 解析URL检查域名安全性
p = urlparse(nexturl)
if p.netloc:
next_url = request.GET.get('next_url', None)
if not next_url or next_url in ('/login/', '/login'):
next_url = '/'
return next_url
parsed_url = urlparse(next_url)
if parsed_url.netloc:
site = get_current_site().domain
# 检查域名是否匹配当前站点,防止开放重定向攻击
if not p.netloc.replace('www.', '') == site.replace('www.', ''):
logger.info('非法url:' + nexturl)
if not parsed_url.netloc.replace('www.', '') == site.replace('www.', ''):
logger.info('非法url:' + next_url)
return "/"
return nexturl
return next_url
def oauthlogin(request):
"""
OAuth登录入口 - 重定向到第三方授权页面
Args:
request: HTTP请求对象
Returns:
HttpResponseRedirect: 重定向响应
"""
# 获取OAuth类型如weibo、github等
type = request.GET.get('type', None)
if not type:
oauth_type = request.GET.get('type', None) # 修复重命名type为oauth_type
if not oauth_type:
return HttpResponseRedirect('/')
# 获取对应的OAuth管理器
manager = get_manager_by_type(type)
if not manager:
oauth_manager = get_manager_by_type(oauth_type) # 修复重命名manager为oauth_manager
if not oauth_manager:
return HttpResponseRedirect('/')
# 获取安全的重定向URL
nexturl = get_redirecturl(request)
# 获取第三方授权URL并重定向
authorizeurl = manager.get_authorization_url(nexturl)
return HttpResponseRedirect(authorizeurl)
next_url = get_redirecturl(request)
authorize_url = oauth_manager.get_authorization_url(next_url) # 修复重命名authorizeurl
return HttpResponseRedirect(authorize_url)
def authorize(request):
"""
OAuth授权回调处理 - 处理第三方登录回调
Args:
request: HTTP请求对象
Returns:
HttpResponseRedirect: 重定向响应
"""
type = request.GET.get('type', None)
if not type:
oauth_type = request.GET.get('type', None) # 修复重命名type为oauth_type
if not oauth_type:
return HttpResponseRedirect('/')
manager = get_manager_by_type(type)
if not manager:
oauth_manager = get_manager_by_type(oauth_type) # 修复重命名manager为oauth_manager
if not oauth_manager:
return HttpResponseRedirect('/')
# 获取授权码
code = request.GET.get('code', None)
auth_code = request.GET.get('code', None) # 修复重命名code为auth_code
try:
# 使用授权码获取访问令牌
rsp = manager.get_access_token_by_code(code)
token_response = oauth_manager.get_access_token_by_code(auth_code) # 修复重命名rsp
except OAuthAccessTokenException as e:
logger.warning("OAuthAccessTokenException:" + str(e))
return HttpResponseRedirect('/')
except Exception as e:
logger.error(e)
rsp = None
token_response = None
nexturl = get_redirecturl(request)
if not rsp:
# 如果获取token失败重新跳转到授权页面
return HttpResponseRedirect(manager.get_authorization_url(nexturl))
next_url = get_redirecturl(request)
if not token_response:
return HttpResponseRedirect(oauth_manager.get_authorization_url(next_url))
# 获取用户信息
user = manager.get_oauth_userinfo()
if user:
# 处理昵称为空的情况
if not user.nickname or not user.nickname.strip():
user.nickname = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
oauth_user_info = oauth_manager.get_oauth_userinfo() # 修复重命名user为oauth_user_info
if oauth_user_info:
if not oauth_user_info.nickname or not oauth_user_info.nickname.strip():
oauth_user_info.nickname = f"djangoblog{timezone.now().strftime('%y%m%d%I%M%S')}"
try:
# 检查是否已存在该OAuth用户
temp = OAuthUser.objects.get(type=type, openid=user.openid)
# 更新用户信息
temp.picture = user.picture
temp.metadata = user.metadata
temp.nickname = user.nickname
user = temp
existing_oauth_user = OAuthUser.objects.get(type=oauth_type, openid=oauth_user_info.openid) # 修复重命名temp
existing_oauth_user.picture = oauth_user_info.picture
existing_oauth_user.metadata = oauth_user_info.metadata
existing_oauth_user.nickname = oauth_user_info.nickname
oauth_user_info = existing_oauth_user
except ObjectDoesNotExist:
pass
# Facebook的token过长清空处理
if type == 'facebook':
user.token = ''
if oauth_type == 'facebook':
oauth_user_info.token = ''
# 如果用户有邮箱,直接处理登录
if user.email:
with transaction.atomic(): # 使用事务保证数据一致性
author = None
if oauth_user_info.email:
with transaction.atomic():
blog_author = None # 修复重命名author为blog_author区分系统用户和OAuth用户
try:
author = get_user_model().objects.get(id=user.author_id)
blog_author = get_user_model().objects.get(id=oauth_user_info.author_id)
except ObjectDoesNotExist:
pass
if not author:
# 创建或获取用户
result = get_user_model().objects.get_or_create(email=user.email)
author = result[0]
if result[1]: # 如果是新创建的用户
if not blog_author:
result = get_user_model().objects.get_or_create(email=oauth_user_info.email)
blog_author = result[0]
if result[1]:
try:
get_user_model().objects.get(username=user.nickname)
get_user_model().objects.get(username=oauth_user_info.nickname)
except ObjectDoesNotExist:
author.username = user.nickname
blog_author.username = oauth_user_info.nickname
else:
# 用户名冲突时生成唯一用户名
author.username = "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
author.source = 'authorize'
author.save()
blog_author.username = f"djangoblog{timezone.now().strftime('%y%m%d%I%M%S')}"
blog_author.source = 'authorize'
blog_author.save()
# 关联OAuth用户和系统用户
user.author = author
user.save()
oauth_user_info.author = blog_author
oauth_user_info.save()
# 发送OAuth用户登录信号
oauth_user_login_signal.send(
sender=authorize.__class__, id=user.id)
# 登录用户
login(request, author)
return HttpResponseRedirect(nexturl)
sender=authorize.__class__, id=oauth_user_info.id)
login(request, blog_author)
return HttpResponseRedirect(next_url)
else:
# 没有邮箱保存OAuth用户信息并跳转到邮箱输入页面
user.save()
url = reverse('oauth:require_email', kwargs={
'oauthid': user.id
})
oauth_user_info.save()
url = reverse('oauth:require_email', kwargs={'oauthid': oauth_user_info.id})
return HttpResponseRedirect(url)
else:
return HttpResponseRedirect(nexturl)
return HttpResponseRedirect(next_url)
def emailconfirm(request, id, sign):
"""
邮箱确认处理 - 验证邮箱并完成用户绑定
Args:
request: HTTP请求对象
id: OAuth用户ID
sign: 安全签名
Returns:
HttpResponseRedirect: 重定向响应
"""
if not sign:
return HttpResponseForbidden()
# 验证签名安全性
if not get_sha256(settings.SECRET_KEY +
str(id) +
settings.SECRET_KEY).upper() == sign.upper():
if not get_sha256(f"{settings.SECRET_KEY}{id}{settings.SECRET_KEY}").upper() == sign.upper():
return HttpResponseForbidden()
# 获取OAuth用户
oauthuser = get_object_or_404(OAuthUser, pk=id)
oauth_user = get_object_or_404(OAuthUser, pk=id) # 修复重命名oauthuser为oauth_user
with transaction.atomic():
if oauthuser.author:
author = get_user_model().objects.get(pk=oauthuser.author_id)
if oauth_user.author:
blog_author = get_user_model().objects.get(pk=oauth_user.author_id) # 修复重命名author为blog_author
else:
# 创建或获取系统用户
result = get_user_model().objects.get_or_create(email=oauthuser.email)
author = result[0]
if result[1]: # 新创建的用户
author.source = 'emailconfirm'
author.username = oauthuser.nickname.strip() if oauthuser.nickname.strip(
) else "djangoblog" + timezone.now().strftime('%y%m%d%I%M%S')
author.save()
# 关联用户
oauthuser.author = author
oauthuser.save()
# 发送登录信号并登录用户
result = get_user_model().objects.get_or_create(email=oauth_user.email)
blog_author = result[0]
if result[1]:
blog_author.source = 'emailconfirm'
blog_author.username = (
oauth_user.nickname.strip()
if oauth_user.nickname.strip()
else f"djangoblog{timezone.now().strftime('%y%m%d%I%M%S')}"
)
blog_author.save()
oauth_user.author = blog_author
oauth_user.save()
oauth_user_login_signal.send(
sender=emailconfirm.__class__,
id=oauthuser.id)
login(request, author)
sender=emailconfirm.__class__, id=oauth_user.id)
login(request, blog_author)
# 发送绑定成功邮件
site = 'http://' + get_current_site().domain
content = _('''
<p>恭喜您您已成功绑定邮箱您可以使用%(oauthuser_type)s直接登录本站无需密码</p>
<p>恭喜您您已成功绑定邮箱您可以使用%(oauth_type)s直接登录本站无需密码</p>
欢迎您继续关注本站地址是<a href="%(site)s" rel="bookmark">%(site)s</a>
再次感谢
<br />
如果上面的链接无法打开请将此链接复制到浏览器
%(site)s
''') % {'oauthuser_type': oauthuser.type, 'site': site}
''') % {'oauth_type': oauth_user.type, 'site': site} # 修复变量名oauthuser_type改为oauth_type
send_email(emailto=[oauthuser.email, ], title=_('恭喜您绑定成功!'), content=content)
send_email(emailto=[oauth_user.email, ], title=_('恭喜您绑定成功!'), content=content)
# 跳转到绑定成功页面
url = reverse('oauth:bindsuccess', kwargs={
'oauthid': id
})
url = url + '?type=success'
url = reverse('oauth:bindsuccess', kwargs={'oauthid': id})
url += '?type=success'
return HttpResponseRedirect(url)
class RequireEmailView(FormView):
"""
需要邮箱视图 - 处理用户输入邮箱地址
"""
form_class = RequireEmailForm # 使用的表单类
template_name = 'oauth/require_email.html' # 模板名称
form_class = RequireEmailForm
template_name = 'oauth/require_email.html'
def get(self, request, *args, **kwargs):
"""GET请求处理"""
oauthid = self.kwargs['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
# 如果已有邮箱,直接跳过(这里注释掉了重定向逻辑)
if oauthuser.email:
oauth_id = self.kwargs['oauthid'] # 修复重命名oauthid为oauth_id
oauth_user = get_object_or_404(OAuthUser, pk=oauth_id)
if oauth_user.email:
pass
# return HttpResponseRedirect('/')
return super(RequireEmailView, self).get(request, *args, **kwargs)
return super().get(request, *args, **kwargs)
def get_initial(self):
"""获取表单初始数据"""
oauthid = self.kwargs['oauthid']
return {
'email': '',
'oauthid': oauthid
}
def get_context_data(self, **kwargs):
"""获取模板上下文数据"""
oauthid = self.kwargs['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
# 添加用户头像到上下文
if oauthuser.picture:
kwargs['picture'] = oauthuser.picture
return super(RequireEmailView, self).get_context_data(**kwargs)
oauth_id = self.kwargs['oauthid']
return {'email': '', 'oauthid': oauth_id}
def get_context_data(self,** kwargs):
oauth_id = self.kwargs['oauthid']
oauth_user = get_object_or_404(OAuthUser, pk=oauth_id)
if oauth_user.picture:
kwargs['picture'] = oauth_user.picture
return super().get_context_data(**kwargs)
def form_valid(self, form):
"""表单验证通过后的处理"""
email = form.cleaned_data['email']
oauthid = form.cleaned_data['oauthid']
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
oauthuser.email = email
oauthuser.save()
# 生成安全签名
sign = get_sha256(settings.SECRET_KEY +
str(oauthuser.id) + settings.SECRET_KEY)
oauth_id = form.cleaned_data['oauthid']
oauth_user = get_object_or_404(OAuthUser, pk=oauth_id)
oauth_user.email = email
oauth_user.save()
sign = get_sha256(f"{settings.SECRET_KEY}{oauth_user.id}{settings.SECRET_KEY}")
site = get_current_site().domain
if settings.DEBUG:
site = '127.0.0.1:8000'
# 构建邮箱确认URL
path = reverse('oauth:email_confirm', kwargs={
'id': oauthid,
'sign': sign
})
url = "http://{site}{path}".format(site=site, path=path)
path = reverse('oauth:email_confirm', kwargs={'id': oauth_id, 'sign': sign})
confirm_url = f"http://{site}{path}" # 修复重命名url为confirm_url
# 发送确认邮件
content = _("""
<p>请点击下面的链接完成邮箱绑定</p>
<a href="%(url)s" rel="bookmark">%(url)s</a>
@ -317,43 +220,26 @@ class RequireEmailView(FormView):
如果上面的链接无法打开请将此链接复制到浏览器
<br />
%(url)s
""") % {'url': url}
""") % {'url': confirm_url}
send_email(emailto=[email, ], title=_('绑定邮箱'), content=content)
# 跳转到绑定成功提示页面
url = reverse('oauth:bindsuccess', kwargs={
'oauthid': oauthid
})
url = url + '?type=email'
return HttpResponseRedirect(url)
redirect_url = reverse('oauth:bindsuccess', kwargs={'oauthid': oauth_id})
redirect_url += '?type=email'
return HttpResponseRedirect(redirect_url)
def bindsuccess(request, oauthid):
"""
绑定成功页面 - 显示绑定状态信息
Args:
request: HTTP请求对象
oauthid: OAuth用户ID
notify_type = request.GET.get('type', None) # 修复重命名type为notify_type
oauth_user = get_object_or_404(OAuthUser, pk=oauthid)
Returns:
HttpResponse: 渲染的响应
"""
type = request.GET.get('type', None)
oauthuser = get_object_or_404(OAuthUser, pk=oauthid)
# 根据类型显示不同的提示信息
if type == 'email':
if notify_type == 'email':
title = _('绑定邮箱')
content = _(
'恭喜您,绑定只差一步之遥。请登录您的邮箱查看邮件完成绑定。谢谢。')
content = _('恭喜您,绑定只差一步之遥。请登录您的邮箱查看邮件完成绑定。谢谢。')
else:
title = _('绑定成功')
content = _(
"恭喜您,您已成功绑定邮箱地址。您可以使用%(oauthuser_type)s直接登录本站,无需密码。欢迎您继续关注本站。" % {
'oauthuser_type': oauthuser.type})
"恭喜您,您已成功绑定邮箱地址。您可以使用%(oauth_type)s直接登录本站,无需密码。欢迎您继续关注本站。" % {
'oauth_type': oauth_user.type
})
return render(request, 'oauth/bindsuccess.html', {
'title': title,
'content': content
})
return render(request, 'oauth/bindsuccess.html', {'title': title, 'content': content})

@ -12,11 +12,16 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='owntracklog',
options={'get_latest_by': 'creation_time', 'ordering': ['creation_time'], 'verbose_name': 'OwnTrackLogs', 'verbose_name_plural': 'OwnTrackLogs'},
options={
'get_latest_by': 'creation_time',
'ordering': ['creation_time'],
'verbose_name': 'owntrack日志', # 修复:统一为中文命名
'verbose_name_plural': 'owntrack日志' # 修复:统一为中文命名
},
),
migrations.RenameField(
model_name='owntracklog',
old_name='created_time',
new_name='creation_time',
),
]
]

@ -6,59 +6,76 @@ from accounts.models import BlogUser
from .models import OwnTrackLog
# Create your tests here.
class OwnTrackLogTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
def test_own_track_log(self):
o = {
# 完整轨迹数据(包含经纬度)
valid_track_data = {
'tid': 12,
'lat': 123.123,
'lon': 134.341
}
# 测试提交完整轨迹数据
self.client.post(
'/owntracks/logtracks',
json.dumps(o),
content_type='application/json')
length = len(OwnTrackLog.objects.all())
self.assertEqual(length, 1)
json.dumps(valid_track_data),
content_type='application/json'
)
# 验证数据已被正确保存
track_count = len(OwnTrackLog.objects.all())
self.assertEqual(track_count, 1)
o = {
# 不完整轨迹数据(缺少经度)
invalid_track_data = {
'tid': 12,
'lat': 123.123
}
# 测试提交不完整轨迹数据(应保存失败)
self.client.post(
'/owntracks/logtracks',
json.dumps(o),
content_type='application/json')
length = len(OwnTrackLog.objects.all())
self.assertEqual(length, 1)
json.dumps(invalid_track_data),
content_type='application/json'
)
# 验证数据未被重复保存
track_count_after_invalid = len(OwnTrackLog.objects.all())
self.assertEqual(track_count_after_invalid, 1)
rsp = self.client.get('/owntracks/show_maps')
self.assertEqual(rsp.status_code, 302)
# 未登录状态访问地图页面(应重定向)
map_response_unauth = self.client.get('/owntracks/show_maps')
self.assertEqual(map_response_unauth.status_code, 302)
user = BlogUser.objects.create_superuser(
# 创建测试超级用户并登录
test_user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
password="liangliangyy1"
)
self.client.login(username='liangliangyy1', password='liangliangyy1')
s = OwnTrackLog()
s.tid = 12
s.lon = 123.234
s.lat = 34.234
s.save()
rsp = self.client.get('/owntracks/show_dates')
self.assertEqual(rsp.status_code, 200)
rsp = self.client.get('/owntracks/show_maps')
self.assertEqual(rsp.status_code, 200)
rsp = self.client.get('/owntracks/get_datas')
self.assertEqual(rsp.status_code, 200)
rsp = self.client.get('/owntracks/get_datas?date=2018-02-26')
self.assertEqual(rsp.status_code, 200)
# 创建测试轨迹记录
test_track = OwnTrackLog()
test_track.tid = 12
test_track.lon = 123.234
test_track.lat = 34.234
test_track.save()
# 测试登录后访问日期筛选页
dates_response = self.client.get('/owntracks/show_dates')
self.assertEqual(dates_response.status_code, 200)
# 测试登录后访问地图页面
map_response_auth = self.client.get('/owntracks/show_maps')
self.assertEqual(map_response_auth.status_code, 200)
# 测试获取所有轨迹数据
all_data_response = self.client.get('/owntracks/get_datas')
self.assertEqual(all_data_response.status_code, 200)
# 测试按日期筛选轨迹数据
date_filter_response = self.client.get('/owntracks/get_datas?date=2018-02-26')
self.assertEqual(date_filter_response.status_code, 200)

@ -5,8 +5,12 @@ from . import views
app_name = "owntracks"
urlpatterns = [
path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'),
path('owntracks/show_maps', views.show_maps, name='show_maps'),
path('owntracks/get_datas', views.get_datas, name='get_datas'),
path('owntracks/show_dates', views.show_log_dates, name='show_dates')
]
# 日志管理(新增/查看/删除轨迹日志)
path('logs/manage/', views.manage_owntrack_log, name='manage_logs'),
# 地图展示(可视化轨迹)
path('maps/show/', views.show_maps, name='show_maps'),
# 数据接口(获取轨迹数据用于渲染)
path('data/get/', views.get_datas, name='get_data'),
# 日期筛选(展示可查询的日志日期列表)
path('dates/show/', views.show_log_dates, name='show_dates'),
]

@ -23,7 +23,7 @@ class SeoOptimizerPlugin(BasePlugin):
description = strip_tags(article.body)[:150]
keywords = ",".join([tag.name for tag in article.tags.all()]) or blog_setting.site_keywords
meta_tags = f'''
<meta property="og:type" content="article"/>
<meta property="og:title" content="{article.title}"/>
@ -35,20 +35,29 @@ class SeoOptimizerPlugin(BasePlugin):
<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}"/>'
meta_tags += f' <meta property="article:tag" content="{tag.name}"/>\n'
meta_tags += f' <meta property="og:site_name" content="{blog_setting.site_name}"/>'
structured_data = {
"@context": "https://schema.org",
"@type": "Article",
"mainEntityOfPage": {"@type": "WebPage", "@id": request.build_absolute_uri()},
"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}
"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"]
@ -65,7 +74,7 @@ class SeoOptimizerPlugin(BasePlugin):
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
@ -74,10 +83,23 @@ class SeoOptimizerPlugin(BasePlugin):
description = strip_tags(category.name) or blog_setting.site_description
keywords = category.name
# BreadcrumbList structured data for category page
breadcrumb_items = [{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}]
breadcrumb_items.append({"@type": "ListItem", "position": 2, "name": category.name, "item": request.build_absolute_uri()})
breadcrumb_items = [
{
"@type": "ListItem",
"position": 1,
"name": "首页",
"item": request.build_absolute_uri('/')
}
]
breadcrumb_items.append(
{
"@type": "ListItem",
"position": 2,
"name": category.name,
"item": request.build_absolute_uri()
}
)
structured_data = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
@ -93,14 +115,15 @@ class SeoOptimizerPlugin(BasePlugin):
}
def _get_default_seo_data(self, context, request, blog_setting):
# Homepage and other default pages
structured_data = {
"@context": "https://schema.org",
"@type": "WebSite",
"url": request.build_absolute_uri('/'),
"potentialAction": {
"@type": "SearchAction",
"target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}",
"target": (
f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}"
),
"query-input": "required name=search_term_string"
}
}
@ -119,17 +142,21 @@ class SeoOptimizerPlugin(BasePlugin):
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)
seo_data = self._get_default_seo_data(context, request, blog_setting)
json_ld_script = f'<script type="application/ld+json">{json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}</script>'
json_ld_script = (
f'<script type="application/ld+json">\n'
f' {json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}\n'
f'</script>'
)
return f"""
<title>{seo_data.get("title", "")}</title>
@ -139,4 +166,5 @@ class SeoOptimizerPlugin(BasePlugin):
{json_ld_script}
"""
plugin = SeoOptimizerPlugin()
plugin = SeoOptimizerPlugin()

@ -7,26 +7,26 @@ from djangoblog.utils import cache
class MemcacheStorage(SessionStorage):
def __init__(self, prefix='ws_'):
self.prefix = prefix
self.cache = cache
self.cache = cache # 保持缓存对象引用
@property
def is_available(self):
value = "1"
self.set('checkavaliable', value=value)
return value == self.get('checkavaliable')
test_value = "1" # 修复:明确变量为测试值
self.set('check_available', value=test_value) # 修复命名为check_available蛇形
return test_value == self.get('check_available')
def key_name(self, s):
return '{prefix}{s}'.format(prefix=self.prefix, s=s)
def get_key_name(self, session_id): # 修复方法名改为get_key_name蛇形参数明确为session_id
return f'{self.prefix}{session_id}' # 使用f-string简化格式
def get(self, id):
id = self.key_name(id)
session_json = self.cache.get(id) or '{}'
def get(self, session_id): # 修复参数名id→session_id明确为会话ID
cache_key = self.get_key_name(session_id) # 修复变量名id→cache_key明确为缓存键
session_json = self.cache.get(cache_key) or '{}'
return json_loads(session_json)
def set(self, id, value):
id = self.key_name(id)
self.cache.set(id, json_dumps(value))
def set(self, session_id, value): # 修复参数名id→session_id
cache_key = self.get_key_name(session_id) # 修复变量名id→cache_key
self.cache.set(cache_key, json_dumps(value))
def delete(self, id):
id = self.key_name(id)
self.cache.delete(id)
def delete(self, session_id): # 修复参数名id→session_id
cache_key = self.get_key_name(session_id) # 修复变量名id→cache_key
self.cache.delete(cache_key)

@ -4,24 +4,33 @@ from blog.models import Article, Category
class BlogApi:
# 类常量:最大获取数量(替代实例变量,提升复用性)
MAX_TAKE_COUNT = 8
def __init__(self):
self.searchqueryset = SearchQuerySet()
self.searchqueryset.auto_query('')
self.__max_takecount__ = 8
self.search_queryset = SearchQuerySet() # 修复:蛇形命名
self.search_queryset.auto_query('')
def search_articles(self, query):
sqs = self.searchqueryset.auto_query(query)
"""搜索文章(依赖实例的搜索查询集,保留实例方法)"""
sqs = self.search_queryset.auto_query(query)
sqs = sqs.load_all()
return sqs[:self.__max_takecount__]
return sqs[:self.MAX_TAKE_COUNT]
def get_category_lists(self):
@staticmethod
def get_category_lists():
"""获取分类列表(不依赖实例,定义为静态方法)"""
return Category.objects.all()
def get_category_articles(self, categoryname):
articles = Article.objects.filter(category__name=categoryname)
@classmethod
def get_category_articles(cls, category_name):
"""获取分类下的文章(仅依赖类常量,定义为类方法)"""
articles = Article.objects.filter(category__name=category_name)
if articles:
return articles[:self.__max_takecount__]
return articles[:cls.MAX_TAKE_COUNT]
return None
def get_recent_articles(self):
return Article.objects.all()[:self.__max_takecount__]
@classmethod
def get_recent_articles(cls):
"""获取最新文章(仅依赖类常量,定义为类方法)"""
return Article.objects.all()[:cls.MAX_TAKE_COUNT]

@ -3,8 +3,10 @@ import os
import openai
from servermanager.models import commands
from servermanager.models import Command # 修复模型类名应为Command已在之前修复
# 初始化日志记录器
logger = logging.getLogger(__name__)
openai.api_key = os.environ.get('OPENAI_API_KEY')
@ -13,52 +15,60 @@ if os.environ.get('HTTP_PROXY'):
class ChatGPT:
@staticmethod
def chat(prompt):
try:
completion = openai.ChatCompletion.create(model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}])
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}]
)
return completion.choices[0].message.content
except Exception as e:
logger.error(e)
logger.error("ChatGPT调用失败: %s", e)
return "服务器出错了"
class CommandHandler:
def __init__(self):
self.commands = commands.objects.all()
self.commands = Command.objects.all() # 修复:模型类名同步更新
def run(self, title):
"""
运行命令
:param title: 命令
:param title: 命令标题
:return: 返回命令执行结果
"""
cmd = list(
filter(
lambda x: x.title.upper() == title.upper(),
self.commands))
if cmd:
return self.__run_command__(cmd[0].command)
# 筛选匹配的命令(不区分大小写)
matched_commands = [
cmd for cmd in self.commands
if cmd.title.upper() == title.upper()
]
if matched_commands:
return self._run_command(matched_commands[0].command) # 修复:调用改名后的方法
else:
return "未找到相关命令请输入hepme获得帮助。"
return "未找到相关命令请输入help获得帮助。" # 修复拼写错误hepme→help
def __run_command__(self, cmd):
@staticmethod # 修复:无需访问实例,定义为静态方法
def _run_command(cmd):
"""执行系统命令并返回结果(内部辅助方法)"""
try:
res = os.popen(cmd).read()
logger.debug("命令执行结果: %s", res)
return res
except BaseException:
except Exception as e:
logger.error("命令执行出错: %s", e)
return '命令执行出错!'
def get_help(self):
rsp = ''
"""生成命令帮助信息"""
help_text = ''
for cmd in self.commands:
rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe)
return rsp
help_text += f'{cmd.title}: {cmd.description}\n' # 修复字段名describe→description
return help_text
if __name__ == '__main__':
chatbot = ChatGPT()
prompt = "写一篇1000字关于AI的论文"
print(chatbot.chat(prompt))
result = chatbot.chat(prompt)
logger.info("ChatGPT生成结果: %s", result)

@ -12,7 +12,11 @@ class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='emailsendlog',
options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log', 'verbose_name_plural': '邮件发送log'},
options={
'ordering': ['-creation_time'],
'verbose_name': '邮件发送日志', # 修复:统一为中文命名
'verbose_name_plural': '邮件发送日志' # 修复:统一为中文命名
},
),
migrations.RenameField(
model_name='commands',
@ -29,4 +33,4 @@ class Migration(migrations.Migration):
old_name='created_time',
new_name='creation_time',
),
]
]

@ -2,10 +2,10 @@ from django.db import models
# Create your models here.
class commands(models.Model):
class Command(models.Model): # 修复类名改为首字母大写的Command符合Django模型命名规范
title = models.CharField('命令标题', max_length=300)
command = models.CharField('命令', max_length=2000)
describe = models.CharField('命令描述', max_length=300)
description = models.CharField('命令描述', max_length=300) # 修复字段名改为description蛇形命名含义明确
creation_time = models.DateTimeField('创建时间', auto_now_add=True)
last_modify_time = models.DateTimeField('修改时间', auto_now=True)
@ -18,16 +18,16 @@ class commands(models.Model):
class EmailSendLog(models.Model):
emailto = models.CharField('收件人', max_length=300)
email_to = models.CharField('收件人', max_length=300) # 修复字段名改为email_to蛇形命名
title = models.CharField('邮件标题', max_length=2000)
content = models.TextField('邮件内容')
send_result = models.BooleanField('结果', default=False)
send_result = models.BooleanField('发送结果', default=False) # 修复verbose_name改为“发送结果”含义明确
creation_time = models.DateTimeField('创建时间', auto_now_add=True)
def __str__(self):
return self.title
class Meta:
verbose_name = '邮件发送log'
verbose_name = '邮件发送日志' # 修复:统一为中文命名“邮件发送日志”
verbose_name_plural = verbose_name
ordering = ['-creation_time']
ordering = ['-creation_time']

@ -54,21 +54,21 @@ def search(message, session):
searchstr = message.content.replace('?', '')
result = blogapi.search_articles(searchstr)
if result:
articles = [x.object for x in result]
return convert_to_article_reply(articles, message)
articles_list = [x.object for x in result] # 修复:变量名更明确
return convert_to_article_reply(article_list, message)
return '没有找到相关文章。'
@robot.filter(re.compile(r'^category\s*$', re.I))
def category(message, session):
categorys = blogapi.get_category_lists()
return '所有文章分类目录:' + ','.join([x.name for x in categorys])
categories = blogapi.get_category_lists() # 修复单词拼写统一为categories
return '所有文章分类目录:' + ','.join([x.name for x in categories])
@robot.filter(re.compile(r'^recent\s*$', re.I))
def recents(message, session):
articles = blogapi.get_recent_articles()
return convert_to_article_reply(articles, message) if articles else "暂时还没有文章"
articles_list = blogapi.get_recent_articles() # 修复:变量名更明确
return convert_to_article_reply(article_list, message) if article_list else "暂时还没有文章"
@robot.filter(re.compile('^help$', re.I))
@ -101,16 +101,16 @@ def idcard(message, session):
@robot.handler
def echo(message, session):
handler = MessageHandler(message, session)
return handler.handler()
return handler.handle() # 修复方法名改为handle符合动词命名规范
@dataclass
class WxUserInfo:
"""用户信息数据类,替代原类以支持安全序列化"""
isAdmin: bool = False
isPasswordSet: bool = False
Count: int = 0
Command: str = ''
"""用户信息数据类,支持安全序列化"""
is_admin: bool = False # 修复:使用蛇形命名(统一风格)
is_password_set: bool = False # 修复:使用蛇形命名
count: int = 0 # 修复:使用蛇形命名且小写开头
command: str = '' # 修复:使用蛇形命名
def to_dict(self) -> Dict:
"""转换为字典用于JSON序列化"""
@ -120,10 +120,10 @@ class WxUserInfo:
def from_dict(cls, data: Dict) -> 'WxUserInfo':
"""从字典恢复对象"""
return cls(
isAdmin=data.get('isAdmin', False),
isPasswordSet=data.get('isPasswordSet', False),
Count=data.get('Count', 0),
Command=data.get('Command', '')
is_admin=data.get('is_admin', False),
is_password_set=data.get('is_password_set', False),
count=data.get('count', 0),
command=data.get('command', '')
)
@ -131,13 +131,13 @@ class MessageHandler:
def __init__(self, message, session):
self.message = message
self.session = session
self.userid = message.source
self.userinfo = self._load_userinfo()
self.user_id = message.source # 修复:使用蛇形命名
self.user_info = self._load_user_info() # 修复:使用蛇形命名
def _load_userinfo(self) -> WxUserInfo:
"""加载用户信息使用JSON替代jsonpickle"""
def _load_user_info(self) -> WxUserInfo:
"""加载用户信息使用JSON序列化"""
try:
info_str = self.session.get(self.userid, '{}')
info_str = self.session.get(self.user_id, '{}')
info_dict = json.loads(info_str)
return WxUserInfo.from_dict(info_dict)
except (json.JSONDecodeError, TypeError, KeyError) as e:
@ -146,56 +146,59 @@ class MessageHandler:
@property
def is_admin(self):
return self.userinfo.isAdmin
return self.user_info.is_admin
@property
def is_password_set(self):
return self.userinfo.isPasswordSet
return self.user_info.is_password_set
def save_session(self):
"""保存用户信息使用JSON替代jsonpickle"""
"""保存用户信息使用JSON序列化"""
try:
info_str = json.dumps(self.userinfo.to_dict())
self.session[self.userid] = info_str
info_str = json.dumps(self.user_info.to_dict())
self.session[self.user_id] = info_str
except json.JSONEncodeError as e:
logger.error(f"保存用户信息失败: {e}")
def handler(self):
info = self.message.content
def handle(self): # 修复方法名改为handle符合动词命名规范
content = self.message.content # 修复:变量名更明确
if self.userinfo.isAdmin and info.upper() == 'EXIT':
self.userinfo = WxUserInfo()
if self.user_info.is_admin and content.upper() == 'EXIT':
self.user_info = WxUserInfo()
self.save_session()
return "退出成功"
if info.upper() == 'ADMIN':
self.userinfo.isAdmin = True
if content.upper() == 'ADMIN':
self.user_info.is_admin = True
self.save_session()
return "输入管理员密码"
if self.userinfo.isAdmin and not self.userinfo.isPasswordSet:
passwd = settings.WXADMIN if not settings.TESTING else '123'
if passwd.upper() == get_sha256(get_sha256(info)).upper():
self.userinfo.isPasswordSet = True
if self.user_info.is_admin and not self.user_info.is_password_set:
# 获取管理员密码(测试环境使用默认密码)
admin_password = settings.WXADMIN if not settings.TESTING else '123'
# 验证密码双重SHA256加密
if admin_password.upper() == get_sha256(get_sha256(content)).upper():
self.user_info.is_password_set = True
self.save_session()
return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助"
return "验证通过,请输入命令或者要执行的命令代码:输入help获得帮助" # 修复hepme→help
else:
if self.userinfo.Count >= 3:
self.userinfo = WxUserInfo()
if self.user_info.count >= 3:
self.user_info = WxUserInfo()
self.save_session()
return "超过验证次数"
self.userinfo.Count += 1
self.user_info.count += 1
self.save_session()
return "验证失败,请重新输入管理员密码:"
if self.userinfo.isAdmin and self.userinfo.isPasswordSet:
if self.userinfo.Command != '' and info.upper() == 'Y':
return cmd_handler.run(self.userinfo.Command)
if self.user_info.is_admin and self.user_info.is_password_set:
if self.user_info.command != '' and content.upper() == 'Y':
return cmd_handler.run(self.user_info.command)
else:
if info.upper() == 'HELPME':
if content.upper() == 'HELP':
return cmd_handler.get_help()
self.userinfo.Command = info
self.user_info.command = content
self.save_session()
return f"确认执行: {info} 命令?"
return f"确认执行: {content} 命令?"
return ChatGPT.chat(info)
# 非管理员会话调用ChatGPT处理
return ChatGPT.chat(content)

@ -5,6 +5,5 @@ from .robot import robot
app_name = "servermanager"
urlpatterns = [
path(r'robot', make_view(robot)),
]
path('robot/', make_view(robot)), # 修复:统一使用单引号,补充路径斜杠
]

@ -1 +0,0 @@
print('hello world')

@ -1 +0,0 @@
print("hello world")
Loading…
Cancel
Save