Compare commits

..

3 Commits

@ -5,6 +5,8 @@ from abc import abstractmethod
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.dispatch import receiver
from django.db.models.signals import post_save
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@ -177,39 +179,64 @@ class Article(BaseModel):
return ""
#zbw 模块功能定义文章分类模型Category继承自BaseModel用于存储博客系统中的文章分类信息
#zbw 支持分类的层级结构父分类与子分类提供获取分类绝对URL、分类树含父级及子分类的方法
#zbw 优化了缓存策略、添加循环引用检测并移除冗余代码,提升了模型的健壮性和性能
class Category(BaseModel):
"""文章分类"""
#zbw 分类名称字符类型最大长度30唯一不允许重名使用翻译函数支持多语言
name = models.CharField(_('category name'), max_length=30, unique=True)
#zbw 父分类,自关联外键,允许为空(表示顶级分类),级联删除(父分类删除时子分类也删除)
parent_category = models.ForeignKey(
'self',
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
#zbw URL别名用于生成友好URL默认值'no-slug'设置唯一约束避免URL冲突
slug = models.SlugField(default='no-slug', max_length=60, blank=True, unique=True)
#zbw 排序索引数值越小越靠前默认0用于控制分类展示顺序
index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta:
#zbw 按index降序排列数值小的在前若index相同则按默认主键排序
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
verbose_name_plural = verbose_name # 复数形式与单数相同
def clean(self):
#zbw 验证逻辑检测分类是否形成循环引用如A的父级是BB的父级是A
current = self
while current.parent_category:
if current.parent_category == self:
raise ValidationError(_("Category cannot form a circular reference"))
current = current.parent_category
def save(self, *args, **kwargs):
#zbw 保存前先执行验证逻辑,确保数据合法性
self.full_clean()
super().save(*args, **kwargs)
#zbw 方法功能获取分类详情页的绝对URL
#zbw 返回值反向解析blog应用的category_detail视图参数为当前分类的slug
def get_absolute_url(self):
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
#zbw 方法功能:定义对象的字符串表示形式
#zbw 返回值分类名称便于在admin后台及调试时识别对象
def __str__(self):
return self.name
@cache_decorator(60 * 60 * 10)
#zbw 方法功能:递归获取当前分类的所有父级分类(含自身),形成分类树
#zbw 缓存装饰器结果缓存1小时平衡性能与数据新鲜度减少数据库查询
#zbw 返回值list of Category objects从当前分类到顶级分类的有序列表
@cache_decorator(60 * 60 * 1)
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
"""
categorys = []
#zbw 内部递归函数:将分类添加到列表,若有父分类则继续递归
def parse(category):
categorys.append(category)
if category.parent_category:
@ -218,28 +245,39 @@ class Category(BaseModel):
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
#zbw 方法功能:获取当前分类的所有子分类(含自身)
#zbw 缓存装饰器结果缓存1小时优化性能
#zbw 返回值list of Category objects包含当前分类及所有子孙分类
@cache_decorator(60 * 60 * 1)
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
"""
categorys = []
#zbw 一次性查询所有分类避免递归中多次查询数据库N+1问题优化
all_categorys = Category.objects.all()
#zbw 内部递归函数:递归获取所有子分类,移除冗余判断
def parse(category):
if category not in categorys:
categorys.append(category)
#zbw 从已查询的所有分类中筛选子分类,减少数据库交互
childs = all_categorys.filter(parent_category=category)
for child in childs:
if category not in categorys:
categorys.append(child)
parse(child)
parse(child) # 直接递归子分类,内部已做去重判断
parse(self)
return categorys
#zbw 信号处理:当分类保存(新增/更新清除相关缓存需缓存装饰器支持key生成逻辑
@receiver(post_save, sender=Category) # type: ignore
def clear_category_cache(sender, instance, **kwargs):
#zbw 清除当前分类的缓存
instance.get_category_tree.clear_cache()
instance.get_sub_categorys.clear_cache()
#zbw 清除父分类的子分类缓存(因当前分类变化可能影响父分类的子树)
if instance.parent_category:
instance.parent_category.get_sub_categorys.clear_cache()
class Tag(BaseModel):
"""文章标签"""
name = models.CharField(_('tag name'), max_length=30, unique=True)

@ -1,7 +1,5 @@
# xjz: 评论系统数据模型模块
# xjz: 定义评论相关的数据库模型和数据结构
from django.db import models
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@ -9,26 +7,19 @@ from blog.models import Article
# Create your models here.
# xjz: 评论模型类
# xjz: 存储用户对文章的评论信息,包括内容、时间等
class Comment(models.Model):
# xjz: 评论内容正文Text类型支持长文本
body = models.TextField('正文', max_length=300)
# xjz: 评论创建时间,自动记录创建时刻
creation_time = models.DateTimeField(_('creation time'), default=now)
# xjz: 评论最后修改时间,记录最后编辑时刻
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
# xjz: 评论作者,外键关联用户模型
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
# xjz: 所属文章,外键关联博客文章
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
# xjz: 父级评论,支持评论回复功能,允许为空
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
@ -37,11 +28,9 @@ class Comment(models.Model):
on_delete=models.CASCADE)
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
# xjz: Comment模型的元数据配置
class Meta:
# xjz: 按ID降序排列新的评论显示在前面
ordering = ['-id']
# xjz: 在管理后台显示的名称
verbose_name = _('comment')
verbose_name_plural = verbose_name
get_latest_by = 'id'

Binary file not shown.

@ -13,8 +13,8 @@ from oauth.admin import *
from oauth.models import *
from owntracks.admin import *
from owntracks.models import *
from servermanager.admin import *
from servermanager.models import *
# from servermanager.admin import * # 暂时禁用
# from servermanager.models import * # 暂时禁用
class DjangoBlogAdminSite(AdminSite):
@ -47,8 +47,8 @@ admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
# admin_site.register(commands, CommandsAdmin) # 暂时禁用
# admin_site.register(EmailSendLog, EmailSendLogAdmin) # 暂时禁用
admin_site.register(BlogUser, BlogUserAdmin)

@ -58,7 +58,7 @@ INSTALLED_APPS = [
'accounts',
'comments',
'oauth',
'servermanager',
# 'servermanager', # 暂时禁用因为缺少werobot模块
'owntracks',
'compressor',
'djangoblog'
@ -108,14 +108,8 @@ WSGI_APPLICATION = 'djangoblog.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangoblog',
'USER': 'root',
'PASSWORD':'root',
'HOST': '127.0.0.1',
'PORT': 3306,
'OPTIONS': {
'charset': 'utf8mb4'},
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}}
# Password validation

@ -56,7 +56,7 @@ urlpatterns += i18n_patterns(
re_path(r'^rss/$', DjangoBlogFeed()),
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
re_path(r'', include('servermanager.urls', namespace='servermanager')),
# re_path(r'', include('servermanager.urls', namespace='servermanager')), # 暂时禁用
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG:

@ -0,0 +1,12 @@
import os
import traceback
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
try:
from djangoblog.wsgi import application
print("WSGI application loaded successfully!")
except Exception as e:
print(f"Error loading WSGI application: {e}")
print("Detailed traceback:")
traceback.print_exc()

@ -1,408 +0,0 @@
import logging
import re
from abc import abstractmethod
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from mdeditor.fields import MDTextField # Markdown编辑器字段
from uuslug import slugify # 智能slug生成支持中文转拼音
from djangoblog.utils import cache_decorator, cache
from djangoblog.utils import get_current_site
logger = logging.getLogger(__name__)
class LinkShowType(models.TextChoices):
"""链接显示位置枚举 - 控制友情链接在不同页面的显示"""
I = ('i', _('index')) # 首页显示
L = ('l', _('list')) # 列表页显示
P = ('p', _('post')) # 文章详情页显示
A = ('a', _('all')) # 全站显示
S = ('s', _('slide')) # 幻灯片形式显示
class BaseModel(models.Model):
"""
抽象基模型 - 提供所有模型的通用字段和方法
采用抽象基类避免代码重复符合DRY原则
"""
id = models.AutoField(primary_key=True) # 自增主键
creation_time = models.DateTimeField(_('creation time'), default=now) # 创建时间,自动设置为当前时间
last_modify_time = models.DateTimeField(_('modify time'), default=now) # 最后修改时间
def save(self, *args, **kwargs):
"""
重写save方法实现智能slug生成和浏览量更新优化
特殊处理当只更新views字段时使用update提高性能
"""
# 检查是否是文章视图更新操作 - 性能优化点
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
# 直接使用update语句更新浏览量避免触发其他信号和save逻辑
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
# 自动生成slug如果模型有slug字段基于title或name生成
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
# 使用uuslug智能生成支持中文转拼音
setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs)
def get_full_url(self):
"""生成包含域名的完整URL用于分享和SEO"""
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
class Meta:
abstract = True # 声明为抽象类,不会创建对应的数据库表
@abstractmethod
def get_absolute_url(self):
"""抽象方法 - 子类必须实现返回对象的相对URL"""
pass
class Article(BaseModel):
"""
文章模型 - 博客核心内容模型
包含文章的所有属性和业务逻辑
"""
# 状态选择项 - 使用元组保持Django传统也可用TextChoicesDjango3.0+
STATUS_CHOICES = (
('d', _('Draft')), # 草稿状态
('p', _('Published')), # 已发布状态
)
COMMENT_STATUS = (
('o', _('Open')), # 开放评论
('c', _('Close')), # 关闭评论
)
TYPE = (
('a', _('Article')), # 普通文章
('p', _('Page')), # 独立页面(如关于页面)
)
# 核心字段
title = models.CharField(_('title'), max_length=200, unique=True) # 标题,唯一约束
body = MDTextField(_('body')) # 正文使用Markdown编辑器
pub_time = models.DateTimeField(_('publish time'), default=now) # 发布时间
status = models.CharField(_('status'), max_length=1, choices=STATUS_CHOICES, default='p') # 状态
comment_status = models.CharField(_('comment status'), max_length=1, choices=COMMENT_STATUS, default='o') # 评论状态
type = models.CharField(_('type'), max_length=1, choices=TYPE, default='a') # 类型
views = models.PositiveIntegerField(_('views'), default=0) # 浏览量统计
author = models.ForeignKey(
settings.AUTH_USER_MODEL, # 关联Django用户模型
verbose_name=_('author'),
on_delete=models.CASCADE) # 用户删除时级联删除文章
article_order = models.IntegerField(_('order'), default=0) # 文章排序权重
show_toc = models.BooleanField(_('show toc'), default=False) # 是否显示文章目录
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE) # 文章分类
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True) # 文章标签,多对多关系
def body_to_string(self):
"""将body内容转为字符串 - 可能用于全文搜索或预览"""
return self.body
def __str__(self):
"""字符串表示用于Admin后台显示"""
return self.title
class Meta:
"""模型元数据配置"""
ordering = ['-article_order', '-pub_time'] # 默认排序先按order倒序再按发布时间倒序
verbose_name = _('article') # 单数名称
verbose_name_plural = verbose_name # 复数名称
get_latest_by = 'id' # 指定获取最新记录的字段
def get_absolute_url(self):
"""
生成文章详情页URL
采用年月日+ID的URL结构有利于SEO和归档
"""
return reverse('blog:detailbyid', kwargs={
'article_id': self.id,
'year': self.creation_time.year,
'month': self.creation_time.month,
'day': self.creation_time.day
})
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
"""
获取分类树结构 - 用于面包屑导航
返回格式[(分类名, URL), ...]
"""
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def viewed(self):
"""增加文章浏览量 - 使用update_fields优化性能"""
self.views += 1
self.save(update_fields=['views']) # 只更新views字段
def comment_list(self):
"""
获取文章评论列表 - 带缓存功能
缓存策略缓存100分钟减少数据库查询
"""
cache_key = 'article_comments_{id}'.format(id=self.id)
value = cache.get(cache_key)
if value:
logger.info('get article comments:{id}'.format(id=self.id))
return value
else:
# 获取已启用的评论按ID倒序新的在前
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100) # 缓存100分钟
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
"""生成Django Admin后台编辑URL"""
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def next_article(self):
"""
获取下一篇已发布文章
查询逻辑ID大于当前文章的第一个已发布文章
"""
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100) # 缓存100分钟
def prev_article(self):
"""
获取上一篇已发布文章
查询逻辑ID小于当前文章的最后一个已发布文章
注意这里用了first()但实际是获取ID最大的那个
"""
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
从Markdown内容中提取第一张图片URL
用于文章列表的缩略图显示
正则匹配![alt text](image_url)
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1) # 返回第一个捕获组图片URL
return "" # 没有图片返回空字符串
class Category(BaseModel):
"""
文章分类模型 - 支持多级分类结构
通过parent_category自关联实现无限级分类
"""
name = models.CharField(_('category name'), max_length=30, unique=True)
parent_category = models.ForeignKey(
'self', # 自关联
verbose_name=_('parent category'),
blank=True,
null=True,
on_delete=models.CASCADE) # 父分类删除时级联删除子分类
slug = models.SlugField(default='no-slug', max_length=60, blank=True) # URL友好标识
index = models.IntegerField(default=0, verbose_name=_('index')) # 分类显示顺序
class Meta:
ordering = ['-index'] # 按index倒序排列数值越大越靠前
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
"""生成分类页面URL使用slug作为URL参数"""
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
return self.name
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_category_tree(self):
"""
递归获取分类的父级链 - 用于面包屑导航
返回从当前分类到根分类的列表
示例 [当前分类, 父分类, 祖父分类, ...]
"""
categorys = []
def parse(category):
"""递归解析分类层级"""
categorys.append(category)
if category.parent_category:
parse(category.parent_category) # 递归调用
parse(self) # 从当前分类开始
return categorys
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_sub_categorys(self):
"""
递归获取当前分类的所有子分类 - 用于分类导航
返回包含当前分类和所有子孙分类的列表
"""
categorys = []
all_categorys = Category.objects.all() # 获取所有分类
def parse(category):
"""递归解析子分类"""
if category not in categorys:
categorys.append(category)
# 查找直接子分类
childs = all_categorys.filter(parent_category=category)
for child in childs:
if child not in categorys:
categorys.append(child)
parse(child) # 递归查找子分类的子分类
parse(self) # 从当前分类开始
return categorys
class Tag(BaseModel):
"""
文章标签模型 - 简单的标签系统
多对多关系一篇文章可以有多个标签一个标签可以对应多篇文章
"""
name = models.CharField(_('tag name'), max_length=30, unique=True)
slug = models.SlugField(default='no-slug', max_length=60, blank=True)
def __str__(self):
return self.name
def get_absolute_url(self):
"""生成标签页面URL"""
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 10) # 缓存10小时
def get_article_count(self):
"""获取使用该标签的文章数量 - 带缓存"""
return Article.objects.filter(tags__name=self.name).distinct().count()
class Meta:
ordering = ['name'] # 按标签名字母顺序排序
verbose_name = _('tag')
verbose_name_plural = verbose_name
class Links(models.Model):
"""
友情链接模型 - 管理外部链接
支持控制链接显示位置和顺序
"""
name = models.CharField(_('link name'), max_length=30, unique=True) # 链接名称
link = models.URLField(_('link')) # 链接地址
sequence = models.IntegerField(_('order'), unique=True) # 显示顺序,唯一约束
is_enable = models.BooleanField(_('is show'), default=True) # 是否启用
show_type = models.CharField(
_('show type'),
max_length=1,
choices=LinkShowType.choices, # 使用前面定义的枚举
default=LinkShowType.I) # 默认在首页显示
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
ordering = ['sequence'] # 按sequence顺序排列
verbose_name = _('link')
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class SideBar(models.Model):
"""
侧边栏模型 - 管理侧边栏内容
支持HTML内容可以放置广告公告等自定义内容
"""
name = models.CharField(_('title'), max_length=100) # 侧边栏标题
content = models.TextField(_('content')) # HTML内容可以包含脚本和样式
sequence = models.IntegerField(_('order'), unique=True) # 显示顺序
is_enable = models.BooleanField(_('is enable'), default=True) # 是否启用
creation_time = models.DateTimeField(_('creation time'), default=now)
last_mod_time = models.DateTimeField(_('modify time'), default=now)
class Meta:
ordering = ['sequence'] # 按sequence顺序排列
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class BlogSettings(models.Model):
"""
博客全局配置模型 - 单例模式
存储博客的所有可配置项整个系统应该只有一个配置实例
"""
# 站点基本信息
site_name = models.CharField(_('site name'), max_length=200, default='')
site_description = models.TextField(_('site description'), max_length=1000, default='') # 站点描述
site_seo_description = models.TextField(_('site seo description'), max_length=1000, default='') # SEO描述
site_keywords = models.TextField(_('site keywords'), max_length=1000, default='') # 站点关键词
# 内容显示设置
article_sub_length = models.IntegerField(_('article sub length'), default=300) # 文章摘要长度
sidebar_article_count = models.IntegerField(_('sidebar article count'), default=10) # 侧边栏文章数量
sidebar_comment_count = models.IntegerField(_('sidebar comment count'), default=5) # 侧边栏评论数量
article_comment_count = models.IntegerField(_('article comment count'), default=5) # 文章页评论数量
# 广告设置
show_google_adsense = models.BooleanField(_('show adsense'), default=False) # 是否显示Google广告
google_adsense_codes = models.TextField(_('adsense code'), max_length=2000, default='') # 广告代码
# 评论系统设置
open_site_comment = models.BooleanField(_('open site comment'), default=True) # 是否开启全站评论
comment_need_review = models.BooleanField('评论是否需要审核', default=False) # 评论是否需要审核
# 自定义代码和备案信息
global_header = models.TextField("公共头部", default='') # 全局头部HTML代码
global_footer = models.TextField("公共尾部", default='') # 全局尾部HTML代码
beian_code = models.CharField('备案号', max_length=2000, default='') # ICP备案号
analytics_code = models.TextField("网站统计代码", max_length=1000, default='') # 网站统计代码Google Analytics等
show_gongan_code = models.BooleanField('是否显示公安备案号', default=False) # 是否显示公安备案
gongan_beiancode = models.TextField('公安备案号', max_length=2000, default='') # 公安备案号
class Meta:
verbose_name = _('Website configuration') # 在Admin中显示的名称
verbose_name_plural = verbose_name
def __str__(self):
return self.site_name
def clean(self):
"""
数据验证 - 确保配置模型的单例特性
防止创建多个配置实例
"""
if BlogSettings.objects.exclude(id=self.id).count():
raise ValidationError(_('There can only be one configuration'))
def save(self, *args, **kwargs):
"""
重写save方法 - 保存配置时清空所有缓存
确保配置更改立即生效
"""
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear() # 清空整个缓存系统
Loading…
Cancel
Save